Repository: felangel/bloc Branch: master Commit: e7f581facd7f Files: 1697 Total size: 4.3 MB Directory structure: gitextract_snx3nznv/ ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── build.md │ │ ├── chore.md │ │ ├── ci.md │ │ ├── config.yml │ │ ├── documentation.md │ │ ├── feature_request.md │ │ ├── performance.md │ │ ├── refactor.md │ │ ├── revert.md │ │ ├── style.md │ │ └── test.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── actions/ │ │ ├── angular_dart_package/ │ │ │ └── action.yaml │ │ ├── astro_site/ │ │ │ └── action.yaml │ │ ├── dart_compile/ │ │ │ └── action.yaml │ │ ├── dart_package/ │ │ │ └── action.yaml │ │ └── flutter_package/ │ │ └── action.yaml │ ├── codecov.yml │ ├── dependabot.yml │ └── workflows/ │ └── main.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── bricks/ │ ├── README.md │ ├── bloc/ │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── __brick__/ │ │ │ ├── {{name.snakeCase()}}_bloc.dart │ │ │ ├── {{name.snakeCase()}}_event.dart │ │ │ ├── {{name.snakeCase()}}_state.dart │ │ │ ├── {{~ basic_bloc }} │ │ │ ├── {{~ basic_event }} │ │ │ ├── {{~ basic_state }} │ │ │ ├── {{~ equatable_bloc }} │ │ │ ├── {{~ equatable_event }} │ │ │ ├── {{~ equatable_state }} │ │ │ ├── {{~ freezed_bloc }} │ │ │ ├── {{~ freezed_event }} │ │ │ └── {{~ freezed_state }} │ │ ├── brick.yaml │ │ └── hooks/ │ │ ├── pre_gen.dart │ │ └── pubspec.yaml │ ├── cubit/ │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── __brick__/ │ │ │ ├── {{name.snakeCase()}}_cubit.dart │ │ │ ├── {{name.snakeCase()}}_state.dart │ │ │ ├── {{~ basic_cubit }} │ │ │ ├── {{~ basic_state }} │ │ │ ├── {{~ equatable_cubit }} │ │ │ ├── {{~ equatable_state }} │ │ │ ├── {{~ freezed_cubit }} │ │ │ └── {{~ freezed_state }} │ │ ├── brick.yaml │ │ └── hooks/ │ │ ├── pre_gen.dart │ │ └── pubspec.yaml │ ├── flutter_bloc_feature/ │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── __brick__/ │ │ │ ├── {{name.snakeCase()}}/ │ │ │ │ ├── view/ │ │ │ │ │ ├── view.dart │ │ │ │ │ └── {{name.snakeCase()}}_page.dart │ │ │ │ └── {{name.snakeCase()}}.dart │ │ │ ├── {{~ bloc_builder }} │ │ │ ├── {{~ bloc_provider }} │ │ │ ├── {{~ cubit_bloc_builder }} │ │ │ └── {{~ cubit_bloc_provider }} │ │ ├── brick.yaml │ │ └── hooks/ │ │ ├── .gitignore │ │ ├── post_gen.dart │ │ ├── pre_gen.dart │ │ └── pubspec.yaml │ ├── hydrated_bloc/ │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── __brick__/ │ │ │ ├── {{name.snakeCase()}}_bloc.dart │ │ │ ├── {{name.snakeCase()}}_event.dart │ │ │ ├── {{name.snakeCase()}}_state.dart │ │ │ ├── {{~ basic_bloc }} │ │ │ ├── {{~ basic_event }} │ │ │ ├── {{~ basic_state }} │ │ │ ├── {{~ equatable_bloc }} │ │ │ ├── {{~ equatable_event }} │ │ │ ├── {{~ equatable_state }} │ │ │ ├── {{~ freezed_bloc }} │ │ │ ├── {{~ freezed_event }} │ │ │ └── {{~ freezed_state }} │ │ ├── brick.yaml │ │ └── hooks/ │ │ ├── pre_gen.dart │ │ └── pubspec.yaml │ ├── hydrated_cubit/ │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── __brick__/ │ │ │ ├── {{name.snakeCase()}}_cubit.dart │ │ │ ├── {{name.snakeCase()}}_state.dart │ │ │ ├── {{~ basic_cubit }} │ │ │ ├── {{~ basic_state }} │ │ │ ├── {{~ equatable_cubit }} │ │ │ ├── {{~ equatable_state }} │ │ │ ├── {{~ freezed_cubit }} │ │ │ └── {{~ freezed_state }} │ │ ├── brick.yaml │ │ └── hooks/ │ │ ├── pre_gen.dart │ │ └── pubspec.yaml │ ├── mason.yaml │ ├── replay_bloc/ │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── __brick__/ │ │ │ ├── {{name.snakeCase()}}_bloc.dart │ │ │ ├── {{name.snakeCase()}}_event.dart │ │ │ ├── {{name.snakeCase()}}_state.dart │ │ │ ├── {{~ basic_bloc }} │ │ │ ├── {{~ basic_event }} │ │ │ ├── {{~ basic_state }} │ │ │ ├── {{~ equatable_bloc }} │ │ │ ├── {{~ equatable_event }} │ │ │ ├── {{~ equatable_state }} │ │ │ ├── {{~ freezed_bloc }} │ │ │ ├── {{~ freezed_event }} │ │ │ └── {{~ freezed_state }} │ │ ├── brick.yaml │ │ └── hooks/ │ │ ├── pre_gen.dart │ │ └── pubspec.yaml │ └── replay_cubit/ │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── __brick__/ │ │ ├── {{name.snakeCase()}}_cubit.dart │ │ ├── {{name.snakeCase()}}_state.dart │ │ ├── {{~ basic_cubit }} │ │ ├── {{~ basic_state }} │ │ ├── {{~ equatable_cubit }} │ │ ├── {{~ equatable_state }} │ │ ├── {{~ freezed_cubit }} │ │ └── {{~ freezed_state }} │ ├── brick.yaml │ └── hooks/ │ ├── pre_gen.dart │ └── pubspec.yaml ├── docs/ │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc │ ├── astro.config.mjs │ ├── package.json │ ├── public/ │ │ └── CNAME │ ├── src/ │ │ ├── components/ │ │ │ ├── architecture/ │ │ │ │ ├── AppIdeaRankingBlocSnippet.astro │ │ │ │ ├── AppIdeasRepositorySnippet.astro │ │ │ │ ├── BlocLooseCouplingPresentationSnippet.astro │ │ │ │ ├── BlocTightCouplingSnippet.astro │ │ │ │ ├── BusinessLogicComponentSnippet.astro │ │ │ │ ├── DataProviderSnippet.astro │ │ │ │ ├── PresentationComponentSnippet.astro │ │ │ │ └── RepositorySnippet.astro │ │ │ ├── code/ │ │ │ │ └── RemoteCode.astro │ │ │ ├── concepts/ │ │ │ │ ├── bloc/ │ │ │ │ │ ├── AuthenticationChangeSnippet.astro │ │ │ │ │ ├── AuthenticationStateSnippet.astro │ │ │ │ │ ├── AuthenticationTransitionSnippet.astro │ │ │ │ │ ├── CountStreamSnippet.astro │ │ │ │ │ ├── CounterBlocEventHandlerSnippet.astro │ │ │ │ │ ├── CounterBlocFullSnippet.astro │ │ │ │ │ ├── CounterBlocIncrementSnippet.astro │ │ │ │ │ ├── CounterBlocOnChangeOutputSnippet.astro │ │ │ │ │ ├── CounterBlocOnChangeSnippet.astro │ │ │ │ │ ├── CounterBlocOnChangeUsageSnippet.astro │ │ │ │ │ ├── CounterBlocOnErrorOutputSnippet.astro │ │ │ │ │ ├── CounterBlocOnErrorSnippet.astro │ │ │ │ │ ├── CounterBlocOnEventSnippet.astro │ │ │ │ │ ├── CounterBlocOnTransitionOutputSnippet.astro │ │ │ │ │ ├── CounterBlocOnTransitionSnippet.astro │ │ │ │ │ ├── CounterBlocSnippet.astro │ │ │ │ │ ├── CounterBlocStreamUsageSnippet.astro │ │ │ │ │ ├── CounterBlocUsageSnippet.astro │ │ │ │ │ ├── CounterCubitBasicUsageSnippet.astro │ │ │ │ │ ├── CounterCubitFullSnippet.astro │ │ │ │ │ ├── CounterCubitIncrementSnippet.astro │ │ │ │ │ ├── CounterCubitInitialStateSnippet.astro │ │ │ │ │ ├── CounterCubitInstantiationSnippet.astro │ │ │ │ │ ├── CounterCubitOnChangeOutputSnippet.astro │ │ │ │ │ ├── CounterCubitOnChangeSnippet.astro │ │ │ │ │ ├── CounterCubitOnChangeUsageSnippet.astro │ │ │ │ │ ├── CounterCubitOnErrorOutputSnippet.astro │ │ │ │ │ ├── CounterCubitOnErrorSnippet.astro │ │ │ │ │ ├── CounterCubitSnippet.astro │ │ │ │ │ ├── CounterCubitStreamUsageSnippet.astro │ │ │ │ │ ├── DebounceEventTransformerSnippet.astro │ │ │ │ │ ├── SimpleBlocObserverOnChangeOutputSnippet.astro │ │ │ │ │ ├── SimpleBlocObserverOnChangeSnippet.astro │ │ │ │ │ ├── SimpleBlocObserverOnChangeUsageSnippet.astro │ │ │ │ │ ├── SimpleBlocObserverOnErrorSnippet.astro │ │ │ │ │ ├── SimpleBlocObserverOnEventOutputSnippet.astro │ │ │ │ │ ├── SimpleBlocObserverOnEventSnippet.astro │ │ │ │ │ ├── SimpleBlocObserverOnTransitionOutputSnippet.astro │ │ │ │ │ ├── SimpleBlocObserverOnTransitionSnippet.astro │ │ │ │ │ ├── SimpleBlocObserverOnTransitionUsageSnippet.astro │ │ │ │ │ ├── StreamsMainSnippet.astro │ │ │ │ │ └── SumStreamSnippet.astro │ │ │ │ └── flutter-bloc/ │ │ │ │ ├── BlocBuilderConditionSnippet.astro │ │ │ │ ├── BlocBuilderExplicitBlocSnippet.astro │ │ │ │ ├── BlocBuilderSnippet.astro │ │ │ │ ├── BlocConsumerConditionSnippet.astro │ │ │ │ ├── BlocConsumerSnippet.astro │ │ │ │ ├── BlocListenerConditionSnippet.astro │ │ │ │ ├── BlocListenerExplicitBlocSnippet.astro │ │ │ │ ├── BlocListenerSnippet.astro │ │ │ │ ├── BlocProviderEagerSnippet.astro │ │ │ │ ├── BlocProviderLookupSnippet.astro │ │ │ │ ├── BlocProviderSnippet.astro │ │ │ │ ├── BlocProviderValueSnippet.astro │ │ │ │ ├── BlocSelectorSnippet.astro │ │ │ │ ├── CounterBlocSnippet.astro │ │ │ │ ├── CounterMainSnippet.astro │ │ │ │ ├── CounterPageSnippet.astro │ │ │ │ ├── MultiBlocListenerSnippet.astro │ │ │ │ ├── MultiBlocProviderSnippet.astro │ │ │ │ ├── MultiRepositoryProviderSnippet.astro │ │ │ │ ├── NestedBlocListenerSnippet.astro │ │ │ │ ├── NestedBlocProviderSnippet.astro │ │ │ │ ├── NestedRepositoryProviderSnippet.astro │ │ │ │ ├── RepositoryProviderDisposeSnippet.astro │ │ │ │ ├── RepositoryProviderLookupSnippet.astro │ │ │ │ ├── RepositoryProviderSnippet.astro │ │ │ │ ├── WeatherAppSnippet.astro │ │ │ │ ├── WeatherMainSnippet.astro │ │ │ │ ├── WeatherPageSnippet.astro │ │ │ │ └── WeatherRepositorySnippet.astro │ │ │ ├── faqs/ │ │ │ │ ├── BlocExternalForEachSnippet.astro │ │ │ │ ├── BlocInternalAddEventSnippet.astro │ │ │ │ ├── BlocInternalEventSnippet.astro │ │ │ │ ├── BlocProviderBad1Snippet.astro │ │ │ │ ├── BlocProviderGood1Snippet.astro │ │ │ │ ├── BlocProviderGood2Snippet.astro │ │ │ │ ├── EquatableBlocTestSnippet.astro │ │ │ │ ├── EquatableEmitSnippet.astro │ │ │ │ ├── NoEquatableBlocTestSnippet.astro │ │ │ │ ├── SingleStateSnippet.astro │ │ │ │ ├── SingleStateUsageSnippet.astro │ │ │ │ ├── StateNotUpdatingBad1Snippet.astro │ │ │ │ ├── StateNotUpdatingBad2Snippet.astro │ │ │ │ ├── StateNotUpdatingBad3Snippet.astro │ │ │ │ ├── StateNotUpdatingGood1Snippet.astro │ │ │ │ ├── StateNotUpdatingGood2Snippet.astro │ │ │ │ └── StateNotUpdatingGood3Snippet.astro │ │ │ ├── getting-started/ │ │ │ │ ├── ImportTabs.astro │ │ │ │ └── InstallationTabs.astro │ │ │ ├── landing/ │ │ │ │ ├── Card.astro │ │ │ │ ├── Discord.astro │ │ │ │ ├── ListCard.astro │ │ │ │ ├── SplitCard.astro │ │ │ │ └── SponsorsGrid.astro │ │ │ ├── lint/ │ │ │ │ ├── BlocLintBasicAnalysisOptionsSnippet.astro │ │ │ │ ├── BlocLintChangingSeveritySnippet.astro │ │ │ │ ├── BlocLintDisablingRulesSnippet.astro │ │ │ │ ├── BlocLintEnablingRulesSnippet.astro │ │ │ │ ├── BlocLintExcludingFilesSnippet.astro │ │ │ │ ├── BlocLintIgnoreForFileSnippet.astro │ │ │ │ ├── BlocLintIgnoreForLineSnippet.astro │ │ │ │ ├── BlocLintMultipleRecommendedAnalysisOptionsSnippet.astro │ │ │ │ ├── BlocLintRecommendedAnalysisOptionsSnippet.astro │ │ │ │ ├── BlocToolsLintHelpOutputSnippet.astro │ │ │ │ ├── ImportFlutterInfoOutputSnippet.astro │ │ │ │ ├── ImportFlutterInfoSnippet.mdx │ │ │ │ ├── ImportFlutterWarningOutputSnippet.astro │ │ │ │ ├── ImportFlutterWarningSnippet.mdx │ │ │ │ ├── InstallBlocLintSnippet.astro │ │ │ │ ├── InstallBlocToolsSnippet.astro │ │ │ │ ├── RunBlocLintCounterCubitSnippet.astro │ │ │ │ ├── RunBlocLintInCurrentDirectorySnippet.astro │ │ │ │ └── RunBlocLintInSrcTestSnippet.astro │ │ │ ├── lint-rules/ │ │ │ │ ├── EnableRuleSnippet.astro │ │ │ │ ├── avoid_build_context_extensions/ │ │ │ │ │ ├── BadSnippet.mdx │ │ │ │ │ └── GoodSnippet.astro │ │ │ │ ├── avoid_flutter_imports/ │ │ │ │ │ ├── BadSnippet.mdx │ │ │ │ │ └── GoodSnippet.astro │ │ │ │ ├── avoid_public_bloc_methods/ │ │ │ │ │ ├── BadSnippet.mdx │ │ │ │ │ └── GoodSnippet.astro │ │ │ │ ├── avoid_public_fields/ │ │ │ │ │ ├── BadSnippet.mdx │ │ │ │ │ └── GoodSnippet.astro │ │ │ │ ├── prefer_bloc/ │ │ │ │ │ ├── BadSnippet.mdx │ │ │ │ │ └── GoodSnippet.astro │ │ │ │ ├── prefer_build_context_extensions/ │ │ │ │ │ ├── BadSnippet.mdx │ │ │ │ │ └── GoodSnippet.astro │ │ │ │ ├── prefer_cubit/ │ │ │ │ │ ├── BadSnippet.mdx │ │ │ │ │ └── GoodSnippet.astro │ │ │ │ ├── prefer_file_naming_conventions/ │ │ │ │ │ ├── BadSnippet.mdx │ │ │ │ │ └── GoodSnippet.astro │ │ │ │ └── prefer_void_public_cubit_methods/ │ │ │ │ ├── BadSnippet.mdx │ │ │ │ └── GoodSnippet.astro │ │ │ ├── modeling-state/ │ │ │ │ ├── ConcreteClassAndStatusEnumSnippet.astro │ │ │ │ └── SealedClassAndSubclassesSnippet.astro │ │ │ ├── naming-conventions/ │ │ │ │ ├── EventExamplesBad1Snippet.astro │ │ │ │ ├── EventExamplesGood1Snippet.astro │ │ │ │ ├── SingleStateExamplesGood1Snippet.astro │ │ │ │ ├── StateExamplesBad1Snippet.astro │ │ │ │ └── StateExamplesGood1Snippet.astro │ │ │ ├── testing/ │ │ │ │ ├── AddDevDependenciesSnippet.astro │ │ │ │ ├── CounterBlocSnippet.astro │ │ │ │ ├── CounterBlocTestBlocTestSnippet.astro │ │ │ │ ├── CounterBlocTestImportsSnippet.astro │ │ │ │ ├── CounterBlocTestInitialStateSnippet.astro │ │ │ │ ├── CounterBlocTestMainSnippet.astro │ │ │ │ └── CounterBlocTestSetupSnippet.astro │ │ │ └── tutorials/ │ │ │ ├── FlutterPubGetSnippet.astro │ │ │ ├── flutter-counter/ │ │ │ │ └── FlutterCreateSnippet.astro │ │ │ ├── flutter-firebase-login/ │ │ │ │ └── FlutterCreateSnippet.astro │ │ │ ├── flutter-infinite-list/ │ │ │ │ ├── FlutterCreateSnippet.astro │ │ │ │ ├── FlutterPubGetSnippet.astro │ │ │ │ ├── PostBlocInitialStateSnippet.astro │ │ │ │ ├── PostBlocOnPostFetchedSnippet.astro │ │ │ │ ├── PostBlocTransformerSnippet.astro │ │ │ │ └── PostsJsonSnippet.astro │ │ │ ├── flutter-login/ │ │ │ │ └── FlutterCreateSnippet.astro │ │ │ ├── flutter-timer/ │ │ │ │ ├── ActionsSnippet.astro │ │ │ │ ├── BackgroundSnippet.astro │ │ │ │ ├── FlutterCreateSnippet.astro │ │ │ │ ├── TimerBlocEmptySnippet.astro │ │ │ │ ├── TimerBlocInitialStateSnippet.astro │ │ │ │ ├── TimerBlocOnPausedSnippet.astro │ │ │ │ ├── TimerBlocOnResumedSnippet.astro │ │ │ │ ├── TimerBlocOnStartedSnippet.astro │ │ │ │ ├── TimerBlocOnTickedSnippet.astro │ │ │ │ ├── TimerBlocTickerSnippet.astro │ │ │ │ └── TimerPageSnippet.astro │ │ │ ├── flutter-todos/ │ │ │ │ ├── ActivateVeryGoodCLISnippet.astro │ │ │ │ ├── EditTodosPageTreeSnippet.astro │ │ │ │ ├── FlutterCreatePackagesSnippet.astro │ │ │ │ ├── FlutterCreateSnippet.astro │ │ │ │ ├── HomePageTreeSnippet.astro │ │ │ │ ├── ProjectStructureSnippet.astro │ │ │ │ ├── StatsPageTreeSnippet.astro │ │ │ │ ├── TodosOverviewPageTreeSnippet.astro │ │ │ │ └── VeryGoodPackagesGetSnippet.astro │ │ │ ├── flutter-weather/ │ │ │ │ ├── BuildRunnerBuildSnippet.astro │ │ │ │ ├── FeatureTreeSnippet.astro │ │ │ │ ├── FlutterCreateApiClientSnippet.astro │ │ │ │ ├── FlutterCreateRepositorySnippet.astro │ │ │ │ ├── FlutterCreateSnippet.astro │ │ │ │ ├── FlutterTestCoverageSnippet.astro │ │ │ │ ├── GetWeatherMethodSnippet.astro │ │ │ │ ├── LocationDartSnippet.astro │ │ │ │ ├── LocationJsonSnippet.astro │ │ │ │ ├── LocationSearchMethodSnippet.astro │ │ │ │ ├── OpenMeteoApiClientTreeSnippet.astro │ │ │ │ ├── OpenMeteoLibrarySnippet.astro │ │ │ │ ├── OpenMeteoModelsBarrelTreeSnippet.astro │ │ │ │ ├── OpenMeteoModelsTreeSnippet.astro │ │ │ │ ├── RepositoryModelsBarrelTreeSnippet.astro │ │ │ │ ├── WeatherBarrelDartSnippet.astro │ │ │ │ ├── WeatherCubitTreeSnippet.astro │ │ │ │ ├── WeatherDartSnippet.astro │ │ │ │ ├── WeatherJsonSnippet.astro │ │ │ │ └── WeatherRepositoryLibrarySnippet.astro │ │ │ ├── github-search/ │ │ │ │ ├── ActivateStagehandSnippet.astro │ │ │ │ ├── DartPubGetSnippet.astro │ │ │ │ ├── FlutterCreateSnippet.astro │ │ │ │ ├── SetupSnippet.astro │ │ │ │ └── StagehandSnippet.astro │ │ │ └── ngdart-counter/ │ │ │ ├── ActivateStagehandSnippet.astro │ │ │ ├── InstallDependenciesSnippet.astro │ │ │ └── StagehandSnippet.astro │ │ ├── content/ │ │ │ ├── config.ts │ │ │ └── docs/ │ │ │ ├── ar/ │ │ │ │ ├── architecture.mdx │ │ │ │ ├── bloc-concepts.mdx │ │ │ │ ├── faqs.mdx │ │ │ │ ├── flutter-bloc-concepts.mdx │ │ │ │ ├── getting-started.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── lint/ │ │ │ │ │ ├── configuration.mdx │ │ │ │ │ ├── customizing-rules.mdx │ │ │ │ │ ├── index.mdx │ │ │ │ │ └── installation.mdx │ │ │ │ ├── lint-rules/ │ │ │ │ │ ├── avoid_build_context_extensions.mdx │ │ │ │ │ ├── avoid_flutter_imports.mdx │ │ │ │ │ ├── avoid_public_bloc_methods.mdx │ │ │ │ │ ├── avoid_public_fields.mdx │ │ │ │ │ ├── prefer_bloc.mdx │ │ │ │ │ ├── prefer_build_context_extensions.mdx │ │ │ │ │ ├── prefer_cubit.mdx │ │ │ │ │ ├── prefer_file_naming_conventions.mdx │ │ │ │ │ └── prefer_void_public_cubit_methods.mdx │ │ │ │ ├── migration.mdx │ │ │ │ ├── modeling-state.mdx │ │ │ │ ├── naming-conventions.mdx │ │ │ │ ├── testing.mdx │ │ │ │ ├── tutorials/ │ │ │ │ │ ├── flutter-counter.mdx │ │ │ │ │ ├── flutter-firebase-login.mdx │ │ │ │ │ ├── flutter-infinite-list.mdx │ │ │ │ │ ├── flutter-login.mdx │ │ │ │ │ ├── flutter-timer.mdx │ │ │ │ │ ├── flutter-todos.mdx │ │ │ │ │ ├── flutter-weather.mdx │ │ │ │ │ ├── github-search.mdx │ │ │ │ │ └── ngdart-counter.mdx │ │ │ │ └── why-bloc.mdx │ │ │ ├── architecture.mdx │ │ │ ├── bloc-concepts.mdx │ │ │ ├── bn/ │ │ │ │ ├── architecture.mdx │ │ │ │ ├── bloc-concepts.mdx │ │ │ │ ├── faqs.mdx │ │ │ │ ├── flutter-bloc-concepts.mdx │ │ │ │ ├── getting-started.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── migration.mdx │ │ │ │ ├── modeling-state.mdx │ │ │ │ ├── naming-conventions.mdx │ │ │ │ ├── testing.mdx │ │ │ │ └── why-bloc.mdx │ │ │ ├── de/ │ │ │ │ ├── bloc-concepts.mdx │ │ │ │ ├── getting-started.mdx │ │ │ │ ├── index.mdx │ │ │ │ └── why-bloc.mdx │ │ │ ├── es/ │ │ │ │ ├── architecture.mdx │ │ │ │ ├── bloc-concepts.mdx │ │ │ │ ├── faqs.mdx │ │ │ │ ├── flutter-bloc-concepts.mdx │ │ │ │ ├── getting-started.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── migration.mdx │ │ │ │ ├── modeling-state.mdx │ │ │ │ ├── naming-conventions.mdx │ │ │ │ ├── testing.mdx │ │ │ │ └── why-bloc.mdx │ │ │ ├── fa/ │ │ │ │ ├── architecture.mdx │ │ │ │ ├── bloc-concepts.mdx │ │ │ │ ├── faqs.mdx │ │ │ │ ├── flutter-bloc-concepts.mdx │ │ │ │ ├── getting-started.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── lint/ │ │ │ │ │ ├── configuration.mdx │ │ │ │ │ ├── customizing-rules.mdx │ │ │ │ │ ├── index.mdx │ │ │ │ │ └── installation.mdx │ │ │ │ ├── lint-rules/ │ │ │ │ │ ├── avoid_build_context_extensions.mdx │ │ │ │ │ ├── avoid_flutter_imports.mdx │ │ │ │ │ ├── avoid_public_bloc_methods.mdx │ │ │ │ │ ├── avoid_public_fields.mdx │ │ │ │ │ ├── prefer_bloc.mdx │ │ │ │ │ ├── prefer_build_context_extensions.mdx │ │ │ │ │ ├── prefer_cubit.mdx │ │ │ │ │ ├── prefer_file_naming_conventions.mdx │ │ │ │ │ └── prefer_void_public_cubit_methods.mdx │ │ │ │ ├── migration.mdx │ │ │ │ ├── modeling-state.mdx │ │ │ │ ├── naming-conventions.mdx │ │ │ │ ├── testing.mdx │ │ │ │ └── why-bloc.mdx │ │ │ ├── faqs.mdx │ │ │ ├── fil/ │ │ │ │ ├── getting-started.mdx │ │ │ │ ├── index.mdx │ │ │ │ └── why-bloc.mdx │ │ │ ├── flutter-bloc-concepts.mdx │ │ │ ├── fr/ │ │ │ │ ├── architecture.mdx │ │ │ │ ├── getting-started.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── lint/ │ │ │ │ │ ├── index.mdx │ │ │ │ │ └── installation.mdx │ │ │ │ ├── testing.mdx │ │ │ │ └── why-bloc.mdx │ │ │ ├── getting-started.mdx │ │ │ ├── index.mdx │ │ │ ├── it/ │ │ │ │ ├── architecture.mdx │ │ │ │ ├── bloc-concepts.mdx │ │ │ │ ├── faqs.mdx │ │ │ │ ├── flutter-bloc-concepts.mdx │ │ │ │ ├── getting-started.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── lint/ │ │ │ │ │ ├── configuration.mdx │ │ │ │ │ ├── customizing-rules.mdx │ │ │ │ │ ├── index.mdx │ │ │ │ │ └── installation.mdx │ │ │ │ ├── lint-rules/ │ │ │ │ │ ├── avoid_build_context_extensions.mdx │ │ │ │ │ ├── avoid_flutter_imports.mdx │ │ │ │ │ ├── avoid_public_bloc_methods.mdx │ │ │ │ │ ├── avoid_public_fields.mdx │ │ │ │ │ ├── prefer_bloc.mdx │ │ │ │ │ ├── prefer_build_context_extensions.mdx │ │ │ │ │ ├── prefer_cubit.mdx │ │ │ │ │ ├── prefer_file_naming_conventions.mdx │ │ │ │ │ └── prefer_void_public_cubit_methods.mdx │ │ │ │ ├── migration.mdx │ │ │ │ ├── modeling-state.mdx │ │ │ │ ├── naming-conventions.mdx │ │ │ │ ├── testing.mdx │ │ │ │ ├── tutorials/ │ │ │ │ │ ├── flutter-counter.mdx │ │ │ │ │ ├── flutter-firebase-login.mdx │ │ │ │ │ ├── flutter-infinite-list.mdx │ │ │ │ │ ├── flutter-login.mdx │ │ │ │ │ ├── flutter-timer.mdx │ │ │ │ │ ├── flutter-todos.mdx │ │ │ │ │ ├── flutter-weather.mdx │ │ │ │ │ ├── github-search.mdx │ │ │ │ │ └── ngdart-counter.mdx │ │ │ │ └── why-bloc.mdx │ │ │ ├── ja/ │ │ │ │ ├── bloc-concepts.mdx │ │ │ │ ├── flutter-bloc-concepts.mdx │ │ │ │ ├── getting-started.mdx │ │ │ │ ├── index.mdx │ │ │ │ └── why-bloc.mdx │ │ │ ├── ko/ │ │ │ │ ├── architecture.mdx │ │ │ │ ├── bloc-concepts.mdx │ │ │ │ ├── faqs.mdx │ │ │ │ ├── flutter-bloc-concepts.mdx │ │ │ │ ├── getting-started.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── modeling-state.mdx │ │ │ │ ├── naming-conventions.mdx │ │ │ │ ├── testing.mdx │ │ │ │ ├── tutorials/ │ │ │ │ │ ├── flutter-counter.mdx │ │ │ │ │ ├── flutter-firebase-login.mdx │ │ │ │ │ ├── flutter-infinite-list.mdx │ │ │ │ │ ├── flutter-login.mdx │ │ │ │ │ ├── flutter-timer.mdx │ │ │ │ │ ├── flutter-todos.mdx │ │ │ │ │ ├── flutter-weather.mdx │ │ │ │ │ └── github-search.mdx │ │ │ │ └── why-bloc.mdx │ │ │ ├── lint/ │ │ │ │ ├── configuration.mdx │ │ │ │ ├── customizing-rules.mdx │ │ │ │ ├── index.mdx │ │ │ │ └── installation.mdx │ │ │ ├── lint-rules/ │ │ │ │ ├── avoid_build_context_extensions.mdx │ │ │ │ ├── avoid_flutter_imports.mdx │ │ │ │ ├── avoid_public_bloc_methods.mdx │ │ │ │ ├── avoid_public_fields.mdx │ │ │ │ ├── prefer_bloc.mdx │ │ │ │ ├── prefer_build_context_extensions.mdx │ │ │ │ ├── prefer_cubit.mdx │ │ │ │ ├── prefer_file_naming_conventions.mdx │ │ │ │ └── prefer_void_public_cubit_methods.mdx │ │ │ ├── migration.mdx │ │ │ ├── modeling-state.mdx │ │ │ ├── naming-conventions.mdx │ │ │ ├── pt-br/ │ │ │ │ ├── architecture.mdx │ │ │ │ ├── bloc-concepts.mdx │ │ │ │ ├── flutter-bloc-concepts.mdx │ │ │ │ ├── getting-started.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── modeling-state.mdx │ │ │ │ └── why-bloc.mdx │ │ │ ├── ru/ │ │ │ │ ├── architecture.mdx │ │ │ │ ├── bloc-concepts.mdx │ │ │ │ ├── faqs.mdx │ │ │ │ ├── flutter-bloc-concepts.mdx │ │ │ │ ├── getting-started.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── lint/ │ │ │ │ │ ├── configuration.mdx │ │ │ │ │ ├── customizing-rules.mdx │ │ │ │ │ ├── index.mdx │ │ │ │ │ └── installation.mdx │ │ │ │ ├── lint-rules/ │ │ │ │ │ ├── avoid_build_context_extensions.mdx │ │ │ │ │ ├── avoid_flutter_imports.mdx │ │ │ │ │ ├── avoid_public_bloc_methods.mdx │ │ │ │ │ ├── avoid_public_fields.mdx │ │ │ │ │ ├── prefer_bloc.mdx │ │ │ │ │ ├── prefer_build_context_extensions.mdx │ │ │ │ │ ├── prefer_cubit.mdx │ │ │ │ │ ├── prefer_file_naming_conventions.mdx │ │ │ │ │ └── prefer_void_public_cubit_methods.mdx │ │ │ │ ├── migration.mdx │ │ │ │ ├── modeling-state.mdx │ │ │ │ ├── naming-conventions.mdx │ │ │ │ ├── testing.mdx │ │ │ │ ├── tutorials/ │ │ │ │ │ ├── flutter-counter.mdx │ │ │ │ │ ├── flutter-firebase-login.mdx │ │ │ │ │ ├── flutter-infinite-list.mdx │ │ │ │ │ ├── flutter-login.mdx │ │ │ │ │ ├── flutter-timer.mdx │ │ │ │ │ ├── flutter-todos.mdx │ │ │ │ │ ├── flutter-weather.mdx │ │ │ │ │ ├── github-search.mdx │ │ │ │ │ └── ngdart-counter.mdx │ │ │ │ └── why-bloc.mdx │ │ │ ├── testing.mdx │ │ │ ├── tutorials/ │ │ │ │ ├── flutter-counter.mdx │ │ │ │ ├── flutter-firebase-login.mdx │ │ │ │ ├── flutter-infinite-list.mdx │ │ │ │ ├── flutter-login.mdx │ │ │ │ ├── flutter-timer.mdx │ │ │ │ ├── flutter-todos.mdx │ │ │ │ ├── flutter-weather.mdx │ │ │ │ ├── github-search.mdx │ │ │ │ └── ngdart-counter.mdx │ │ │ ├── uk/ │ │ │ │ ├── architecture.mdx │ │ │ │ ├── bloc-concepts.mdx │ │ │ │ ├── faqs.mdx │ │ │ │ ├── flutter-bloc-concepts.mdx │ │ │ │ ├── getting-started.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── lint/ │ │ │ │ │ ├── configuration.mdx │ │ │ │ │ ├── customizing-rules.mdx │ │ │ │ │ ├── index.mdx │ │ │ │ │ └── installation.mdx │ │ │ │ ├── lint-rules/ │ │ │ │ │ ├── avoid_build_context_extensions.mdx │ │ │ │ │ ├── avoid_flutter_imports.mdx │ │ │ │ │ ├── avoid_public_bloc_methods.mdx │ │ │ │ │ ├── avoid_public_fields.mdx │ │ │ │ │ ├── prefer_bloc.mdx │ │ │ │ │ ├── prefer_build_context_extensions.mdx │ │ │ │ │ ├── prefer_cubit.mdx │ │ │ │ │ ├── prefer_file_naming_conventions.mdx │ │ │ │ │ └── prefer_void_public_cubit_methods.mdx │ │ │ │ ├── migration.mdx │ │ │ │ ├── modeling-state.mdx │ │ │ │ ├── naming-conventions.mdx │ │ │ │ ├── testing.mdx │ │ │ │ ├── tutorials/ │ │ │ │ │ ├── flutter-counter.mdx │ │ │ │ │ ├── flutter-firebase-login.mdx │ │ │ │ │ ├── flutter-infinite-list.mdx │ │ │ │ │ ├── flutter-login.mdx │ │ │ │ │ ├── flutter-timer.mdx │ │ │ │ │ ├── flutter-todos.mdx │ │ │ │ │ ├── flutter-weather.mdx │ │ │ │ │ ├── github-search.mdx │ │ │ │ │ └── ngdart-counter.mdx │ │ │ │ └── why-bloc.mdx │ │ │ ├── why-bloc.mdx │ │ │ └── zh-cn/ │ │ │ ├── bloc-concepts.mdx │ │ │ ├── flutter-bloc-concepts.mdx │ │ │ ├── getting-started.mdx │ │ │ ├── index.mdx │ │ │ ├── tutorials/ │ │ │ │ └── flutter-counter.mdx │ │ │ └── why-bloc.mdx │ │ ├── env.d.ts │ │ ├── styles/ │ │ │ └── landing.css │ │ └── tailwind.css │ └── tsconfig.json ├── examples/ │ ├── angular_counter/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── lib/ │ │ │ ├── app_component.css │ │ │ ├── app_component.dart │ │ │ ├── app_component.html │ │ │ └── src/ │ │ │ └── counter_page/ │ │ │ ├── counter_bloc.dart │ │ │ ├── counter_page_component.css │ │ │ ├── counter_page_component.dart │ │ │ └── counter_page_component.html │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ └── web/ │ │ ├── index.html │ │ ├── main.dart │ │ └── styles.css │ ├── bloc_concurrency_visualizer/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── lib/ │ │ │ ├── main.dart │ │ │ └── timeline/ │ │ │ ├── bloc/ │ │ │ │ ├── timeline_bloc.dart │ │ │ │ ├── timeline_event.dart │ │ │ │ └── timeline_state.dart │ │ │ ├── models/ │ │ │ │ ├── models.dart │ │ │ │ ├── task.dart │ │ │ │ └── transformer.dart │ │ │ ├── timeline.dart │ │ │ └── view/ │ │ │ └── timeline_page.dart │ │ ├── pubspec.yaml │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ ├── flutter_bloc_with_stream/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── ios/ │ │ │ └── .gitignore │ │ ├── lib/ │ │ │ ├── bloc/ │ │ │ │ ├── ticker_bloc.dart │ │ │ │ ├── ticker_event.dart │ │ │ │ └── ticker_state.dart │ │ │ ├── main.dart │ │ │ └── ticker/ │ │ │ └── ticker.dart │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ ├── test/ │ │ │ ├── app_test.dart │ │ │ ├── bloc/ │ │ │ │ ├── ticker_bloc_test.dart │ │ │ │ ├── ticker_event_test.dart │ │ │ │ └── ticker_state_test.dart │ │ │ └── ticker_page_test.dart │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ ├── flutter_complex_list/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── ios/ │ │ │ └── .gitignore │ │ ├── lib/ │ │ │ ├── app.dart │ │ │ ├── complex_list/ │ │ │ │ ├── complex_list.dart │ │ │ │ ├── cubit/ │ │ │ │ │ ├── complex_list_cubit.dart │ │ │ │ │ └── complex_list_state.dart │ │ │ │ ├── models/ │ │ │ │ │ ├── item.dart │ │ │ │ │ └── models.dart │ │ │ │ └── view/ │ │ │ │ ├── complex_list_page.dart │ │ │ │ └── view.dart │ │ │ ├── main.dart │ │ │ ├── repository.dart │ │ │ └── simple_bloc_observer.dart │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ ├── test/ │ │ │ ├── app_test.dart │ │ │ ├── complex_list/ │ │ │ │ ├── cubit/ │ │ │ │ │ ├── complex_list_cubit_test.dart │ │ │ │ │ └── complex_list_state_test.dart │ │ │ │ ├── models/ │ │ │ │ │ └── item_test.dart │ │ │ │ └── view/ │ │ │ │ └── complex_list_page_test.dart │ │ │ └── repository_test.dart │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ ├── flutter_counter/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── integration_test/ │ │ │ └── app_test.dart │ │ ├── lib/ │ │ │ ├── app.dart │ │ │ ├── counter/ │ │ │ │ ├── counter.dart │ │ │ │ ├── cubit/ │ │ │ │ │ └── counter_cubit.dart │ │ │ │ └── view/ │ │ │ │ ├── counter_page.dart │ │ │ │ ├── counter_view.dart │ │ │ │ └── view.dart │ │ │ ├── counter_observer.dart │ │ │ └── main.dart │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ ├── test/ │ │ │ ├── app_test.dart │ │ │ └── counter/ │ │ │ ├── cubit/ │ │ │ │ └── counter_cubit_test.dart │ │ │ └── view/ │ │ │ ├── counter_page_test.dart │ │ │ └── counter_view_test.dart │ │ ├── test_driver/ │ │ │ └── integration_test.dart │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ ├── flutter_dynamic_form/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── ios/ │ │ │ └── .gitignore │ │ ├── lib/ │ │ │ ├── app.dart │ │ │ ├── main.dart │ │ │ ├── new_car/ │ │ │ │ ├── bloc/ │ │ │ │ │ ├── new_car_bloc.dart │ │ │ │ │ ├── new_car_event.dart │ │ │ │ │ └── new_car_state.dart │ │ │ │ ├── new_car.dart │ │ │ │ └── view/ │ │ │ │ └── new_car_page.dart │ │ │ └── new_car_repository.dart │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ ├── test/ │ │ │ ├── app_test.dart │ │ │ ├── new_car/ │ │ │ │ ├── bloc/ │ │ │ │ │ ├── new_car_bloc_test.dart │ │ │ │ │ ├── new_car_event_test.dart │ │ │ │ │ └── new_car_state_test.dart │ │ │ │ └── view/ │ │ │ │ └── new_car_page_test.dart │ │ │ └── new_car_repository_test.dart │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ ├── flutter_firebase_login/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── android/ │ │ │ ├── .gitignore │ │ │ ├── app/ │ │ │ │ ├── build.gradle.kts │ │ │ │ ├── google-services.json │ │ │ │ └── src/ │ │ │ │ ├── debug/ │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ ├── main/ │ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ │ ├── kotlin/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── example/ │ │ │ │ │ │ └── flutter_firebase_login/ │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ │ └── res/ │ │ │ │ │ ├── drawable/ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21/ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values/ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night/ │ │ │ │ │ └── styles.xml │ │ │ │ └── profile/ │ │ │ │ └── AndroidManifest.xml │ │ │ ├── build.gradle.kts │ │ │ ├── gradle/ │ │ │ │ └── wrapper/ │ │ │ │ └── gradle-wrapper.properties │ │ │ ├── gradle.properties │ │ │ └── settings.gradle.kts │ │ ├── ios/ │ │ │ ├── .gitignore │ │ │ ├── Flutter/ │ │ │ │ ├── AppFrameworkInfo.plist │ │ │ │ ├── Debug.xcconfig │ │ │ │ └── Release.xcconfig │ │ │ ├── Podfile │ │ │ ├── Runner/ │ │ │ │ ├── AppDelegate.swift │ │ │ │ ├── Assets.xcassets/ │ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── LaunchImage.imageset/ │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── README.md │ │ │ │ ├── Base.lproj/ │ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ │ └── Main.storyboard │ │ │ │ ├── GoogleService-Info.plist │ │ │ │ ├── Info.plist │ │ │ │ └── Runner-Bridging-Header.h │ │ │ ├── Runner.xcodeproj/ │ │ │ │ ├── project.pbxproj │ │ │ │ ├── project.xcworkspace/ │ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ │ └── xcshareddata/ │ │ │ │ └── xcschemes/ │ │ │ │ └── Runner.xcscheme │ │ │ ├── Runner.xcworkspace/ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata/ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ └── RunnerTests/ │ │ │ └── RunnerTests.swift │ │ ├── lib/ │ │ │ ├── app/ │ │ │ │ ├── app.dart │ │ │ │ ├── bloc/ │ │ │ │ │ ├── app_bloc.dart │ │ │ │ │ ├── app_event.dart │ │ │ │ │ └── app_state.dart │ │ │ │ ├── bloc_observer.dart │ │ │ │ ├── routes/ │ │ │ │ │ └── routes.dart │ │ │ │ └── view/ │ │ │ │ └── app.dart │ │ │ ├── home/ │ │ │ │ ├── home.dart │ │ │ │ ├── view/ │ │ │ │ │ └── home_page.dart │ │ │ │ └── widgets/ │ │ │ │ ├── avatar.dart │ │ │ │ └── widgets.dart │ │ │ ├── login/ │ │ │ │ ├── cubit/ │ │ │ │ │ ├── login_cubit.dart │ │ │ │ │ └── login_state.dart │ │ │ │ ├── login.dart │ │ │ │ └── view/ │ │ │ │ ├── login_form.dart │ │ │ │ ├── login_page.dart │ │ │ │ └── view.dart │ │ │ ├── main.dart │ │ │ ├── sign_up/ │ │ │ │ ├── cubit/ │ │ │ │ │ ├── sign_up_cubit.dart │ │ │ │ │ └── sign_up_state.dart │ │ │ │ ├── sign_up.dart │ │ │ │ └── view/ │ │ │ │ ├── sign_up_form.dart │ │ │ │ ├── sign_up_page.dart │ │ │ │ └── view.dart │ │ │ └── theme.dart │ │ ├── packages/ │ │ │ ├── authentication_repository/ │ │ │ │ ├── analysis_options.yaml │ │ │ │ ├── lib/ │ │ │ │ │ ├── authentication_repository.dart │ │ │ │ │ └── src/ │ │ │ │ │ ├── authentication_repository.dart │ │ │ │ │ └── models/ │ │ │ │ │ ├── models.dart │ │ │ │ │ └── user.dart │ │ │ │ ├── pubspec.yaml │ │ │ │ └── test/ │ │ │ │ ├── authentication_repository_test.dart │ │ │ │ └── models/ │ │ │ │ └── user_test.dart │ │ │ ├── cache/ │ │ │ │ ├── analysis_options.yaml │ │ │ │ ├── lib/ │ │ │ │ │ └── cache.dart │ │ │ │ ├── pubspec.yaml │ │ │ │ └── test/ │ │ │ │ └── cache_test.dart │ │ │ └── form_inputs/ │ │ │ ├── analysis_options.yaml │ │ │ ├── lib/ │ │ │ │ ├── form_inputs.dart │ │ │ │ └── src/ │ │ │ │ ├── confirmed_password.dart │ │ │ │ ├── email.dart │ │ │ │ └── password.dart │ │ │ └── pubspec.yaml │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ └── test/ │ │ ├── app/ │ │ │ ├── bloc/ │ │ │ │ ├── app_bloc_test.dart │ │ │ │ └── app_state_test.dart │ │ │ ├── bloc_observer_test.dart │ │ │ ├── routes/ │ │ │ │ └── routes_test.dart │ │ │ └── view/ │ │ │ └── app_test.dart │ │ ├── home/ │ │ │ ├── view/ │ │ │ │ └── home_page_test.dart │ │ │ └── widgets/ │ │ │ └── avatar_test.dart │ │ ├── login/ │ │ │ ├── cubit/ │ │ │ │ ├── login_cubit_test.dart │ │ │ │ └── login_state_test.dart │ │ │ └── view/ │ │ │ ├── login_form_test.dart │ │ │ └── login_page_test.dart │ │ └── sign_up/ │ │ ├── cubit/ │ │ │ ├── sign_up_cubit_test.dart │ │ │ └── sign_up_state_test.dart │ │ └── view/ │ │ ├── sign_up_form_test.dart │ │ └── sign_up_page_test.dart │ ├── flutter_form_validation/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── ios/ │ │ │ └── .gitignore │ │ ├── lib/ │ │ │ ├── bloc/ │ │ │ │ ├── my_form_bloc.dart │ │ │ │ ├── my_form_event.dart │ │ │ │ └── my_form_state.dart │ │ │ ├── main.dart │ │ │ └── models/ │ │ │ ├── email.dart │ │ │ ├── models.dart │ │ │ └── password.dart │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ ├── flutter_infinite_list/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── ios/ │ │ │ └── .gitignore │ │ ├── lib/ │ │ │ ├── app.dart │ │ │ ├── main.dart │ │ │ ├── posts/ │ │ │ │ ├── bloc/ │ │ │ │ │ ├── post_bloc.dart │ │ │ │ │ ├── post_event.dart │ │ │ │ │ └── post_state.dart │ │ │ │ ├── models/ │ │ │ │ │ ├── models.dart │ │ │ │ │ └── post.dart │ │ │ │ ├── posts.dart │ │ │ │ ├── view/ │ │ │ │ │ ├── posts_list.dart │ │ │ │ │ ├── posts_page.dart │ │ │ │ │ └── view.dart │ │ │ │ └── widgets/ │ │ │ │ ├── bottom_loader.dart │ │ │ │ ├── post_list_item.dart │ │ │ │ └── widgets.dart │ │ │ └── simple_bloc_observer.dart │ │ ├── macos/ │ │ │ ├── .gitignore │ │ │ └── Flutter/ │ │ │ └── GeneratedPluginRegistrant.swift │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ ├── test/ │ │ │ ├── app_test.dart │ │ │ └── posts/ │ │ │ ├── bloc/ │ │ │ │ ├── post_bloc_test.dart │ │ │ │ ├── post_event_test.dart │ │ │ │ └── post_state_test.dart │ │ │ ├── models/ │ │ │ │ └── post_test.dart │ │ │ └── view/ │ │ │ ├── posts_list_test.dart │ │ │ └── posts_page_test.dart │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ ├── flutter_login/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── ios/ │ │ │ └── .gitignore │ │ ├── lib/ │ │ │ ├── app.dart │ │ │ ├── authentication/ │ │ │ │ ├── authentication.dart │ │ │ │ └── bloc/ │ │ │ │ ├── authentication_bloc.dart │ │ │ │ ├── authentication_event.dart │ │ │ │ └── authentication_state.dart │ │ │ ├── home/ │ │ │ │ ├── home.dart │ │ │ │ └── view/ │ │ │ │ └── home_page.dart │ │ │ ├── login/ │ │ │ │ ├── bloc/ │ │ │ │ │ ├── login_bloc.dart │ │ │ │ │ ├── login_event.dart │ │ │ │ │ └── login_state.dart │ │ │ │ ├── login.dart │ │ │ │ ├── models/ │ │ │ │ │ ├── models.dart │ │ │ │ │ ├── password.dart │ │ │ │ │ └── username.dart │ │ │ │ └── view/ │ │ │ │ ├── login_form.dart │ │ │ │ ├── login_page.dart │ │ │ │ └── view.dart │ │ │ ├── main.dart │ │ │ └── splash/ │ │ │ ├── splash.dart │ │ │ └── view/ │ │ │ └── splash_page.dart │ │ ├── packages/ │ │ │ ├── authentication_repository/ │ │ │ │ ├── analysis_options.yaml │ │ │ │ ├── lib/ │ │ │ │ │ ├── authentication_repository.dart │ │ │ │ │ └── src/ │ │ │ │ │ └── authentication_repository.dart │ │ │ │ └── pubspec.yaml │ │ │ └── user_repository/ │ │ │ ├── analysis_options.yaml │ │ │ ├── lib/ │ │ │ │ ├── src/ │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── models.dart │ │ │ │ │ │ └── user.dart │ │ │ │ │ └── user_repository.dart │ │ │ │ └── user_repository.dart │ │ │ └── pubspec.yaml │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ ├── test/ │ │ │ ├── authentication/ │ │ │ │ ├── authentication_bloc_test.dart │ │ │ │ └── authentication_state_test.dart │ │ │ └── login/ │ │ │ ├── bloc/ │ │ │ │ ├── login_bloc_test.dart │ │ │ │ ├── login_event_test.dart │ │ │ │ └── login_state_test.dart │ │ │ ├── models/ │ │ │ │ ├── password_test.dart │ │ │ │ └── username_test.dart │ │ │ └── view/ │ │ │ ├── login_form_test.dart │ │ │ └── login_page_test.dart │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ ├── flutter_shopping_cart/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── ios/ │ │ │ └── .gitignore │ │ ├── lib/ │ │ │ ├── app.dart │ │ │ ├── cart/ │ │ │ │ ├── bloc/ │ │ │ │ │ ├── cart_bloc.dart │ │ │ │ │ ├── cart_event.dart │ │ │ │ │ └── cart_state.dart │ │ │ │ ├── cart.dart │ │ │ │ ├── models/ │ │ │ │ │ ├── cart.dart │ │ │ │ │ └── models.dart │ │ │ │ └── view/ │ │ │ │ └── cart_page.dart │ │ │ ├── catalog/ │ │ │ │ ├── bloc/ │ │ │ │ │ ├── catalog_bloc.dart │ │ │ │ │ ├── catalog_event.dart │ │ │ │ │ └── catalog_state.dart │ │ │ │ ├── catalog.dart │ │ │ │ ├── models/ │ │ │ │ │ ├── catalog.dart │ │ │ │ │ ├── item.dart │ │ │ │ │ └── models.dart │ │ │ │ └── view/ │ │ │ │ └── catalog_page.dart │ │ │ ├── main.dart │ │ │ ├── shopping_repository.dart │ │ │ └── simple_bloc_observer.dart │ │ ├── macos/ │ │ │ ├── .gitignore │ │ │ └── Flutter/ │ │ │ └── GeneratedPluginRegistrant.swift │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ ├── test/ │ │ │ ├── app_test.dart │ │ │ ├── cart/ │ │ │ │ ├── bloc/ │ │ │ │ │ ├── cart_bloc_test.dart │ │ │ │ │ ├── cart_event_test.dart │ │ │ │ │ └── cart_state_test.dart │ │ │ │ ├── models/ │ │ │ │ │ └── cart_test.dart │ │ │ │ └── view/ │ │ │ │ └── cart_page_test.dart │ │ │ ├── catalog/ │ │ │ │ ├── bloc/ │ │ │ │ │ ├── catalog_bloc_test.dart │ │ │ │ │ ├── catalog_event_test.dart │ │ │ │ │ └── catalog_state_test.dart │ │ │ │ ├── models/ │ │ │ │ │ ├── catalog_test.dart │ │ │ │ │ └── item_test.dart │ │ │ │ └── view/ │ │ │ │ └── catalog_page_test.dart │ │ │ ├── helper.dart │ │ │ └── shopping_repository_test.dart │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ ├── flutter_timer/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── ios/ │ │ │ └── .gitignore │ │ ├── lib/ │ │ │ ├── app.dart │ │ │ ├── main.dart │ │ │ ├── ticker.dart │ │ │ └── timer/ │ │ │ ├── bloc/ │ │ │ │ ├── timer_bloc.dart │ │ │ │ ├── timer_event.dart │ │ │ │ └── timer_state.dart │ │ │ ├── timer.dart │ │ │ └── view/ │ │ │ └── timer_page.dart │ │ ├── macos/ │ │ │ ├── .gitignore │ │ │ └── Flutter/ │ │ │ └── GeneratedPluginRegistrant.swift │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ ├── test/ │ │ │ ├── app_test.dart │ │ │ ├── ticker_test.dart │ │ │ └── timer/ │ │ │ ├── bloc/ │ │ │ │ ├── timer_bloc_test.dart │ │ │ │ └── timer_state_test.dart │ │ │ └── view/ │ │ │ └── timer_page_test.dart │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ ├── flutter_todos/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── .vscode/ │ │ │ └── launch.json │ │ ├── LICENSE │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── ios/ │ │ │ ├── .gitignore │ │ │ └── Podfile │ │ ├── l10n.yaml │ │ ├── lib/ │ │ │ ├── app/ │ │ │ │ ├── app.dart │ │ │ │ └── app_bloc_observer.dart │ │ │ ├── bootstrap.dart │ │ │ ├── edit_todo/ │ │ │ │ ├── bloc/ │ │ │ │ │ ├── edit_todo_bloc.dart │ │ │ │ │ ├── edit_todo_event.dart │ │ │ │ │ └── edit_todo_state.dart │ │ │ │ ├── edit_todo.dart │ │ │ │ └── view/ │ │ │ │ ├── edit_todo_page.dart │ │ │ │ └── view.dart │ │ │ ├── home/ │ │ │ │ ├── cubit/ │ │ │ │ │ ├── home_cubit.dart │ │ │ │ │ └── home_state.dart │ │ │ │ ├── home.dart │ │ │ │ └── view/ │ │ │ │ ├── home_page.dart │ │ │ │ └── view.dart │ │ │ ├── l10n/ │ │ │ │ ├── app_en.arb │ │ │ │ ├── app_localizations.dart │ │ │ │ ├── app_localizations_en.dart │ │ │ │ └── l10n.dart │ │ │ ├── main_development.dart │ │ │ ├── main_production.dart │ │ │ ├── main_staging.dart │ │ │ ├── stats/ │ │ │ │ ├── bloc/ │ │ │ │ │ ├── stats_bloc.dart │ │ │ │ │ ├── stats_event.dart │ │ │ │ │ └── stats_state.dart │ │ │ │ ├── stats.dart │ │ │ │ └── view/ │ │ │ │ ├── stats_page.dart │ │ │ │ └── view.dart │ │ │ ├── theme/ │ │ │ │ └── theme.dart │ │ │ └── todos_overview/ │ │ │ ├── bloc/ │ │ │ │ ├── todos_overview_bloc.dart │ │ │ │ ├── todos_overview_event.dart │ │ │ │ └── todos_overview_state.dart │ │ │ ├── models/ │ │ │ │ ├── models.dart │ │ │ │ └── todos_view_filter.dart │ │ │ ├── todos_overview.dart │ │ │ ├── view/ │ │ │ │ ├── todos_overview_page.dart │ │ │ │ └── view.dart │ │ │ └── widgets/ │ │ │ ├── todo_list_tile.dart │ │ │ ├── todos_overview_filter_button.dart │ │ │ ├── todos_overview_options_button.dart │ │ │ └── widgets.dart │ │ ├── packages/ │ │ │ ├── local_storage_todos_api/ │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── analysis_options.yaml │ │ │ │ ├── lib/ │ │ │ │ │ ├── local_storage_todos_api.dart │ │ │ │ │ └── src/ │ │ │ │ │ └── local_storage_todos_api.dart │ │ │ │ ├── pubspec.yaml │ │ │ │ └── test/ │ │ │ │ └── local_storage_todos_api_test.dart │ │ │ ├── todos_api/ │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── analysis_options.yaml │ │ │ │ ├── lib/ │ │ │ │ │ ├── src/ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── json_map.dart │ │ │ │ │ │ │ ├── models.dart │ │ │ │ │ │ │ ├── todo.dart │ │ │ │ │ │ │ └── todo.g.dart │ │ │ │ │ │ └── todos_api.dart │ │ │ │ │ └── todos_api.dart │ │ │ │ ├── pubspec.yaml │ │ │ │ └── test/ │ │ │ │ ├── models/ │ │ │ │ │ └── todo_test.dart │ │ │ │ └── todos_api_test.dart │ │ │ └── todos_repository/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── analysis_options.yaml │ │ │ ├── lib/ │ │ │ │ ├── src/ │ │ │ │ │ └── todos_repository.dart │ │ │ │ └── todos_repository.dart │ │ │ ├── pubspec.yaml │ │ │ └── test/ │ │ │ └── todos_repository_test.dart │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ ├── test/ │ │ │ ├── app_test.dart │ │ │ ├── edit_todo/ │ │ │ │ ├── bloc/ │ │ │ │ │ ├── edit_todo_bloc_test.dart │ │ │ │ │ ├── edit_todo_event_test.dart │ │ │ │ │ └── edit_todo_state_test.dart │ │ │ │ └── view/ │ │ │ │ └── edit_todo_page_test.dart │ │ │ ├── helpers/ │ │ │ │ ├── finders.dart │ │ │ │ ├── helpers.dart │ │ │ │ ├── l10n.dart │ │ │ │ └── pump_app.dart │ │ │ ├── home/ │ │ │ │ ├── cubit/ │ │ │ │ │ └── home_cubit_test.dart │ │ │ │ └── view/ │ │ │ │ └── home_page_test.dart │ │ │ ├── stats/ │ │ │ │ ├── bloc/ │ │ │ │ │ ├── stats_bloc_test.dart │ │ │ │ │ ├── stats_event_test.dart │ │ │ │ │ └── stats_state_test.dart │ │ │ │ └── view/ │ │ │ │ └── stats_page_test.dart │ │ │ └── todos_overview/ │ │ │ ├── bloc/ │ │ │ │ ├── todos_overview_bloc_test.dart │ │ │ │ ├── todos_overview_event_test.dart │ │ │ │ └── todos_overview_state_test.dart │ │ │ ├── models/ │ │ │ │ └── todos_view_filter_test.dart │ │ │ ├── view/ │ │ │ │ └── todos_overview_page_test.dart │ │ │ └── widgets/ │ │ │ ├── todo_list_tile_test.dart │ │ │ ├── todos_overview_filter_button_test.dart │ │ │ └── todos_overview_options_button_test.dart │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ ├── flutter_weather/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── build.yaml │ │ ├── ios/ │ │ │ ├── .gitignore │ │ │ └── Podfile │ │ ├── lib/ │ │ │ ├── app.dart │ │ │ ├── main.dart │ │ │ ├── search/ │ │ │ │ ├── search.dart │ │ │ │ └── view/ │ │ │ │ └── search_page.dart │ │ │ ├── settings/ │ │ │ │ ├── settings.dart │ │ │ │ └── view/ │ │ │ │ └── settings_page.dart │ │ │ ├── weather/ │ │ │ │ ├── cubit/ │ │ │ │ │ ├── weather_cubit.dart │ │ │ │ │ ├── weather_cubit.g.dart │ │ │ │ │ └── weather_state.dart │ │ │ │ ├── models/ │ │ │ │ │ ├── models.dart │ │ │ │ │ ├── weather.dart │ │ │ │ │ └── weather.g.dart │ │ │ │ ├── view/ │ │ │ │ │ └── weather_page.dart │ │ │ │ ├── weather.dart │ │ │ │ └── widgets/ │ │ │ │ ├── weather_empty.dart │ │ │ │ ├── weather_error.dart │ │ │ │ ├── weather_loading.dart │ │ │ │ ├── weather_populated.dart │ │ │ │ └── widgets.dart │ │ │ └── weather_bloc_observer.dart │ │ ├── linux/ │ │ │ ├── .gitignore │ │ │ └── flutter/ │ │ │ ├── generated_plugin_registrant.cc │ │ │ ├── generated_plugin_registrant.h │ │ │ └── generated_plugins.cmake │ │ ├── macos/ │ │ │ ├── .gitignore │ │ │ ├── Flutter/ │ │ │ │ └── GeneratedPluginRegistrant.swift │ │ │ └── Podfile │ │ ├── packages/ │ │ │ ├── open_meteo_api/ │ │ │ │ ├── analysis_options.yaml │ │ │ │ ├── build.yaml │ │ │ │ ├── lib/ │ │ │ │ │ ├── open_meteo_api.dart │ │ │ │ │ └── src/ │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── location.dart │ │ │ │ │ │ ├── location.g.dart │ │ │ │ │ │ ├── models.dart │ │ │ │ │ │ ├── weather.dart │ │ │ │ │ │ └── weather.g.dart │ │ │ │ │ └── open_meteo_api_client.dart │ │ │ │ ├── pubspec.yaml │ │ │ │ └── test/ │ │ │ │ ├── location_test.dart │ │ │ │ ├── open_meteo_api_client_test.dart │ │ │ │ └── weather_test.dart │ │ │ └── weather_repository/ │ │ │ ├── analysis_options.yaml │ │ │ ├── build.yaml │ │ │ ├── lib/ │ │ │ │ ├── src/ │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── models.dart │ │ │ │ │ │ ├── weather.dart │ │ │ │ │ │ └── weather.g.dart │ │ │ │ │ └── weather_repository.dart │ │ │ │ └── weather_repository.dart │ │ │ ├── pubspec.yaml │ │ │ └── test/ │ │ │ ├── src/ │ │ │ │ └── models/ │ │ │ │ └── weather_test.dart │ │ │ └── weather_repository_test.dart │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ ├── test/ │ │ │ ├── app_test.dart │ │ │ ├── helpers/ │ │ │ │ └── hydrated_bloc.dart │ │ │ ├── search/ │ │ │ │ └── view/ │ │ │ │ └── search_page_test.dart │ │ │ ├── settings/ │ │ │ │ └── view/ │ │ │ │ └── settings_page_test.dart │ │ │ └── weather/ │ │ │ ├── cubit/ │ │ │ │ ├── weather_cubit_test.dart │ │ │ │ └── weather_state_test.dart │ │ │ ├── view/ │ │ │ │ └── weather_page_test.dart │ │ │ └── widgets/ │ │ │ ├── weather_empty_test.dart │ │ │ ├── weather_error_test.dart │ │ │ ├── weather_loading_test.dart │ │ │ └── weather_populated_test.dart │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ ├── flutter_wizard/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── ios/ │ │ │ └── .gitignore │ │ ├── lib/ │ │ │ ├── bloc/ │ │ │ │ ├── profile_wizard_bloc.dart │ │ │ │ ├── profile_wizard_event.dart │ │ │ │ └── profile_wizard_state.dart │ │ │ └── main.dart │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ └── github_search/ │ ├── README.md │ ├── angular_github_search/ │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── lib/ │ │ │ ├── app_component.dart │ │ │ └── src/ │ │ │ ├── github_search.dart │ │ │ ├── search_form/ │ │ │ │ ├── search_bar/ │ │ │ │ │ ├── search_bar_component.dart │ │ │ │ │ └── search_bar_component.html │ │ │ │ ├── search_body/ │ │ │ │ │ ├── search_body_component.dart │ │ │ │ │ ├── search_body_component.html │ │ │ │ │ └── search_results/ │ │ │ │ │ ├── search_result_item/ │ │ │ │ │ │ ├── search_result_item_component.dart │ │ │ │ │ │ └── search_result_item_component.html │ │ │ │ │ ├── search_results_component.dart │ │ │ │ │ └── search_results_component.html │ │ │ │ ├── search_form_component.dart │ │ │ │ └── search_form_component.html │ │ │ └── src.dart │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ └── web/ │ │ ├── index.html │ │ ├── main.dart │ │ └── styles.css │ ├── common_github_search/ │ │ ├── analysis_options.yaml │ │ ├── lib/ │ │ │ ├── common_github_search.dart │ │ │ └── src/ │ │ │ ├── github_cache.dart │ │ │ ├── github_client.dart │ │ │ ├── github_repository.dart │ │ │ ├── github_search_bloc/ │ │ │ │ ├── github_search_bloc.dart │ │ │ │ ├── github_search_event.dart │ │ │ │ └── github_search_state.dart │ │ │ └── models/ │ │ │ ├── github_user.dart │ │ │ ├── models.dart │ │ │ ├── search_result.dart │ │ │ ├── search_result_error.dart │ │ │ └── search_result_item.dart │ │ ├── pubspec.yaml │ │ └── pubspec_overrides.yaml │ └── flutter_github_search/ │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── analysis_options.yaml │ ├── ios/ │ │ ├── .gitignore │ │ └── Podfile │ ├── lib/ │ │ ├── main.dart │ │ └── search_form.dart │ ├── pubspec.yaml │ ├── pubspec_overrides.yaml │ └── web/ │ ├── index.html │ └── manifest.json ├── extensions/ │ ├── intellij/ │ │ ├── README.md │ │ └── intellij_generator_plugin/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ ├── gradle.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ ├── settings.gradle │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── bloc/ │ │ │ └── intellij_generator_plugin/ │ │ │ ├── action/ │ │ │ │ ├── BlocTemplateType.java │ │ │ │ ├── GenerateBlocAction.kt │ │ │ │ ├── GenerateBlocDialog.form │ │ │ │ ├── GenerateBlocDialog.java │ │ │ │ ├── GenerateCubitAction.kt │ │ │ │ └── GenerateEquatablePropsAction.kt │ │ │ ├── generator/ │ │ │ │ ├── BlocGenerator.kt │ │ │ │ ├── BlocGeneratorFactory.kt │ │ │ │ ├── CubitGenerator.kt │ │ │ │ ├── CubitGeneratorFactory.kt │ │ │ │ └── components/ │ │ │ │ ├── BlocEventGenerator.kt │ │ │ │ ├── BlocGenerator.kt │ │ │ │ ├── BlocStateGenerator.kt │ │ │ │ ├── CubitGenerator.kt │ │ │ │ └── CubitStateGenerator.kt │ │ │ ├── intention_action/ │ │ │ │ ├── BlocConvertToMultiBlocListenerIntentionAction.kt │ │ │ │ ├── BlocConvertToMultiBlocProviderIntentionAction.kt │ │ │ │ ├── BlocConvertToMultiIntentionAction.kt │ │ │ │ ├── BlocConvertToMultiRepositoryProviderIntentionAction.kt │ │ │ │ ├── BlocWrapWithBlocBuilderIntentionAction.kt │ │ │ │ ├── BlocWrapWithBlocConsumerIntentionAction.kt │ │ │ │ ├── BlocWrapWithBlocListenerIntentionAction.kt │ │ │ │ ├── BlocWrapWithBlocProviderIntentionAction.kt │ │ │ │ ├── BlocWrapWithBlocSelectorIntentionAction.kt │ │ │ │ ├── BlocWrapWithIntentionAction.kt │ │ │ │ ├── BlocWrapWithRepositoryProviderIntentionAction.kt │ │ │ │ ├── Common.kt │ │ │ │ ├── SnippetType.kt │ │ │ │ ├── Snippets.kt │ │ │ │ └── WrapHelper.kt │ │ │ ├── language_server/ │ │ │ │ ├── BlocLanguageServer.kt │ │ │ │ ├── BlocLanguageServerFactory.kt │ │ │ │ └── BlocLanguageServerInstaller.kt │ │ │ ├── live_templates/ │ │ │ │ └── BlocContext.kt │ │ │ └── util/ │ │ │ └── BlocPluginNotification.kt │ │ └── resources/ │ │ ├── META-INF/ │ │ │ └── plugin.xml │ │ ├── intentionDescriptions/ │ │ │ ├── BlocConvertToMultiBlocListenerIntentionAction/ │ │ │ │ ├── after.java.template │ │ │ │ ├── before.java.template │ │ │ │ └── description.html │ │ │ ├── BlocConvertToMultiBlocProviderIntentionAction/ │ │ │ │ ├── after.java.template │ │ │ │ ├── before.java.template │ │ │ │ └── description.html │ │ │ ├── BlocConvertToMultiRepositoryProviderIntentionAction/ │ │ │ │ ├── after.java.template │ │ │ │ ├── before.java.template │ │ │ │ └── description.html │ │ │ ├── BlocWrapWithBlocBuilderIntentionAction/ │ │ │ │ ├── after.java.template │ │ │ │ ├── before.java.template │ │ │ │ └── description.html │ │ │ ├── BlocWrapWithBlocConsumerIntentionAction/ │ │ │ │ ├── after.java.template │ │ │ │ ├── before.java.template │ │ │ │ └── description.html │ │ │ ├── BlocWrapWithBlocListenerIntentionAction/ │ │ │ │ ├── after.java.template │ │ │ │ ├── before.java.template │ │ │ │ └── description.html │ │ │ ├── BlocWrapWithBlocProviderIntentionAction/ │ │ │ │ ├── after.java.template │ │ │ │ ├── before.java.template │ │ │ │ └── description.html │ │ │ ├── BlocWrapWithBlocSelectorIntentionAction/ │ │ │ │ ├── after.java.template │ │ │ │ ├── before.java.template │ │ │ │ └── description.html │ │ │ └── BlocWrapWithRepositoryProviderIntentionAction/ │ │ │ ├── after.java.template │ │ │ ├── before.java.template │ │ │ └── description.html │ │ ├── liveTemplates/ │ │ │ └── Bloc.xml │ │ └── templates/ │ │ ├── bloc_basic/ │ │ │ ├── bloc.dart.template │ │ │ ├── bloc_event.dart.template │ │ │ └── bloc_state.dart.template │ │ ├── bloc_equatable/ │ │ │ ├── bloc.dart.template │ │ │ ├── bloc_event.dart.template │ │ │ └── bloc_state.dart.template │ │ ├── bloc_freezed/ │ │ │ ├── bloc.dart.template │ │ │ ├── bloc_event.dart.template │ │ │ └── bloc_state.dart.template │ │ ├── cubit_basic/ │ │ │ ├── cubit.dart.template │ │ │ └── cubit_state.dart.template │ │ ├── cubit_equatable/ │ │ │ ├── cubit.dart.template │ │ │ └── cubit_state.dart.template │ │ └── cubit_freezed/ │ │ ├── cubit.dart.template │ │ └── cubit_state.dart.template │ ├── vscode/ │ │ ├── .gitignore │ │ ├── .vscode/ │ │ │ └── launch.json │ │ ├── .vscodeignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── snippets/ │ │ │ ├── bloc.json │ │ │ ├── bloc_test.json │ │ │ ├── flutter_bloc.json │ │ │ └── freezed_bloc.json │ │ ├── src/ │ │ │ ├── code-actions/ │ │ │ │ ├── bloc-code-action-provider.ts │ │ │ │ └── index.ts │ │ │ ├── commands/ │ │ │ │ ├── convert-to.command.ts │ │ │ │ ├── index.ts │ │ │ │ ├── new-bloc.command.ts │ │ │ │ ├── new-cubit.command.ts │ │ │ │ └── wrap-with.command.ts │ │ │ ├── extension.ts │ │ │ ├── language-server/ │ │ │ │ ├── index.ts │ │ │ │ ├── language-server.ts │ │ │ │ └── selectors.ts │ │ │ ├── templates/ │ │ │ │ ├── bloc-event.template.ts │ │ │ │ ├── bloc-state.template.ts │ │ │ │ ├── bloc.template.ts │ │ │ │ ├── cubit-state.template.ts │ │ │ │ ├── cubit.template.ts │ │ │ │ └── index.ts │ │ │ └── utils/ │ │ │ ├── analyze-dependencies.ts │ │ │ ├── convert-to.ts │ │ │ ├── downloader.ts │ │ │ ├── exec.ts │ │ │ ├── get-bloc-tools-executable.ts │ │ │ ├── get-bloc-type.ts │ │ │ ├── get-dart-version.ts │ │ │ ├── get-latest-package-version.ts │ │ │ ├── get-pubspec-path.ts │ │ │ ├── get-pubspec.ts │ │ │ ├── get-selected-text.ts │ │ │ ├── get-template-setting.ts │ │ │ ├── has-dependency.ts │ │ │ ├── index.ts │ │ │ ├── install-bloc-tools.ts │ │ │ ├── retry.ts │ │ │ ├── set-show-context-menu.ts │ │ │ ├── update-pubspec-dependency.ts │ │ │ └── wrap-with.ts │ │ ├── tsconfig.json │ │ ├── tslint.json │ │ └── webpack.config.js │ └── zed/ │ ├── .gitignore │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ ├── extension.toml │ ├── extension.wasm │ ├── snippets/ │ │ └── dart.json │ └── src/ │ └── lib.rs └── packages/ ├── angular_bloc/ │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── analysis_options.yaml │ ├── dart_test.yaml │ ├── example/ │ │ └── example.dart │ ├── lib/ │ │ ├── angular_bloc.dart │ │ └── src/ │ │ └── pipes/ │ │ ├── bloc_pipe.dart │ │ └── pipes.dart │ ├── pubspec.yaml │ ├── pubspec_overrides.yaml │ └── test/ │ └── bloc_pipe_test.dart ├── bloc/ │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── analysis_options.yaml │ ├── example/ │ │ └── main.dart │ ├── lib/ │ │ ├── bloc.dart │ │ └── src/ │ │ ├── bloc.dart │ │ ├── bloc_base.dart │ │ ├── bloc_observer.dart │ │ ├── change.dart │ │ ├── cubit.dart │ │ ├── emitter.dart │ │ └── transition.dart │ ├── pubspec.yaml │ └── test/ │ ├── bloc_event_transformer_test.dart │ ├── bloc_event_transformer_test_legacy.dart │ ├── bloc_observer_test.dart │ ├── bloc_on_test.dart │ ├── bloc_test.dart │ ├── blocs/ │ │ ├── async/ │ │ │ ├── async_bloc.dart │ │ │ ├── async_event.dart │ │ │ └── async_state.dart │ │ ├── blocs.dart │ │ ├── complex/ │ │ │ ├── complex_bloc.dart │ │ │ ├── complex_event.dart │ │ │ └── complex_state.dart │ │ ├── counter/ │ │ │ ├── counter.dart │ │ │ ├── counter_bloc.dart │ │ │ ├── counter_error_bloc.dart │ │ │ ├── counter_exception_bloc.dart │ │ │ ├── merge_bloc.dart │ │ │ ├── on_error_bloc.dart │ │ │ ├── on_event_error_bloc.dart │ │ │ ├── on_exception_bloc.dart │ │ │ └── on_transition_error_bloc.dart │ │ ├── seeded/ │ │ │ └── seeded_bloc.dart │ │ ├── simple/ │ │ │ └── simple_bloc.dart │ │ ├── stream/ │ │ │ ├── restartable_stream_bloc.dart │ │ │ ├── stream.dart │ │ │ └── stream_bloc.dart │ │ └── unawaited/ │ │ └── unawaited_bloc.dart │ ├── cubit_test.dart │ ├── cubits/ │ │ ├── counter_cubit.dart │ │ ├── cubits.dart │ │ ├── fake_async_cubit.dart │ │ └── seeded_cubit.dart │ └── transition_test.dart ├── bloc_concurrency/ │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── analysis_options.yaml │ ├── example/ │ │ └── main.dart │ ├── lib/ │ │ ├── bloc_concurrency.dart │ │ └── src/ │ │ ├── concurrent.dart │ │ ├── droppable.dart │ │ ├── restartable.dart │ │ └── sequential.dart │ ├── pubspec.yaml │ ├── pubspec_overrides.yaml │ └── test/ │ └── src/ │ ├── concurrent_test.dart │ ├── droppable_test.dart │ ├── helpers.dart │ ├── restartable_test.dart │ └── sequential_test.dart ├── bloc_lint/ │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── analysis_options.yaml │ ├── build.yaml │ ├── example/ │ │ └── main.dart │ ├── lib/ │ │ ├── all.yaml │ │ ├── bloc_lint.dart │ │ ├── recommended.yaml │ │ └── src/ │ │ ├── analysis_options.dart │ │ ├── analysis_options.g.dart │ │ ├── diagnostic.dart │ │ ├── env.dart │ │ ├── lint_rule.dart │ │ ├── linter.dart │ │ ├── rules/ │ │ │ ├── avoid_build_context_extensions.dart │ │ │ ├── avoid_flutter_imports.dart │ │ │ ├── avoid_public_bloc_methods.dart │ │ │ ├── avoid_public_fields.dart │ │ │ ├── prefer_bloc.dart │ │ │ ├── prefer_build_context_extensions.dart │ │ │ ├── prefer_cubit.dart │ │ │ ├── prefer_file_naming_conventions.dart │ │ │ ├── prefer_void_public_cubit_methods.dart │ │ │ └── rules.dart │ │ ├── string_case.dart │ │ └── text_document.dart │ ├── pubspec.yaml │ └── test/ │ └── src/ │ ├── analysis_options_test.dart │ ├── diagnostic_test.dart │ ├── lint_test_helper.dart │ ├── linter_test.dart │ ├── rules/ │ │ ├── avoid_build_context_extensions_test.dart │ │ ├── avoid_flutter_imports_test.dart │ │ ├── avoid_public_bloc_methods_test.dart │ │ ├── avoid_public_fields_test.dart │ │ ├── prefer_bloc_test.dart │ │ ├── prefer_build_context_extensions_test.dart │ │ ├── prefer_cubit_test.dart │ │ ├── prefer_file_naming_conventions_test.dart │ │ └── prefer_void_public_cubit_methods_test.dart │ └── text_document_test.dart ├── bloc_test/ │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── analysis_options.yaml │ ├── example/ │ │ └── main.dart │ ├── lib/ │ │ ├── bloc_test.dart │ │ └── src/ │ │ ├── bloc_test.dart │ │ ├── mock_bloc.dart │ │ └── when_listen.dart │ ├── pubspec.yaml │ ├── pubspec_overrides.yaml │ └── test/ │ ├── bloc_bloc_test_test.dart │ ├── bloc_observer_test.dart │ ├── blocs/ │ │ ├── async_counter_bloc.dart │ │ ├── blocs.dart │ │ ├── complex_bloc.dart │ │ ├── counter_bloc.dart │ │ ├── debounce_counter_bloc.dart │ │ ├── error_counter_bloc.dart │ │ ├── exception_counter_bloc.dart │ │ ├── instant_emit_bloc.dart │ │ ├── multi_counter_bloc.dart │ │ ├── side_effect_counter_bloc.dart │ │ └── sum_bloc.dart │ ├── cubit_bloc_test_test.dart │ ├── cubits/ │ │ ├── async_counter_cubit.dart │ │ ├── complex_cubit.dart │ │ ├── counter_cubit.dart │ │ ├── cubits.dart │ │ ├── delayed_counter_cubit.dart │ │ ├── error_cubit.dart │ │ ├── exception_cubit.dart │ │ ├── instant_emit_cubit.dart │ │ ├── multi_counter_cubit.dart │ │ ├── side_effect_counter_cubit.dart │ │ └── sum_cubit.dart │ ├── mock_bloc_test.dart │ └── when_listen_test.dart ├── bloc_tools/ │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── analysis_options.yaml │ ├── bin/ │ │ └── bloc.dart │ ├── dart_test.yaml │ ├── e2e/ │ │ ├── main.dart │ │ └── pubspec.yaml │ ├── example/ │ │ └── README.md │ ├── lib/ │ │ ├── bloc_tools.dart │ │ └── src/ │ │ ├── command_runner.dart │ │ ├── commands/ │ │ │ ├── commands.dart │ │ │ ├── language_server/ │ │ │ │ └── language_server_command.dart │ │ │ ├── lint/ │ │ │ │ └── lint_command.dart │ │ │ └── new/ │ │ │ ├── bundles/ │ │ │ │ ├── bloc_bundle.dart │ │ │ │ ├── bundles.dart │ │ │ │ ├── cubit_bundle.dart │ │ │ │ ├── hydrated_bloc_bundle.dart │ │ │ │ ├── hydrated_cubit_bundle.dart │ │ │ │ ├── replay_bloc_bundle.dart │ │ │ │ └── replay_cubit_bundle.dart │ │ │ └── new_command.dart │ │ ├── lsp/ │ │ │ ├── language_server.dart │ │ │ ├── text_document.dart │ │ │ └── text_documents.dart │ │ └── version.dart │ ├── pubspec.yaml │ ├── pubspec_overrides.yaml │ └── test/ │ ├── ensure_build_test.dart │ └── src/ │ ├── command_runner_test.dart │ ├── commands/ │ │ ├── language_server/ │ │ │ └── language_server_command_test.dart │ │ ├── lint/ │ │ │ └── lint_command_test.dart │ │ └── new/ │ │ └── new_test.dart │ └── lsp/ │ ├── language_server_test.dart │ └── text_document_test.dart ├── flutter_bloc/ │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── analysis_options.yaml │ ├── example/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── lib/ │ │ │ └── main.dart │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ ├── lib/ │ │ ├── flutter_bloc.dart │ │ └── src/ │ │ ├── bloc_builder.dart │ │ ├── bloc_consumer.dart │ │ ├── bloc_listener.dart │ │ ├── bloc_provider.dart │ │ ├── bloc_selector.dart │ │ ├── multi_bloc_listener.dart │ │ ├── multi_bloc_provider.dart │ │ ├── multi_repository_provider.dart │ │ └── repository_provider.dart │ ├── pubspec.yaml │ ├── pubspec_overrides.yaml │ └── test/ │ ├── bloc_builder_test.dart │ ├── bloc_consumer_test.dart │ ├── bloc_listener_test.dart │ ├── bloc_provider_test.dart │ ├── bloc_selector_test.dart │ ├── multi_bloc_listener_test.dart │ ├── multi_bloc_provider_test.dart │ ├── multi_repository_provider_test.dart │ └── repository_provider_test.dart ├── hydrated_bloc/ │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── analysis_options.yaml │ ├── benchmark/ │ │ └── README.md │ ├── build.yaml │ ├── example/ │ │ ├── .gitignore │ │ ├── .metadata │ │ ├── README.md │ │ ├── analysis_options.yaml │ │ ├── lib/ │ │ │ └── main.dart │ │ ├── pubspec.yaml │ │ ├── pubspec_overrides.yaml │ │ └── web/ │ │ ├── index.html │ │ └── manifest.json │ ├── lib/ │ │ ├── hydrated_bloc.dart │ │ └── src/ │ │ ├── _migration/ │ │ │ ├── _migration_io.dart │ │ │ └── _migration_stub.dart │ │ ├── hydrated_bloc.dart │ │ ├── hydrated_cipher.dart │ │ └── hydrated_storage.dart │ ├── pubspec.yaml │ ├── pubspec_overrides.yaml │ └── test/ │ ├── cubits/ │ │ ├── bad_cubit.dart │ │ ├── cubits.dart │ │ ├── cyclic_cubit.dart │ │ ├── freezed_cubit.dart │ │ ├── freezed_cubit.freezed.dart │ │ ├── freezed_cubit.g.dart │ │ ├── from_json_state_cubit.dart │ │ ├── json_serializable_cubit.dart │ │ ├── json_serializable_cubit.g.dart │ │ ├── list_cubit.dart │ │ ├── manual_cubit.dart │ │ ├── season_palette_cubit.dart │ │ └── simple_cubit.dart │ ├── e2e_test.dart │ ├── hive_interference_test.dart │ ├── hydrated_aes_cipher_test.dart │ ├── hydrated_bloc_test.dart │ ├── hydrated_cubit_test.dart │ ├── hydrated_cyclic_error_test.dart │ ├── hydrated_mixin_test.dart │ └── hydrated_storage_test.dart └── replay_bloc/ ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example/ │ ├── .gitignore │ ├── .metadata │ ├── README.md │ ├── analysis_options.yaml │ ├── lib/ │ │ └── main.dart │ ├── pubspec.yaml │ ├── pubspec_overrides.yaml │ └── web/ │ ├── index.html │ └── manifest.json ├── lib/ │ ├── replay_bloc.dart │ └── src/ │ ├── change_stack.dart │ ├── replay_bloc.dart │ └── replay_cubit.dart ├── pubspec.yaml ├── pubspec_overrides.yaml └── test/ ├── blocs/ │ └── counter_bloc.dart ├── cubits/ │ └── counter_cubit.dart ├── main.dart ├── replay_bloc_test.dart └── replay_cubit_test.dart ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODEOWNERS ================================================ # Every request must be reviewed and accepted by: * @felangel ================================================ FILE: .github/FUNDING.yml ================================================ github: [felangel] patreon: felangel open_collective: bloc ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug Report about: Create a report to help us improve title: "fix: " labels: bug --- **Description** A clear and concise description of what the bug is. **Steps To Reproduce** 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected Behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Additional Context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/build.md ================================================ --- name: Build System about: Changes that affect the build system or external dependencies title: "build: " labels: build --- **Description** Describe what changes need to be done to the build system and why ================================================ FILE: .github/ISSUE_TEMPLATE/chore.md ================================================ --- name: Chore about: Other changes that don't modify src or test files title: "chore: " labels: chore --- **Description** Clearly describe what change is needed and why. If this changes code then please use another issue type. ================================================ FILE: .github/ISSUE_TEMPLATE/ci.md ================================================ --- name: Continuous Integration about: Changes to the CI configuration files and scripts title: "ci: " labels: ci --- **Description** Describe what changes need to be done to the ci/cd system and why ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false ================================================ FILE: .github/ISSUE_TEMPLATE/documentation.md ================================================ --- name: Documentation about: Improve the documentation so all collaborators have a common understanding title: "docs: " labels: documentation --- **Description** Clearly describe what documentation you are looking to add or improve. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature Request about: A new feature to be added to the project title: "feat: " labels: feature --- **Description** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Desired Solution** A clear and concise description of what you want to happen. **Alternatives Considered** A clear and concise description of any alternative solutions or features you've considered. **Additional Context** Add any other context or screenshots about the feature request go here. ================================================ FILE: .github/ISSUE_TEMPLATE/performance.md ================================================ --- name: Performance Update about: A code change that improves performance title: "perf: " labels: performance --- **Description** Clearly describe what code needs to be changed and what the performance impact is going to be. ================================================ FILE: .github/ISSUE_TEMPLATE/refactor.md ================================================ --- name: Refactor about: A code change that neither fixes a bug nor adds a feature title: "refactor: " labels: refactor --- **Description** Clearly describe what needs to be refactored any why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize. ================================================ FILE: .github/ISSUE_TEMPLATE/revert.md ================================================ --- name: Revert Commit about: Reverts a previous commit title: "revert: " labels: revert --- **Description** Provide a link to a PR/Commit that you are looking to revert and why. ================================================ FILE: .github/ISSUE_TEMPLATE/style.md ================================================ --- name: Style Changes about: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) title: "style: " labels: style --- **Description** Clearly describe what you are looking to change and why. ================================================ FILE: .github/ISSUE_TEMPLATE/test.md ================================================ --- name: Test about: Adding missing tests or correcting existing tests title: "test: " labels: test --- **Description** List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## Status **READY/IN DEVELOPMENT/HOLD** ## Breaking Changes YES | NO ## Description ## Type of Change - [ ] ✨ New feature (non-breaking change which adds functionality) - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) - [ ] 🧹 Code refactor - [ ] ✅ Build configuration change - [ ] 📝 Documentation - [ ] 🗑️ Chore ================================================ FILE: .github/actions/angular_dart_package/action.yaml ================================================ name: Angular Dart Package Workflow description: Build and test Angular Dart packages. inputs: dart_sdk: required: false default: "stable" description: "The dart sdk version to use" working_directory: required: false default: "." description: The working directory for this workflow analyze_directories: required: false default: "lib test" description: Directories to analyze runs: using: "composite" steps: - name: 🎯 Setup Dart uses: dart-lang/setup-dart@v1 with: sdk: ${{inputs.dart_sdk}} - name: 📦 Install Dependencies working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: dart pub get - name: 🛠️ Build working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: | dart pub global activate webdev webdev build - name: ✨ Format working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: dart format --set-exit-if-changed . - name: 🔍 Analyze working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: dart analyze --fatal-warnings ${{inputs.analyze_directories}} - name: 🧪 Test working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: | if [ -d "test" ]; then dart run build_runner test --fail-on-severe fi ================================================ FILE: .github/actions/astro_site/action.yaml ================================================ name: Astro Site Workflow description: Build and test Astro sites. inputs: working_directory: required: false default: "." description: The working directory for this workflow node_version: required: false default: "20" description: The node version to use. runs: using: "composite" steps: - name: ⚙️ Setup Node uses: actions/setup-node@v4 with: node-version: ${{ inputs.node_version }} - name: ⬇️ Install Dependencies run: npm install shell: ${{ inputs.shell }} working-directory: ${{ inputs.working_directory }} - name: ✨ Check Format run: npm run format:check shell: ${{ inputs.shell }} working-directory: ${{ inputs.working_directory }} - name: 📦 Build Site uses: withastro/action@v2 with: path: ${{ inputs.working_directory }} node-version: ${{ inputs.node_version }} ================================================ FILE: .github/actions/dart_compile/action.yaml ================================================ name: Dart Compile Workflow description: Compile Dart Executables inputs: dart_sdk: required: false default: "stable" description: "The dart sdk version to use" working_directory: required: false default: "." description: The working directory for this workflow entrypoint: required: true description: The path to the Dart entrypoint name: required: true description: The name of the executable os: required: true description: The operating system to compile for arch: required: true description: The architecture to compile for runs: using: "composite" steps: - name: 🎯 Setup Dart uses: dart-lang/setup-dart@v1 with: sdk: ${{inputs.dart_sdk}} - name: 📦 Install Dependencies working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: dart pub get - name: ⚙️ Compile (${{ inputs.os }}, ${{ inputs.arch }}) working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: | mkdir ${{ runner.temp }}/executable dart compile exe --target-os ${{ inputs.os }} --target-arch ${{ inputs.arch }} ${{ inputs.entrypoint }} -o ${{ runner.temp }}/executable/${{ inputs.name }} - name: ⬆️ Upload Artifact uses: actions/upload-artifact@v4 with: name: ${{ inputs.name }} path: ${{ runner.temp }}/executable/${{ inputs.name }} ================================================ FILE: .github/actions/dart_package/action.yaml ================================================ name: Dart Package Workflow description: Build and test Dart packages. inputs: codecov_token: required: true description: The Codecov token used to upload coverage collect_coverage: required: false default: "true" description: Whether to collect code coverage collect_score: required: false default: "true" description: Whether to collect the pana score concurrency: required: false default: "4" description: The value of the concurrency flag (-j) used when running tests coverage_excludes: required: false default: "" description: Globs to exclude from coverage dart_sdk: required: false default: "stable" description: "The dart sdk version to use" working_directory: required: false default: "." description: The working directory for this workflow min_coverage: required: false default: "100" description: The minimum coverage percentage value min_score: required: false default: "120" description: The minimum pana score value analyze_directories: required: false default: "lib test" description: Directories to analyze report_on: required: false default: "lib" description: Directories to report on when collecting coverage runs: using: "composite" steps: - name: 🎯 Setup Dart uses: dart-lang/setup-dart@v1 with: sdk: ${{inputs.dart_sdk}} - name: ⚙️ Setup Bloc Tools run: dart pub global activate --source path ./packages/bloc_tools shell: ${{ inputs.shell }} - name: 📦 Install Dependencies working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: dart pub get - name: ✨ Format working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: dart format --set-exit-if-changed . - name: 🔍 Analyze working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: dart analyze --fatal-warnings ${{inputs.analyze_directories}} - name: 🔍 Bloc Lint working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: bloc lint ${{inputs.analyze_directories}} - name: 🧪 Test working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: | dart pub global activate coverage dart test -j ${{inputs.concurrency}} --coverage=coverage && dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.dart_tool/package_config.json --report-on=${{inputs.report_on}} --check-ignore - name: 📦 Detect Package Name if: inputs.collect_coverage == 'true' env: PACKAGE_PATH: ${{ inputs.working_directory}} id: package shell: ${{ inputs.shell }} run: echo "name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT - name: ⬆️ Upload Coverage if: inputs.collect_coverage == 'true' uses: codecov/codecov-action@v4 env: PACKAGE_PATH: ${{ inputs.working_directory}} with: flags: ${{ steps.package.outputs.name }} token: ${{ inputs.codecov_token }} - name: 📊 Verify Coverage if: inputs.collect_coverage == 'true' uses: VeryGoodOpenSource/very_good_coverage@v3 with: path: ${{inputs.working_directory}}/coverage/lcov.info exclude: ${{inputs.coverage_excludes}} min_coverage: ${{inputs.min_coverage}} - name: 💯 Verify Pub Score if: inputs.collect_score == 'true' working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: | dart pub global activate pana sudo apt-get install webp PANA=$(pana . --no-warning); PANA_SCORE=$(echo $PANA | sed -n "s/.*Points: \([0-9]*\)\/\([0-9]*\)./\1\/\2/p") echo $PANA IFS='/'; read -a SCORE_ARR <<< "$PANA_SCORE"; SCORE=SCORE_ARR[0]; TOTAL=SCORE_ARR[1] if [ -z "$1" ]; then MINIMUM_SCORE=TOTAL; else MINIMUM_SCORE=$1; fi if (( $SCORE < $MINIMUM_SCORE )); then echo "minimum score $MINIMUM_SCORE was not met!"; exit 1; fi ================================================ FILE: .github/actions/flutter_package/action.yaml ================================================ name: Flutter Package Workflow description: Build and test a Flutter package. inputs: codecov_token: required: true description: The Codecov token used to upload coverage collect_coverage: required: false default: "true" description: Whether to collect code coverage collect_score: required: false default: "true" description: Whether to collect the pana score concurrency: required: false default: "4" description: The value of the concurrency flag (-j) used when running tests coverage_excludes: required: false default: "" description: Globs to exclude from coverage flutter_channel: required: false default: "stable" description: The Flutter channel to use working_directory: required: false default: "." description: The working directory for this workflow min_coverage: required: false default: "100" description: The minimum coverage percentage value analyze_directories: required: false default: "lib test" description: Directories to analyze report_on: required: false default: "lib" description: Directories to report on when collecting coverage platform: required: false default: "vm" description: Platform to use when running tests runs: using: "composite" steps: - name: 🐦 Setup Flutter uses: subosito/flutter-action@v2 with: channel: ${{ inputs.flutter_channel }} - name: 📦 Install Dependencies working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: flutter pub get - name: ⚙️ Setup Bloc Tools run: dart pub global activate --source path ./packages/bloc_tools shell: ${{ inputs.shell }} - name: ✨ Format working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: dart format --set-exit-if-changed . - name: 🔍 Analyze working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: dart analyze --fatal-warnings ${{inputs.analyze_directories}} - name: 🔍 Bloc Lint working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: bloc lint ${{inputs.analyze_directories}} - name: 🧪 Test working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: | if [ -d "test" ]; then flutter test --no-pub --test-randomize-ordering-seed random --coverage fi - name: Exclude Generated Code from Coverage if: ${{ inputs.collect_coverage == 'true' && inputs.coverage_excludes != '' }} working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: | mv coverage/lcov.info coverage/lcov.info.bak sudo apt-get -y install lcov lcov --remove coverage/lcov.info.bak "${{inputs.coverage_excludes}}" -o coverage/lcov.info - name: 📦 Detect Package Name if: inputs.collect_coverage == 'true' env: PACKAGE_PATH: ${{ inputs.working_directory}} id: package shell: ${{ inputs.shell }} run: echo "name=${PACKAGE_PATH##*/}" >> $GITHUB_OUTPUT - name: ⬆️ Upload Coverage if: inputs.collect_coverage == 'true' uses: codecov/codecov-action@v4 env: PACKAGE_PATH: ${{ inputs.working_directory}} with: flags: ${{ steps.package.outputs.name }} token: ${{ inputs.codecov_token }} - name: 📊 Verify Coverage if: inputs.collect_coverage == 'true' uses: VeryGoodOpenSource/very_good_coverage@v3 with: path: ${{inputs.working_directory}}/coverage/lcov.info exclude: ${{inputs.coverage_excludes}} min_coverage: ${{inputs.min_coverage}} - name: 💯 Verify Pub Score if: inputs.collect_score == 'true' working-directory: ${{ inputs.working_directory }} shell: ${{ inputs.shell }} run: | dart pub global activate pana sudo apt-get install webp PANA=$(pana . --no-warning); PANA_SCORE=$(echo $PANA | sed -n "s/.*Points: \([0-9]*\)\/\([0-9]*\)./\1\/\2/p") echo $PANA IFS='/'; read -a SCORE_ARR <<< "$PANA_SCORE"; SCORE=SCORE_ARR[0]; TOTAL=SCORE_ARR[1] if [ -z "$1" ]; then MINIMUM_SCORE=TOTAL; else MINIMUM_SCORE=$1; fi if (( $SCORE < $MINIMUM_SCORE )); then echo "minimum score $MINIMUM_SCORE was not met!"; exit 1; fi ================================================ FILE: .github/codecov.yml ================================================ flag_management: default_rules: carryforward: true ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" groups: github_actions: patterns: - "*" ================================================ FILE: .github/workflows/main.yaml ================================================ name: build concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true on: pull_request: push: branches: [master] permissions: contents: read pages: write id-token: write jobs: semantic_pull_request: name: ✅ Semantic Pull Request uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 changes: runs-on: ubuntu-latest if: github.event.pull_request.draft == false outputs: needs_angular_dart_example_checks: ${{ steps.needs_angular_dart_example_checks.outputs.changes }} needs_bloc_tools_e2e_checks: ${{ steps.needs_bloc_tools_e2e_checks.outputs.changes }} needs_dart_package_checks: ${{ steps.needs_dart_package_checks.outputs.changes }} needs_bloc_tools_compile_checks: ${{ steps.needs_bloc_tools_compile_checks.outputs.changes }} needs_flutter_package_checks: ${{ steps.needs_flutter_package_checks.outputs.changes }} needs_flutter_example_checks: ${{ steps.needs_flutter_example_checks.outputs.changes }} needs_docs_checks: ${{ steps.needs_docs_checks.outputs.changes }} name: 👀 Detect Changes steps: - name: 📚 Git Checkout uses: actions/checkout@v6 - uses: dorny/paths-filter@v4 name: Angular Dart Package Detection id: needs_angular_dart_example_checks with: filters: | angular_counter: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/angular_dart_package/action.yaml - examples/angular_counter/** github_search/angular_github_search: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/dart_package/action.yaml - examples/github_search/angular_github_search/** - uses: dorny/paths-filter@v4 name: Bloc Tools E2E Detection id: needs_bloc_tools_e2e_checks with: filters: | bloc_tools: - ./.github/workflows/main.yaml - packages/bloc_tools/** - uses: dorny/paths-filter@v4 name: Dart Package Detection id: needs_dart_package_checks with: filters: | angular_bloc: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/dart_package/action.yaml - packages/angular_bloc/** bloc_concurrency: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/dart_package/action.yaml - packages/bloc_concurrency/** bloc_lint: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/dart_package/action.yaml - packages/bloc_lint/** bloc_test: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/dart_package/action.yaml - packages/bloc_test/** bloc_tools: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/dart_package/action.yaml - packages/bloc_tools/** bloc: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/dart_package/action.yaml - packages/bloc/** - uses: dorny/paths-filter@v4 name: Bloc Tools Compile Detection id: needs_bloc_tools_compile_checks with: filters: | bloc_tools: - ./.github/actions/dart_compile/action.yaml - ./.github/workflows/main.yaml - packages/bloc_tools/** - uses: dorny/paths-filter@v4 name: Flutter Package Detection id: needs_flutter_package_checks with: filters: | flutter_bloc: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - packages/flutter_bloc/** replay_bloc: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - packages/replay_bloc/** hydrated_bloc: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - packages/hydrated_bloc/** - uses: dorny/paths-filter@v4 name: Example Detection id: needs_flutter_example_checks with: filters: | bloc_concurrency_visualizer: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - examples/bloc_concurrency_visualizer/** flutter_bloc_with_stream: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - examples/flutter_bloc_with_stream/** flutter_complex_list: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - examples/flutter_complex_list/** flutter_counter: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - examples/flutter_counter/** flutter_dynamic_form: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - examples/flutter_dynamic_form/** flutter_firebase_login: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - examples/flutter_firebase_login/** flutter_form_validation: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - examples/flutter_form_validation/** flutter_infinite_list: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - examples/flutter_infinite_list/** flutter_login: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - examples/flutter_login/** flutter_shopping_cart: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - examples/flutter_shopping_cart/** flutter_timer: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - examples/flutter_timer/** flutter_todos: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - examples/flutter_todos/** flutter_weather: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - examples/flutter_weather/** flutter_wizard: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - examples/flutter_wizard/** github_search/flutter_github_search: - ./.github/codecov.yml - ./.github/workflows/main.yaml - ./.github/actions/flutter_package/action.yaml - examples/github_search/flutter_github_search/** - uses: dorny/paths-filter@v4 name: Docs Detection id: needs_docs_checks with: filters: | - ./.github/workflows/main.yaml - ./.github/actions/astro_site/action.yaml - examples/** - docs/** bloc_tools_e2e_checks: needs: changes if: ${{ needs.changes.outputs.needs_bloc_tools_e2e_checks != '[]' }} strategy: fail-fast: false matrix: os: ["ubuntu-latest", "macos-latest", "windows-latest"] package: ${{ fromJSON(needs.changes.outputs.needs_bloc_tools_e2e_checks) }} runs-on: ${{ matrix.os }} name: 🧪 ${{ matrix.package }} (${{ matrix.os }}) steps: - name: 📚 Git Checkout uses: actions/checkout@v6 - name: 🐦 Setup Flutter uses: subosito/flutter-action@v2 with: cache: true - name: ⚙️ Setup Bloc Tools run: dart pub global activate --source path ./packages/bloc_tools - name: 📦 Install Dependencies working-directory: packages/${{ matrix.package }}/e2e run: flutter pub get - name: 🧪 E2E working-directory: packages/${{ matrix.package }}/e2e run: dart run main.dart dart_package_checks: needs: changes if: ${{ needs.changes.outputs.needs_dart_package_checks != '[]' }} strategy: fail-fast: false matrix: package: ${{ fromJSON(needs.changes.outputs.needs_dart_package_checks) }} runs-on: ubuntu-latest name: 🎯 ${{ matrix.package }} steps: - name: 📚 Git Checkout uses: actions/checkout@v6 - name: 🎯 Build ${{ matrix.package }} uses: ./.github/actions/dart_package with: codecov_token: ${{ secrets.CODECOV_TOKEN }} working_directory: packages/${{ matrix.package }} min_coverage: 100 bloc_tools_compile_checks: needs: changes if: ${{ needs.changes.outputs.needs_bloc_tools_compile_checks != '[]' }} strategy: fail-fast: false matrix: config: [ { "runs-on": "ubuntu-latest", "os": "linux", "arch": "x64" }, { "runs-on": "ubuntu-latest", "os": "linux", "arch": "arm64" }, { "runs-on": "macos-15-intel", "os": "macos", "arch": "x64" }, { "runs-on": "macos-latest", "os": "macos", "arch": "arm64" }, { "runs-on": "windows-latest", "os": "windows", "arch": "x64" }, ] runs-on: ${{ matrix.config.runs-on }} name: ⚙️ Compile Bloc Tools steps: - name: 📚 Git Checkout uses: actions/checkout@v6 - name: ⚙️ Compile Bloc Tools for ${{ matrix.config.os }} ${{ matrix.config.arch }} uses: ./.github/actions/dart_compile with: entrypoint: bin/bloc.dart working_directory: packages/bloc_tools name: bloc_${{ matrix.config.os }}_${{ matrix.config.arch }} os: ${{ matrix.config.os }} arch: ${{ matrix.config.arch }} flutter_package_checks: needs: changes if: ${{ needs.changes.outputs.needs_flutter_package_checks != '[]' }} strategy: fail-fast: false matrix: package: ${{ fromJSON(needs.changes.outputs.needs_flutter_package_checks) }} runs-on: ubuntu-latest name: 🐦 ${{ matrix.package }} steps: - name: 📚 Git Checkout uses: actions/checkout@v6 - name: 🎯 Build ${{ matrix.package }} uses: ./.github/actions/flutter_package with: codecov_token: ${{ secrets.CODECOV_TOKEN }} working_directory: packages/${{ matrix.package }} min_coverage: 100 angular_dart_example_checks: needs: changes if: ${{ needs.changes.outputs.needs_angular_dart_example_checks != '[]' }} strategy: fail-fast: false matrix: example: ${{ fromJSON(needs.changes.outputs.needs_angular_dart_example_checks) }} runs-on: ubuntu-latest name: 🛡️ ${{ matrix.example }} steps: - name: 📚 Git Checkout uses: actions/checkout@v6 - name: 🎯 Build ${{ matrix.example }} uses: ./.github/actions/angular_dart_package with: analyze_directories: lib working_directory: examples/${{ matrix.example }} flutter_example_checks: needs: changes if: ${{ needs.changes.outputs.needs_flutter_example_checks != '[]' }} strategy: fail-fast: false matrix: example: ${{ fromJSON(needs.changes.outputs.needs_flutter_example_checks) }} runs-on: ubuntu-latest name: 🧑‍🎓 ${{ matrix.example }} steps: - name: 📚 Git Checkout uses: actions/checkout@v6 - name: 🎯 Build ${{ matrix.example }} uses: ./.github/actions/flutter_package with: analyze_directories: lib collect_coverage: false collect_score: false flutter_channel: master working_directory: examples/${{ matrix.example }} docs_checks: needs: changes if: ${{ needs.changes.outputs.needs_docs_checks != '[]' }} runs-on: ubuntu-latest name: 📖 docs steps: - name: 📚 Git Checkout uses: actions/checkout@v6 - name: 📦 Build Docs uses: ./.github/actions/astro_site with: working_directory: ./docs deploy: needs: docs_checks if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: 🚀 Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 build: needs: [ angular_dart_example_checks, bloc_tools_compile_checks, bloc_tools_e2e_checks, dart_package_checks, docs_checks, flutter_example_checks, flutter_package_checks, semantic_pull_request, ] if: ${{ always() }} runs-on: ubuntu-latest steps: - name: ⛔️ exit(1) on failure if: ${{ contains(join(needs.*.result, ','), 'failure') }} run: exit 1 ================================================ FILE: .gitignore ================================================ # Miscellaneous *.class *.lock *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # Visual Studio Code related .classpath .project .settings/ .vscode/* # Flutter repo-specific /bin/cache/ /bin/mingit/ /dev/benchmarks/mega_gallery/ /dev/bots/.recipe_deps /dev/bots/android_tools/ /dev/docs/doc/ /dev/docs/flutter.docs.zip /dev/docs/lib/ /dev/docs/pubspec.yaml /dev/integration_tests/**/xcuserdata /dev/integration_tests/**/Pods /packages/flutter/coverage/ version # packages file containing multi-root paths .packages.generated # Flutter/Dart/Pub related **/doc/api/ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ build/ linked_*.ds unlinked.ds unlinked_spec.ds # Android related **/android/**/gradle-wrapper.jar **/android/.gradle **/android/captures/ **/android/gradlew **/android/gradlew.bat **/android/local.properties **/android/**/GeneratedPluginRegistrant.java **/android/key.properties *.jks # iOS/XCode related **/ios/**/*.mode1v3 **/ios/**/*.mode2v3 **/ios/**/*.moved-aside **/ios/**/*.pbxuser **/ios/**/*.perspectivev3 **/ios/**/*sync/ **/ios/**/.sconsign.dblite **/ios/**/.tags* **/ios/**/.vagrant/ **/ios/**/DerivedData/ **/ios/**/Icon? **/ios/**/Pods/ **/ios/**/.symlinks/ **/ios/**/profile **/ios/**/xcuserdata **/ios/.generated/ **/ios/Flutter/App.framework **/ios/Flutter/Flutter.framework **/ios/Flutter/Flutter.podspec **/ios/Flutter/Generated.xcconfig **/ios/Flutter/app.flx **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ **/ios/Flutter/flutter_export_environment.sh **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* # Coverage coverage/ coverage_badge.svg .test_coverage.dart *.lcov nohup.out # Mason mason-lock.json .mason # Exceptions to above rules. !.vscode/launch.json !**/ios/**/default.mode1v3 !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages !/dev/ci/**/Gemfile.lock ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at felangelov@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Bloc 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 The following is a set of guidelines for contributing to Bloc and its packages. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. ## Proposing a Change If you intend to change the public API, or make any non-trivial changes to the implementation, we recommend filing an issue. This lets us reach an agreement on your proposal before you put significant effort into it. If you’re only fixing a bug, it’s fine to submit a pull request right away but we still recommend to file an issue detailing what you’re fixing. This is helpful in case we don’t accept that specific fix but want to keep track of the issue. ## Creating a Pull Request Before creating a pull request please: 1. Fork the repository and create your branch from `master`. 1. Install all dependencies (`flutter pub get`). 1. Squash your commits and ensure you have a meaningful commit message. 1. If you’ve fixed a bug or added code that should be tested, add tests! Pull Requests without 100% test coverage will not be approved. 1. Ensure the test suite passes. 1. If you've changed the public API, make sure to update/add documentation. 1. Format your code (`dart format .`). 1. Analyze your code (`dart analyze --fatal-infos --fatal-warnings .`). 1. Create the Pull Request. 1. Verify that all status checks are passing. While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. ## Contributing to Documentation If you would like to contribute to the [documentation](https://bloclibrary.dev) please follow the same process for "Creating a Pull Request" and double check that your changes look good by running the docs locally. ```sh # Change directories into docs cd ./docs # Install dependencies npm install # Start the dev server npm start # Navigate to http://localhost:4321 in your browser ``` If you wish to add translations, ensure the locale is included in the [locales list](https://github.com/felangel/bloc/blob/8a714a6923a6480032319b45f461d1f9ccd025de/docs/astro.config.mjs#L7) and create the translated `mdx` file in the corresponding subdirectory within `docs/src/content/docs/`. Refer to [this pull request](https://github.com/felangel/bloc/pull/4084) for an example. ## Adding an example Examples live in the `examples` folder. When you're adding an example, make sure to add CI checks for it in [main.yaml](https://github.com/felangel/bloc/blob/master/.github/workflows/main.yaml): - For a Flutter example, add it to the `folder` list in the `examples-flutter` step. - For a web example, add it to the `folder` list in the `examples-web` step. - For a pure Dart example, add it to the `folder` list in the `examples-pure` step. ## Getting in Touch If you want to just ask a question or get feedback on an idea you can post it on [Discord](https://discord.gg/bloc). ## License By contributing to Bloc, you agree that your contributions will be licensed under its [MIT license](LICENSE). ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2026 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

Bloc

build codecov Star on Github style: bloc lint Flutter Website Awesome Flutter Flutter Samples License: MIT Discord Bloc Library

--- A predictable state management library that helps implement the BLoC design pattern. | Package | Pub | | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | --- ## Sponsors Our top sponsors are shown below! [[Become a Sponsor](https://github.com/sponsors/felangel)]
--- ## Overview Bloc Architecture The goal of this library is to make it easy to separate _presentation_ from _business logic_, facilitating testability and reusability. ## Documentation - [Official Documentation](https://bloclibrary.dev) - [Angular Bloc Package](https://github.com/felangel/bloc/tree/master/packages/angular_bloc/README.md) - [Bloc Package](https://github.com/felangel/bloc/tree/master/packages/bloc/README.md) - [Bloc Concurrency Package](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency/README.md) - [Bloc Lint Package](https://github.com/felangel/bloc/tree/master/packages/bloc_lint/README.md) - [Bloc Test Package](https://github.com/felangel/bloc/tree/master/packages/bloc_test/README.md) - [Bloc Tools Package](https://github.com/felangel/bloc/tree/master/packages/bloc_tools/README.md) - [Flutter Bloc Package](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc/README.md) - [Hydrated Bloc Package](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc/README.md) - [Replay Bloc Package](https://github.com/felangel/bloc/tree/master/packages/replay_bloc/README.md) ## Migration - [Migration Guide](https://bloclibrary.dev/migration) ## Examples
### Dart - [Counter](https://github.com/felangel/bloc/tree/master/packages/bloc/example) - an example of how to create a `CounterBloc` (pure dart). ### Flutter - [Counter](https://bloclibrary.dev/tutorials/flutter-counter) - an example of how to create a `CounterBloc` to implement the classic Flutter Counter app. - [Form Validation](https://github.com/felangel/bloc/tree/master/examples/flutter_form_validation) - an example of how to use the `bloc` and `flutter_bloc` packages to implement form validation. - [Bloc with Stream](https://github.com/felangel/bloc/tree/master/examples/flutter_bloc_with_stream) - an example of how to hook up a `bloc` to a `Stream` and update the UI in response to data from the `Stream`. - [Complex List](https://github.com/felangel/bloc/tree/master/examples/flutter_complex_list) - an example of how to manage a list of items and asynchronously delete items one at a time using `bloc` and `flutter_bloc`. - [Infinite List](https://bloclibrary.dev/tutorials/flutter-infinite-list) - an example of how to use the `bloc` and `flutter_bloc` packages to implement an infinite scrolling list. - [Login Flow](https://bloclibrary.dev/tutorials/flutter-login) - an example of how to use the `bloc` and `flutter_bloc` packages to implement a Login Flow. - [Firebase Login](https://bloclibrary.dev/tutorials/flutter-firebase-login) - an example of how to use the `bloc` and `flutter_bloc` packages to implement login via Firebase. - [Github Search](https://bloclibrary.dev/tutorials/github-search) - an example of how to create a Github Search Application using the `bloc` and `flutter_bloc` packages. - [Weather](https://bloclibrary.dev/tutorials/flutter-weather) - an example of how to create a Weather Application using the `bloc` and `flutter_bloc` packages. The app uses a `RefreshIndicator` to implement "pull-to-refresh" as well as dynamic theming. - [Todos](https://bloclibrary.dev/tutorials/flutter-todos) - an example of how to create a Todos Application using the `bloc` and `flutter_bloc` packages. - [Timer](https://bloclibrary.dev/tutorials/flutter-timer) - an example of how to create a Timer using the `bloc` and `flutter_bloc` packages. - [Shopping Cart](https://github.com/felangel/bloc/tree/master/examples/flutter_shopping_cart) - an example of how to create a Shopping Cart Application using the `bloc` and `flutter_bloc` packages based on [flutter samples](https://github.com/flutter/samples/tree/master/provider_shopper). - [Dynamic Form](https://github.com/felangel/bloc/tree/master/examples/flutter_dynamic_form) - an example of how to use the `bloc` and `flutter_bloc` packages to implement a dynamic form which pulls data from a repository. - [Wizard](https://github.com/felangel/bloc/tree/master/examples/flutter_wizard) - an example of how to build a multi-step wizard using the `bloc` and `flutter_bloc` packages. - [Bloc Concurrency Visualizer](https://github.com/felangel/bloc/tree/master/examples/bloc_concurrency_visualizer) - an example of visualizing the various `bloc_concurrency` transformers. - [Fluttersaurus](https://github.com/felangel/fluttersaurus) - an example of how to use the `bloc` and `flutter_bloc` packages to create a thesaurus app -- made for Bytconf Flutter 2020. - [I/O Photo Booth](https://github.com/flutter/photobooth) - an example of how to use the `bloc` and `flutter_bloc` packages to create a virtual photo booth web app -- made for Google I/O 2021. - [I/O Pinball](https://github.com/flutter/pinball) - an example of how to use the `bloc` and `flutter_bloc` packages to create a pinball web app -- made for Google I/O 2022. - [I/O Holobooth](https://github.com/flutter/holobooth) - an example of how to use the `bloc` and `flutter_bloc` packages to create a virtual photobooth experience -- made for Flutter Forward. - [I/O Flip](https://github.com/flutter/io_flip) - an example of how to use the `bloc`, `flutter_bloc`, and `flame_bloc` packages to create a card game -- made for Google I/O 2023. ### Web - [Counter](https://github.com/felangel/Bloc/tree/master/examples/angular_counter) - an example of how to use a `CounterBloc` in an AngularDart app. - [Github Search](https://github.com/felangel/Bloc/tree/master/examples/github_search/angular_github_search) - an example of how to create a Github Search Application using the `bloc` and `angular_bloc` packages. ### Flutter + Web - [Github Search](https://github.com/felangel/Bloc/tree/master/examples/github_search) - an example of how to create a Github Search Application and share code between Flutter and AngularDart. ## Articles - [Bloc package](https://medium.com/flutter-community/flutter-bloc-package-295b53e95c5c) - An intro to the bloc package with high level architecture and examples. - [Login tutorial with flutter_bloc](https://medium.com/flutter-community/flutter-login-tutorial-with-flutter-bloc-ea606ef701ad) - How to create a full login flow using the bloc and flutter_bloc packages. - [Unit testing with bloc](https://medium.com/@felangelov/unit-testing-with-bloc-b94de9655d86) - How to unit test the blocs created in the flutter login tutorial. - [Infinite list tutorial with flutter_bloc](https://medium.com/flutter-community/flutter-infinite-list-tutorial-with-flutter-bloc-2fc7a272ec67) - How to create an infinite list using the bloc and flutter_bloc packages. - [Code sharing with bloc](https://medium.com/flutter-community/code-sharing-with-bloc-b867302c18ef) - How to share code between a mobile application written with Flutter and a web application written with AngularDart. - [Weather app tutorial with flutter_bloc](https://medium.com/flutter-community/weather-app-with-flutter-bloc-e24a7253340d) - How to build a weather app which supports dynamic theming, pull-to-refresh, and interacting with a REST API using the bloc and flutter_bloc packages. - [Todos app tutorial with flutter_bloc](https://medium.com/flutter-community/flutter-todos-tutorial-with-flutter-bloc-d9dd833f9df3) - How to build a todos app using the bloc and flutter_bloc packages. - [Firebase login tutorial with flutter_bloc](https://medium.com/flutter-community/firebase-login-with-flutter-bloc-47455e6047b0) - How to create a fully functional login/sign up flow using the bloc and flutter_bloc packages with Firebase Authentication and Google Sign In. - [Flutter timer tutorial with flutter_bloc](https://medium.com/flutter-community/flutter-timer-with-flutter-bloc-a464e8332ceb) - How to create a timer app using the bloc and flutter_bloc packages. - [Firestore todos tutorial with flutter_bloc](https://medium.com/flutter-community/firestore-todos-with-flutter-bloc-7b2d5fadcc80) - How to create a todos app using the bloc and flutter_bloc packages that integrates with cloud firestore. ## Books - [Flutter Complete Reference](https://fluttercompletereference.com/) - A book about the Dart programming language (version 2.10, with null safety support) and the Flutter framework (version 1.20). It covers the bloc package (version 6.0.3) in all flavors: bloc, flutter_bloc hydrated_bloc, replay_bloc, bloc_test and cubit. ## Extensions - [IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc-code-generator) - extends IntelliJ/Android Studio with support for the Bloc library and provides tools for effectively creating Blocs for both Flutter and AngularDart apps. - [VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc#overview) - extends VSCode with support for the Bloc library and provides tools for effectively creating Blocs for both Flutter and AngularDart apps. ## Community Learn more at the following links, which have been contributed by the community. ### Packages - [Bloc.js](https://github.com/felangel/bloc.js) - A port of the `bloc` state management library from Dart to JavaScript, by [Felix Angelov](https://github.com/felangel). - [Firebase Auth](https://pub.dev/packages/fb_auth) - A Web, Mobile Firebase Auth Plugin, by [Rody Davis](https://github.com/AppleEducate). - [Form Bloc](https://pub.dev/packages/form_bloc) - An easy way to create forms with BLoC pattern without writing a lot of boilerplate code, by [Giancarlo](https://github.com/GiancarloCode). - [Flame Bloc](https://pub.dev/packages/flame_bloc) - Bloc integration for the Flame game engine, by [Flame Engine](https://github.com/flame-engine). ### Video Tutorials - [Bloc Library: Basics and Beyond 🚀](https://youtu.be/knMvKPKBzGE) - Talk given at [Flutter Europe](https://fluttereurope.dev) about the basics of the bloc library, by [Felix Angelov](https://github.com/felangel). - [Flutter Bloc Library Tutorial](https://www.youtube.com/watch?v=hTExlt1nJZI) - Introduction to the Bloc Library, by [Reso Coder](https://resocoder.com). - [Flutter Youtube Search](https://www.youtube.com/watch?v=BJY8nuYUM7M) - How to build a Youtube Search app which interacts with an API using the bloc and flutter_bloc packages, by [Reso Coder](https://resocoder.com). - [Flutter Bloc - AUTOMATIC LOOKUP - v0.20 (and Up), Updated Tutorial](https://www.youtube.com/watch?v=_vOpPuVfmiU) - Updated Tutorial on the Flutter Bloc Package, by [Reso Coder](https://resocoder.com). - [Dynamic Theming with flutter_bloc](https://www.youtube.com/watch?v=YYbhkg-W8Mg) - Tutorial on how to use the flutter_bloc package to implement dynamic theming, by [Reso Coder](https://resocoder.com). - [Persist Bloc State in Flutter](https://www.youtube.com/watch?v=vSOpZd_FFEY) - Tutorial on how to use the hydrated_bloc package to automatically persist app state, by [Reso Coder](https://resocoder.com). - [State Management Foundation](https://www.youtube.com/watch?v=S2KmxzgsTwk&t=731s) - Introduction to state management using the flutter_bloc package, by [Techie Blossom](https://techieblossom.com). - [Flutter Football Player Search](https://www.youtube.com/watch?v=S2KmxzgsTwk) - How to build a Football Player Search app which interacts with an API using the bloc and flutter_bloc packages, by [Techie Blossom](https://techieblossom.com). - [Learning the Flutter Bloc Package](https://www.youtube.com/watch?v=eAiCPl3yk9A&t=1s) - Learning the flutter_bloc package live, by [Robert Brunhage](https://www.youtube.com/channel/UCSLIg5O0JiYO1i2nD4RclaQ) - [Bloc Test Tutorial](https://www.youtube.com/watch?v=S6jFBiiP0Mc) - Tutorial on how to unit test blocs using the bloc_test package, by [Reso Coder](https://resocoder.com). - [Bloc - from Zero to Hero](https://www.youtube.com/playlist?list=PLptHs0ZDJKt_T-oNj_6Q98v-tBnVf-S_o) - Playlist which includes everything needed to get started with bloc, by [Flutterly](https://www.youtube.com/channel/UC5PYcSe3to4mtm3SPCUmjvw). - [Bloc (Full Course, 11+ Hours) - Flutter State Management Course](https://youtu.be/Mn254cnduOY) - 11+ hour video tutorial on Bloc and Flutter Bloc. In this video you will learn how to create fully fledged production-ready apps with Bloc and Firebase as your backend, by [Vandad Nahavandipoor](https://www.youtube.com/channel/UC8NpGP0AOQ0kX9ZRcohiPeQ). ### Written Resources - [DevonFw Flutter Guide](https://github.com/devonfw-forge/devonfw4flutter) - A guide on building structured & scalable applications with Flutter and BLoC, by [Sebastian Faust](https://github.com/Fasust) - [Using Google´s Flutter Framework for the Development of a Large-Scale Reference Application](https://epb.bibl.th-koeln.de/frontdoor/index/index/docId/1498) - Scientific paper describing how to build [a large-scale Flutter application](https://github.com/devonfw-forge/devonfw4flutter-mts-app) with BLoC, by [Sebastian Faust](https://github.com/Fasust) ### Extensions - [Feature Scaffolding for VSCode](https://marketplace.visualstudio.com/items?itemName=KiritchoukC.flutter-clean-architecture) - A VSCode extension inspired by [Reso Coder's](https://resocoder.com) clean architecture tutorials, which helps quickly scaffold features, by [Kiritchouk Clément](https://github.com/KiritchoukC). ## Maintainers - [Felix Angelov](https://github.com/felangel) ================================================ FILE: analysis_options.yaml ================================================ analyzer: exclude: - bricks/** language: strict-casts: true strict-inference: true strict-raw-types: true errors: close_sinks: ignore missing_required_param: error missing_return: error record_literal_one_positional_no_trailing_comma: error formatter: page_width: 80 trailing_commas: preserve linter: rules: - always_declare_return_types - always_put_required_named_parameters_first - always_use_package_imports - annotate_overrides - annotate_redeclares - avoid_bool_literals_in_conditional_expressions - avoid_catching_errors - avoid_double_and_int_checks - avoid_dynamic_calls - avoid_empty_else - avoid_equals_and_hash_code_on_mutable_classes - avoid_escaping_inner_quotes - avoid_field_initializers_in_const_classes - avoid_final_parameters - avoid_function_literals_in_foreach_calls - avoid_init_to_null - avoid_js_rounded_ints - avoid_multiple_declarations_per_line - avoid_null_checks_in_equality_operators - avoid_positional_boolean_parameters - avoid_print - avoid_private_typedef_functions - avoid_redundant_argument_values - avoid_relative_lib_imports - avoid_renaming_method_parameters - avoid_return_types_on_setters - avoid_returning_null_for_void - avoid_returning_this - avoid_setters_without_getters - avoid_shadowing_type_parameters - avoid_single_cascade_in_expression_statements - avoid_slow_async_io - avoid_type_to_string - avoid_types_as_parameter_names - avoid_unnecessary_containers - avoid_unused_constructor_parameters - avoid_void_async - avoid_web_libraries_in_flutter - await_only_futures - camel_case_extensions - camel_case_types - cancel_subscriptions - cascade_invocations - cast_nullable_to_non_nullable - collection_methods_unrelated_type - combinators_ordering - comment_references - conditional_uri_does_not_exist - constant_identifier_names - control_flow_in_finally - curly_braces_in_flow_control_structures - dangling_library_doc_comments - depend_on_referenced_packages - deprecated_consistency - deprecated_member_use_from_same_package - directives_ordering - do_not_use_environment - empty_catches - empty_constructor_bodies - empty_statements - eol_at_end_of_file - exhaustive_cases - file_names - flutter_style_todos - hash_and_equals - implementation_imports - implicit_call_tearoffs - implicit_reopen - invalid_case_patterns - join_return_with_assignment - leading_newlines_in_multiline_strings - library_annotations - library_names - library_prefixes - library_private_types_in_public_api - lines_longer_than_80_chars - literal_only_boolean_expressions - matching_super_parameters - missing_code_block_language_in_doc_comment - missing_whitespace_between_adjacent_strings - no_adjacent_strings_in_list - no_default_cases - no_duplicate_case_values - no_leading_underscores_for_library_prefixes - no_leading_underscores_for_local_identifiers - no_literal_bool_comparisons - no_logic_in_create_state - no_runtimeType_toString - no_self_assignments - no_wildcard_variable_uses - non_constant_identifier_names - noop_primitive_operations - null_check_on_nullable_type_parameter - null_closures - omit_local_variable_types - omit_obvious_local_variable_types - one_member_abstracts - only_throw_errors - overridden_fields - package_api_docs - package_names - package_prefixed_library_names - parameter_assignments - prefer_adjacent_string_concatenation - prefer_asserts_in_initializer_lists - prefer_asserts_with_message - prefer_collection_literals - prefer_conditional_assignment - prefer_const_constructors - prefer_const_constructors_in_immutables - prefer_const_declarations - prefer_const_literals_to_create_immutables - prefer_constructors_over_static_methods - prefer_contains - prefer_final_fields - prefer_final_in_for_each - prefer_final_locals - prefer_for_elements_to_map_fromIterable - prefer_foreach - prefer_function_declarations_over_variables - prefer_generic_function_type_aliases - prefer_if_elements_to_conditional_expressions - prefer_if_null_operators - prefer_initializing_formals - prefer_inlined_adds - prefer_int_literals - prefer_interpolation_to_compose_strings - prefer_is_empty - prefer_is_not_empty - prefer_is_not_operator - prefer_iterable_whereType - prefer_mixin - prefer_null_aware_method_calls - prefer_null_aware_operators - prefer_single_quotes - prefer_spread_collections - prefer_typing_uninitialized_variables - prefer_void_to_null - provide_deprecation_message - public_member_api_docs - recursive_getters - require_trailing_commas - secure_pubspec_urls - sized_box_for_whitespace - sized_box_shrink_expand - slash_for_doc_comments - sort_child_properties_last - sort_constructors_first - sort_pub_dependencies - sort_unnamed_constructors_first - strict_top_level_inference - switch_on_type - test_types_in_equals - throw_in_finally - tighten_type_of_initializing_formals - type_annotate_public_apis - type_init_formals - type_literal_in_constant_pattern - unawaited_futures - unnecessary_await_in_return - unnecessary_brace_in_string_interps - unnecessary_breaks - unnecessary_const - unnecessary_constructor_name - unnecessary_getters_setters - unnecessary_ignore - unnecessary_lambdas - unnecessary_late - unnecessary_library_directive - unnecessary_new - unnecessary_null_aware_assignments - unnecessary_null_aware_operator_on_extension_on_nullable - unnecessary_null_checks - unnecessary_null_in_if_null_operators - unnecessary_nullable_for_final_variable_declarations - unnecessary_overrides - unnecessary_parenthesis - unnecessary_raw_strings - unnecessary_statements - unnecessary_string_escapes - unnecessary_string_interpolations - unnecessary_this - unnecessary_to_list_in_spreads - unnecessary_unawaited - unnecessary_underscores - unreachable_from_main - unrelated_type_equality_checks - unsafe_html - use_build_context_synchronously - use_colored_box - use_decorated_box - use_enums - use_full_hex_values_for_flutter_colors - use_function_type_syntax_for_parameters - use_if_null_to_convert_nulls_to_bools - use_is_even_rather_than_modulo - use_key_in_widget_constructors - use_late_for_private_fields_and_variables - use_named_constants - use_null_aware_elements - use_raw_strings - use_rethrow_when_possible - use_setters_to_change_properties - use_string_buffers - use_string_in_part_of_directives - use_super_parameters - use_test_throws_matchers - use_to_and_as_if_applicable - use_truncating_division - valid_regexps - void_checks ================================================ FILE: bricks/README.md ================================================ # Bloc Bricks 🧱 [Mason](https://github.com/felangel/mason) support for the [Bloc Library](https://bloclibrary.dev). A collection of bricks for effectively working with the bloc state management library. ## Installation Ensure you have the [mason_cli](https://github.com/felangel/mason/tree/master/packages/mason_cli) installed. Bricks can be installed from [brickhub.dev](https://brickhub.dev). ```sh mason add ``` ## Bricks | Brick | Description | | ---------------------- | ---------------------------------------- | | `bloc` | Generate a new Bloc | | `cubit` | Generate a new Cubit | | `hydrated_bloc` | Generate a new HydratedBloc | | `hydrated_cubit` | Generate a new HydratedCubit | | `replay_bloc` | Generate a new ReplayBloc | | `replay_cubit` | Generate a new ReplayCubit | | `flutter_bloc_feature` | Generate a new Flutter feature with bloc | ================================================ FILE: bricks/bloc/CHANGELOG.md ================================================ # 0.4.0 - chore(deps): upgrade to `mason ^0.1.0` - chore(deps): upgrade hooks to `dart ^3.5.4` # 0.3.1 - chore: update copyright year - chore: update logo image refs # 0.3.0 - feat: add support for `sealed` events using Dart 3 # 0.2.0 - feat: add support for `equatable` - feat: add support for `freezed` # 0.1.3+2 - docs: use dark logo variant # 0.1.3+1 - docs: adjust logo size in README # 0.1.3 - docs: add badges to README # 0.1.2 - docs: minor README update # 0.1.1 - refactor: upgrade to shorthand lambda syntax (`mason >= 0.1.0-dev.15`) # 0.1.0 - feat: initial release with support for basic bloc generation ================================================ FILE: bricks/bloc/LICENSE ================================================ MIT License Copyright (c) 2026 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: bricks/bloc/README.md ================================================

Bloc

build codecov Star on Github License: MIT Discord Bloc Library Powered by Mason

Generate a new Bloc in [Dart][1]. Built for the [bloc state management library][2]. ## Usage 🚀 ```sh mason make bloc --name counter --style basic ``` ## Variables ✨ | Variable | Description | Default | Type | | -------- | --------------------------- | ----------------------------------- | -------- | | `name` | The name of the bloc class | `counter` | `string` | | `style` | The style of bloc generated | `basic (basic, equatable, freezed)` | `enum` | ## Output 📦 ```sh ├── counter_bloc.dart ├── counter_event.dart └── counter_state.dart ``` [1]: https://dart.dev [2]: https://github.com/felangel/bloc ================================================ FILE: bricks/bloc/__brick__/{{name.snakeCase()}}_bloc.dart ================================================ {{#use_freezed}}{{> freezed_bloc }}{{/use_freezed}}{{#use_equatable}}{{> equatable_bloc }}{{/use_equatable}}{{#use_basic}}{{> basic_bloc }}{{/use_basic}} ================================================ FILE: bricks/bloc/__brick__/{{name.snakeCase()}}_event.dart ================================================ {{#use_freezed}}{{> freezed_event }}{{/use_freezed}}{{#use_equatable}}{{> equatable_event }}{{/use_equatable}}{{#use_basic}}{{> basic_event }}{{/use_basic}} ================================================ FILE: bricks/bloc/__brick__/{{name.snakeCase()}}_state.dart ================================================ {{#use_freezed}}{{> freezed_state }}{{/use_freezed}}{{#use_equatable}}{{> equatable_state }}{{/use_equatable}}{{#use_basic}}{{> basic_state }}{{/use_basic}} ================================================ FILE: bricks/bloc/__brick__/{{~ basic_bloc }} ================================================ import 'package:bloc/bloc.dart'; part '{{name.snakeCase()}}_event.dart'; part '{{name.snakeCase()}}_state.dart'; class {{name.pascalCase()}}Bloc extends Bloc<{{name.pascalCase()}}Event, {{name.pascalCase()}}State> { {{name.pascalCase()}}Bloc() : super(const {{name.pascalCase()}}State()) { on<{{name.pascalCase()}}Event>((event, emit) { // TODO: implement event handler }); } } ================================================ FILE: bricks/bloc/__brick__/{{~ basic_event }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; sealed class {{name.pascalCase()}}Event { const {{name.pascalCase()}}Event(); } ================================================ FILE: bricks/bloc/__brick__/{{~ basic_state }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; class {{name.pascalCase()}}State { const {{name.pascalCase()}}State(); } ================================================ FILE: bricks/bloc/__brick__/{{~ equatable_bloc }} ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; part '{{name.snakeCase()}}_event.dart'; part '{{name.snakeCase()}}_state.dart'; class {{name.pascalCase()}}Bloc extends Bloc<{{name.pascalCase()}}Event, {{name.pascalCase()}}State> { {{name.pascalCase()}}Bloc() : super(const {{name.pascalCase()}}State()) { on<{{name.pascalCase()}}Event>((event, emit) { // TODO: implement event handler }); } } ================================================ FILE: bricks/bloc/__brick__/{{~ equatable_event }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; sealed class {{name.pascalCase()}}Event extends Equatable { const {{name.pascalCase()}}Event(); @override List get props => []; } ================================================ FILE: bricks/bloc/__brick__/{{~ equatable_state }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; class {{name.pascalCase()}}State extends Equatable { const {{name.pascalCase()}}State(); @override List get props => []; } ================================================ FILE: bricks/bloc/__brick__/{{~ freezed_bloc }} ================================================ import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part '{{name.snakeCase()}}_event.dart'; part '{{name.snakeCase()}}_state.dart'; part '{{name.snakeCase()}}_bloc.freezed.dart'; class {{name.pascalCase()}}Bloc extends Bloc<{{name.pascalCase()}}Event, {{name.pascalCase()}}State> { {{name.pascalCase()}}Bloc() : super(const {{name.pascalCase()}}State.initial()) { on<{{name.pascalCase()}}Event>((event, emit) { // TODO: implement event handler }); } } ================================================ FILE: bricks/bloc/__brick__/{{~ freezed_event }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; @freezed class {{name.pascalCase()}}Event with _${{name.pascalCase()}}Event { const factory {{name.pascalCase()}}Event.started() = _Started; } ================================================ FILE: bricks/bloc/__brick__/{{~ freezed_state }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; @freezed class {{name.pascalCase()}}State with _${{name.pascalCase()}}State { const factory {{name.pascalCase()}}State.initial() = _Initial; } ================================================ FILE: bricks/bloc/brick.yaml ================================================ name: bloc description: Generate a new Bloc in Dart. Built for the bloc state management library. version: 0.4.0 repository: https://github.com/felangel/bloc/tree/master/bricks/bloc environment: mason: ^0.1.0 vars: name: type: string description: The name of the bloc class. default: counter prompt: What is the bloc name? style: type: enum description: The style of bloc generated. default: basic prompt: What is the bloc style? values: - basic - equatable - freezed ================================================ FILE: bricks/bloc/hooks/pre_gen.dart ================================================ import 'package:mason/mason.dart'; Future run(HookContext context) async { final style = context.vars['style']; context.vars = { ...context.vars, 'use_basic': style == 'basic', 'use_equatable': style == 'equatable', 'use_freezed': style == 'freezed', }; } ================================================ FILE: bricks/bloc/hooks/pubspec.yaml ================================================ name: bloc_hooks environment: sdk: ^3.10.0 dependencies: mason: ^0.1.0 ================================================ FILE: bricks/cubit/CHANGELOG.md ================================================ # 0.3.0 - chore(deps): upgrade to `mason ^0.1.0` - chore(deps): upgrade hooks to `dart ^3.5.4` # 0.2.1 - chore: update copyright year - chore: update logo image refs # 0.2.0 - feat: add support for `equatable` - feat: add support for `freezed` # 0.1.3 - docs: add badges to README - docs: use dark logo variant # 0.1.2 - docs: minor README update # 0.1.1 - refactor: upgrade to shorthand lambda syntax (`mason >= 0.1.0-dev.15`) # 0.1.0 - feat: initial release with support for basic cubit generation ================================================ FILE: bricks/cubit/LICENSE ================================================ MIT License Copyright (c) 2026 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: bricks/cubit/README.md ================================================

Cubit

build codecov Star on Github License: MIT Discord Bloc Library Powered by Mason

Generate a new Cubit in [Dart][1]. Built for the [bloc state management library][2]. ## Usage 🚀 ```sh mason make cubit --name counter --style basic ``` ## Variables ✨ | Variable | Description | Default | Type | | -------- | ---------------------------- | ----------------------------------- | -------- | | `name` | The name of the cubit class | `counter` | `string` | | `style` | The style of cubit generated | `basic (basic, equatable, freezed)` | `enum` | ## Output 📦 ```sh ├── counter_cubit.dart └── counter_state.dart ``` [1]: https://dart.dev [2]: https://github.com/felangel/bloc ================================================ FILE: bricks/cubit/__brick__/{{name.snakeCase()}}_cubit.dart ================================================ {{#use_freezed}}{{> freezed_cubit }}{{/use_freezed}}{{#use_equatable}}{{> equatable_cubit }}{{/use_equatable}}{{#use_basic}}{{> basic_cubit }}{{/use_basic}} ================================================ FILE: bricks/cubit/__brick__/{{name.snakeCase()}}_state.dart ================================================ {{#use_freezed}}{{> freezed_state }}{{/use_freezed}}{{#use_equatable}}{{> equatable_state }}{{/use_equatable}}{{#use_basic}}{{> basic_state }}{{/use_basic}} ================================================ FILE: bricks/cubit/__brick__/{{~ basic_cubit }} ================================================ import 'package:bloc/bloc.dart'; part '{{name.snakeCase()}}_state.dart'; class {{name.pascalCase()}}Cubit extends Cubit<{{name.pascalCase()}}State> { {{name.pascalCase()}}Cubit() : super(const {{name.pascalCase()}}State()); } ================================================ FILE: bricks/cubit/__brick__/{{~ basic_state }} ================================================ part of '{{name.snakeCase()}}_cubit.dart'; class {{name.pascalCase()}}State { const {{name.pascalCase()}}State(); } ================================================ FILE: bricks/cubit/__brick__/{{~ equatable_cubit }} ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; part '{{name.snakeCase()}}_state.dart'; class {{name.pascalCase()}}Cubit extends Cubit<{{name.pascalCase()}}State> { {{name.pascalCase()}}Cubit() : super(const {{name.pascalCase()}}State()); } ================================================ FILE: bricks/cubit/__brick__/{{~ equatable_state }} ================================================ part of '{{name.snakeCase()}}_cubit.dart'; class {{name.pascalCase()}}State extends Equatable { const {{name.pascalCase()}}State(); @override List get props => []; } ================================================ FILE: bricks/cubit/__brick__/{{~ freezed_cubit }} ================================================ import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part '{{name.snakeCase()}}_state.dart'; part '{{name.snakeCase()}}_cubit.freezed.dart'; class {{name.pascalCase()}}Cubit extends Cubit<{{name.pascalCase()}}State> { {{name.pascalCase()}}Cubit() : super(const {{name.pascalCase()}}State.initial()); } ================================================ FILE: bricks/cubit/__brick__/{{~ freezed_state }} ================================================ part of '{{name.snakeCase()}}_cubit.dart'; @freezed class {{name.pascalCase()}}State with _${{name.pascalCase()}}State { const factory {{name.pascalCase()}}State.initial() = _Initial; } ================================================ FILE: bricks/cubit/brick.yaml ================================================ name: cubit description: Generate a new Cubit in Dart. Built for the bloc state management library. version: 0.3.0 repository: https://github.com/felangel/bloc/tree/master/bricks/cubit environment: mason: ^0.1.0 vars: name: type: string description: The name of the cubit class. default: counter prompt: Please enter the cubit name. style: type: enum description: The style of cubit generated. default: basic prompt: What is the cubit style? values: - basic - equatable - freezed ================================================ FILE: bricks/cubit/hooks/pre_gen.dart ================================================ import 'package:mason/mason.dart'; Future run(HookContext context) async { final style = context.vars['style']; context.vars = { ...context.vars, 'use_basic': style == 'basic', 'use_equatable': style == 'equatable', 'use_freezed': style == 'freezed', }; } ================================================ FILE: bricks/cubit/hooks/pubspec.yaml ================================================ name: cubit_hooks environment: sdk: ^3.10.0 dependencies: mason: ^0.1.0 ================================================ FILE: bricks/flutter_bloc_feature/CHANGELOG.md ================================================ # 0.4.0 - chore(deps): upgrade to `mason ^0.1.0` - chore(deps): upgrade hooks to `dart ^3.5.4` # 0.3.2 - deps: upgrade to mason v0.1.0-dev.57 # 0.3.1 - chore: update copyright year - chore: update logo image refs # 0.3.0 - feat: upgrade to Dart 3.0 - use `sealed` classes for bloc events # 0.2.1 - fix: upgrade to mason v0.1.0-dev.40 - deps: remove dependency on `package:recase` # 0.2.0 - feat: add support for `equatable` - feat: add support for `freezed` # 0.1.0+1 - feat: initial release 🎉 ================================================ FILE: bricks/flutter_bloc_feature/LICENSE ================================================ MIT License Copyright (c) 2026 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: bricks/flutter_bloc_feature/README.md ================================================

Bloc

build codecov Star on Github License: MIT Discord Bloc Library Powered by Mason

Generate a new Flutter feature with bloc. Built for the [bloc state management library][1]. ## Usage 🚀 ```sh mason make flutter_bloc_feature --name counter --type bloc --style basic ``` ## Variables ✨ | Variable | Description | Default | Type | | -------- | --------------------------- | ----------------------------------- | -------- | | `name` | The name of the feature | `counter` | `string` | | `type` | The type of the bloc | `bloc` | `enum` | | `style` | The style of bloc generated | `basic (basic, equatable, freezed)` | `enum` | ## Output 📦 ```sh ── counter │ ├── bloc │ │ ├── counter_bloc.dart │ │ ├── counter_event.dart │ │ └── counter_state.dart │ ├── counter.dart │ └── view │ ├── counter_page.dart │ └── view.dart ``` [1]: https://github.com/felangel/bloc ================================================ FILE: bricks/flutter_bloc_feature/__brick__/{{name.snakeCase()}}/view/view.dart ================================================ export './{{name.snakeCase()}}_page.dart'; ================================================ FILE: bricks/flutter_bloc_feature/__brick__/{{name.snakeCase()}}/view/{{name.snakeCase()}}_page.dart ================================================ import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../{{name.snakeCase()}}.dart'; class {{name.pascalCase()}}Page extends StatelessWidget { const {{name.pascalCase()}}Page({super.key}); @override Widget build(BuildContext context) { {{#is_bloc}}{{> bloc_provider }}{{/is_bloc}}{{^is_bloc}}{{> cubit_bloc_provider }}{{/is_bloc}} } } class {{name.pascalCase()}}View extends StatelessWidget { const {{name.pascalCase()}}View({super.key}); @override Widget build(BuildContext context) { {{#is_bloc}}{{> bloc_builder }}{{/is_bloc}}{{^is_bloc}}{{> cubit_bloc_builder }}{{/is_bloc}} } } ================================================ FILE: bricks/flutter_bloc_feature/__brick__/{{name.snakeCase()}}/{{name.snakeCase()}}.dart ================================================ export '{{{bloc_export}}}'; export './view/view.dart'; ================================================ FILE: bricks/flutter_bloc_feature/__brick__/{{~ bloc_builder }} ================================================ return BlocBuilder<{{name.pascalCase()}}Bloc, {{name.pascalCase()}}State>( builder: (context, state) { // TODO: return correct widget based on the state. return const SizedBox(); }, ); ================================================ FILE: bricks/flutter_bloc_feature/__brick__/{{~ bloc_provider }} ================================================ return BlocProvider( create: (_) => {{name.pascalCase()}}Bloc(), child: const {{name.pascalCase()}}View(), ); ================================================ FILE: bricks/flutter_bloc_feature/__brick__/{{~ cubit_bloc_builder }} ================================================ return BlocBuilder<{{name.pascalCase()}}Cubit, {{name.pascalCase()}}State>( builder: (context, state) { // TODO: return correct widget based on the state. return const SizedBox(); }, ); ================================================ FILE: bricks/flutter_bloc_feature/__brick__/{{~ cubit_bloc_provider }} ================================================ return BlocProvider( create: (_) => {{name.pascalCase()}}Cubit(), child: const {{name.pascalCase()}}View(), ); ================================================ FILE: bricks/flutter_bloc_feature/brick.yaml ================================================ name: flutter_bloc_feature description: Generate a new Flutter feature with bloc. Built for the bloc state management library. repository: https://github.com/felangel/bloc/tree/master/bricks/flutter_bloc_feature version: 0.4.0 environment: mason: ^0.1.0 vars: name: type: string description: The name of the feature default: counter prompt: What is your feature called? type: type: enum description: The type of bloc used. default: bloc prompt: What type of bloc do you want to use? values: - bloc - cubit - hydrated_bloc - hydrated_cubit - replay_bloc - replay_cubit style: type: enum description: The style of bloc generated. default: basic prompt: What is the bloc style? values: - basic - equatable - freezed ================================================ FILE: bricks/flutter_bloc_feature/hooks/.gitignore ================================================ .dart_tool .packages pubspec.lock ================================================ FILE: bricks/flutter_bloc_feature/hooks/post_gen.dart ================================================ import 'dart:io'; import 'package:mason/mason.dart'; Future run(HookContext context) async { await _runDartFormat(context); await _runDartFix(context); } Future _runDartFormat(HookContext context) async { final formatProgress = context.logger.progress('Running "dart format ."'); await Process.run('dart', ['format', '.']); formatProgress.complete(); } Future _runDartFix(HookContext context) async { final formatProgress = context.logger.progress('Running "dart fix --apply"'); await Process.run('dart', ['fix', '--apply']); formatProgress.complete(); } ================================================ FILE: bricks/flutter_bloc_feature/hooks/pre_gen.dart ================================================ // ignore_for_file: constant_identifier_names import 'dart:io'; import 'package:mason/mason.dart'; import 'package:path/path.dart' as path; enum BlocType { bloc, cubit, hydrated_bloc, hydrated_cubit, replay_bloc, replay_cubit, } final brickVersions = { BlocType.bloc: '^0.4.0', BlocType.cubit: '^0.3.0', BlocType.hydrated_bloc: '^0.4.0', BlocType.hydrated_cubit: '^0.3.0', BlocType.replay_bloc: '^0.3.0', BlocType.replay_cubit: '^0.3.0', }; Future run(HookContext context) async { final blocType = _blocTypeFromContext(context); final progress = context.logger.progress('Making brick ${blocType.name}'); final name = context.vars['name'] as String; final style = context.vars['style'] as String; final brick = Brick.version( name: blocType.name, version: brickVersions[blocType]!, ); final generator = await MasonGenerator.fromBrick(brick); final blocDirectoryName = blocType.toDirectoryName(); final directory = Directory( path.join(Directory.current.path, name.snakeCase, blocDirectoryName), ); final target = DirectoryGeneratorTarget(directory); var vars = {'name': name, 'style': style}; await generator.hooks.preGen(vars: vars, onVarsChanged: (v) => vars = v); final files = await generator.generate( target, vars: vars, logger: context.logger, fileConflictResolution: FileConflictResolution.overwrite, ); await generator.hooks.postGen(vars: vars); final blocExport = './$blocDirectoryName/${name.snakeCase}_$blocDirectoryName.dart'; progress.complete('Made brick ${blocType.name}'); context.logger.logFilesGenerated(files.length); context.vars = { ...context.vars, 'bloc_export': blocExport, 'is_bloc': blocDirectoryName == 'bloc', }; } BlocType _blocTypeFromContext(HookContext context) { final type = context.vars['type'] as String; switch (type) { case 'cubit': return BlocType.cubit; case 'hydrated_bloc': return BlocType.hydrated_bloc; case 'hydrated_cubit': return BlocType.hydrated_cubit; case 'replay_bloc': return BlocType.replay_bloc; case 'replay_cubit': return BlocType.replay_cubit; case 'bloc': default: return BlocType.bloc; } } extension on BlocType { String toDirectoryName() { switch (this) { case BlocType.bloc: case BlocType.hydrated_bloc: case BlocType.replay_bloc: return 'bloc'; case BlocType.cubit: case BlocType.hydrated_cubit: case BlocType.replay_cubit: return 'cubit'; } } } extension on Logger { void logFilesGenerated(int fileCount) { if (fileCount == 1) { this ..info( '${lightGreen.wrap('\u2713')} ' 'Generated $fileCount file:', ) ..flush((message) => info(darkGray.wrap(message))); } else { this ..info( '${lightGreen.wrap('\u2713')} ' 'Generated $fileCount file(s):', ) ..flush((message) => info(darkGray.wrap(message))); } } } ================================================ FILE: bricks/flutter_bloc_feature/hooks/pubspec.yaml ================================================ name: flutter_bloc_feature_hooks environment: sdk: ^3.10.0 dependencies: mason: ^0.1.0 path: ^1.8.2 ================================================ FILE: bricks/hydrated_bloc/CHANGELOG.md ================================================ # 0.4.0 - chore(deps): upgrade to `mason ^0.1.0` - chore(deps): upgrade hooks to `dart ^3.5.4` # 0.3.1 - chore: update copyright year - chore: update logo image refs # 0.3.0 - feat: add support for `sealed` events using Dart 3 # 0.2.0 - feat: add support for `equatable` - feat: add support for `freezed` # 0.1.2 - docs: add badges to README - docs: use dark logo variant # 0.1.1 - docs: minor README update # 0.1.0 - feat: initial release with support for basic hydrated bloc generation ================================================ FILE: bricks/hydrated_bloc/LICENSE ================================================ MIT License Copyright (c) 2026 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: bricks/hydrated_bloc/README.md ================================================

Bloc

build codecov Star on Github License: MIT Discord Bloc Library Powered by Mason

Generate a new HydratedBloc in [Dart][1]. Built for the [bloc state management library][2]. ## Usage 🚀 ```sh mason make hydrated_bloc --name counter --style basic ``` ## Variables ✨ | Variable | Description | Default | Type | | -------- | --------------------------- | ----------------------------------- | -------- | | `name` | The name of the bloc class | `counter` | `string` | | `style` | The style of bloc generated | `basic (basic, equatable, freezed)` | `enum` | ## Output 📦 ```sh ├── counter_bloc.dart ├── counter_event.dart └── counter_state.dart ``` [1]: https://dart.dev [2]: https://github.com/felangel/bloc ================================================ FILE: bricks/hydrated_bloc/__brick__/{{name.snakeCase()}}_bloc.dart ================================================ {{#use_freezed}}{{> freezed_bloc }}{{/use_freezed}}{{#use_equatable}}{{> equatable_bloc }}{{/use_equatable}}{{#use_basic}}{{> basic_bloc }}{{/use_basic}} ================================================ FILE: bricks/hydrated_bloc/__brick__/{{name.snakeCase()}}_event.dart ================================================ {{#use_freezed}}{{> freezed_event }}{{/use_freezed}}{{#use_equatable}}{{> equatable_event }}{{/use_equatable}}{{#use_basic}}{{> basic_event }}{{/use_basic}} ================================================ FILE: bricks/hydrated_bloc/__brick__/{{name.snakeCase()}}_state.dart ================================================ {{#use_freezed}}{{> freezed_state }}{{/use_freezed}}{{#use_equatable}}{{> equatable_state }}{{/use_equatable}}{{#use_basic}}{{> basic_state }}{{/use_basic}} ================================================ FILE: bricks/hydrated_bloc/__brick__/{{~ basic_bloc }} ================================================ import 'package:hydrated_bloc/hydrated_bloc.dart'; part '{{name.snakeCase()}}_event.dart'; part '{{name.snakeCase()}}_state.dart'; class {{name.pascalCase()}}Bloc extends HydratedBloc<{{name.pascalCase()}}Event, {{name.pascalCase()}}State> { {{name.pascalCase()}}Bloc() : super(const {{name.pascalCase()}}State()) { on<{{name.pascalCase()}}Event>((event, emit) { // TODO: implement event handler }); } @override Map toJson({{name.pascalCase()}}State state) { // TODO: implement toJson } @override {{name.pascalCase()}}State fromJson(Map json) { // TODO: implement fromJson } } ================================================ FILE: bricks/hydrated_bloc/__brick__/{{~ basic_event }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; sealed class {{name.pascalCase()}}Event { const {{name.pascalCase()}}Event(); } ================================================ FILE: bricks/hydrated_bloc/__brick__/{{~ basic_state }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; class {{name.pascalCase()}}State { const {{name.pascalCase()}}State(); } ================================================ FILE: bricks/hydrated_bloc/__brick__/{{~ equatable_bloc }} ================================================ import 'package:equatable/equatable.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; part '{{name.snakeCase()}}_event.dart'; part '{{name.snakeCase()}}_state.dart'; class {{name.pascalCase()}}Bloc extends HydratedBloc<{{name.pascalCase()}}Event, {{name.pascalCase()}}State> { {{name.pascalCase()}}Bloc() : super(const {{name.pascalCase()}}State()) { on<{{name.pascalCase()}}Event>((event, emit) { // TODO: implement event handler }); } @override Map toJson({{name.pascalCase()}}State state) { // TODO: implement toJson } @override {{name.pascalCase()}}State fromJson(Map json) { // TODO: implement fromJson } } ================================================ FILE: bricks/hydrated_bloc/__brick__/{{~ equatable_event }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; sealed class {{name.pascalCase()}}Event extends Equatable { const {{name.pascalCase()}}Event(); @override List get props => []; } ================================================ FILE: bricks/hydrated_bloc/__brick__/{{~ equatable_state }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; class {{name.pascalCase()}}State extends Equatable { const {{name.pascalCase()}}State(); @override List get props => []; } ================================================ FILE: bricks/hydrated_bloc/__brick__/{{~ freezed_bloc }} ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; part '{{name.snakeCase()}}_event.dart'; part '{{name.snakeCase()}}_state.dart'; part '{{name.snakeCase()}}_bloc.freezed.dart'; class {{name.pascalCase()}}Bloc extends HydratedBloc<{{name.pascalCase()}}Event, {{name.pascalCase()}}State> { {{name.pascalCase()}}Bloc() : super(const {{name.pascalCase()}}State.initial()) { on<{{name.pascalCase()}}Event>((event, emit) { // TODO: implement event handler }); } @override Map toJson({{name.pascalCase()}}State state) { // TODO: implement toJson } @override {{name.pascalCase()}}State fromJson(Map json) { // TODO: implement fromJson } } ================================================ FILE: bricks/hydrated_bloc/__brick__/{{~ freezed_event }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; @freezed class {{name.pascalCase()}}Event with _${{name.pascalCase()}}Event { const factory {{name.pascalCase()}}Event.started() = _Started; } ================================================ FILE: bricks/hydrated_bloc/__brick__/{{~ freezed_state }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; @freezed class {{name.pascalCase()}}State with _${{name.pascalCase()}}State { const factory {{name.pascalCase()}}State.initial() = _Initial; } ================================================ FILE: bricks/hydrated_bloc/brick.yaml ================================================ name: hydrated_bloc description: Generate a new HydratedBloc in Dart. Built for the bloc state management library. version: 0.4.0 repository: https://github.com/felangel/bloc/tree/master/bricks/hydrated_bloc environment: mason: ^0.1.0 vars: name: type: string description: The name of the bloc class. default: counter prompt: Please enter the bloc name. style: type: enum description: The style of bloc generated. default: basic prompt: What is the bloc style? values: - basic - equatable - freezed ================================================ FILE: bricks/hydrated_bloc/hooks/pre_gen.dart ================================================ import 'package:mason/mason.dart'; Future run(HookContext context) async { final style = context.vars['style']; context.vars = { ...context.vars, 'use_basic': style == 'basic', 'use_equatable': style == 'equatable', 'use_freezed': style == 'freezed', }; } ================================================ FILE: bricks/hydrated_bloc/hooks/pubspec.yaml ================================================ name: hydrated_bloc_hooks environment: sdk: ^3.10.0 dependencies: mason: ^0.1.0 ================================================ FILE: bricks/hydrated_cubit/CHANGELOG.md ================================================ # 0.3.0 - chore(deps): upgrade to `mason ^0.1.0` - chore(deps): upgrade hooks to `dart ^3.5.4` # 0.2.1 - chore: update copyright year - chore: update logo image refs # 0.2.0 - feat: add support for `equatable` - feat: add support for `freezed` # 0.1.3 - fix: part and imports # 0.1.2 - docs: add badges to README - docs: use dark logo variant # 0.1.1 - docs: minor README update # 0.1.0 - feat: initial release with support for basic hydrated cubit generation ================================================ FILE: bricks/hydrated_cubit/LICENSE ================================================ MIT License Copyright (c) 2026 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: bricks/hydrated_cubit/README.md ================================================

Hydrated Cubit

build codecov Star on Github License: MIT Discord Bloc Library Powered by Mason

Generate a new HydratedCubit in [Dart][1]. Built for the [bloc state management library][2]. ## Usage 🚀 ```sh mason make hydrated_cubit --name counter --style basic ``` ## Variables ✨ | Variable | Description | Default | Type | | -------- | ---------------------------- | ----------------------------------- | -------- | | `name` | The name of the cubit class | `counter` | `string` | | `style` | The style of cubit generated | `basic (basic, equatable, freezed)` | `enum` | ## Output 📦 ```sh ├── counter_cubit.dart └── counter_state.dart ``` [1]: https://dart.dev [2]: https://github.com/felangel/bloc ================================================ FILE: bricks/hydrated_cubit/__brick__/{{name.snakeCase()}}_cubit.dart ================================================ {{#use_freezed}}{{> freezed_cubit }}{{/use_freezed}}{{#use_equatable}}{{> equatable_cubit }}{{/use_equatable}}{{#use_basic}}{{> basic_cubit }}{{/use_basic}} ================================================ FILE: bricks/hydrated_cubit/__brick__/{{name.snakeCase()}}_state.dart ================================================ {{#use_freezed}}{{> freezed_state }}{{/use_freezed}}{{#use_equatable}}{{> equatable_state }}{{/use_equatable}}{{#use_basic}}{{> basic_state }}{{/use_basic}} ================================================ FILE: bricks/hydrated_cubit/__brick__/{{~ basic_cubit }} ================================================ import 'package:hydrated_bloc/hydrated_bloc.dart'; part '{{name.snakeCase()}}_state.dart'; class {{name.pascalCase()}}Cubit extends HydratedCubit<{{name.pascalCase()}}State> { {{name.pascalCase()}}Cubit() : super(const {{name.pascalCase()}}State()); @override Map toJson({{name.pascalCase()}}State state) { // TODO: implement toJson } @override {{name.pascalCase()}}State fromJson(Map json) { // TODO: implement fromJson } } ================================================ FILE: bricks/hydrated_cubit/__brick__/{{~ basic_state }} ================================================ part of '{{name.snakeCase()}}_cubit.dart'; class {{name.pascalCase()}}State { const {{name.pascalCase()}}State(); } ================================================ FILE: bricks/hydrated_cubit/__brick__/{{~ equatable_cubit }} ================================================ import 'package:equatable/equatable.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; part '{{name.snakeCase()}}_state.dart'; class {{name.pascalCase()}}Cubit extends HydratedCubit<{{name.pascalCase()}}State> { {{name.pascalCase()}}Cubit() : super(const {{name.pascalCase()}}State()); @override Map toJson({{name.pascalCase()}}State state) { // TODO: implement toJson } @override {{name.pascalCase()}}State fromJson(Map json) { // TODO: implement fromJson } } ================================================ FILE: bricks/hydrated_cubit/__brick__/{{~ equatable_state }} ================================================ part of '{{name.snakeCase()}}_cubit.dart'; class {{name.pascalCase()}}State extends Equatable { const {{name.pascalCase()}}State(); @override List get props => []; } ================================================ FILE: bricks/hydrated_cubit/__brick__/{{~ freezed_cubit }} ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; part '{{name.snakeCase()}}_state.dart'; part '{{name.snakeCase()}}_cubit.freezed.dart'; class {{name.pascalCase()}}Cubit extends HydratedCubit<{{name.pascalCase()}}State> { {{name.pascalCase()}}Cubit() : super(const {{name.pascalCase()}}State.initial()); @override Map toJson({{name.pascalCase()}}State state) { // TODO: implement toJson } @override {{name.pascalCase()}}State fromJson(Map json) { // TODO: implement fromJson } } ================================================ FILE: bricks/hydrated_cubit/__brick__/{{~ freezed_state }} ================================================ part of '{{name.snakeCase()}}_cubit.dart'; @freezed class {{name.pascalCase()}}State with _${{name.pascalCase()}}State { const factory {{name.pascalCase()}}State.initial() = _Initial; } ================================================ FILE: bricks/hydrated_cubit/brick.yaml ================================================ name: hydrated_cubit description: Generate a new HydratedCubit in Dart. Built for the bloc state management library. version: 0.3.0 repository: https://github.com/felangel/bloc/tree/master/bricks/hydrated_cubit environment: mason: ^0.1.0 vars: name: type: string description: The name of the cubit class. default: counter prompt: Please enter the cubit name. style: type: enum description: The style of cubit generated. default: basic prompt: What is the cubit style? values: - basic - equatable - freezed ================================================ FILE: bricks/hydrated_cubit/hooks/pre_gen.dart ================================================ import 'package:mason/mason.dart'; Future run(HookContext context) async { final style = context.vars['style']; context.vars = { ...context.vars, 'use_basic': style == 'basic', 'use_equatable': style == 'equatable', 'use_freezed': style == 'freezed', }; } ================================================ FILE: bricks/hydrated_cubit/hooks/pubspec.yaml ================================================ name: hydrated_cubit_hooks environment: sdk: ^3.10.0 dependencies: mason: ^0.1.0 ================================================ FILE: bricks/mason.yaml ================================================ bricks: bloc: path: ./bloc cubit: path: ./cubit hydrated_bloc: path: ./hydrated_bloc hydrated_cubit: path: ./hydrated_cubit replay_bloc: path: ./replay_bloc replay_cubit: path: ./replay_cubit flutter_bloc_feature: path: ./flutter_bloc_feature ================================================ FILE: bricks/replay_bloc/CHANGELOG.md ================================================ # 0.3.0 - chore(deps): upgrade to `mason ^0.1.0` - chore(deps): upgrade hooks to `dart ^3.5.4` # 0.2.1 - chore: update copyright year - chore: update logo image refs # 0.2.0 - feat: add support for `equatable` - feat: add support for `freezed` # 0.1.3 - fix: add missing `extends ReplayEvent` # 0.1.2 - docs: add badges to README - docs: use dark logo variant # 0.1.1 - docs: minor README update # 0.1.0 - feat: initial release with support for basic replay bloc generation ================================================ FILE: bricks/replay_bloc/LICENSE ================================================ MIT License Copyright (c) 2026 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: bricks/replay_bloc/README.md ================================================

Bloc

build codecov Star on Github License: MIT Discord Bloc Library Powered by Mason

Generate a new ReplayBloc in [Dart][1]. Built for the [bloc state management library][2]. ## Usage 🚀 ```sh mason make replay_bloc --name counter --style basic ``` ## Variables ✨ | Variable | Description | Default | Type | | -------- | --------------------------- | ----------------------------------- | -------- | | `name` | The name of the bloc class | `counter` | `string` | | `style` | The style of bloc generated | `basic (basic, equatable, freezed)` | `enum` | ## Output 📦 ```sh ├── counter_bloc.dart ├── counter_event.dart └── counter_state.dart ``` [1]: https://dart.dev [2]: https://github.com/felangel/bloc ================================================ FILE: bricks/replay_bloc/__brick__/{{name.snakeCase()}}_bloc.dart ================================================ {{#use_freezed}}{{> freezed_bloc }}{{/use_freezed}}{{#use_equatable}}{{> equatable_bloc }}{{/use_equatable}}{{#use_basic}}{{> basic_bloc }}{{/use_basic}} ================================================ FILE: bricks/replay_bloc/__brick__/{{name.snakeCase()}}_event.dart ================================================ {{#use_freezed}}{{> freezed_event }}{{/use_freezed}}{{#use_equatable}}{{> equatable_event }}{{/use_equatable}}{{#use_basic}}{{> basic_event }}{{/use_basic}} ================================================ FILE: bricks/replay_bloc/__brick__/{{name.snakeCase()}}_state.dart ================================================ {{#use_freezed}}{{> freezed_state }}{{/use_freezed}}{{#use_equatable}}{{> equatable_state }}{{/use_equatable}}{{#use_basic}}{{> basic_state }}{{/use_basic}} ================================================ FILE: bricks/replay_bloc/__brick__/{{~ basic_bloc }} ================================================ import 'package:replay_bloc/replay_bloc.dart'; part '{{name.snakeCase()}}_event.dart'; part '{{name.snakeCase()}}_state.dart'; class {{name.pascalCase()}}Bloc extends ReplayBloc<{{name.pascalCase()}}Event, {{name.pascalCase()}}State> { {{name.pascalCase()}}Bloc() : super(const {{name.pascalCase()}}State()) { on<{{name.pascalCase()}}Event>((event, emit) { // TODO: implement event handler }); } } ================================================ FILE: bricks/replay_bloc/__brick__/{{~ basic_event }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; abstract class {{name.pascalCase()}}Event extends ReplayEvent { const {{name.pascalCase()}}Event(); } ================================================ FILE: bricks/replay_bloc/__brick__/{{~ basic_state }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; class {{name.pascalCase()}}State { const {{name.pascalCase()}}State(); } ================================================ FILE: bricks/replay_bloc/__brick__/{{~ equatable_bloc }} ================================================ import 'package:equatable/equatable.dart'; import 'package:replay_bloc/replay_bloc.dart'; part '{{name.snakeCase()}}_event.dart'; part '{{name.snakeCase()}}_state.dart'; class {{name.pascalCase()}}Bloc extends ReplayBloc<{{name.pascalCase()}}Event, {{name.pascalCase()}}State> { {{name.pascalCase()}}Bloc() : super(const {{name.pascalCase()}}State()) { on<{{name.pascalCase()}}Event>((event, emit) { // TODO: implement event handler }); } } ================================================ FILE: bricks/replay_bloc/__brick__/{{~ equatable_event }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; abstract class {{name.pascalCase()}}Event extends ReplayEvent with EquatableMixin { const {{name.pascalCase()}}Event(); @override List get props => []; } ================================================ FILE: bricks/replay_bloc/__brick__/{{~ equatable_state }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; class {{name.pascalCase()}}State extends Equatable { const {{name.pascalCase()}}State(); @override List get props => []; } ================================================ FILE: bricks/replay_bloc/__brick__/{{~ freezed_bloc }} ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:replay_bloc/replay_bloc.dart'; part '{{name.snakeCase()}}_event.dart'; part '{{name.snakeCase()}}_state.dart'; part '{{name.snakeCase()}}_bloc.freezed.dart'; class {{name.pascalCase()}}Bloc extends ReplayBloc<{{name.pascalCase()}}Event, {{name.pascalCase()}}State> { {{name.pascalCase()}}Bloc() : super(const {{name.pascalCase()}}State.initial()) { on<{{name.pascalCase()}}Event>((event, emit) { // TODO: implement event handler }); } } ================================================ FILE: bricks/replay_bloc/__brick__/{{~ freezed_event }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; @freezed class {{name.pascalCase()}}Event extends ReplayEvent with _${{name.pascalCase()}}Event { const factory {{name.pascalCase()}}Event.started() = _Started; } ================================================ FILE: bricks/replay_bloc/__brick__/{{~ freezed_state }} ================================================ part of '{{name.snakeCase()}}_bloc.dart'; @freezed class {{name.pascalCase()}}State with _${{name.pascalCase()}}State { const factory {{name.pascalCase()}}State.initial() = _Initial; } ================================================ FILE: bricks/replay_bloc/brick.yaml ================================================ name: replay_bloc description: Generate a new ReplayBloc in Dart. Built for the bloc state management library. version: 0.3.0 repository: https://github.com/felangel/bloc/tree/master/bricks/replay_bloc environment: mason: ^0.1.0 vars: name: type: string description: The name of the bloc class. default: counter prompt: Please enter the bloc name. style: type: enum description: The style of bloc generated. default: basic prompt: What is the bloc style? values: - basic - equatable - freezed ================================================ FILE: bricks/replay_bloc/hooks/pre_gen.dart ================================================ import 'package:mason/mason.dart'; Future run(HookContext context) async { final style = context.vars['style']; context.vars = { ...context.vars, 'use_basic': style == 'basic', 'use_equatable': style == 'equatable', 'use_freezed': style == 'freezed', }; } ================================================ FILE: bricks/replay_bloc/hooks/pubspec.yaml ================================================ name: replay_bloc_hooks environment: sdk: ^3.10.0 dependencies: mason: ^0.1.0 ================================================ FILE: bricks/replay_cubit/CHANGELOG.md ================================================ # 0.3.0 - chore(deps): upgrade to `mason ^0.1.0` - chore(deps): upgrade hooks to `dart ^3.5.4` # 0.2.1 - chore: update copyright year - chore: update logo image refs # 0.2.0 - feat: add support for `equatable` - feat: add support for `freezed` # 0.1.3 - fix: part and imports # 0.1.2 - docs: add badges to README - docs: use dark logo variant # 0.1.1 - docs: minor README update # 0.1.0 - feat: initial release with support for basic replay cubit generation ================================================ FILE: bricks/replay_cubit/LICENSE ================================================ MIT License Copyright (c) 2026 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: bricks/replay_cubit/README.md ================================================

Replay Cubit

build codecov Star on Github License: MIT Discord Bloc Library Powered by Mason

Generate a new ReplayCubit in [Dart][1]. Built for the [bloc state management library][2]. ## Usage 🚀 ```sh mason make replay_cubit --name counter --style basic ``` ## Variables ✨ | Variable | Description | Default | Type | | -------- | ---------------------------- | ----------------------------------- | -------- | | `name` | The name of the cubit class | `counter` | `string` | | `style` | The style of cubit generated | `basic (basic, equatable, freezed)` | `enum` | ## Output 📦 ```sh ├── counter_cubit.dart └── counter_state.dart ``` [1]: https://dart.dev [2]: https://github.com/felangel/bloc ================================================ FILE: bricks/replay_cubit/__brick__/{{name.snakeCase()}}_cubit.dart ================================================ {{#use_freezed}}{{> freezed_cubit }}{{/use_freezed}}{{#use_equatable}}{{> equatable_cubit }}{{/use_equatable}}{{#use_basic}}{{> basic_cubit }}{{/use_basic}} ================================================ FILE: bricks/replay_cubit/__brick__/{{name.snakeCase()}}_state.dart ================================================ {{#use_freezed}}{{> freezed_state }}{{/use_freezed}}{{#use_equatable}}{{> equatable_state }}{{/use_equatable}}{{#use_basic}}{{> basic_state }}{{/use_basic}} ================================================ FILE: bricks/replay_cubit/__brick__/{{~ basic_cubit }} ================================================ import 'package:replay_bloc/replay_bloc.dart'; part '{{name.snakeCase()}}_state.dart'; class {{name.pascalCase()}}Cubit extends ReplayCubit<{{name.pascalCase()}}State> { {{name.pascalCase()}}Cubit() : super(const {{name.pascalCase()}}State()); } ================================================ FILE: bricks/replay_cubit/__brick__/{{~ basic_state }} ================================================ part of '{{name.snakeCase()}}_cubit.dart'; class {{name.pascalCase()}}State { const {{name.pascalCase()}}State(); } ================================================ FILE: bricks/replay_cubit/__brick__/{{~ equatable_cubit }} ================================================ import 'package:equatable/equatable.dart'; import 'package:replay_bloc/replay_bloc.dart'; part '{{name.snakeCase()}}_state.dart'; class {{name.pascalCase()}}Cubit extends ReplayCubit<{{name.pascalCase()}}State> { {{name.pascalCase()}}Cubit() : super(const {{name.pascalCase()}}State()); } ================================================ FILE: bricks/replay_cubit/__brick__/{{~ equatable_state }} ================================================ part of '{{name.snakeCase()}}_cubit.dart'; class {{name.pascalCase()}}State extends Equatable { const {{name.pascalCase()}}State(); @override List get props => []; } ================================================ FILE: bricks/replay_cubit/__brick__/{{~ freezed_cubit }} ================================================ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:replay_bloc/replay_bloc.dart'; part '{{name.snakeCase()}}_state.dart'; part '{{name.snakeCase()}}_cubit.freezed.dart'; class {{name.pascalCase()}}Cubit extends ReplayCubit<{{name.pascalCase()}}State> { {{name.pascalCase()}}Cubit() : super(const {{name.pascalCase()}}State.initial()); } ================================================ FILE: bricks/replay_cubit/__brick__/{{~ freezed_state }} ================================================ part of '{{name.snakeCase()}}_cubit.dart'; @freezed class {{name.pascalCase()}}State with _${{name.pascalCase()}}State { const factory {{name.pascalCase()}}State.initial() = _Initial; } ================================================ FILE: bricks/replay_cubit/brick.yaml ================================================ name: replay_cubit description: Generate a new ReplayCubit in Dart. Built for the bloc state management library. version: 0.3.0 repository: https://github.com/felangel/bloc/tree/master/bricks/replay_cubit environment: mason: ^0.1.0 vars: name: type: string description: The name of the cubit class. default: counter prompt: Please enter the cubit name. style: type: enum description: The style of cubit generated. default: basic prompt: What is the cubit style? values: - basic - equatable - freezed ================================================ FILE: bricks/replay_cubit/hooks/pre_gen.dart ================================================ import 'package:mason/mason.dart'; Future run(HookContext context) async { final style = context.vars['style']; context.vars = { ...context.vars, 'use_basic': style == 'basic', 'use_equatable': style == 'equatable', 'use_freezed': style == 'freezed', }; } ================================================ FILE: bricks/replay_cubit/hooks/pubspec.yaml ================================================ name: replay_cubit_hooks environment: sdk: ^3.10.0 dependencies: mason: ^0.1.0 ================================================ FILE: docs/.gitignore ================================================ # build output dist/ # generated types .astro/ # dependencies node_modules/ # logs npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # environment variables .env .env.production # macOS-specific files .DS_Store # firebase .firebase ================================================ FILE: docs/.prettierignore ================================================ src/components/**/*.mdx ================================================ FILE: docs/.prettierrc ================================================ { "printWidth": 100, "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "es5", "useTabs": true, "plugins": ["prettier-plugin-astro"], "overrides": [ { "files": [".*", "*.json", "*.md", "*.toml", "*.yml"], "options": { "useTabs": false } }, { "files": ["*.md", "*.mdx"], "options": { "printWidth": 80, "proseWrap": "always" } } ] } ================================================ FILE: docs/astro.config.mjs ================================================ import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; import starlightLinksValidator from 'starlight-links-validator'; import tailwindcss from '@tailwindcss/vite'; const site = 'https://bloclibrary.dev/'; const locales = { root: { label: 'English', lang: 'en' }, az: { label: 'Azərbaycan', lang: 'az' }, cs: { label: 'Čeština', lang: 'cs' }, de: { label: 'Deutsch', lang: 'de' }, es: { label: 'Español', lang: 'es' }, fil: { label: 'Filipino', lang: 'fil' }, fr: { label: 'Français', lang: 'fr' }, it: { label: 'Italiano', lang: 'it' }, ja: { label: '日本語', lang: 'ja' }, ko: { label: '한국어', lang: 'ko' }, 'pt-br': { label: 'Português', lang: 'pt-BR' }, ru: { label: 'Русский', lang: 'ru' }, 'zh-cn': { label: '简体中文', lang: 'zh-CN' }, uk: { label: 'Українська', lang: 'uk' }, ar: { label: 'العربية', lang: 'ar', dir: 'rtl' }, fa: { label: 'فارسی', lang: 'fa', dir: 'rtl' }, bn: { label: 'বাংলা', lang: 'bn' }, }; // https://astro.build/config export default defineConfig({ site, integrations: [ starlight({ expressiveCode: { themes: ['dark-plus', 'github-light'] }, logo: { light: 'src/assets/light-bloc-logo.svg', dark: 'src/assets/dark-bloc-logo.svg', replacesTitle: true, }, title: 'Bloc', editLink: { baseUrl: 'https://github.com/felangel/bloc/edit/master/docs/' }, tagline: 'A predictable state management library for Dart.', favicon: 'favicon.ico', head: [ { tag: 'meta', attrs: { property: 'og:image', content: site + 'og.png?v=1' } }, { tag: 'meta', attrs: { property: 'twitter:image', content: site + 'og.png?v=1' } }, ], customCss: ['src/tailwind.css', 'src/styles/landing.css', '@fontsource-variable/figtree'], social: [ { icon: 'github', label: 'GitHub', href: 'https://github.com/felangel/bloc' }, { icon: 'discord', label: 'Discord', href: 'https://discord.gg/bloc' }, ], defaultLocale: 'root', locales, sidebar: [ { label: 'Introduction', translations: { ar: 'المقدمة', 'zh-CN': '介绍', fa: 'مقدمه', es: 'Introducción', ja: '紹介', ru: 'Введение', uk: 'Вступ', }, items: [ { label: 'Getting Started', link: '/getting-started/', translations: { ar: 'ابدأ الآن', 'zh-CN': '快速入门', fa: 'شروع شدن', es: 'Empezando', ja: 'はじめに', ru: 'Начало работы', uk: 'Початок роботи', }, }, { label: 'Why Bloc?', link: '/why-bloc/', translations: { ar: 'لماذا Bloc؟', 'zh-CN': '为什么用 Bloc?', fa: 'چرا Bloc؟', es: '¿Por qué Bloc?', ja: 'なぜBloc?', ru: 'Почему Bloc?', uk: 'Чому Bloc?', }, }, { label: 'Bloc Concepts', link: '/bloc-concepts/', translations: { ar: 'مفاهيم Bloc', 'zh-CN': 'Bloc 核心概念', fa: 'مفاهیم Bloc', es: 'Conceptos de Bloc', ja: 'Blocのコンセプト', ru: 'Концепции Bloc', uk: 'Концепції Bloc', }, }, { label: 'Flutter Bloc Concepts', link: '/flutter-bloc-concepts/', translations: { ar: 'مفاهيم Flutter Bloc', 'zh-CN': 'Flutter Bloc 核心概念', fa: 'مفاهیم Bloc فلاتر', es: 'Conceptos de Flutter Bloc', ja: 'Flutter Blocのコンセプト', ru: 'Концепции Flutter Bloc', uk: 'Концепції Flutter Bloc', }, }, { label: 'Architecture', link: '/architecture/', translations: { ar: 'البنية المعمارية', fa: 'معماری', es: 'Arquitectura', ja: 'アーキテクチャー', ru: 'Архитектура', uk: 'Архітектура', }, }, { label: 'Modeling State', link: '/modeling-state/', translations: { ar: 'نمذجة الحالة', fa: 'حالت (State) مدل سازی', es: 'Modelando el Estado', ja: '状態のモデリング', ru: 'Моделирование состояния', uk: 'Моделювання стану', }, }, { label: 'Testing', link: '/testing/', translations: { ar: 'الاختبارات', fa: 'آزمایش کردن', es: 'Pruebas', ja: 'テスト', ru: 'Тестирование', uk: 'Тестування', }, }, { label: 'Naming Conventions', link: '/naming-conventions/', translations: { ar: 'اتفاقيات التسمية', fa: 'قراردادهای نامگذاری', es: 'Convenciones de Nomenclatura', ja: '命名規則', ru: 'Соглашения об именовании', uk: 'Угоди про іменування', }, }, { label: 'Migration Guide', link: '/migration/', translations: { ar: 'دليل الترقية', fa: 'راهنمای مهاجرت', es: 'Guía de Migración', ja: '移行ガイド', ru: 'Руководство по миграции', uk: 'Посібник з міграції', }, }, { label: 'FAQs', link: '/faqs/', translations: { ar: 'الأسئلة الشائعة', fa: 'سوالات متداول', es: 'Preguntas Frecuentes', ja: 'よくある質問', ru: 'Часто задаваемые вопросы', uk: 'Часті запитання', }, }, ], }, { label: 'Linter', badge: { text: 'new' }, translations: { ar: 'المدقق', uk: 'Лінтер' }, items: [ { label: 'Overview ', link: '/lint/', translations: { ar: 'نظرة عامة', fa: 'بررسی اجمالی', ru: 'Обзор', uk: 'Огляд' }, }, { label: 'Installation ', link: '/lint/installation/', translations: { ar: 'التثبيت', fa: 'نصب', ru: 'Установка', uk: 'Встановлення' }, }, { label: 'Configuration ', link: '/lint/configuration/', translations: { ar: 'الإعداد', fa: 'پیکربندی', ru: 'Конфигурация', uk: 'Конфігурація', }, }, { label: 'Customizing Rules ', link: '/lint/customizing-rules/', translations: { ar: 'تخصيص القواعد', fa: 'سفارشی سازی قوانین', ru: 'Настройка правил', uk: 'Налаштування правил', }, }, { label: 'Rules', autogenerate: { directory: '/lint-rules' }, translations: { ar: 'القواعد', fa: 'قوانین', ru: 'Правила', uk: 'Правила' }, }, ], }, { label: 'Tutorials', translations: { ar: 'الدروس التعليمية', fa: 'آموزش ها', es: 'Tutoriales', ja: 'チュートリアル', ru: 'Руководства', uk: 'Посібники', }, autogenerate: { directory: 'tutorials' }, }, { label: 'Tools', translations: { ar: 'الأدوات', fa: 'ابزار', es: 'Herramientas', ja: 'ツール', ru: 'Инструменты', uk: 'Інструменти', }, items: [ { label: 'IntelliJ Plugin', link: 'https://plugins.jetbrains.com/plugin/12129-bloc', translations: { ar: 'إضافة IntelliJ', fa: 'پلاگین IntelliJ', es: 'Plugin de IntelliJ', ru: 'Плагин IntelliJ', uk: 'Плагін IntelliJ', }, }, { label: 'VSCode Extension', link: 'https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc', translations: { ar: 'إضافة VS Code', fa: 'پلاگین VSCode', es: 'Extensión de VSCode', ru: 'Расширение VSCode', uk: 'Розширення VSCode', }, }, ], }, { label: 'Reference', translations: { ar: 'المرجع', fa: 'مرجع', es: 'Referencia', ja: 'APIリファレンス', ru: 'Справочник', uk: 'Довідник', }, items: [ { label: 'angular_bloc', link: 'https://pub.dev/documentation/angular_bloc/latest/index.html', }, { label: 'bloc', link: 'https://pub.dev/documentation/bloc/latest/index.html' }, { label: 'bloc_concurrency', link: 'https://pub.dev/documentation/bloc_concurrency/latest/index.html', }, { label: 'bloc_lint', link: 'https://pub.dev/documentation/bloc_lint/latest/index.html', }, { label: 'bloc_test', link: 'https://pub.dev/documentation/bloc_test/latest/index.html', }, { label: 'bloc_tools', link: 'https://pub.dev/documentation/bloc_tools/latest/index.html', }, { label: 'flutter_bloc', link: 'https://pub.dev/documentation/flutter_bloc/latest/index.html', }, { label: 'hydrated_bloc', link: 'https://pub.dev/documentation/hydrated_bloc/latest/index.html', }, { label: 'replay_bloc', link: 'https://pub.dev/documentation/replay_bloc/latest/index.html', }, ], }, ], plugins: [ starlightLinksValidator({ errorOnFallbackPages: false, errorOnInconsistentLocale: true }), ], }), ], vite: { plugins: [tailwindcss()] }, }); ================================================ FILE: docs/package.json ================================================ { "name": "docs", "type": "module", "version": "0.0.1", "scripts": { "dev": "astro dev", "start": "astro dev", "build": "astro check && astro build", "preview": "astro preview", "astro": "astro", "format": "prettier --write .", "format:check": "prettier --check ." }, "dependencies": { "@astrojs/check": "^0.9.6", "@astrojs/starlight": "^0.37.6", "@astrojs/starlight-tailwind": "^4.0.2", "@fontsource-variable/figtree": "^5.2.6", "astro": "^5.17.1", "sharp": "^0.34.1", "tailwindcss": "^4.1.4", "typescript": "^5.7.3" }, "devDependencies": { "@shikijs/transformers": "^3.3.0", "@tailwindcss/vite": "^4.1.4", "prettier": "^3.5.3", "prettier-plugin-astro": "^0.14.1", "prettier-plugin-tailwindcss": "^0.6.11", "starlight-links-validator": "^0.16.0" } } ================================================ FILE: docs/public/CNAME ================================================ bloclibrary.dev ================================================ FILE: docs/src/components/architecture/AppIdeaRankingBlocSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class AppIdeaRankingBloc extends Bloc { AppIdeaRankingBloc({required AppIdeasRepository appIdeasRepo}) : _appIdeasRepo = appIdeasRepo, super(AppIdeaInitialRankingState()) { on((event, emit) async { // When we are told to start ranking app ideas, we will listen to the // stream of app ideas and emit a state for each one. await emit.forEach( _appIdeasRepo.productIdeas(), onData: (String idea) => AppIdeaRankingIdeaState(idea: idea), ); }); } final AppIdeasRepository _appIdeasRepo; } `; --- ================================================ FILE: docs/src/components/architecture/AppIdeasRepositorySnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class AppIdeasRepository { int _currentAppIdea = 0; final List _ideas = [ "Future prediction app that rewards you if you predict the next day's news", 'Dating app for fish that lets your aquarium occupants find true love', 'Social media app that pays you when your data is sold', 'JavaScript framework gambling app that lets you bet on the next big thing', 'Solitaire app that freezes before you can win', ]; Stream productIdeas() async* { while (true) { yield _ideas[_currentAppIdea++ % _ideas.length]; await Future.delayed(const Duration(minutes: 1)); } } } `; --- ================================================ FILE: docs/src/components/architecture/BlocLooseCouplingPresentationSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class MyWidget extends StatelessWidget { const MyWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { // When the first bloc's state changes, this will be called. // // Now we can add an event to the second bloc without it having // to know about the first bloc. context.read().add(SecondEvent()); }, child: TextButton( child: const Text('Hello'), onPressed: () { context.read().add(FirstEvent()); }, ), ); } } `; --- ================================================ FILE: docs/src/components/architecture/BlocTightCouplingSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class TightlyCoupledBloc extends Bloc { final OtherBloc otherBloc; late final StreamSubscription otherBlocSubscription; TightlyCoupledBloc(this.otherBloc) { // No matter how much you are tempted to do this, you should not do this! // Keep reading for better alternatives! otherBlocSubscription = otherBloc.stream.listen((state) { add(MyEvent()); }); } @override Future close() { otherBlocSubscription.cancel(); return super.close(); } } `; --- ================================================ FILE: docs/src/components/architecture/BusinessLogicComponentSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class BusinessLogicComponent extends Bloc { BusinessLogicComponent(this.repository) { on((event, emit) async { try { final data = await repository.getAllDataThatMeetsRequirements(); emit(Success(data)); } catch (error) { emit(Failure(error)); } }); } final Repository repository; } `; --- ================================================ FILE: docs/src/components/architecture/DataProviderSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class DataProvider { Future readData() async { // Read from DB or make network request etc... } } `; --- ================================================ FILE: docs/src/components/architecture/PresentationComponentSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class PresentationComponent { PresentationComponent({required this.bloc}) { bloc.add(AppStarted()); } final Bloc bloc; build() { // render UI based on bloc state } } `; --- ================================================ FILE: docs/src/components/architecture/RepositorySnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class Repository { final DataProviderA dataProviderA; final DataProviderB dataProviderB; Future getAllDataThatMeetsRequirements() async { final RawDataA dataSetA = await dataProviderA.readData(); final RawDataB dataSetB = await dataProviderB.readData(); final Data filteredData = _filterData(dataSetA, dataSetB); return filteredData; } } `; --- ================================================ FILE: docs/src/components/code/RemoteCode.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; import { z } from 'astro/zod'; interface Props { title: string; url: string; } const propsSchema = z.object({ title: z.string(), url: z.string(), }); const { url, title } = propsSchema.parse(Astro.props); const segments = url.split('.'); const lang = segments[segments.length - 1]; const response = await fetch(url); const code = await response.text(); --- ================================================ FILE: docs/src/components/concepts/bloc/AuthenticationChangeSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` Change { currentState: AuthenticationState.authenticated, nextState: AuthenticationState.unauthenticated } `; --- ================================================ FILE: docs/src/components/concepts/bloc/AuthenticationStateSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` enum AuthenticationState { unknown, authenticated, unauthenticated } `; --- ================================================ FILE: docs/src/components/concepts/bloc/AuthenticationTransitionSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` Transition { currentState: AuthenticationState.authenticated, event: LogoutRequested, nextState: AuthenticationState.unauthenticated } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CountStreamSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` Stream countStream(int max) async* { for (int i = 0; i < max; i++) { yield i; } } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterBlocEventHandlerSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class CounterEvent {} final class CounterIncrementPressed extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) { // handle incoming \`CounterIncrementPressed\` event }); } } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterBlocFullSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class CounterEvent {} final class CounterIncrementPressed extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterBlocIncrementSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class CounterEvent {} final class CounterIncrementPressed extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) { emit(state + 1); }); } } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterBlocOnChangeOutputSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` Change { currentState: 0, nextState: 1 } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterBlocOnChangeSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class CounterEvent {} final class CounterIncrementPressed extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } @override void onChange(Change change) { super.onChange(change); print(change); } } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterBlocOnChangeUsageSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` void main() { CounterBloc() ..add(CounterIncrementPressed()) ..close(); } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterBlocOnErrorOutputSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` Exception: increment error! #0 new CounterBloc. (file:///main.dart:10:58) #1 Bloc.on..handleEvent (package:bloc/src/bloc.dart:229:26) #2 Bloc.on. (package:bloc/src/bloc.dart:238:9) #3 _MapStream._handleData (dart:async/stream_pipe.dart:213:31) #4 _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:153:13) #5 _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10) #6 CastStreamSubscription._onData (dart:_internal/async_cast.dart:85:11) #7 _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10) #8 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11) #9 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7) #10 _ForwardingStreamSubscription._add (dart:async/stream_pipe.dart:123:11) #11 _WhereStream._handleData (dart:async/stream_pipe.dart:195:12) #12 _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:153:13) #13 _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10) #14 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11) #15 _DelayedData.perform (dart:async/stream_impl.dart:515:14) #16 _PendingEvents.handleNext (dart:async/stream_impl.dart:620:11) #17 _PendingEvents.schedule. (dart:async/stream_impl.dart:591:7) #18 _microtaskLoop (dart:async/schedule_microtask.dart:40:21) #19 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5) #20 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:118:13) #21 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:185:5) CounterBloc Exception: increment error! #0 new CounterBloc. (file:///main.dart:10:58) #1 Bloc.on..handleEvent (package:bloc/src/bloc.dart:229:26) #2 Bloc.on. (package:bloc/src/bloc.dart:238:9) #3 _MapStream._handleData (dart:async/stream_pipe.dart:213:31) #4 _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:153:13) #5 _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10) #6 CastStreamSubscription._onData (dart:_internal/async_cast.dart:85:11) #7 _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10) #8 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11) #9 _BufferingStreamSubscription._add (dart:async/stream_impl.dart:271:7) #10 _ForwardingStreamSubscription._add (dart:async/stream_pipe.dart:123:11) #11 _WhereStream._handleData (dart:async/stream_pipe.dart:195:12) #12 _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:153:13) #13 _RootZone.runUnaryGuarded (dart:async/zone.dart:1594:10) #14 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11) #15 _DelayedData.perform (dart:async/stream_impl.dart:515:14) #16 _PendingEvents.handleNext (dart:async/stream_impl.dart:620:11) #17 _PendingEvents.schedule. (dart:async/stream_impl.dart:591:7) #18 _microtaskLoop (dart:async/schedule_microtask.dart:40:21) #19 _startMicrotaskLoop (dart:async/schedule_microtask.dart:49:5) #20 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:118:13) #21 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:185:5) Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 } CounterBloc Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 } CounterBloc Change { currentState: 0, nextState: 1 } Change { currentState: 0, nextState: 1 } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterBlocOnErrorSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class CounterEvent {} final class CounterIncrementPressed extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) { addError(Exception('increment error!'), StackTrace.current); emit(state + 1); }); } @override void onChange(Change change) { super.onChange(change); print(change); } @override void onTransition(Transition transition) { print(transition); super.onTransition(transition); } @override void onError(Object error, StackTrace stackTrace) { print('$error, $stackTrace'); super.onError(error, stackTrace); } } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterBlocOnEventSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class CounterEvent {} final class CounterIncrementPressed extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } @override void onEvent(CounterEvent event) { super.onEvent(event); print(event); } @override void onChange(Change change) { super.onChange(change); print(change); } @override void onTransition(Transition transition) { super.onTransition(transition); print(transition); } } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterBlocOnTransitionOutputSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 } Change { currentState: 0, nextState: 1 } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterBlocOnTransitionSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class CounterEvent {} final class CounterIncrementPressed extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } @override void onChange(Change change) { super.onChange(change); print(change); } @override void onTransition(Transition transition) { super.onTransition(transition); print(transition); } } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterBlocSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class CounterEvent {} final class CounterIncrementPressed extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0); } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterBlocStreamUsageSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` Future main() async { final bloc = CounterBloc(); final subscription = bloc.stream.listen(print); // 1 bloc.add(CounterIncrementPressed()); await Future.delayed(Duration.zero); await subscription.cancel(); await bloc.close(); } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterBlocUsageSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` Future main() async { final bloc = CounterBloc(); print(bloc.state); // 0 bloc.add(CounterIncrementPressed()); await Future.delayed(Duration.zero); print(bloc.state); // 1 await bloc.close(); } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterCubitBasicUsageSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` void main() { final cubit = CounterCubit(); print(cubit.state); // 0 cubit.increment(); print(cubit.state); // 1 cubit.close(); } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterCubitFullSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class CounterCubit extends Cubit { CounterCubit() : super(0); void increment() => emit(state + 1); } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterCubitIncrementSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class CounterCubit extends Cubit { CounterCubit() : super(0); void increment() => emit(state + 1); } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterCubitInitialStateSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class CounterCubit extends Cubit { CounterCubit(int initialState) : super(initialState); } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterCubitInstantiationSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` final cubitA = CounterCubit(0); // state starts at 0 final cubitB = CounterCubit(10); // state starts at 10 `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterCubitOnChangeOutputSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` Change { currentState: 0, nextState: 1 } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterCubitOnChangeSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class CounterCubit extends Cubit { CounterCubit() : super(0); void increment() => emit(state + 1); @override void onChange(Change change) { super.onChange(change); print(change); } } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterCubitOnChangeUsageSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` void main() { CounterCubit() ..increment() ..close(); } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterCubitOnErrorOutputSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` Exception: increment error! #0 CounterCubit.increment (file:///main.dart:7:56) #1 main (file:///main.dart:41:7) #2 _delayEntrypointInvocation. (dart:isolate-patch/isolate_patch.dart:297:19) #3 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12) CounterCubit Exception: increment error! #0 CounterCubit.increment (file:///main.dart:7:56) #1 main (file:///main.dart:41:7) #2 _delayEntrypointInvocation. (dart:isolate-patch/isolate_patch.dart:297:19) #3 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12) CounterCubit Change { currentState: 0, nextState: 1 } Change { currentState: 0, nextState: 1 } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterCubitOnErrorSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class CounterCubit extends Cubit { CounterCubit() : super(0); void increment() { addError(Exception('increment error!'), StackTrace.current); emit(state + 1); } @override void onChange(Change change) { super.onChange(change); print(change); } @override void onError(Object error, StackTrace stackTrace) { print('$error, $stackTrace'); super.onError(error, stackTrace); } } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterCubitSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class CounterCubit extends Cubit { CounterCubit() : super(0); } `; --- ================================================ FILE: docs/src/components/concepts/bloc/CounterCubitStreamUsageSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` Future main() async { final cubit = CounterCubit(); final subscription = cubit.stream.listen(print); // 1 cubit.increment(); await Future.delayed(Duration.zero); await subscription.cancel(); await cubit.close(); } `; --- ================================================ FILE: docs/src/components/concepts/bloc/DebounceEventTransformerSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` EventTransformer debounce(Duration duration) { return (events, mapper) => events.debounceTime(duration).flatMap(mapper); } CounterBloc() : super(0) { on( (event, emit) => emit(state + 1), /// Apply the custom \`EventTransformer\` to the \`EventHandler\`. transformer: debounce(const Duration(milliseconds: 300)), ); } `; --- ================================================ FILE: docs/src/components/concepts/bloc/SimpleBlocObserverOnChangeOutputSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` CounterCubit Change { currentState: 0, nextState: 1 } Change { currentState: 0, nextState: 1 } `; --- ================================================ FILE: docs/src/components/concepts/bloc/SimpleBlocObserverOnChangeSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class SimpleBlocObserver extends BlocObserver { @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); print('\${bloc.runtimeType} $change'); } } `; --- ================================================ FILE: docs/src/components/concepts/bloc/SimpleBlocObserverOnChangeUsageSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` void main() { Bloc.observer = SimpleBlocObserver(); CounterCubit() ..increment() ..close(); } `; --- ================================================ FILE: docs/src/components/concepts/bloc/SimpleBlocObserverOnErrorSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class SimpleBlocObserver extends BlocObserver { @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); print('\${bloc.runtimeType} $change'); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { print('\${bloc.runtimeType} $error $stackTrace'); super.onError(bloc, error, stackTrace); } } `; --- ================================================ FILE: docs/src/components/concepts/bloc/SimpleBlocObserverOnEventOutputSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` CounterBloc Instance of 'CounterIncrementPressed' Instance of 'CounterIncrementPressed' CounterBloc Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 } Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 } CounterBloc Change { currentState: 0, nextState: 1 } Change { currentState: 0, nextState: 1 } `; --- ================================================ FILE: docs/src/components/concepts/bloc/SimpleBlocObserverOnEventSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class SimpleBlocObserver extends BlocObserver { @override void onEvent(Bloc bloc, Object? event) { super.onEvent(bloc, event); print('\${bloc.runtimeType} $event'); } @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); print('\${bloc.runtimeType} $change'); } @override void onTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition); print('\${bloc.runtimeType} $transition'); } } `; --- ================================================ FILE: docs/src/components/concepts/bloc/SimpleBlocObserverOnTransitionOutputSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` CounterBloc Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 } Transition { currentState: 0, event: Instance of 'CounterIncrementPressed', nextState: 1 } CounterBloc Change { currentState: 0, nextState: 1 } Change { currentState: 0, nextState: 1 } `; --- ================================================ FILE: docs/src/components/concepts/bloc/SimpleBlocObserverOnTransitionSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class SimpleBlocObserver extends BlocObserver { @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); print('\${bloc.runtimeType} $change'); } @override void onTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition); print('\${bloc.runtimeType} $transition'); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { print('\${bloc.runtimeType} $error $stackTrace'); super.onError(bloc, error, stackTrace); } } `; --- ================================================ FILE: docs/src/components/concepts/bloc/SimpleBlocObserverOnTransitionUsageSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` void main() { Bloc.observer = SimpleBlocObserver(); CounterBloc() ..add(CounterIncrementPressed()) ..close(); } `; --- ================================================ FILE: docs/src/components/concepts/bloc/StreamsMainSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` void main() async { /// Initialize a stream of integers 0-9 Stream stream = countStream(10); /// Compute the sum of the stream of integers int sum = await sumStream(stream); /// Print the sum print(sum); // 45 } `; --- ================================================ FILE: docs/src/components/concepts/bloc/SumStreamSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` Future sumStream(Stream stream) async { int sum = 0; await for (int value in stream) { sum += value; } return sum; } `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/BlocBuilderConditionSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` BlocBuilder( buildWhen: (previousState, state) { // return true/false to determine whether or not // to rebuild the widget with state }, builder: (context, state) { // return widget here based on BlocA's state }, ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/BlocBuilderExplicitBlocSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` BlocBuilder( bloc: blocA, // provide the local bloc instance builder: (context, state) { // return widget here based on BlocA's state }, ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/BlocBuilderSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` BlocBuilder( builder: (context, state) { // return widget here based on BlocA's state }, ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/BlocConsumerConditionSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` BlocConsumer( listenWhen: (previous, current) { // return true/false to determine whether or not // to invoke listener with state }, listener: (context, state) { // do stuff here based on BlocA's state }, buildWhen: (previous, current) { // return true/false to determine whether or not // to rebuild the widget with state }, builder: (context, state) { // return widget here based on BlocA's state }, ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/BlocConsumerSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` BlocConsumer( listener: (context, state) { // do stuff here based on BlocA's state }, builder: (context, state) { // return widget here based on BlocA's state }, ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/BlocListenerConditionSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` BlocListener( listenWhen: (previousState, state) { // return true/false to determine whether or not // to call listener with state }, listener: (context, state) { // do stuff here based on BlocA's state }, child: const SizedBox(), ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/BlocListenerExplicitBlocSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` BlocListener( bloc: blocA, listener: (context, state) { // do stuff here based on BlocA's state }, child: const SizedBox(), ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/BlocListenerSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` BlocListener( listener: (context, state) { // do stuff here based on BlocA's state }, child: const SizedBox(), ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/BlocProviderEagerSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` BlocProvider( lazy: false, create: (BuildContext context) => BlocA(), child: ChildA(), ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/BlocProviderLookupSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` // with extensions context.read(); // without extensions BlocProvider.of(context); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/BlocProviderSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` BlocProvider( create: (BuildContext context) => BlocA(), child: ChildA(), ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/BlocProviderValueSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` BlocProvider.value( value: BlocProvider.of(context), child: ScreenA(), ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/BlocSelectorSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` BlocSelector( selector: (state) { // return selected state based on the provided state. }, builder: (context, state) { // return widget here based on the selected state. }, ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/CounterBlocSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class CounterEvent {} final class CounterIncrementPressed extends CounterEvent {} final class CounterDecrementPressed extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); on((event, emit) => emit(state - 1)); } } `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/CounterMainSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` void main() => runApp(CounterApp()); class CounterApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: BlocProvider( create: (_) => CounterBloc(), child: CounterPage(), ), ); } } `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/CounterPageSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Counter')), body: BlocBuilder( builder: (context, count) { return Center( child: Text( '$count', style: TextStyle(fontSize: 24.0), ), ); }, ), floatingActionButton: Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end, children: [ Padding( padding: EdgeInsets.symmetric(vertical: 5.0), child: FloatingActionButton( child: Icon(Icons.add), onPressed: () => context.read().add(CounterIncrementPressed()), ), ), Padding( padding: EdgeInsets.symmetric(vertical: 5.0), child: FloatingActionButton( child: Icon(Icons.remove), onPressed: () => context.read().add(CounterDecrementPressed()), ), ), ], ), ); } } `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/MultiBlocListenerSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` MultiBlocListener( listeners: [ BlocListener( listener: (context, state) {}, ), BlocListener( listener: (context, state) {}, ), BlocListener( listener: (context, state) {}, ), ], child: ChildA(), ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/MultiBlocProviderSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` MultiBlocProvider( providers: [ BlocProvider( create: (BuildContext context) => BlocA(), ), BlocProvider( create: (BuildContext context) => BlocB(), ), BlocProvider( create: (BuildContext context) => BlocC(), ), ], child: ChildA(), ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/MultiRepositoryProviderSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` MultiRepositoryProvider( providers: [ RepositoryProvider( create: (context) => RepositoryA(), ), RepositoryProvider( create: (context) => RepositoryB(), ), RepositoryProvider( create: (context) => RepositoryC(), ), ], child: ChildA(), ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/NestedBlocListenerSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` BlocListener( listener: (context, state) {}, child: BlocListener( listener: (context, state) {}, child: BlocListener( listener: (context, state) {}, child: ChildA(), ), ), ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/NestedBlocProviderSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` BlocProvider( create: (BuildContext context) => BlocA(), child: BlocProvider( create: (BuildContext context) => BlocB(), child: BlocProvider( create: (BuildContext context) => BlocC(), child: ChildA(), ), ), ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/NestedRepositoryProviderSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` RepositoryProvider( create: (context) => RepositoryA(), child: RepositoryProvider( create: (context) => RepositoryB(), child: RepositoryProvider( create: (context) => RepositoryC(), child: ChildA(), ), ), ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/RepositoryProviderDisposeSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` RepositoryProvider( create: (context) => RepositoryA(), dispose: (repository) => repository.dispose(), child: ChildA(), ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/RepositoryProviderLookupSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` // with extensions context.read(); // without extensions RepositoryProvider.of(context) `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/RepositoryProviderSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` RepositoryProvider( create: (context) => RepositoryA(), child: ChildA(), ); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/WeatherAppSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:weather_repository/weather_repository.dart'; class WeatherApp extends StatelessWidget { const WeatherApp({super.key}); @override Widget build(BuildContext context) { return RepositoryProvider( create: (_) => WeatherRepository(), dispose: (repository) => repository.dispose(), child: BlocProvider( create: (context) => WeatherCubit(context.read()), child: const WeatherAppView(), ), ); } } `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/WeatherMainSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:flutter/material.dart'; import 'package:flutter_weather/app.dart'; void main() => runApp(const WeatherApp()); `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/WeatherPageSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_weather/weather/weather.dart'; import 'package:weather_repository/weather_repository.dart'; class WeatherPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( create: (context) => WeatherCubit(context.read()), child: WeatherView(), ); } } `; --- ================================================ FILE: docs/src/components/concepts/flutter-bloc/WeatherRepositorySnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class WeatherRepository { WeatherRepository({ WeatherApiClient? weatherApiClient }) : _weatherApiClient = weatherApiClient ?? WeatherApiClient(); final WeatherApiClient _weatherApiClient; Future getWeather(String city) async { final location = await _weatherApiClient.locationSearch(city); final woeid = location.woeid; final weather = await _weatherApiClient.getWeather(woeid); return Weather( temperature: weather.theTemp, location: location.title, condition: weather.weatherStateAbbr.toCondition, ); } void dispose() => _weatherApiClient.close(); } `; --- ================================================ FILE: docs/src/components/faqs/BlocExternalForEachSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class MyBloc extends Bloc { MyBloc({required UserRepository userRepository}) : _userRepository = userRepository, super(...) { on(_onStarted); } Future _onStarted(Started event, Emitter emit) { return emit.forEach( _userRepository.user, onData: (user) => MyState(...) ); } } `; --- ================================================ FILE: docs/src/components/faqs/BlocInternalAddEventSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class MyBloc extends Bloc { MyBloc({required UserRepository userRepository}) : super(...) { on<_UserChanged>(_onUserChanged); _userSubscription = userRepository.user.listen( (user) => add(_UserChanged(user)), ); } } `; --- ================================================ FILE: docs/src/components/faqs/BlocInternalEventSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class MyEvent {} // \`EventA\` is an external event. final class EventA extends MyEvent {} // \`EventB\` is an internal event. // We are explicitly making \`EventB\` private so that it can only be used // within the bloc. final class _EventB extends MyEvent {} `; --- ================================================ FILE: docs/src/components/faqs/BlocProviderBad1Snippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` @override Widget build(BuildContext context) { return BlocProvider( create: (_) => BlocA(), child: ElevatedButton( onPressed: () { final blocA = BlocProvider.of(context); ... } ) ); } `; --- ================================================ FILE: docs/src/components/faqs/BlocProviderGood1Snippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` @override Widget build(BuildContext context) { return BlocProvider( create: (_) => BlocA(), child: MyChild(); ); } class MyChild extends StatelessWidget { @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () { final blocA = BlocProvider.of(context); ... }, ) ... } } `; --- ================================================ FILE: docs/src/components/faqs/BlocProviderGood2Snippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` @override Widget build(BuildContext context) { return BlocProvider( create: (_) => BlocA(), child: Builder( builder: (context) => ElevatedButton( onPressed: () { final blocA = BlocProvider.of(context); ... }, ), ), ); } `; --- ================================================ FILE: docs/src/components/faqs/EquatableBlocTestSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` blocTest( '...', build: () => MyBloc(), act: (bloc) => bloc.add(MyEvent()), expect: [ MyStateA(), MyStateB(), ], ); `; --- ================================================ FILE: docs/src/components/faqs/EquatableEmitSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` MyBloc() { on((event, emit) { emit(StateA('hi')); emit(StateA('hi')); }); } `; --- ================================================ FILE: docs/src/components/faqs/NoEquatableBlocTestSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` blocTest( '...', build: () => MyBloc(), act: (bloc) => bloc.add(MyEvent()), expect: [ isA(), isA(), ], ); `; --- ================================================ FILE: docs/src/components/faqs/SingleStateSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` enum Status { initial, loading, success, failure } class MyState { const MyState({ this.data = Data.empty, this.error = '', this.status = Status.initial, }); final Data data; final String error; final Status status; MyState copyWith({Data data, String error, Status status}) { return MyState( data: data ?? this.data, error: error ?? this.error, status: status ?? this.status, ); } } `; --- ================================================ FILE: docs/src/components/faqs/SingleStateUsageSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` on((event, emit) { try { final data = await _repository.getData(); emit(state.copyWith(status: Status.success, data: data)); } catch(error) { emit(state.copyWith(status: Status.failure, error: 'Something went wrong!')); } }); `; --- ================================================ FILE: docs/src/components/faqs/StateNotUpdatingBad1Snippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class MyState extends Equatable { const MyState(); } final class StateA extends MyState { final String property; const StateA(this.property); @override List get props => []; } `; --- ================================================ FILE: docs/src/components/faqs/StateNotUpdatingBad2Snippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class MyState extends Equatable { const MyState(); } final class StateA extends MyState { final String property; const StateA(this.property); @override List get props => null; } `; --- ================================================ FILE: docs/src/components/faqs/StateNotUpdatingBad3Snippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` MyBloc() { on((event, emit) { // never modify/mutate state state.property = event.property; // never emit the same instance of state emit(state); }); } `; --- ================================================ FILE: docs/src/components/faqs/StateNotUpdatingGood1Snippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class MyState extends Equatable { const MyState(); } final class StateA extends MyState { final String property; const StateA(this.property); @override List get props => [property]; // pass all properties to props } `; --- ================================================ FILE: docs/src/components/faqs/StateNotUpdatingGood2Snippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` MyBloc() { on((event, emit) { // always create a new instance of the state you are going to yield emit(state.copyWith(property: event.property)); }); } `; --- ================================================ FILE: docs/src/components/faqs/StateNotUpdatingGood3Snippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` MyBloc() { on((event, emit) { final data = _getData(event.info); // always create a new instance of the state you are going to yield emit(MyState(data: data)); }); } `; --- ================================================ FILE: docs/src/components/getting-started/ImportTabs.astro ================================================ --- import { Code, Tabs, TabItem } from '@astrojs/starlight/components'; --- ================================================ FILE: docs/src/components/getting-started/InstallationTabs.astro ================================================ --- import { Code, Tabs, TabItem } from '@astrojs/starlight/components'; const installBloc = ` # Add bloc to your project. dart pub add bloc `; const installFlutterBloc = ` # Add flutter_bloc to your project. flutter pub add flutter_bloc `; const installAngularBloc = ` # Add angular_bloc to your project. dart pub add angular_bloc `; --- ================================================ FILE: docs/src/components/landing/Card.astro ================================================ --- import { Card as StarlightCard } from '@astrojs/starlight/components'; export type Props = Parameters[0]; ---
================================================ FILE: docs/src/components/landing/Discord.astro ================================================ --- export type Props = { joinDiscord: string; }; import { LinkCard } from '@astrojs/starlight/components'; import { Image } from 'astro:assets'; import BlocLogo from '~/assets/bloc.svg'; const { joinDiscord = 'Join our Discord' } = Astro.props; ---
bloc logo
================================================ FILE: docs/src/components/landing/ListCard.astro ================================================ --- import Card from './Card.astro'; export type Props = Parameters[0]; --- ================================================ FILE: docs/src/components/landing/SplitCard.astro ================================================ --- import { Card } from '@astrojs/starlight/components'; export type Props = Parameters[0]; ---
================================================ FILE: docs/src/components/landing/SponsorsGrid.astro ================================================ --- export type Props = { sponsoredBy: string; becomeASponsor: string; }; import { Image } from 'astro:assets'; import { LinkCard } from '@astrojs/starlight/components'; import shorebirdLight from '~/assets/sponsors/shorebird-light.png'; import shorebirdDark from '~/assets/sponsors/shorebird-dark.png'; import stream from '~/assets/sponsors/stream.png'; import rettel from '~/assets/sponsors/rettel.png'; const { sponsoredBy = 'Sponsored with 💖 by', becomeASponsor = 'Become a Sponsor' } = Astro.props; interface DynamicImageMetadata { light: ImageMetadata; dark: ImageMetadata; } interface Sponsor { img: DynamicImageMetadata | ImageMetadata; href: string; alt: string; invert: boolean; } const sponsors: Sponsor[] = [ { alt: 'Shorebird Logo', img: { light: shorebirdLight, dark: shorebirdDark, }, href: 'https://shorebird.dev', invert: false, }, { alt: 'Stream Logo', img: stream, href: 'https://getstream.io/chat/flutter/tutorial/?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Jan2022_FlutterChat&utm_term=bloc', invert: true, }, { alt: 'Rettel Logo', img: rettel, href: 'https://rettelgame.com', invert: true, }, ]; function isDynamicImage( value: DynamicImageMetadata | ImageMetadata ): value is DynamicImageMetadata { return value.hasOwnProperty('light') && value.hasOwnProperty('dark'); } ---

{sponsoredBy}

{ sponsors.map((sponsor) => { return ( ); }) }
================================================ FILE: docs/src/components/lint/BlocLintBasicAnalysisOptionsSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` bloc: rules: - avoid_flutter_imports `; --- ================================================ FILE: docs/src/components/lint/BlocLintChangingSeveritySnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` bloc: rules: avoid_flutter_imports: info `; --- ================================================ FILE: docs/src/components/lint/BlocLintDisablingRulesSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` include: package:bloc_lint/recommended.yaml bloc: rules: avoid_public_bloc_methods: false prefer_bloc: true `; --- ================================================ FILE: docs/src/components/lint/BlocLintEnablingRulesSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` bloc: rules: - avoid_flutter_imports - avoid_public_bloc_methods `; --- ================================================ FILE: docs/src/components/lint/BlocLintExcludingFilesSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` include: package:bloc_lint/recommended.yaml analyzer: exclude: - "**.g.dart" `; --- ================================================ FILE: docs/src/components/lint/BlocLintIgnoreForFileSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` // ignore_for_file: prefer_file_naming_conventions import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit {} enum CounterEvent { increment, decrement } class CounterBloc extends Bloc {} `; --- ================================================ FILE: docs/src/components/lint/BlocLintIgnoreForLineSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit {} // ignore: prefer_file_naming_conventions enum CounterEvent { increment, decrement } // ignore: prefer_file_naming_conventions class CounterBloc extends Bloc {} `; --- ================================================ FILE: docs/src/components/lint/BlocLintMultipleRecommendedAnalysisOptionsSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` include: - package:lints/recommended.yaml - package:bloc_lint/recommended.yaml `; --- ================================================ FILE: docs/src/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` include: package:bloc_lint/recommended.yaml `; --- ================================================ FILE: docs/src/components/lint/BlocToolsLintHelpOutputSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` $ bloc lint --help Lint Dart source code. Usage: bloc lint [arguments] -h, --help Print this usage information. Run "bloc help" to see global options. `; --- ================================================ FILE: docs/src/components/lint/ImportFlutterInfoOutputSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` info[avoid_flutter_imports]: Avoid importing Flutter within bloc instances. --> counter_cubit.dart:2 | | import 'package:flutter/material.dart'; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ = hint: Blocs should be decoupled from Flutter. docs: https://bloclibrary.dev/lint-rules/avoid_flutter_imports 1 issue found Analyzed 1 file `; --- ================================================ FILE: docs/src/components/lint/ImportFlutterInfoSnippet.mdx ================================================ import { Code } from '@astrojs/starlight/components'; import { transformerMetaHighlight } from '@shikijs/transformers'; { CounterCubit() : super(0); } `} lang="dart" title="counter_cubit.dart" transformers={[transformerMetaHighlight()]} class="info" meta="{2}" /> ================================================ FILE: docs/src/components/lint/ImportFlutterWarningOutputSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` warning[avoid_flutter_imports]: Avoid importing Flutter within bloc instances. --> counter_cubit.dart:2 | | import 'package:flutter/material.dart'; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ = hint: Blocs should be decoupled from Flutter. docs: https://bloclibrary.dev/lint-rules/avoid_flutter_imports 1 issue found Analyzed 1 file `; --- ================================================ FILE: docs/src/components/lint/ImportFlutterWarningSnippet.mdx ================================================ import { Code } from '@astrojs/starlight/components'; import { transformerMetaHighlight } from '@shikijs/transformers'; { CounterCubit() : super(0); } `} lang="dart" title="counter_cubit.dart" transformers={[transformerMetaHighlight()]} class="warning" meta="{2}" /> ================================================ FILE: docs/src/components/lint/InstallBlocLintSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = `dart pub add --dev bloc_lint`; --- ================================================ FILE: docs/src/components/lint/InstallBlocToolsSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = `dart pub global activate bloc_tools`; --- ================================================ FILE: docs/src/components/lint/RunBlocLintCounterCubitSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = `bloc lint counter_cubit.dart`; --- ================================================ FILE: docs/src/components/lint/RunBlocLintInCurrentDirectorySnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = `bloc lint .`; --- ================================================ FILE: docs/src/components/lint/RunBlocLintInSrcTestSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = `bloc lint src/ test/`; --- ================================================ FILE: docs/src/components/lint-rules/EnableRuleSnippet.astro ================================================ --- import { Code, TabItem, Tabs } from '@astrojs/starlight/components'; const { name } = Astro.props; const yamlListCode = ` bloc: rules: - ${name} `; const yamlMapCode = ` bloc: rules: ${name}: true `; --- ================================================ FILE: docs/src/components/lint-rules/avoid_build_context_extensions/BadSnippet.mdx ================================================ import { Code } from '@astrojs/starlight/components'; import { transformerMetaHighlight } from '@shikijs/transformers'; ().state; return Column( children: [ Text('Count: $count'), ElevatedButton( onPressed: () { context.read().add(CounterIncrement()); }, child: const Text('Increment'), ), ], ); } } `} lang="dart" title="counter_page.dart" transformers={[transformerMetaHighlight()]} class='warning' meta="{9,16}" /> ================================================ FILE: docs/src/components/lint-rules/avoid_build_context_extensions/GoodSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CounterPage extends StatelessWidget { const CounterPage({super.key}); @override Widget build(BuildContext context) { return Column( children: [ BlocBuilder( builder: (context, state) => Text('Count: \$state'), ), ElevatedButton( onPressed: () { BlocProvider.of(context).add(CounterIncrement()); }, child: const Text('Increment'), ), ], ); } } `; --- ================================================ FILE: docs/src/components/lint-rules/avoid_flutter_imports/BadSnippet.mdx ================================================ import { Code } from '@astrojs/starlight/components'; import { transformerMetaHighlight } from '@shikijs/transformers'; { CounterCubit() : super(0); } `} lang="dart" title="counter_cubit.dart" transformers={[transformerMetaHighlight()]} class='warning' meta="{5}" /> ================================================ FILE: docs/src/components/lint-rules/avoid_flutter_imports/GoodSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); } `; --- ================================================ FILE: docs/src/components/lint-rules/avoid_public_bloc_methods/BadSnippet.mdx ================================================ import { Code } from '@astrojs/starlight/components'; import { transformerMetaHighlight } from '@shikijs/transformers'; { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } // Avoid public bloc methods! // Prefer to use [add] directly. void increment() => add(CounterEvent.increment); } `} lang="dart" title="counter_bloc.dart" transformers={[transformerMetaHighlight()]} class='warning' meta="{12}" /> ================================================ FILE: docs/src/components/lint-rules/avoid_public_bloc_methods/GoodSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:bloc/bloc.dart'; enum CounterEvent { increment }; class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } } `; --- ================================================ FILE: docs/src/components/lint-rules/avoid_public_fields/BadSnippet.mdx ================================================ import { Code } from '@astrojs/starlight/components'; import { transformerMetaHighlight } from '@shikijs/transformers'; { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } // Avoid public fields! // Prefer to keep all external state in the [state] object. int count = 0; } `} lang="dart" title="counter_bloc.dart" transformers={[transformerMetaHighlight()]} class='warning' meta="{12}" /> ================================================ FILE: docs/src/components/lint-rules/avoid_public_fields/GoodSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:bloc/bloc.dart'; enum CounterEvent { increment }; class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } } `; --- ================================================ FILE: docs/src/components/lint-rules/prefer_bloc/BadSnippet.mdx ================================================ import { Code } from '@astrojs/starlight/components'; import { transformerMetaHighlight } from '@shikijs/transformers'; { CounterCubit() : super(0); void increment() => emit(state + 1); } `} lang="dart" title="counter_cubit.dart" transformers={[transformerMetaHighlight()]} class='warning' meta="{3}" /> ================================================ FILE: docs/src/components/lint-rules/prefer_bloc/GoodSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:bloc/bloc.dart'; enum CounterEvent { increment }; class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } } `; --- ================================================ FILE: docs/src/components/lint-rules/prefer_build_context_extensions/BadSnippet.mdx ================================================ import { Code } from '@astrojs/starlight/components'; import { transformerMetaHighlight } from '@shikijs/transformers'; ( builder: (context, state) => Text('Count: \$state'), ), ElevatedButton( onPressed: () { BlocProvider.of(context).add(CounterIncrement()); }, child: const Text('Increment'), ), ], ); } } `} lang="dart" title="counter_page.dart" transformers={[transformerMetaHighlight()]} class='warning' meta="{11,16}" /> ================================================ FILE: docs/src/components/lint-rules/prefer_build_context_extensions/GoodSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CounterPage extends StatelessWidget { const CounterPage({super.key}); @override Widget build(BuildContext context) { final count = context.watch().state; return Column( children: [ Text('Count: $count'), ElevatedButton( onPressed: () { context.read().add(CounterIncrement()); }, child: const Text('Increment'), ), ], ); } } `; --- ================================================ FILE: docs/src/components/lint-rules/prefer_cubit/BadSnippet.mdx ================================================ import { Code } from '@astrojs/starlight/components'; import { transformerMetaHighlight } from '@shikijs/transformers'; { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } } `} lang="dart" title="counter_bloc.dart" transformers={[transformerMetaHighlight()]} class='warning' meta="{5}" /> ================================================ FILE: docs/src/components/lint-rules/prefer_cubit/GoodSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); void increment() => emit(state + 1); } `; --- ================================================ FILE: docs/src/components/lint-rules/prefer_file_naming_conventions/BadSnippet.mdx ================================================ import { Code } from '@astrojs/starlight/components'; import { transformerMetaHighlight } from '@shikijs/transformers'; { CounterCubit() : super(0); int increment() { emit(state + 1); return state; } } `} lang="dart" title="main.dart" transformers={[transformerMetaHighlight()]} class='warning' meta="{3}" /> ================================================ FILE: docs/src/components/lint-rules/prefer_file_naming_conventions/GoodSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); void increment() => emit(state + 1); } `; --- ================================================ FILE: docs/src/components/lint-rules/prefer_void_public_cubit_methods/BadSnippet.mdx ================================================ import { Code } from '@astrojs/starlight/components'; import { transformerMetaHighlight } from '@shikijs/transformers'; { CounterCubit() : super(0); int increment() { emit(state + 1); return state; } } `} lang="dart" title="counter_cubit.dart" transformers={[transformerMetaHighlight()]} class='warning' meta="{6}" /> ================================================ FILE: docs/src/components/lint-rules/prefer_void_public_cubit_methods/GoodSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); void increment() => emit(state + 1); } `; --- ================================================ FILE: docs/src/components/modeling-state/ConcreteClassAndStatusEnumSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` enum TodoStatus { initial, loading, success, failure } final class TodoState { const TodoState({ this.status = TodoStatus.initial, this.todos = const [], this.exception = null, }); final TodoStatus status; final List todos; final Exception? exception; } `; --- ================================================ FILE: docs/src/components/modeling-state/SealedClassAndSubclassesSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class WeatherState { const WeatherState(); } final class WeatherInitial extends WeatherState { const WeatherInitial(); } final class WeatherLoadInProgress extends WeatherState { const WeatherLoadInProgress(); } final class WeatherLoadSuccess extends WeatherState { const WeatherLoadSuccess({required this.weather}); final Weather weather; } final class WeatherLoadFailure extends WeatherState { const WeatherLoadFailure({required this.exception}); final Exception exception; } `; --- ================================================ FILE: docs/src/components/naming-conventions/EventExamplesBad1Snippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class CounterEvent {} final class Initial extends CounterEvent {} final class CounterInitialized extends CounterEvent {} final class Increment extends CounterEvent {} final class DoIncrement extends CounterEvent {} final class IncrementCounter extends CounterEvent {} `; --- ================================================ FILE: docs/src/components/naming-conventions/EventExamplesGood1Snippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class CounterEvent {} final class CounterStarted extends CounterEvent {} final class CounterIncrementPressed extends CounterEvent {} final class CounterDecrementPressed extends CounterEvent {} final class CounterIncrementRetried extends CounterEvent {} `; --- ================================================ FILE: docs/src/components/naming-conventions/SingleStateExamplesGood1Snippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` enum CounterStatus { initial, loading, success, failure } final class CounterState { const CounterState({this.status = CounterStatus.initial}); final CounterStatus status; } `; --- ================================================ FILE: docs/src/components/naming-conventions/StateExamplesBad1Snippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class CounterState {} final class Initial extends CounterState {} final class Loading extends CounterState {} final class Success extends CounterState {} final class Succeeded extends CounterState {} final class Loaded extends CounterState {} final class Failure extends CounterState {} final class Failed extends CounterState {} `; --- ================================================ FILE: docs/src/components/naming-conventions/StateExamplesGood1Snippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class CounterState {} final class CounterInitial extends CounterState {} final class CounterLoadInProgress extends CounterState {} final class CounterLoadSuccess extends CounterState {} final class CounterLoadFailure extends CounterState {} `; --- ================================================ FILE: docs/src/components/testing/AddDevDependenciesSnippet.astro ================================================ --- import { Code, Tabs, TabItem } from '@astrojs/starlight/components'; const dartSnippet = ` dart pub add dev:test dev:bloc_test `; const flutterSnippet = ` flutter pub add dev:test dev:bloc_test `; --- ================================================ FILE: docs/src/components/testing/CounterBlocSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` sealed class CounterEvent {} final class CounterIncrementPressed extends CounterEvent {} final class CounterDecrementPressed extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); on((event, emit) => emit(state - 1)); } } `; --- ================================================ FILE: docs/src/components/testing/CounterBlocTestBlocTestSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` blocTest( 'emits [1] when CounterIncrementPressed is added', build: () => counterBloc, act: (bloc) => bloc.add(CounterIncrementPressed()), expect: () => [1], ); blocTest( 'emits [-1] when CounterDecrementPressed is added', build: () => counterBloc, act: (bloc) => bloc.add(CounterDecrementPressed()), expect: () => [-1], ); `; --- ================================================ FILE: docs/src/components/testing/CounterBlocTestImportsSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:test/test.dart'; import 'package:bloc_test/bloc_test.dart'; `; --- ================================================ FILE: docs/src/components/testing/CounterBlocTestInitialStateSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` group(CounterBloc, () { late CounterBloc counterBloc; setUp(() { counterBloc = CounterBloc(); }); test('initial state is 0', () { expect(counterBloc.state, equals(0)); }); }); `; --- ================================================ FILE: docs/src/components/testing/CounterBlocTestMainSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` void main() { group(CounterBloc, () { }); } `; --- ================================================ FILE: docs/src/components/testing/CounterBlocTestSetupSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` group(CounterBloc, () { late CounterBloc counterBloc; setUp(() { counterBloc = CounterBloc(); }); }); `; --- ================================================ FILE: docs/src/components/tutorials/FlutterPubGetSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter pub get `; --- ================================================ FILE: docs/src/components/tutorials/flutter-counter/FlutterCreateSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter create flutter_counter `; --- ================================================ FILE: docs/src/components/tutorials/flutter-firebase-login/FlutterCreateSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter create flutter_firebase_login `; --- ================================================ FILE: docs/src/components/tutorials/flutter-infinite-list/FlutterCreateSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter create flutter_infinite_list `; --- ================================================ FILE: docs/src/components/tutorials/flutter-infinite-list/FlutterPubGetSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter pub get `; --- ================================================ FILE: docs/src/components/tutorials/flutter-infinite-list/PostBlocInitialStateSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import 'package:http/http.dart' as http; import 'package:flutter_infinite_list/bloc/bloc.dart'; import 'package:flutter_infinite_list/post.dart'; part 'post_event.dart'; part 'post_state.dart'; class PostBloc extends Bloc { PostBloc({required this.httpClient}) : super(const PostState()) { /// TODO: register on event } final http.Client httpClient; } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-infinite-list/PostBlocOnPostFetchedSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` PostBloc({required this.httpClient}) : super(const PostState()) { on(_onFetched); } Future _onFetched(PostFetched event, Emitter emit) async { if (state.hasReachedMax) return; try { final posts = await _fetchPosts(startIndex: state.posts.length); if (posts.isEmpty) { return emit(state.copyWith(hasReachedMax: true)); } emit( state.copyWith( status: PostStatus.success, posts: [...state.posts, ...posts], ), ); } catch (_) { emit(state.copyWith(status: PostStatus.failure)); } } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-infinite-list/PostBlocTransformerSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:stream_transform/stream_transform.dart'; const throttleDuration = Duration(milliseconds: 100); EventTransformer throttleDroppable(Duration duration) { return (events, mapper) { return droppable().call(events.throttle(duration), mapper); }; } class PostBloc extends Bloc { PostBloc({required this.httpClient}) : super(const PostState()) { on( _onFetched, transformer: throttleDroppable(throttleDuration), ); } } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-infinite-list/PostsJsonSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` [ { "userId": 1, "id": 1, "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" }, { "userId": 1, "id": 2, "title": "qui est esse", "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla" } ] `; --- ================================================ FILE: docs/src/components/tutorials/flutter-login/FlutterCreateSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter create flutter_login `; --- ================================================ FILE: docs/src/components/tutorials/flutter-timer/ActionsSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class Actions extends StatelessWidget { const Actions({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (prev, state) => prev.runtimeType != state.runtimeType, builder: (context, state) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ...switch (state) { TimerInitial() => [ FloatingActionButton( child: const Icon(Icons.play_arrow), onPressed: () => context .read() .add(TimerStarted(duration: state.duration)), ), ], TimerRunInProgress() => [ FloatingActionButton( child: const Icon(Icons.pause), onPressed: () => context.read().add(const TimerPaused()), ), FloatingActionButton( child: const Icon(Icons.replay), onPressed: () => context.read().add(const TimerReset()), ), ], TimerRunPause() => [ FloatingActionButton( child: const Icon(Icons.play_arrow), onPressed: () => context.read().add(const TimerResumed()), ), FloatingActionButton( child: const Icon(Icons.replay), onPressed: () => context.read().add(const TimerReset()), ), ], TimerRunComplete() => [ FloatingActionButton( child: const Icon(Icons.replay), onPressed: () => context.read().add(const TimerReset()), ), ] } ], ); }, ); } } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-timer/BackgroundSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class Background extends StatelessWidget { const Background({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.blue.shade50, Colors.blue.shade500, ], ), ), ); } } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-timer/FlutterCreateSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter create flutter_timer `; --- ================================================ FILE: docs/src/components/tutorials/flutter-timer/TimerBlocEmptySnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:bloc/bloc.dart'; part 'timer_event.dart'; part 'timer_state.dart'; class TimerBloc extends Bloc { // TODO: set initial state TimerBloc(): super() { // TODO: implement event handlers } } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-timer/TimerBlocInitialStateSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:bloc/bloc.dart'; part 'timer_event.dart'; part 'timer_state.dart'; class TimerBloc extends Bloc { static const int _duration = 60; TimerBloc() : super(TimerInitial(_duration)) { // TODO: implement event handlers } } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-timer/TimerBlocOnPausedSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:flutter_timer/ticker.dart'; part 'timer_event.dart'; part 'timer_state.dart'; class TimerBloc extends Bloc { final Ticker _ticker; static const int _duration = 60; StreamSubscription? _tickerSubscription; TimerBloc({required Ticker ticker}) : _ticker = ticker, super(TimerInitial(_duration)) { on(_onStarted); on(_onPaused); on<_TimerTicked>(_onTicked); } @override Future close() { _tickerSubscription?.cancel(); return super.close(); } void _onStarted(TimerStarted event, Emitter emit) { emit(TimerRunInProgress(event.duration)); _tickerSubscription?.cancel(); _tickerSubscription = _ticker .tick(ticks: event.duration) .listen((duration) => add(_TimerTicked(duration: duration))); } void _onPaused(TimerPaused event, Emitter emit) { if (state is TimerRunInProgress) { _tickerSubscription?.pause(); emit(TimerRunPause(state.duration)); } } void _onTicked(_TimerTicked event, Emitter emit) { emit( event.duration > 0 ? TimerRunInProgress(event.duration) : TimerRunComplete(), ); } } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-timer/TimerBlocOnResumedSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:flutter_timer/ticker.dart'; part 'timer_event.dart'; part 'timer_state.dart'; class TimerBloc extends Bloc { final Ticker _ticker; static const int _duration = 60; StreamSubscription? _tickerSubscription; TimerBloc({required Ticker ticker}) : _ticker = ticker, super(TimerInitial(_duration)) { on(_onStarted); on(_onPaused); on(_onResumed); on<_TimerTicked>(_onTicked); } @override Future close() { _tickerSubscription?.cancel(); return super.close(); } void _onStarted(TimerStarted event, Emitter emit) { emit(TimerRunInProgress(event.duration)); _tickerSubscription?.cancel(); _tickerSubscription = _ticker .tick(ticks: event.duration) .listen((duration) => add(_TimerTicked(duration: duration))); } void _onPaused(TimerPaused event, Emitter emit) { if (state is TimerRunInProgress) { _tickerSubscription?.pause(); emit(TimerRunPause(state.duration)); } } void _onResumed(TimerResumed resume, Emitter emit) { if (state is TimerRunPause) { _tickerSubscription?.resume(); emit(TimerRunInProgress(state.duration)); } } void _onTicked(_TimerTicked event, Emitter emit) { emit( event.duration > 0 ? TimerRunInProgress(event.duration) : TimerRunComplete(), ); } } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-timer/TimerBlocOnStartedSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:flutter_timer/ticker.dart'; part 'timer_event.dart'; part 'timer_state.dart'; class TimerBloc extends Bloc { final Ticker _ticker; static const int _duration = 60; StreamSubscription? _tickerSubscription; TimerBloc({required Ticker ticker}) : _ticker = ticker, super(TimerInitial(_duration)) { on(_onStarted); } @override Future close() { _tickerSubscription?.cancel(); return super.close(); } void _onStarted(TimerStarted event, Emitter emit) { emit(TimerRunInProgress(event.duration)); _tickerSubscription?.cancel(); _tickerSubscription = _ticker .tick(ticks: event.duration) .listen((duration) => add(_TimerTicked(duration: duration))); } } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-timer/TimerBlocOnTickedSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:flutter_timer/ticker.dart'; part 'timer_event.dart'; part 'timer_state.dart'; class TimerBloc extends Bloc { final Ticker _ticker; static const int _duration = 60; StreamSubscription? _tickerSubscription; TimerBloc({required Ticker ticker}) : _ticker = ticker, super(TimerInitial(_duration)) { on(_onStarted); on<_TimerTicked>(_onTicked); } @override Future close() { _tickerSubscription?.cancel(); return super.close(); } void _onStarted(TimerStarted event, Emitter emit) { emit(TimerRunInProgress(event.duration)); _tickerSubscription?.cancel(); _tickerSubscription = _ticker .tick(ticks: event.duration) .listen((duration) => add(_TimerTicked(duration: duration))); } void _onTicked(_TimerTicked event, Emitter emit) { emit( event.duration > 0 ? TimerRunInProgress(event.duration) : TimerRunComplete(), ); } } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-timer/TimerBlocTickerSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:flutter_timer/ticker.dart'; part 'timer_event.dart'; part 'timer_state.dart'; class TimerBloc extends Bloc { final Ticker _ticker; static const int _duration = 60; StreamSubscription? _tickerSubscription; TimerBloc({required Ticker ticker}) : _ticker = ticker, super(TimerInitial(_duration)) { // TODO: implement event handlers } } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-timer/TimerPageSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_timer/ticker.dart'; import 'package:flutter_timer/timer/timer.dart'; class TimerPage extends StatelessWidget { const TimerPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => TimerBloc(ticker: Ticker()), child: const TimerView(), ); } } class TimerView extends StatelessWidget { const TimerView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Flutter Timer')), body: Stack( children: [ const Background(), Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: const [ Padding( padding: EdgeInsets.symmetric(vertical: 100.0), child: Center(child: TimerText()), ), Actions(), ], ), ], ), ); } } class TimerText extends StatelessWidget { const TimerText({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final duration = context.select((TimerBloc bloc) => bloc.state.duration); final minutesStr = ((duration / 60) % 60).floor().toString().padLeft(2, '0'); final secondsStr = (duration % 60).floor().toString().padLeft(2, '0'); return Text( '$minutesStr:$secondsStr', style: Theme.of( context, ).textTheme.displayLarge?.copyWith(fontWeight: FontWeight.w500), ); } } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-todos/ActivateVeryGoodCLISnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` dart pub global activate very_good_cli `; --- ================================================ FILE: docs/src/components/tutorials/flutter-todos/EditTodosPageTreeSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` ├── BlocProvider │ └── EditTodosPage │ └── BlocListener │ └── EditTodosView │ ├── TitleField │ ├── DescriptionField │ └── Floating Action Button `; --- ================================================ FILE: docs/src/components/tutorials/flutter-todos/FlutterCreatePackagesSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` # create package:todos_api under packages/todos_api very_good create dart_package todos_api --desc "The interface and models for an API providing access to todos." -o packages # create package:local_storage_todos_api under packages/local_storage_todos_api very_good create flutter_package local_storage_todos_api --desc "A Flutter implementation of the TodosApi that uses local storage." -o packages # create package:todos_repository under packages/todos_repository very_good create dart_package todos_repository --desc "A repository that handles todo related requests." -o packages `; --- ================================================ FILE: docs/src/components/tutorials/flutter-todos/FlutterCreateSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` very_good create flutter_app flutter_todos --desc "An example todos app that showcases bloc state management patterns." `; --- ================================================ FILE: docs/src/components/tutorials/flutter-todos/HomePageTreeSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` ├── HomePage │ └── BlocProvider │ └── HomeView │ ├── context.select │ └── BottomAppBar │ └── HomeTabButton(s) │ └── context.read `; --- ================================================ FILE: docs/src/components/tutorials/flutter-todos/ProjectStructureSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` ├── lib ├── packages │ ├── local_storage_todos_api │ ├── todos_api │ └── todos_repository └── test `; --- ================================================ FILE: docs/src/components/tutorials/flutter-todos/StatsPageTreeSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` ├── StatsPage │ └── BlocProvider │ └── StatsView │ ├── context.watch │ └── Column `; --- ================================================ FILE: docs/src/components/tutorials/flutter-todos/TodosOverviewPageTreeSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` ├── TodosOverviewPage │ └── BlocProvider │ └── TodosOverviewView │ ├── BlocListener │ └── BlocListener │ └── BlocBuilder │ └── ListView `; --- ================================================ FILE: docs/src/components/tutorials/flutter-todos/VeryGoodPackagesGetSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` very_good packages get --recursive `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/BuildRunnerBuildSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` dart run build_runner build `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/FeatureTreeSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter_weather |-- lib/ |-- search/ |-- settings/ |-- theme/ |-- weather/ |-- main.dart |-- test/ `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/FlutterCreateApiClientSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter create --template=package open_meteo_api `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/FlutterCreateRepositorySnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter create --template=package weather_repository `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/FlutterCreateSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter create flutter_weather `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/FlutterTestCoverageSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter test --coverage genhtml coverage/lcov.info -o coverage open coverage/index.html `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/GetWeatherMethodSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` /// Fetches [Weather] for a given [latitude] and [longitude]. Future getWeather({ required double latitude, required double longitude, }) async { final weatherRequest = Uri.https(_baseUrlWeather, 'v1/forecast', { 'latitude': '$latitude', 'longitude': '$longitude', 'current_weather': 'true' }); final weatherResponse = await _httpClient.get(weatherRequest); if (weatherResponse.statusCode != 200) { throw WeatherRequestFailure(); } final bodyJson = jsonDecode(weatherResponse.body) as Map; if (!bodyJson.containsKey('current_weather')) { throw WeatherNotFoundFailure(); } final weatherJson = bodyJson['current_weather'] as Map; return Weather.fromJson(weatherJson); } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/LocationDartSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class Location { const Location({ required this.id, required this.name, required this.latitude, required this.longitude, }); final int id; final String name; final double latitude; final double longitude; } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/LocationJsonSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` { "results": [ { "id": 4887398, "name": "Chicago", "latitude": 41.85003, "longitude": -87.65005 } ] } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/LocationSearchMethodSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` /// Finds a [Location] \`/v1/search/?name=(query)\`. Future locationSearch(String query) async { final locationRequest = Uri.https( _baseUrlGeocoding, '/v1/search', {'name': query, 'count': '1'}, ); final locationResponse = await _httpClient.get(locationRequest); if (locationResponse.statusCode != 200) { throw LocationRequestFailure(); } final locationJson = jsonDecode(locationResponse.body) as Map; if (!locationJson.containsKey('results')) throw LocationNotFoundFailure(); final results = locationJson['results'] as List; if (results.isEmpty) throw LocationNotFoundFailure(); return Location.fromJson(results.first as Map); } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/OpenMeteoApiClientTreeSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter_weather |-- lib/ |-- test/ |-- packages/ |-- open_meteo_api/ |-- lib/ |-- src/ |-- models/ |-- location.dart |-- location.g.dart |-- weather.dart |-- weather.g.dart |-- models.dart |-- open_meteo_api_client.dart |-- open_meteo_api.dart |-- test/ `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/OpenMeteoLibrarySnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` library open_meteo_api; export 'src/models/models.dart'; `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/OpenMeteoModelsBarrelTreeSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter_weather |-- lib/ |-- test/ |-- packages/ |-- open_meteo_api/ |-- lib/ |-- src/ |-- models/ |-- location.dart |-- weather.dart |-- models.dart |-- open_meteo_api.dart |-- test/ `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/OpenMeteoModelsTreeSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter_weather |-- lib/ |-- test/ |-- packages/ |-- open_meteo_api/ |-- lib/ |-- src/ |-- models/ |-- location.dart |-- weather.dart |-- test/ `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/RepositoryModelsBarrelTreeSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter_weather |-- lib/ |-- test/ |-- packages/ |-- open_meteo_api/ |-- weather_repository/ |-- lib/ |-- src/ |-- models/ |-- models.dart |-- weather.dart |-- test/ `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/WeatherBarrelDartSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` export 'models/models.dart'; `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/WeatherCubitTreeSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter_weather |-- lib/ |-- weather/ |-- cubit/ |-- weather_cubit.dart |-- weather_state.dart `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/WeatherDartSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` class Weather { const Weather({required this.temperature, required this.weatherCode}); final double temperature; final double weatherCode; } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/WeatherJsonSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` { "current_weather": { "temperature": 15.3, "weathercode": 63 } } `; --- ================================================ FILE: docs/src/components/tutorials/flutter-weather/WeatherRepositoryLibrarySnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` library weather_repository; export 'src/models/models.dart'; `; --- ================================================ FILE: docs/src/components/tutorials/github-search/ActivateStagehandSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` dart pub global activate stagehand `; --- ================================================ FILE: docs/src/components/tutorials/github-search/DartPubGetSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` dart pub get `; --- ================================================ FILE: docs/src/components/tutorials/github-search/FlutterCreateSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` flutter create flutter_github_search `; --- ================================================ FILE: docs/src/components/tutorials/github-search/SetupSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` mkdir -p github_search/common_github_search `; --- ================================================ FILE: docs/src/components/tutorials/github-search/StagehandSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` stagehand web-angular `; --- ================================================ FILE: docs/src/components/tutorials/ngdart-counter/ActivateStagehandSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` dart pub global activate stagehand `; --- ================================================ FILE: docs/src/components/tutorials/ngdart-counter/InstallDependenciesSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` dart pub get `; --- ================================================ FILE: docs/src/components/tutorials/ngdart-counter/StagehandSnippet.astro ================================================ --- import { Code } from '@astrojs/starlight/components'; const code = ` stagehand web-angular `; --- ================================================ FILE: docs/src/content/config.ts ================================================ import { defineCollection } from 'astro:content'; import { docsSchema } from '@astrojs/starlight/schema'; export const collections = { docs: defineCollection({ schema: docsSchema() }) }; ================================================ FILE: docs/src/content/docs/ar/architecture.mdx ================================================ --- title: الهندسة المعمارية description: نظرة عامة على أنماط الهندسة المعمارية الموصى بها عند استخدام bloc. --- import DataProviderSnippet from '~/components/architecture/DataProviderSnippet.astro'; import RepositorySnippet from '~/components/architecture/RepositorySnippet.astro'; import BusinessLogicComponentSnippet from '~/components/architecture/BusinessLogicComponentSnippet.astro'; import BlocTightCouplingSnippet from '~/components/architecture/BlocTightCouplingSnippet.astro'; import BlocLooseCouplingPresentationSnippet from '~/components/architecture/BlocLooseCouplingPresentationSnippet.astro'; import AppIdeasRepositorySnippet from '~/components/architecture/AppIdeasRepositorySnippet.astro'; import AppIdeaRankingBlocSnippet from '~/components/architecture/AppIdeaRankingBlocSnippet.astro'; import PresentationComponentSnippet from '~/components/architecture/PresentationComponentSnippet.astro'; ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) يتيح لنا استخدام مكتبة bloc تقسيم التطبيق إلى ثلاث طبقات رئيسية: - طبقة العرض (Presentation) - طبقة منطق الأعمال (Business Logic) - طبقة البيانات (Data) - المستودع (Repository) - مزود البيانات (Data Provider) سنبدأ من الطبقة الأدنى (الأبعد عن واجهة المستخدم) ونتدرج صعودًا حتى نصل إلى طبقة العرض. ## طبقة البيانات (Data Layer) تتمثل مسؤولية طبقة البيانات في جلب البيانات ومعالجتها من مصدر واحد أو أكثر. يمكن تقسيم هذه الطبقة إلى جزأين: - المستودع (Repository) - مزود البيانات (Data Provider) تُعد هذه الطبقة الأدنى في التطبيق، وهي المسؤولة عن التفاعل مع قواعد البيانات، وطلبات الشبكة، ومصادر البيانات غير المتزامنة الأخرى. ### مزود البيانات (Data Provider) تتمثل مهمة مزود البيانات في توفير البيانات الخام. يجب أن يكون عامًا ومرنًا وقابلًا لإعادة الاستخدام. عادةً ما يوفّر مزود البيانات واجهات برمجية بسيطة لتنفيذ [عمليات الإنشاء والقراءة والتحديث والحذف (CRUD)](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete). قد يتضمن ذلك دوال مثل `createData` و `readData` و `updateData` و `deleteData` ضمن طبقة البيانات. ### المستودع (Repository) تمثل طبقة المستودع غلافًا (wrapper) حول مزود بيانات واحد أو أكثر، وتتواصل معها طبقة الـ Bloc. كما نلاحظ، يمكن للمستودع التعامل مع عدة مزودي بيانات وإجراء تحويلات على البيانات قبل تمرير النتائج إلى طبقة منطق الأعمال. ## طبقة منطق الأعمال (Business Logic Layer) تتمثل مسؤولية طبقة منطق الأعمال في الاستجابة لمدخلات طبقة العرض بإنتاج حالات جديدة. يمكن أن تعتمد هذه الطبقة على مستودع واحد أو أكثر للحصول على البيانات اللازمة لبناء حالة التطبيق. يمكن اعتبار هذه الطبقة جسرًا بين واجهة المستخدم (طبقة العرض) وطبقة البيانات. فهي تستقبل الأحداث أو الإجراءات من طبقة العرض، ثم تتواصل مع المستودع لبناء حالة جديدة تستهلكها طبقة العرض. ### التواصل بين الـ Blocs (Bloc-to-Bloc Communication) نظرًا لأن الـ blocs تعتمد على الـ streams، قد يكون من المغري إنشاء bloc يستمع إلى bloc آخر. يجب **تجنب** هذا الأسلوب. توجد بدائل أفضل من اللجوء إلى الكود التالي: على الرغم من أن الكود أعلاه صحيح ويعالج الموارد بشكل مناسب، إلا أنه يخلق مشكلة أكبر: وهي إنشاء تبعية مباشرة بين bloc وآخر. بشكل عام، يجب تجنب التبعيات بين كيانات في نفس الطبقة المعمارية قدر الإمكان، لأن ذلك يؤدي إلى اقتران محكم (tight coupling) يصعب صيانته. وبما أن الـ blocs تنتمي إلى طبقة منطق الأعمال، فلا ينبغي لأي bloc أن يعرف عن bloc آخر. ![Application Architecture Layers](~/assets/architecture/architecture.png) يجب أن يتلقى الـ bloc المعلومات فقط عبر الأحداث (events) أو من خلال المستودعات التي يتم حقنها فيه عبر المُنشئ (constructor). إذا كنت بحاجة إلى أن يستجيب bloc لآخر، فهناك خياران أفضل: إما رفع الحل إلى طبقة العرض، أو نقله إلى طبقة النطاق (Domain). #### ربط الـ Blocs عبر طبقة العرض يمكن استخدام `BlocListener` للاستماع إلى bloc معين، وإضافة حدث إلى bloc آخر عند تغيّر حالته. يمنع هذا الأسلوب `SecondBloc` من معرفة أي شيء عن `FirstBloc`، مما يعزز الاقتران المرن (loose coupling). يستخدم تطبيق [flutter_weather](/ar/tutorials/flutter-weather), [هذه التقنية](https://github.com/felangel/bloc/blob/b4c8db938ad71a6b60d4a641ec357905095c3965/examples/flutter_weather/lib/weather/view/weather_page.dart#L38-L42) لتغيير سمة التطبيق بناءً على بيانات الطقس. في بعض الحالات، قد لا يكون من المناسب الربط بين bloc وآخر في طبقة العرض. بدلاً من ذلك، قد يكون من الأفضل أن يشتركا في نفس مصدر البيانات ويقوما بالتحديث عند تغيّرها. #### ربط الـ Blocs عبر طبقة النطاق (Domain) يمكن لبلوكين الاستماع إلى Stream من مستودع مشترك وتحديث حالاتهما بشكل مستقل عند تغير البيانات. يُعد هذا النهج شائعًا في التطبيقات المؤسسية واسعة النطاق. أولًا، أنشئ أو استخدم مستودعًا يوفر Stream للبيانات. على سبيل المثال، يوفر المستودع التالي تدفقًا مستمرًا لبعض أفكار التطبيقات: يمكن حقن نفس المستودع في كل bloc يحتاج إلى الاستجابة لهذه البيانات. فيما يلي `AppIdeaRankingBloc` الذي يُنتج حالة لكل فكرة جديدة واردة من المستودع: لمزيد من التفاصيل حول استخدام streams مع Bloc، راجع: [كيفية استخدام Bloc مع الـ streams والتزامن](https://verygood.ventures/blog/how-to-use-bloc-with-streams-and-concurrency). ## طبقة العرض (Presentation Layer) تتمثل مسؤولية طبقة العرض في تحديد كيفية عرض الواجهة بناءً على حالة bloc واحدة أو أكثر، بالإضافة إلى التعامل مع مدخلات المستخدم وأحداث دورة حياة التطبيق. غالبًا ما تبدأ تدفقات التطبيق بحدث `AppStart` الذي يؤدي إلى جلب البيانات الأولية لعرضها للمستخدم. في هذا السيناريو، تقوم طبقة العرض بإضافة حدث `AppStart`. كما يجب عليها تحديد ما سيتم عرضه على الشاشة بناءً على الحالة القادمة من طبقة الـ bloc. حتى الآن، وعلى الرغم من عرض بعض مقتطفات الكود، ما زال الشرح على مستوى مفاهيمي. في قسم الدروس التعليمية، سنجمع كل هذه المفاهيم معًا أثناء بناء عدة تطبيقات مثالية مختلفة. ================================================ FILE: docs/src/content/docs/ar/bloc-concepts.mdx ================================================ --- title: مفاهيم Bloc description: نظرة عامة على المفاهيم الأساسية لحزمة package:bloc. sidebar: order: 1 --- import CountStreamSnippet from '~/components/concepts/bloc/CountStreamSnippet.astro'; import SumStreamSnippet from '~/components/concepts/bloc/SumStreamSnippet.astro'; import StreamsMainSnippet from '~/components/concepts/bloc/StreamsMainSnippet.astro'; import CounterCubitSnippet from '~/components/concepts/bloc/CounterCubitSnippet.astro'; import CounterCubitInitialStateSnippet from '~/components/concepts/bloc/CounterCubitInitialStateSnippet.astro'; import CounterCubitInstantiationSnippet from '~/components/concepts/bloc/CounterCubitInstantiationSnippet.astro'; import CounterCubitIncrementSnippet from '~/components/concepts/bloc/CounterCubitIncrementSnippet.astro'; import CounterCubitBasicUsageSnippet from '~/components/concepts/bloc/CounterCubitBasicUsageSnippet.astro'; import CounterCubitStreamUsageSnippet from '~/components/concepts/bloc/CounterCubitStreamUsageSnippet.astro'; import CounterCubitOnChangeSnippet from '~/components/concepts/bloc/CounterCubitOnChangeSnippet.astro'; import CounterCubitOnChangeUsageSnippet from '~/components/concepts/bloc/CounterCubitOnChangeUsageSnippet.astro'; import CounterCubitOnChangeOutputSnippet from '~/components/concepts/bloc/CounterCubitOnChangeOutputSnippet.astro'; import SimpleBlocObserverOnChangeSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeSnippet.astro'; import SimpleBlocObserverOnChangeUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeUsageSnippet.astro'; import SimpleBlocObserverOnChangeOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeOutputSnippet.astro'; import CounterCubitOnErrorSnippet from '~/components/concepts/bloc/CounterCubitOnErrorSnippet.astro'; import SimpleBlocObserverOnErrorSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnErrorSnippet.astro'; import CounterCubitOnErrorOutputSnippet from '~/components/concepts/bloc/CounterCubitOnErrorOutputSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/bloc/CounterBlocSnippet.astro'; import CounterBlocEventHandlerSnippet from '~/components/concepts/bloc/CounterBlocEventHandlerSnippet.astro'; import CounterBlocIncrementSnippet from '~/components/concepts/bloc/CounterBlocIncrementSnippet.astro'; import CounterBlocUsageSnippet from '~/components/concepts/bloc/CounterBlocUsageSnippet.astro'; import CounterBlocStreamUsageSnippet from '~/components/concepts/bloc/CounterBlocStreamUsageSnippet.astro'; import CounterBlocOnChangeSnippet from '~/components/concepts/bloc/CounterBlocOnChangeSnippet.astro'; import CounterBlocOnChangeUsageSnippet from '~/components/concepts/bloc/CounterBlocOnChangeUsageSnippet.astro'; import CounterBlocOnChangeOutputSnippet from '~/components/concepts/bloc/CounterBlocOnChangeOutputSnippet.astro'; import CounterBlocOnTransitionSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionSnippet.astro'; import CounterBlocOnTransitionOutputSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionOutputSnippet.astro'; import SimpleBlocObserverOnTransitionSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionSnippet.astro'; import SimpleBlocObserverOnTransitionUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionUsageSnippet.astro'; import SimpleBlocObserverOnTransitionOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionOutputSnippet.astro'; import CounterBlocOnEventSnippet from '~/components/concepts/bloc/CounterBlocOnEventSnippet.astro'; import SimpleBlocObserverOnEventSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventSnippet.astro'; import SimpleBlocObserverOnEventOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventOutputSnippet.astro'; import CounterBlocOnErrorSnippet from '~/components/concepts/bloc/CounterBlocOnErrorSnippet.astro'; import CounterBlocOnErrorOutputSnippet from '~/components/concepts/bloc/CounterBlocOnErrorOutputSnippet.astro'; import CounterCubitFullSnippet from '~/components/concepts/bloc/CounterCubitFullSnippet.astro'; import CounterBlocFullSnippet from '~/components/concepts/bloc/CounterBlocFullSnippet.astro'; import AuthenticationStateSnippet from '~/components/concepts/bloc/AuthenticationStateSnippet.astro'; import AuthenticationTransitionSnippet from '~/components/concepts/bloc/AuthenticationTransitionSnippet.astro'; import AuthenticationChangeSnippet from '~/components/concepts/bloc/AuthenticationChangeSnippet.astro'; import DebounceEventTransformerSnippet from '~/components/concepts/bloc/DebounceEventTransformerSnippet.astro'; :::note يرجى التأكد من قراءة الأقسام التالية بعناية قبل العمل مع [`package:bloc`](https://pub.dev/packages/bloc). ::: هناك عدة مفاهيم أساسية تُعد ضرورية لفهم كيفية استخدام حزمة bloc. في الأقسام القادمة، سنناقش كل مفهوم بالتفصيل، ونستعرض كيف ينطبق ذلك على تطبيق عدّاد (counter app). ## التدفقات (Streams) :::note اطّلع على [توثيق Dart الرسمي](https://dart.dev/tutorials/language/streams) لمزيد من المعلومات حول `Streams`. ::: التدفق (Stream) هو سلسلة من البيانات غير المتزامنة. لاستخدام مكتبة bloc، من الضروري امتلاك فهم أساسي لـ `Streams` وكيف تعمل. إذا لم تكن مألوفًا بـ `Streams`، فتخيلها كأنبوب يجري فيه الماء: الأنبوب هو `Stream` والماء هو البيانات غير المتزامنة. يمكننا إنشاء `Stream` في Dart عبر كتابة دالة `async*` (مولّد غير متزامن - async generator). عند تعليم الدالة بـ `async*` يصبح بإمكاننا استخدام الكلمة المفتاحية `yield` وإرجاع `Stream` من البيانات. في المثال أعلاه، نُرجع `Stream` من أعداد صحيحة حتى قيمة المُعامل `max`. في كل مرة نستخدم فيها `yield` داخل دالة `async*`، فإننا ندفع تلك القيمة عبر الـ `Stream`. يمكننا استهلاك الـ `Stream` أعلاه بعدة طرق. إذا أردنا كتابة دالة تُرجع مجموع `Stream` من الأعداد الصحيحة، فقد تكون كالتالي: عند تعليم الدالة أعلاه بـ `async` يصبح بإمكاننا استخدام `await` وإرجاع `Future` من الأعداد الصحيحة. في هذا المثال، ننتظر كل قيمة في التدفق ثم نُرجع مجموع جميع الأعداد الموجودة فيه. يمكننا جمع كل ذلك معًا كالتالي: الآن بعد أن أصبح لدينا فهم أساسي لكيفية عمل `Streams` في Dart، نحن جاهزون للتعرف على المكوّن الأساسي في حزمة bloc: `Cubit`. ## Cubit `Cubit` هو صنف (class) يوسّع `BlocBase` ويمكن توسيعه لإدارة أي نوع من الحالة. ![هيكلية Cubit](~/assets/concepts/cubit_architecture_full.png) يمكن لـ `Cubit` أن يوفّر دوالًا يمكن استدعاؤها لتحفيز تغييرات الحالة. الحالات (States) هي مخرجات `Cubit` وتمثل جزءًا من حالة تطبيقك. يمكن إبلاغ مكونات واجهة المستخدم (UI) بالحالات وإعادة رسم أجزاء منها بناءً على الحالة الحالية. :::note لمزيد من المعلومات حول أصل `Cubit`، راجع [المشكلة التالية](https://github.com/felangel/cubit/issues/69). ::: ### إنشاء Cubit يمكننا إنشاء `CounterCubit` كالتالي: عند إنشاء `Cubit`، نحتاج إلى تحديد نوع الحالة التي سيديرها. في مثال `CounterCubit` أعلاه، يمكن تمثيل الحالة باستخدام `int`، لكن في الحالات الأكثر تعقيدًا قد نحتاج إلى استخدام `class` بدلًا من النوع البدائي (primitive type). الخطوة الثانية عند إنشاء `Cubit` هي تحديد الحالة الابتدائية (initial state). يمكننا فعل ذلك باستدعاء `super` وتمرير قيمة الحالة الابتدائية. في المثال أعلاه نُعيّن الحالة الابتدائية إلى `0` داخليًا، لكن يمكننا جعل `Cubit` أكثر مرونة بقبول قيمة خارجية: وهذا يتيح لنا إنشاء مثيلات `CounterCubit` بحالات ابتدائية مختلفة مثل: ### تغييرات حالة Cubit كل `Cubit` يمكنه إخراج حالة جديدة عبر `emit`. في المثال أعلاه، يوفّر `CounterCubit` دالة عامة باسم `increment` يمكن استدعاؤها خارجيًا لطلب زيادة الحالة. عند استدعاء `increment`، يمكننا الوصول إلى الحالة الحالية عبر getter باسم `state` ثم `emit` حالة جديدة بإضافة 1 إلى الحالة الحالية. :::caution الدالة `emit` محمية (protected)، ما يعني أنه يجب استخدامها فقط داخل `Cubit`. ::: ### استخدام Cubit يمكننا الآن أخذ `CounterCubit` الذي قمنا ببنائه واستخدامه! #### الاستخدام الأساسي في المثال أعلاه، نبدأ بإنشاء مثيل من `CounterCubit`. ثم نطبع الحالة الحالية وهي الحالة الابتدائية (لأنه لم يتم إصدار حالات جديدة بعد). بعد ذلك، نستدعي `increment` لتحفيز تغيير الحالة. أخيرًا، نطبع حالة `Cubit` مرة أخرى وقد انتقلت من `0` إلى `1`، ثم نستدعي `close` لإغلاق تدفق الحالة الداخلي. #### استخدام التدفق (Stream Usage) يوفّر `Cubit` تدفقًا (`Stream`) يتيح لنا استقبال تحديثات الحالة في الوقت الفعلي: في المثال أعلاه، نشترك في `CounterCubit` ونطبع عند كل تغيير للحالة. ثم نستدعي `increment` لإصدار حالة جديدة. أخيرًا، نستدعي `cancel` على `subscription` عندما لا نعود بحاجة للتحديثات، ونغلق `Cubit`. :::note تمت إضافة `await Future.delayed(Duration.zero)` في هذا المثال لتجنب إلغاء الاشتراك فورًا. ::: :::caution عند استدعاء `listen` على `Cubit` سيتم استقبال تغييرات الحالة اللاحقة فقط. ::: ### مراقبة Cubit عندما يرسل `Cubit` حالة جديدة، يحدث `Change`. يمكننا مراقبة جميع `Changes` لـ `Cubit` معين عبر تجاوز `onChange`. يمكننا بعد ذلك التفاعل مع `Cubit` ومشاهدة جميع التغييرات المطبوعة على وحدة التحكم (console). سيكون الإخراج في المثال أعلاه: :::note يحدث `Change` قبل تحديث حالة `Cubit` مباشرة. ويتكون من `currentState` و `nextState`. ::: #### BlocObserver من مزايا استخدام مكتبة bloc أننا نستطيع الوصول إلى جميع `Changes` في مكان واحد. رغم أن هذا التطبيق يحتوي على `Cubit` واحد فقط، إلا أنه شائع في التطبيقات الكبيرة وجود عدة `Cubits` تدير أجزاء مختلفة من حالة التطبيق. إذا أردنا تنفيذ إجراء استجابةً لجميع `Changes`، يمكننا ببساطة إنشاء `BlocObserver` خاص بنا. :::note كل ما نحتاجه هو توسيع `BlocObserver` وتجاوز `onChange`. ::: لاستخدام `SimpleBlocObserver` نحتاج فقط إلى تعديل الدالة `main`: وسيكون الإخراج: :::note يُستدعى تجاوز `onChange` الداخلي أولًا، ثم يستدعي `super.onChange` لإشعار `onChange` في `BlocObserver`. ::: :::tip في `BlocObserver` لدينا إمكانية الوصول إلى مثيل `Cubit` بالإضافة إلى `Change` نفسه. ::: ### معالجة أخطاء Cubit يمتلك كل `Cubit` دالة `addError` يمكن استخدامها للإشارة إلى حدوث خطأ. :::note يمكن تجاوز `onError` داخل `Cubit` لمعالجة جميع الأخطاء الخاصة بـ `Cubit` محدد. ::: يمكن أيضًا تجاوز `onError` في `BlocObserver` لمعالجة جميع الأخطاء المُبلّغ عنها بشكل عام. إذا شغّلنا نفس البرنامج مرة أخرى، ينبغي أن نرى الإخراج التالي: ## Bloc `Bloc` هو صنف أكثر تقدمًا يعتمد على `events` لتحفيز تغييرات `state` بدلًا من الدوال. كما أنه يوسّع `BlocBase`، ما يعني أن لديه واجهة عامة مشابهة لـ `Cubit`. لكن بدلًا من استدعاء دالة على `Bloc` وإصدار `state` جديد مباشرة، تستقبل `Blocs` أحداثًا (`events`) وتحوّل الأحداث الواردة إلى حالات صادرة (`states`). ![هيكلية Bloc](~/assets/concepts/bloc_architecture_full.png) ### إنشاء Bloc إنشاء `Bloc` يشبه إنشاء `Cubit`، لكن بالإضافة إلى تحديد الحالة التي سنديرها، يجب أيضًا تحديد الحدث الذي سيتمكن `Bloc` من معالجته. الأحداث هي مدخلات Bloc. عادة تُضاف استجابةً لتفاعلات المستخدم مثل ضغط الأزرار أو لأحداث دورة الحياة مثل تحميل الصفحة. وكما في `CounterCubit`، يجب تحديد الحالة الابتدائية بتمريرها إلى الصنف الأب عبر `super`. ### تغييرات حالة Bloc يتطلب `Bloc` تسجيل معالجات الأحداث عبر واجهة `on`، بخلاف الدوال في `Cubit`. معالج الحدث مسؤول عن تحويل أي أحداث واردة إلى صفر أو أكثر من الحالات الصادرة. :::tip يمتلك `EventHandler` إمكانية الوصول إلى الحدث المضاف، إضافةً إلى `Emitter` الذي يمكن استخدامه لإصدار صفر أو أكثر من الحالات استجابةً للحدث الوارد. ::: يمكننا بعد ذلك تحديث `EventHandler` لمعالجة حدث `CounterIncrementPressed`: في المثال أعلاه، سجّلنا `EventHandler` لإدارة جميع أحداث `CounterIncrementPressed`. ولكل حدث وارد، يمكننا الوصول إلى الحالة الحالية عبر getter باسم `state` ثم استدعاء `emit(state + 1)`. :::note بما أن `Bloc` يوسّع `BlocBase`، فيمكننا الوصول إلى الحالة الحالية في أي وقت عبر `state` تمامًا كما في `Cubit`. ::: :::caution لا ينبغي على `Blocs` إصدار حالات جديدة مباشرة عبر `emit`. بدلًا من ذلك، يجب أن ينتج كل تغيير في الحالة استجابةً لحدث وارد داخل `EventHandler`. ::: :::caution يتجاهل كل من blocs و cubits الحالات المكررة. إذا قمنا بإصدار `State nextState` بحيث `state == nextState`، فلن يحدث أي تغيير في الحالة. ::: ### استخدام Bloc في هذه المرحلة، يمكننا إنشاء مثيل من `CounterBloc` واستخدامه! #### الاستخدام الأساسي في المثال أعلاه، نبدأ بإنشاء مثيل من `CounterBloc`. ثم نطبع الحالة الحالية وهي الحالة الابتدائية (لأنه لم يتم إصدار حالات جديدة بعد). بعد ذلك، نضيف حدث `CounterIncrementPressed` لتحفيز تغيير الحالة. أخيرًا، نطبع حالة `Bloc` مرة أخرى وقد انتقلت من `0` إلى `1`، ثم نستدعي `close` لإغلاق تدفق الحالة الداخلي. :::note تمت إضافة `await Future.delayed(Duration.zero)` لضمان انتظار الدورة التالية من event-loop (ما يسمح لـ `EventHandler` بمعالجة الحدث). ::: #### استخدام التدفق (Stream Usage) كما هو الحال مع `Cubit`، فإن `Bloc` نوع خاص من `Stream`، ما يعني أنه يمكننا أيضًا الاشتراك في `Bloc` للحصول على تحديثات فورية لحالته: في المثال أعلاه، نشترك في `CounterBloc` ونطبع عند كل تغيير للحالة. ثم نضيف حدث `CounterIncrementPressed` الذي يفعّل `EventHandler` الخاص بـ `on` ويُصدر حالة جديدة. أخيرًا، نستدعي `cancel` على الاشتراك عندما لا نعود بحاجة للتحديثات، ونغلق `Bloc`. :::note تمت إضافة `await Future.delayed(Duration.zero)` في هذا المثال لتجنب إلغاء الاشتراك فورًا. ::: ### مراقبة Bloc بما أن `Bloc` يوسّع `BlocBase`، يمكننا مراقبة جميع تغييرات الحالة لـ `Bloc` باستخدام `onChange`. يمكننا بعد ذلك تحديث `main.dart` إلى: الآن إذا شغّلنا المثال أعلاه، سيكون الإخراج: أحد الفروق الجوهرية بين `Bloc` و `Cubit` هو أنه بما أن `Bloc` يعتمد على الأحداث، يمكننا أيضًا التقاط معلومات حول ما الذي حفّز تغيير الحالة. يمكننا فعل ذلك بتجاوز `onTransition`. الانتقال من حالة إلى أخرى يُسمى `Transition`. ويتكون `Transition` من الحالة الحالية والحدث والحالة التالية. إذا أعدنا تشغيل نفس مثال `main.dart` السابق، ينبغي أن نرى الإخراج التالي: :::note يُستدعى `onTransition` قبل `onChange` ويحتوي على الحدث الذي حفّز التغيير من `currentState` إلى `nextState`. ::: #### BlocObserver كما في السابق، يمكننا تجاوز `onTransition` في `BlocObserver` مخصص لمراقبة جميع الانتقالات من مكان واحد. يمكننا تهيئة `SimpleBlocObserver` تمامًا كما فعلنا سابقًا: الآن إذا شغّلنا المثال أعلاه، يجب أن يكون الإخراج كالتالي: :::note يُستدعى `onTransition` أولًا (المحلي قبل العام) ثم يليه `onChange`. ::: ميزة أخرى فريدة في `Bloc` هي إمكانية تجاوز `onEvent`، والتي تُستدعى كلما تمت إضافة حدث جديد إلى `Bloc`. وكما في `onChange` و `onTransition`، يمكن تجاوز `onEvent` محليًا وعالميًا. يمكننا تشغيل نفس `main.dart` كما في السابق، وينبغي أن نرى الإخراج التالي: :::note يتم استدعاء `onEvent` بمجرد إضافة الحدث. ويُستدعى `onEvent` المحلي قبل `onEvent` العام داخل `BlocObserver`. ::: ### معالجة أخطاء Bloc كما هو الحال مع `Cubit`، يمتلك كل `Bloc` دالتي `addError` و `onError`. يمكننا الإشارة إلى حدوث خطأ عبر استدعاء `addError` من أي مكان داخل `Bloc`. ثم يمكننا الاستجابة لجميع الأخطاء بتجاوز `onError` تمامًا كما في `Cubit`. إذا أعدنا تشغيل نفس `main.dart` كما في السابق، يمكننا رؤية شكل الإبلاغ عن الخطأ: :::note يُستدعى `onError` المحلي أولًا ثم يليه `onError` العام داخل `BlocObserver`. ::: :::note تعمل `onError` و `onChange` بالطريقة نفسها تمامًا لكل من `Bloc` و `Cubit`. ::: :::caution أي استثناءات غير معالجة تحدث داخل `EventHandler` يتم الإبلاغ عنها أيضًا إلى `onError`. ::: ## Cubit مقابل Bloc الآن بعد أن غطّينا أساسيات `Cubit` و `Bloc`، قد تتساءل متى تستخدم `Cubit` ومتى تستخدم `Bloc`. ### مزايا Cubit #### البساطة من أكبر مزايا `Cubit` هي البساطة. عند إنشاء `Cubit` نحتاج فقط لتحديد الحالة والدوال التي نريد توفيرها لتغيير الحالة. بالمقارنة، عند إنشاء `Bloc` نحتاج لتحديد الحالات والأحداث وتطبيق `EventHandler`. هذا يجعل `Cubit` أسهل في الفهم مع كود أقل. لنلقِ نظرة الآن على تنفيذي العداد: ##### CounterCubit ##### CounterBloc تنفيذ `Cubit` أكثر اختصارًا، وبدلًا من تعريف الأحداث بشكل منفصل، تقوم الدوال بدور الأحداث. إضافةً إلى ذلك، عند استخدام `Cubit` يمكننا ببساطة استدعاء `emit` من أي مكان لتحفيز تغيير الحالة. ### مزايا Bloc #### قابلية التتبع (Traceability) من أكبر مزايا `Bloc` معرفة تسلسل تغييرات الحالة، ومعرفة ما الذي حفّز تلك التغييرات بدقة. عندما تكون الحالة حساسة أو حاسمة لوظائف التطبيق، قد يكون من المفيد استخدام نهج قائم على الأحداث لالتقاط الأحداث إضافةً إلى تغييرات الحالة. حالة استخدام شائعة هي إدارة `AuthenticationState`. وللتبسيط، لنفترض أننا نمثل `AuthenticationState` عبر `enum`: قد توجد أسباب عديدة لتغير حالة التطبيق من `authenticated` إلى `unauthenticated`. على سبيل المثال، قد يضغط المستخدم زر تسجيل الخروج ويطلب تسجيل خروجه. أو ربما تم إلغاء رمز وصول المستخدم وتم تسجيل خروجه إجباريًا. عند استخدام `Bloc` يمكننا تتبّع كيف وصلت حالة التطبيق إلى حالة معينة بوضوح. يوفر لنا `Transition` أعلاه كل المعلومات اللازمة لفهم سبب تغير الحالة. ولو استخدمنا `Cubit` لإدارة `AuthenticationState` فستبدو السجلات كالتالي: هذا يخبرنا أن المستخدم خرج من النظام، لكنه لا يوضح السبب، وقد يكون ذلك مهمًا لتصحيح الأخطاء وفهم كيفية تغيّر حالة التطبيق مع الزمن. #### تحويلات الأحداث المتقدمة مجال آخر يتفوق فيه `Bloc` على `Cubit` هو عندما نحتاج للاستفادة من العوامل التفاعلية مثل `buffer` و `debounceTime` و `throttle` وغيرها. :::tip راجع [`package:stream_transform`](https://pub.dev/packages/stream_transform) و [`package:rxdart`](https://pub.dev/packages/rxdart) لمحولات التدفق (stream transformers). ::: يمتلك `Bloc` قناة إدخال للأحداث (event sink) تتيح لنا التحكم في تدفق الأحداث الواردة وتحويله. على سبيل المثال، إذا كنا نبني بحثًا لحظيًا، فمن المرجح أننا سنحتاج إلى تطبيق `debounce` على طلبات الخلفية لتجنب rate-limiting، وكذلك لتقليل التكلفة/الضغط على الخادم. باستخدام `Bloc`، يمكننا توفير `EventTransformer` مخصص لتغيير طريقة معالجة الأحداث الواردة. باستخدام الكود أعلاه، يمكننا بسهولة تطبيق `debounce` على الأحداث الواردة مع مقدار بسيط جدًا من الكود الإضافي. :::tip اطّلع على [`package:bloc_concurrency`](https://pub.dev/packages/bloc_concurrency) للحصول على مجموعة مُوجهة من محولات الأحداث. ::: إذا لم تكن متأكدًا مما يجب استخدامه، ابدأ بـ `Cubit` ويمكنك لاحقًا إعادة الهيكلة أو التوسّع إلى `Bloc` عند الحاجة. ================================================ FILE: docs/src/content/docs/ar/faqs.mdx ================================================ --- title: الأسئلة الشائعة (FAQs) description: إجابات للأسئلة المتكررة بخصوص مكتبة bloc. --- import StateNotUpdatingGood1Snippet from '~/components/faqs/StateNotUpdatingGood1Snippet.astro'; import StateNotUpdatingGood2Snippet from '~/components/faqs/StateNotUpdatingGood2Snippet.astro'; import StateNotUpdatingGood3Snippet from '~/components/faqs/StateNotUpdatingGood3Snippet.astro'; import StateNotUpdatingBad1Snippet from '~/components/faqs/StateNotUpdatingBad1Snippet.astro'; import StateNotUpdatingBad2Snippet from '~/components/faqs/StateNotUpdatingBad2Snippet.astro'; import StateNotUpdatingBad3Snippet from '~/components/faqs/StateNotUpdatingBad3Snippet.astro'; import EquatableEmitSnippet from '~/components/faqs/EquatableEmitSnippet.astro'; import EquatableBlocTestSnippet from '~/components/faqs/EquatableBlocTestSnippet.astro'; import NoEquatableBlocTestSnippet from '~/components/faqs/NoEquatableBlocTestSnippet.astro'; import SingleStateSnippet from '~/components/faqs/SingleStateSnippet.astro'; import SingleStateUsageSnippet from '~/components/faqs/SingleStateUsageSnippet.astro'; import BlocProviderGood1Snippet from '~/components/faqs/BlocProviderGood1Snippet.astro'; import BlocProviderGood2Snippet from '~/components/faqs/BlocProviderGood2Snippet.astro'; import BlocProviderBad1Snippet from '~/components/faqs/BlocProviderBad1Snippet.astro'; import BlocInternalAddEventSnippet from '~/components/faqs/BlocInternalAddEventSnippet.astro'; import BlocInternalEventSnippet from '~/components/faqs/BlocInternalEventSnippet.astro'; import BlocExternalForEachSnippet from '~/components/faqs/BlocExternalForEachSnippet.astro'; ## عدم تحديث الحالة (State Not Updating) ❔ **سؤال**: أقوم بإصدار حالة داخل الـ bloc، لكن واجهة المستخدم لا تتحدّث. ما الخطأ؟ 💡 **إجابة**: إذا كنت تستخدم `Equatable`، فتأكّد من تضمين جميع الخصائص داخل `props`. ✅ **صحيح (GOOD)** ❌ **خاطئ (BAD)** كذلك تأكّد من إصدار نسخة جديدة من الحالة داخل الـ bloc، وليس إعادة استخدام نفس الكائن. ✅ **صحيح (GOOD)** ❌ **خاطئ (BAD)** :::caution يجب دائمًا نسخ الخصائص في الكائنات التي تستخدم `Equatable` بدل تعديلها مباشرة. وإذا كانت الفئة تحتوي على `List` أو `Map`، فاستخدم `List.of` أو `Map.of` حتى تُحتسب المساواة بناءً على القيم لا على مرجع الكائن. ::: ## متى يجب استخدام Equatable ❔ **سؤال**: متى أستخدم Equatable؟ 💡 **إجابة**: في المثال أعلاه، إذا كانت `StateA` ترث من `Equatable` فلن يحدث سوى انتقال حالة واحد (سيُتجاهل `emit` الثاني). بشكل عام، استخدم `Equatable` عندما تريد تقليل إعادة البناء وتحسين الأداء. ولا تستخدمه إذا كنت تحتاج أن تؤدي الحالة نفسها عند تكرارها إلى انتقالات متعددة. كما أن `Equatable` يسهّل اختبار الـ blocs، لأننا نستطيع توقّع حالات محددة مباشرةً بدل الاعتماد على `Matchers` أو `Predicates`. بدون `Equatable` سيفشل الاختبار أعلاه، وسنحتاج لإعادة كتابته بهذا الشكل: ## التعامل مع الأخطاء (Handling Errors) ❔ **سؤال**: كيف أتعامل مع الخطأ مع الاستمرار في عرض البيانات السابقة؟ 💡 **إجابة**: يعتمد ذلك بدرجة كبيرة على طريقة نمذجة حالة الـ bloc. إذا كنت تحتاج للاحتفاظ بالبيانات حتى عند وقوع خطأ، فمن الأفضل استخدام فئة حالة واحدة. بهذه الطريقة يمكن للـ widgets الوصول إلى `data` و `error` في الوقت نفسه، ويمكن للـ bloc استخدام `state.copyWith` للاحتفاظ بالبيانات السابقة عند حدوث الخطأ. ## Bloc مقابل Redux ❔ **سؤال**: ما الفرق بين Bloc و Redux؟ 💡 **إجابة**: الـ BLoC هو نمط تصميم يقوم على القواعد التالية: 1. مدخلات ومخرجات الـ BLoC هي `Streams` و`Sinks`. 2. يجب أن تكون الاعتماديات قابلة للحقن ومستقلة عن المنصة. 3. لا يُسمح بتفرعات منطقية مرتبطة بمنصة محددة. 4. تفاصيل التنفيذ مرنة ما دمت ملتزمًا بالقواعد السابقة. أما إرشادات طبقة الواجهة فهي: 1. كل مكوّن "معقّد بما يكفي" ينبغي أن يكون له BLoC خاص به. 2. على المكوّنات إرسال المدخلات كما هي. 3. وعلى المكوّنات عرض المخرجات بأقل قدر ممكن من التحويل. 4. وكل التفرعات المنطقية في الواجهة يجب أن تعتمد على مخارج Boolean بسيطة من الـ BLoC. مكتبة Bloc تطبق نمط BLoC، وتهدف إلى تجريد `RxDart` لتبسيط تجربة التطوير. أما Redux فيقوم على ثلاثة مبادئ: 1. مصدر واحد للحقيقة. 2. الحالة للقراءة فقط. 3. التعديلات تتم عبر دوال نقية. مكتبة bloc لا تتوافق مع المبدأ الأول في Redux، لأن الحالة فيها موزعة عبر عدة blocs. كذلك لا يوجد فيها مفهوم `middleware` بالشكل الموجود في Redux، وهي مصممة لتسهيل تغييرات الحالة غير المتزامنة وإتاحة إصدار عدة حالات لحدث واحد. ## Bloc مقابل Provider ❔ **سؤال**: ما الفرق بين Bloc و Provider؟ 💡 **إجابة**: حزمة `provider` مخصّصة أساسًا لحقن الاعتماديات (وهي غلاف حول `InheritedWidget`). ما زلت بحاجة لاختيار أسلوب إدارة الحالة بنفسك (`ChangeNotifier` أو `Bloc` أو `Mobx` وغيرها). مكتبة Bloc تستخدم `provider` داخليًا لتسهيل توفير الـ blocs والوصول إليها عبر شجرة الـ widgets. ## فشل BlocProvider.of() في العثور على Bloc ❔ **سؤال**: عند استخدام `BlocProvider.of(context)`، لا يمكنه العثور على الـ bloc. كيف يمكنني إصلاح ذلك؟ 💡 **إجابة**: لا يمكنك الوصول إلى bloc من نفس الـ `BuildContext` الذي قمت بتوفيره فيه. لذلك تأكّد من استدعاء `BlocProvider.of()` من `BuildContext` تابع (ابن). ✅ **صحيح (GOOD)** ❌ **خاطئ (BAD)** ## هيكلة المشروع (Project Structure) ❔ **سؤال**: كيف يجب أن أقوم بهيكلة مشروعي؟ 💡 **إجابة**: لا توجد إجابة واحدة صحيحة أو خاطئة تمامًا لهذا السؤال، لكن هذه مراجع مفيدة: - [I/O Photobooth](https://github.com/flutter/photobooth) - [I/O Pinball](https://github.com/flutter/pinball) - [Flutter News Toolkit](https://github.com/flutter/news_toolkit) الأهم أن تكون هيكلة المشروع **متسقة** و**مقصودة**. ## إضافة الأحداث داخل Bloc ❔ **سؤال**: هل من المقبول إضافة الأحداث داخل bloc؟ 💡 **إجابة**: في أغلب الحالات تُضاف الأحداث من خارج الـ bloc، لكن في حالات محددة قد يكون من المنطقي إضافتها من داخله. أشيع سيناريو للأحداث الداخلية هو عندما يلزم تغيير الحالة استجابةً لتحديثات لحظية قادمة من `repository`. هنا يكون مصدر التحفيز هو المستودع نفسه، وليس حدثًا خارجيًا مثل ضغط زر. في المثال التالي، تعتمد حالة `MyBloc` على المستخدم الحالي الذي يتم كشفه عبر `Stream` من `UserRepository`. يستمع `MyBloc` لتغيّرات المستخدم الحالي ويضيف حدثًا داخليًا `_UserChanged` كلما صدر مستخدم جديد من هذا الـ stream. عند إضافة حدث داخلي يمكننا أيضًا تحديد `transformer` مخصّص للتحكم في طريقة معالجة أحداث `_UserChanged` المتعددة. افتراضيًا، تُعالج هذه الأحداث بالتوازي. يوصى بشدة بأن تكون الأحداث الداخلية `private`. هذا يوضح أن الحدث مخصّص للاستخدام الداخلي فقط، ويمنع المكوّنات الخارجية من الاعتماد عليه. يمكن أيضًا تعريف حدث خارجي `Started` واستخدام `emit.forEach` للتعامل مع تحديثات المستخدم اللحظية: مزايا هذا النهج: - لا حاجة لحدث داخلي `_UserChanged`. - لا حاجة لإدارة `StreamSubscription` يدويًا. - تحكم كامل في توقيت اشتراك الـ bloc في stream تحديثات المستخدم. عيوب هذا النهج: - لا يمكن إيقاف (`pause`) الاشتراك أو استئنافه (`resume`) بسهولة. - نحتاج إلى كشف حدث عام `Started` ليُضاف من الخارج. - لا يمكن استخدام `transformer` مخصص لضبط طريقة التعامل مع تحديثات المستخدم. ## كشف الدوال العامة (Exposing Public Methods) ❔ **سؤال**: هل من المقبول كشف دوال عامة (public methods) على مثيلات bloc و cubit الخاصة بي؟ 💡 **إجابة** عند إنشاء cubit، يُنصح بأن تكون الدوال العامة مخصّصة فقط لتحفيز تغيّر الحالة. لذلك، من الأفضل غالبًا أن تعيد هذه الدوال `void` أو `Future`. وعند إنشاء bloc، يُنصح بتجنب كشف دوال عامة مخصّصة، والاكتفاء بإبلاغه بالأحداث عبر استدعاء `add`. ================================================ FILE: docs/src/content/docs/ar/flutter-bloc-concepts.mdx ================================================ --- title: مفاهيم Flutter Bloc description: نظرة عامة على المفاهيم الأساسية لحزمة flutter_bloc. sidebar: order: 2 --- import BlocBuilderSnippet from '~/components/concepts/flutter-bloc/BlocBuilderSnippet.astro'; import BlocBuilderExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocBuilderExplicitBlocSnippet.astro'; import BlocBuilderConditionSnippet from '~/components/concepts/flutter-bloc/BlocBuilderConditionSnippet.astro'; import BlocSelectorSnippet from '~/components/concepts/flutter-bloc/BlocSelectorSnippet.astro'; import BlocProviderSnippet from '~/components/concepts/flutter-bloc/BlocProviderSnippet.astro'; import BlocProviderEagerSnippet from '~/components/concepts/flutter-bloc/BlocProviderEagerSnippet.astro'; import BlocProviderValueSnippet from '~/components/concepts/flutter-bloc/BlocProviderValueSnippet.astro'; import BlocProviderLookupSnippet from '~/components/concepts/flutter-bloc/BlocProviderLookupSnippet.astro'; import NestedBlocProviderSnippet from '~/components/concepts/flutter-bloc/NestedBlocProviderSnippet.astro'; import MultiBlocProviderSnippet from '~/components/concepts/flutter-bloc/MultiBlocProviderSnippet.astro'; import BlocListenerSnippet from '~/components/concepts/flutter-bloc/BlocListenerSnippet.astro'; import BlocListenerExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocListenerExplicitBlocSnippet.astro'; import BlocListenerConditionSnippet from '~/components/concepts/flutter-bloc/BlocListenerConditionSnippet.astro'; import NestedBlocListenerSnippet from '~/components/concepts/flutter-bloc/NestedBlocListenerSnippet.astro'; import MultiBlocListenerSnippet from '~/components/concepts/flutter-bloc/MultiBlocListenerSnippet.astro'; import BlocConsumerSnippet from '~/components/concepts/flutter-bloc/BlocConsumerSnippet.astro'; import BlocConsumerConditionSnippet from '~/components/concepts/flutter-bloc/BlocConsumerConditionSnippet.astro'; import RepositoryProviderSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderSnippet.astro'; import RepositoryProviderLookupSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderLookupSnippet.astro'; import RepositoryProviderDisposeSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderDisposeSnippet.astro'; import NestedRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/NestedRepositoryProviderSnippet.astro'; import MultiRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/MultiRepositoryProviderSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/flutter-bloc/CounterBlocSnippet.astro'; import CounterMainSnippet from '~/components/concepts/flutter-bloc/CounterMainSnippet.astro'; import CounterPageSnippet from '~/components/concepts/flutter-bloc/CounterPageSnippet.astro'; import WeatherRepositorySnippet from '~/components/concepts/flutter-bloc/WeatherRepositorySnippet.astro'; import WeatherMainSnippet from '~/components/concepts/flutter-bloc/WeatherMainSnippet.astro'; import WeatherAppSnippet from '~/components/concepts/flutter-bloc/WeatherAppSnippet.astro'; import WeatherPageSnippet from '~/components/concepts/flutter-bloc/WeatherPageSnippet.astro'; :::note يرجى التأكد من قراءة الأقسام التالية بعناية قبل البدء بالعمل مع حزمة [`package:flutter_bloc`](https://pub.dev/packages/flutter_bloc). ::: :::note جميع الـ Widgets التي تصدرها حزمة `flutter_bloc` تتكامل مع كل من مثيلات `Cubit` و `Bloc`. ::: ## Widgets الـ Bloc ### BlocBuilder **BlocBuilder** هو ويدجت (Widget) في Flutter يتطلب `Bloc` ودالة `builder`. يتعامل `BlocBuilder` مع بناء الـ Widget استجابةً للحالات الجديدة (new states). يشبه `BlocBuilder` إلى حد كبير `StreamBuilder` لكنه يوفر واجهة (API) أبسط لتقليل كمية الكود المتكرر (boilerplate) المطلوبة. قد يتم استدعاء دالة `builder` مرات عديدة، لذلك يُفضل أن تكون [دالة نقية (pure function)](https://en.wikipedia.org/wiki/Pure_function) تعيد ويدجت استجابةً للحالة. راجع `BlocListener` إذا كنت تريد "تنفيذ" شيء استجابةً لتغيرات الحالة مثل التنقل (navigation) أو إظهار مربع حوار (dialog) وغيرها. إذا تم حذف معامل `bloc`، فسيقوم `BlocBuilder` تلقائيًا بإجراء بحث (lookup) باستخدام `BlocProvider` و `BuildContext` الحالي. قم بتحديد الـ bloc فقط إذا كنت ترغب في توفير bloc سيكون نطاقه (scoped) مقتصرًا على ويدجت واحد ولا يمكن الوصول إليه عبر `BlocProvider` أبوي و `BuildContext` الحالي. لتحكم أدق في وقت استدعاء دالة `builder`، يمكن تمرير دالة اختيارية `buildWhen`. تأخذ `buildWhen` حالة الـ bloc السابقة والحالة الحالية وتعيد قيمة منطقية (boolean). إذا أعادت `true` فسيتم استدعاء `builder` وإعادة بناء الـ Widget، وإذا أعادت `false` فلن يتم استدعاء `builder` ولن تحدث إعادة بناء. ### BlocSelector **BlocSelector** هو ويدجت في Flutter يشبه `BlocBuilder` لكنه يسمح بتصفية التحديثات عبر اختيار قيمة جديدة بناءً على حالة الـ bloc الحالية. يتم منع عمليات البناء غير الضرورية إذا لم تتغير القيمة المختارة. يجب أن تكون القيمة المختارة غير قابلة للتغيير (immutable) حتى يتمكن `BlocSelector` من تحديد ما إذا كان يجب استدعاء `builder` مجددًا بدقة. إذا تم حذف معامل `bloc`، فسيقوم `BlocSelector` تلقائيًا بإجراء بحث باستخدام `BlocProvider` و `BuildContext` الحالي. ### BlocProvider **BlocProvider** هو ويدجت في Flutter يوفر bloc لأبنائه عبر `BlocProvider.of(context)`. يُستخدم كويدجت لحقن التبعية (Dependency Injection - DI) بحيث يمكن توفير مثيل واحد من الـ bloc لعدة ويدجتات داخل شجرة فرعية (subtree). في أغلب الحالات، يجب استخدام `BlocProvider` لإنشاء blocs جديدة ستكون متاحة لبقية الشجرة الفرعية. وبما أن `BlocProvider` هو المسؤول عن الإنشاء، فسيتولى تلقائيًا إغلاق الـ bloc. بشكل افتراضي، ينشئ `BlocProvider` الـ bloc بشكل كسول (lazily)، أي أن `create` سيتم تنفيذه عند البحث عن الـ bloc عبر `BlocProvider.of(context)`. لتجاوز هذا السلوك وإجبار `create` على العمل فورًا، يمكن تعيين `lazy` إلى `false`. في بعض الحالات، يمكن استخدام `BlocProvider` لتوفير bloc موجود بالفعل لجزء جديد من شجرة الـ Widgets (مثلاً عند فتح Route جديد). في هذه الحالة، لن يقوم `BlocProvider` بإغلاق الـ bloc تلقائيًا لأنه لم يقم بإنشائه. بعد ذلك، من أي من `ChildA` أو `ScreenA` يمكننا استرداد `BlocA` باستخدام: ### MultiBlocProvider **MultiBlocProvider** هو ويدجت في Flutter يدمج عدة `BlocProvider` في ويدجت واحد. يحسن قابلية القراءة ويزيل الحاجة إلى تداخل (nesting) عدة `BlocProvider`. باستخدام `MultiBlocProvider` يمكننا الانتقال من: إلى: :::caution عندما يتم تعريف `BlocProvider` ضمن سياق `MultiBlocProvider`، سيتم تجاهل أي `child`. ::: ### BlocListener **BlocListener** هو ويدجت في Flutter يأخذ `BlocWidgetListener` و `Bloc` اختياري، ويستدعي `listener` استجابةً لتغيرات الحالة في الـ bloc. يُستخدم للوظائف التي يجب أن تحدث مرة واحدة لكل تغيير حالة مثل التنقل، إظهار `SnackBar` أو `Dialog`. يتم استدعاء `listener` مرة واحدة فقط لكل تغيير في الحالة (**لا** يشمل الحالة الأولية) على عكس `builder` في `BlocBuilder`، وهو دالة `void`. إذا تم حذف معامل `bloc`، فسيقوم `BlocListener` تلقائيًا بإجراء بحث باستخدام `BlocProvider` و `BuildContext` الحالي. قم بتحديد الـ bloc فقط إذا كنت ترغب في توفير bloc لا يمكن الوصول إليه عبر `BlocProvider` و `BuildContext` الحالي. لتحكم أدق في وقت استدعاء `listener`، يمكن تمرير `listenWhen`. تأخذ `listenWhen` حالة الـ bloc السابقة والحالة الحالية وتعيد قيمة منطقية. إذا أعادت `true` فسيتم استدعاء `listener`، وإذا أعادت `false` فلن يتم استدعاؤه. ### MultiBlocListener **MultiBlocListener** يدمج عدة `BlocListener` في ويدجت واحد لتحسين قابلية القراءة وتجنب التداخل. باستخدامه يمكننا الانتقال من: إلى: :::caution عندما يتم تعريف `BlocListener` ضمن سياق `MultiBlocListener`، سيتم تجاهل أي `child`. ::: ### BlocConsumer **BlocConsumer** يجمع بين `builder` و `listener` للتفاعل مع الحالات الجديدة. يشبه استخدام `BlocListener` و `BlocBuilder` معًا لكنه يقلل من الكود المتكرر. يُستخدم فقط عندما تحتاج إلى إعادة بناء UI وتنفيذ تفاعل آخر مع تغيرات الحالة في نفس الوقت. إذا تم حذف معامل `bloc`، فسيقوم `BlocConsumer` تلقائيًا بإجراء بحث باستخدام `BlocProvider` و `BuildContext` الحالي. يمكن تمرير `listenWhen` و `buildWhen` اختياريًا للتحكم بشكل أدق في وقت استدعاء `listener` و `builder`. سيتم استدعاؤهما عند كل تغيير في `state` الخاص بالـ bloc. كل منهما يأخذ الحالة السابقة والحالية ويعيد `bool`. إذا لم يتم توفيرهما فالقيمة الافتراضية هي `true`. ### RepositoryProvider **RepositoryProvider** هو ويدجت في Flutter يوفر مستودعًا (repository) لأبنائه عبر `RepositoryProvider.of(context)`. يُستخدم لحقن التبعية (DI) لتوفير مثيل واحد من المستودع لعدة ويدجتات داخل شجرة فرعية. يُستخدم `BlocProvider` للـ blocs بينما يُستخدم `RepositoryProvider` للمستودعات فقط. ثم من `ChildA` يمكننا استرداد مثيل `Repository` باستخدام: يمكن للمستودعات التي تدير موارد يجب التخلص منها (dispose) القيام بذلك عبر دالة `dispose`: ### MultiRepositoryProvider **MultiRepositoryProvider** يدمج عدة `RepositoryProvider` في ويدجت واحد لتحسين قابلية القراءة وتجنب التداخل. باستخدامه يمكننا الانتقال من: إلى: :::caution عندما يتم تعريف `RepositoryProvider` ضمن سياق `MultiRepositoryProvider`، سيتم تجاهل أي `child`. ::: ## استخدام BlocProvider دعونا نلقي نظرة على كيفية استخدام `BlocProvider` لتوفير `CounterBloc` إلى `CounterPage` والتفاعل مع تغيرات الحالة باستخدام `BlocBuilder`. في هذه المرحلة، نجحنا في فصل طبقة العرض (presentational layer) عن طبقة منطق الأعمال (business logic layer). لاحظ أن ويدجت `CounterPage` لا يعرف شيئًا عما يحدث عندما ينقر المستخدم على الأزرار. يخبر الـ Widget ببساطة `CounterBloc` بأن المستخدم ضغط زر الزيادة أو النقصان. ## استخدام RepositoryProvider سنلقي نظرة على كيفية استخدام `RepositoryProvider` ضمن سياق مثال [`flutter_weather`][flutter_weather_link]. في ملف `main.dart` نستدعي `runApp` مع ويدجت `WeatherApp`. سنقوم بحقن مثيل `WeatherRepository` في شجرة الـ Widgets عبر `RepositoryProvider`. عند إنشاء bloc، يمكننا الوصول إلى مثيل المستودع عبر `context.read` وحقن المستودع في الـ bloc عبر المُنشئ (constructor). :::tip إذا كان لديك أكثر من مستودع واحد، يمكنك استخدام `MultiRepositoryProvider` لتوفير عدة مستودعات للشجرة الفرعية. ::: :::note استخدم `dispose` للتعامل مع تحرير الموارد عند إزالة `RepositoryProvider` من شجرة الـ Widgets. ::: [flutter_weather_link]: https://github.com/felangel/bloc/blob/master/examples/flutter_weather ## دوال الامتداد (Extension Methods) [دوال الامتداد](https://dart.dev/guides/language/extension-methods)، التي تم تقديمها في Dart 2.7، هي طريقة لإضافة وظائف إلى المكتبات الموجودة. في هذا القسم، سنلقي نظرة على دوال الامتداد في `package:flutter_bloc` وكيف يمكن استخدامها. تعتمد `flutter_bloc` على [package:provider](https://pub.dev/packages/provider) والتي تبسط استخدام [`InheritedWidget`](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html). داخليًا، تستخدم `package:flutter_bloc` حزمة `provider` لتنفيذ: `BlocProvider` و `MultiBlocProvider` و `RepositoryProvider` و `MultiRepositoryProvider`. كما تصدر امتدادات `ReadContext` و `WatchContext` و `SelectContext` من `provider`. :::note تعرف على المزيد حول [`package:provider`](https://pub.dev/packages/provider). ::: ### context.read تقوم `context.read()` بالبحث عن أقرب مثيل سلف من النوع `T` وهي مكافئة وظيفيًا لـ `BlocProvider.of(context)`. تُستخدم غالبًا لاسترداد مثيل bloc لإضافة حدث داخل `onPressed`. :::note `context.read()` لا تستمع إلى `T` - إذا تغير الكائن المقدم من النوع `T` فلن تؤدي `context.read` إلى إعادة بناء الـ Widget. ::: #### الاستخدام ✅ **افعل** استخدام `context.read` لإضافة الأحداث في callbacks. ```dart onPressed() { context.read().add(CounterIncrementPressed()), } ``` ❌ **تجنب** استخدام `context.read` لاسترداد الحالة داخل `build`. ```dart @override Widget build(BuildContext context) { final state = context.read().state; return Text('$state'); } ``` الاستخدام أعلاه عرضة للخطأ لأن ويدجت `Text` لن يتم إعادة بناؤه إذا تغيرت حالة الـ bloc. :::caution استخدم `BlocBuilder` أو `context.watch` بدلًا من ذلك لإعادة البناء عند تغير الحالة. ::: ### context.watch مثل `context.read()`، توفر `context.watch()` أقرب مثيل سلف من النوع `T` لكنها تستمع أيضًا للتغييرات على المثيل. وهي مكافئة وظيفيًا لـ `BlocProvider.of(context, listen: true)`. إذا تغير الكائن المقدم من النوع `T` فستؤدي `context.watch` إلى إعادة بناء (rebuild). :::caution `context.watch` متاحة فقط داخل `build` في `StatelessWidget` أو `State`. ::: #### الاستخدام ✅ **افعل** استخدام `BlocBuilder` بدلًا من `context.watch` لتحديد نطاق إعادة البناء بشكل صريح. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (context, state) { // Whenever the state changes, only the Text is rebuilt. return Text(state.value); }, ), ), ); } ``` بدلًا من ذلك، استخدم `Builder` لتحديد نطاق إعادة البناء. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Whenever the state changes, only the Text is rebuilt. final state = context.watch().state; return Text(state.value); }, ), ), ); } ``` ✅ **افعل** استخدام `Builder` و `context.watch` كبديل لـ `MultiBlocBuilder`. ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // return a Widget which depends on the state of BlocA, BlocB, and BlocC } ); ``` ❌ **تجنب** استخدام `context.watch` عندما لا يعتمد الـ Widget الأب على الحالة. ```dart @override Widget build(BuildContext context) { // Whenever the state changes, the MaterialApp is rebuilt // even though it is only used in the Text widget. final state = context.watch().state; return MaterialApp( home: Scaffold( body: Text(state.value), ), ); } ``` :::caution سيؤدي استخدام `context.watch` في جذر `build` إلى إعادة بناء الـ Widget بالكامل عند تغير حالة الـ bloc. ::: ### context.select تمامًا مثل `context.watch()`، توفر `context.select(...)` أقرب مثيل سلف من النوع `T` وتستمع إلى التغييرات عليه، لكنها تسمح بالاستماع إلى جزء أصغر من الحالة. ```dart Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); } ``` سيُعاد بناء الـ Widget فقط عند تغير خاصية `name` في حالة `ProfileBloc`. #### الاستخدام ✅ **افعل** استخدام `BlocSelector` بدلًا من `context.select` لتحديد نطاق إعادة البناء بشكل صريح. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector( selector: (state) => state.name, builder: (context, name) { // Whenever the state.name changes, only the Text is rebuilt. return Text(name); }, ), ), ); } ``` بدلًا من ذلك، استخدم `Builder` لتحديد نطاق إعادة البناء. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Whenever state.name changes, only the Text is rebuilt. final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), ); } ``` ❌ **تجنب** استخدام `context.select` عندما لا يعتمد الـ Widget الأب على الحالة. ```dart @override Widget build(BuildContext context) { // Whenever the state.value changes, the MaterialApp is rebuilt // even though it is only used in the Text widget. final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), ); } ``` :::caution سيؤدي استخدام `context.select` في جذر `build` إلى إعادة بناء الـ Widget بالكامل عند تغير الاختيار (selection). ::: ================================================ FILE: docs/src/content/docs/ar/getting-started.mdx ================================================ --- title: دليل البدء description: كل ما تحتاجه لبدء البناء باستخدام Bloc. --- import InstallationTabs from '~/components/getting-started/InstallationTabs.astro'; import ImportTabs from '~/components/getting-started/ImportTabs.astro'; ## الحزم يتكوّن نظام Bloc من عدة حزم مدرجة أدناه: | الحزمة | الوصف | الرابط | | ------------------------------------------------------------------------------------------ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | مكونات AngularDart | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | واجهات برمجة تطبيقات Dart الأساسية | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | محولات الأحداث | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | مدقق مخصص | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | واجهات برمجة تطبيقات للاختبار | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | أدوات سطر الأوامر | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | عناصر واجهة Flutter | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | دعم التخزين المؤقت/الاستمرارية | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | دعم التراجع/الإعادة | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | ## التثبيت :::note من أجل البدء باستخدام bloc، يجب أن يكون [Dart SDK مُثبت](https://dart.dev/get-dart) على جهازك. ::: ## الاستيراد الآن بعد أن قمنا بتثبيت bloc بنجاح، يمكننا إنشاء ملف `main.dart` واستيراد حزمة `bloc` المناسبة. ================================================ FILE: docs/src/content/docs/ar/index.mdx ================================================ --- template: splash title: مكتبة Bloc لإدارة الحالة description: التوثيق الرسمي لمكتبة Bloc لإدارة الحالة. تدعم Dart وFlutter وAngularDart. يتضمن أمثلة ودروسًا تعليمية. banner: content: | ✨ تفضل بزيارة متجر Bloc ✨ editUrl: false lastUpdated: false hero: title: Bloc v9.2.0 tagline: مكتبة لإدارة الحالة في Dart تعتمد على قابلية التنبؤ. image: alt: Bloc logo file: ~/assets/bloc.svg actions: - text: ابدأ الآن link: /ar/getting-started/ variant: primary icon: rocket - text: عرض على GitHub link: https://github.com/felangel/bloc icon: github variant: secondary --- import { CardGrid } from '@astrojs/starlight/components'; import SponsorsGrid from '~/components/landing/SponsorsGrid.astro'; import Card from '~/components/landing/Card.astro'; import ListCard from '~/components/landing/ListCard.astro'; import SplitCard from '~/components/landing/SplitCard.astro'; import Discord from '~/components/landing/Discord.astro';
```sh # .إلى مشروعك bloc أضف dart pub add bloc ``` يوفر [دليل البدء](/ar/getting-started) إرشادات خطوة بخطوة حول كيفية بدء استخدام Bloc خلال بضع دقائق فقط. أكمل [الدروس الرسمية](/ar/tutorials/flutter-counter) لتتعلم أفضل الممارسات وبناء مجموعة متنوعة من التطبيقات المعتمدة على Bloc. استكشف [أمثلة تطبيقات عالية الجودة ومختبرة بالكامل](https://github.com/felangel/bloc/tree/master/examples) مثل العداد، والمؤقت، والقائمة اللانهائية، والطقس، والمهام، والمزيد. - [لماذا Bloc؟](/ar/why-bloc) - [المفاهيم الأساسية](/ar/bloc-concepts) - [البنية المعمارية](/ar/architecture) - [الاختبارات](/ar/testing) - [اتفاقيات التسمية](/ar/naming-conventions) - [الأسئلة الشائعة](/ar/faqs) - [التكامل مع VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) - [التكامل مع IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) - [التكامل مع Neovim](https://github.com/wa11breaker/flutter-bloc.nvim) - [التكامل مع Mason CLI](https://github.com/felangel/bloc/blob/master/bricks/README.md) - [قوالب مخصصة](https://brickhub.dev/search?q=bloc) - [أدوات المطورين](https://github.com/felangel/bloc/blob/master/packages/bloc_tools/README.md) ================================================ FILE: docs/src/content/docs/ar/lint/configuration.mdx ================================================ --- title: إعداد أداة التدقيق (Linter Configuration) description: كيفية إعداد أداة التدقيق الخاصة بـ bloc. sidebar: order: 3 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import BlocLintBasicAnalysisOptionsSnippet from '~/components/lint/BlocLintBasicAnalysisOptionsSnippet.astro'; import RunBlocLintInCurrentDirectorySnippet from '~/components/lint/RunBlocLintInCurrentDirectorySnippet.astro'; import RunBlocLintInSrcTestSnippet from '~/components/lint/RunBlocLintInSrcTestSnippet.astro'; import AvoidFlutterImportsWarningSnippet from '~/components/lint/ImportFlutterWarningSnippet.mdx'; import RunBlocLintCounterCubitSnippet from '~/components/lint/RunBlocLintCounterCubitSnippet.astro'; import AvoidFlutterImportsWarningOutputSnippet from '~/components/lint/ImportFlutterWarningOutputSnippet.astro'; افتراضيًا، لن تعرض أداة التدقيق في bloc أي تشخيصات ما لم تُعدّ خيارات التحليل (analysis options) في المشروع بشكل صريح. للبدء، أنشئ ملف `analysis_options.yaml` أو عدّله في جذر المشروع، بحيث يحتوي على قائمة القواعد تحت المفتاح الأعلى `bloc`: شغّل أداة التدقيق باستخدام الأمر التالي في terminal: سيحلّل الأمر أعلاه جميع الملفات في المجلد الحالي ومجلداته الفرعية. ويمكنك أيضًا تدقيق ملفات أو مجلدات محددة عبر تمريرها كوسائط لسطر الأوامر (command-line arguments): سيحلّل الأمر أعلاه كل الأكواد البرمجية داخل مجلدي `src` و`test`. إذا كانت قاعدة `avoid_flutter_imports` مفعّلة، فسيُبلّغ عن أي ملف `bloc` أو `cubit` يحتوي على `import` لـ Flutter على أنه تحذير: يمكنك مشاهدة التحذير عبر تشغيل الأمر `bloc lint`: يجب أن يكون المخرج (output) بالشكل التالي: :::note فيما يلي جميع قواعد التدقيق المدعومة (lint rules): ::: ================================================ FILE: docs/src/content/docs/ar/lint/customizing-rules.mdx ================================================ --- title: تخصيص قواعد أداة التدقيق (Lint Rules) description: تخصيص قواعد أداة التدقيق الخاصة بـ bloc sidebar: order: 4 --- import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import BlocLintEnablingRulesSnippet from '~/components/lint/BlocLintEnablingRulesSnippet.astro'; import BlocLintDisablingRulesSnippet from '~/components/lint/BlocLintDisablingRulesSnippet.astro'; import BlocLintChangingSeveritySnippet from '~/components/lint/BlocLintChangingSeveritySnippet.astro'; import ImportFlutterInfoSnippet from '~/components/lint/ImportFlutterInfoSnippet.mdx'; import ImportFlutterInfoOutputSnippet from '~/components/lint/ImportFlutterInfoOutputSnippet.astro'; import BlocLintExcludingFilesSnippet from '~/components/lint/BlocLintExcludingFilesSnippet.astro'; import BlocLintIgnoreForLineSnippet from '~/components/lint/BlocLintIgnoreForLineSnippet.astro'; import BlocLintIgnoreForFileSnippet from '~/components/lint/BlocLintIgnoreForFileSnippet.astro'; يمكنك تخصيص سلوك أداة التدقيق في bloc عبر تغيير مستوى الشدة (`severity`) لكل قاعدة، أو تمكين القواعد وتعطيلها بشكل فردي، أو استثناء ملفات من التحليل الساكن. ## تمكين وتعطيل القواعد تدعم أداة التدقيق في bloc قائمة متزايدة من القواعد. وتجدر الإشارة إلى أن هذه القواعد قد لا تكون متوافقة دائمًا مع تفضيلات الجميع. على سبيل المثال، قد يفضّل بعض المطورين استخدام blocs (`prefer_bloc`) بينما يفضّل آخرون استخدام cubits (`prefer_cubit`). :::note بعكس التحليل الساكن، قد تنتج قواعد التدقيق أحيانًا **إنذارات كاذبة (`false positives`)**. إذا صادفت ذلك أو أي مشكلة أخرى، يمكنك الإبلاغ عنها عبر [فتح Issue](https://github.com/felangel/bloc/issues/new/choose). ::: ### تمكين القواعد الموصى بها توفّر مكتبة bloc مجموعة قواعد تدقيق موصى بها ضمن حزمة [`bloc_lint`](https://pub.dev/packages/bloc_lint). لتمكين هذه المجموعة، أضف حزمة `bloc_lint` كاعتماد تطوير (`dev dependency`): ثم عدّل ملف `analysis_options.yaml` لإضافة مجموعة القواعد: :::note عند إصدار نسخة جديدة من `bloc_lint`، قد تبدأ الأكواد البرمجية التي كانت تجتاز التحليل الساكن سابقًا في الفشل. نوصي بتحديث الأكواد البرمجية لتتوافق مع القواعد الجديدة، أو تمكين/تعطيل القواعد الفردية حسب الحاجة. ::: ### تمكين القواعد الفردية لتمكين قواعد فردية، أضف `bloc:` إلى ملف `analysis_options.yaml` كمفتاح من المستوى الأعلى (`top-level key`)، ثم أضف `rules:` كمفتاح من المستوى الثاني. بعد ذلك، اكتب القواعد المطلوبة على شكل قائمة YAML (مسبوقة بعلامة الشرطة `-`). على سبيل المثال: ### تعطيل القواعد الفردية إذا كنت تضمّن مجموعة قواعد موجودة مسبقًا، مثل `recommended`، فقد تحتاج إلى تعطيل قاعدة واحدة أو أكثر من القواعد المضمّنة. آلية التعطيل مشابهة للتمكين، لكنها تتطلب استخدام YAML map بدلًا من قائمة. على سبيل المثال، الإعداد التالي يستخدم مجموعة القواعد الموصى بها مع تعطيل `avoid_public_bloc_methods`، ويُفعّل أيضًا قاعدة `prefer_bloc`: ## تخصيص شدة القاعدة يمكنك تغيير شدة أي قاعدة بالشكل التالي: الآن سيتم الإبلاغ عن القاعدة نفسها بمستوى `info` بدلًا من `warning`: يجب أن تبدو المخرجات (output) لأمر `bloc lint` كما يلي: مستويات الشدة المدعومة: | الشدة (`Severity`) | الوصف (`Description`) | | :----------------- | :----------------------------------------- | | `error` | يشير إلى أن هذا النمط غير مسموح. | | `warning` | يشير إلى أن النمط مريب لكنه مسموح. | | `info` | يقدّم معلومات للمستخدم، لكنه لا يعد مشكلة. | | `hint` | يقترح طريقة أفضل للوصول إلى النتيجة. | ## استثناء الملفات أحيانًا يكون من المقبول تجاهل فشل التحليل الساكن. على سبيل المثال، قد ترغب في تجاهل التحذيرات أو الأخطاء في الأكواد البرمجية المُولّدة (`generated code`) التي لم يكتبها فريقك. وكما في قواعد تدقيق Dart الرسمية، يمكنك استخدام خيار المحلل `exclude:` لاستثناء ملفات من التحليل الساكن. يمكنك إما تحديد ملفات بعينها أو استخدام أنماط [`glob`](https://pub.dev/packages/glob). :::note يجب أن تكون جميع أنماط `glob` نسبيةً إلى المجلد الذي يحتوي على ملف `analysis_options.yaml` المقابل. ::: على سبيل المثال، يمكننا استثناء جميع أكواد Dart المُولّدة عبر إعدادات التحليل التالية: ## تجاهل القواعد تمامًا كما هو الحال مع قواعد تدقيق Dart الرسمية، يمكنك تجاهل قواعد bloc لملف معيّن أو سطر من الأكواد البرمجية باستخدام `// ignore_for_file` و `// ignore` على التوالي. :::note لتجاهل عدة قواعد في سطر أو ملف معيّن، استخدم قائمة مفصولة بفواصل. ::: ### تجاهل الأسطر يمكنك تجاهل حالات محددة من انتهاكات القواعد بإضافة تعليق `ignore` إما فوق السطر المخالف مباشرة، أو في نهاية السطر نفسه. على سبيل المثال، يمكن تجاهل حالات محددة من `prefer_file_naming_conventions` في ملف معيّن: ### تجاهل الملفات يمكنك تجاهل جميع حالات انتهاكات القواعد داخل ملف عبر إضافة تعليق `ignore_for_file` في أي مكان في الملف. على سبيل المثال، يمكن تجاهل جميع حالات `prefer_file_naming_conventions` في ملف معيّن: ================================================ FILE: docs/src/content/docs/ar/lint/index.mdx ================================================ --- title: نظرة عامة على أداة التدقيق (Linter Overview) description: مقدمة إلى أداة التدقيق الخاصة بـ bloc. sidebar: order: 1 --- import AvoidFlutterImportsWarningSnippet from '~/components/lint/ImportFlutterWarningSnippet.mdx'; import AvoidFlutterImportsWarningOutputSnippet from '~/components/lint/ImportFlutterWarningOutputSnippet.astro'; import InstallBlocToolsSnippet from '~/components/lint/InstallBlocToolsSnippet.astro'; import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import RunBlocLintInCurrentDirectorySnippet from '~/components/lint/RunBlocLintInCurrentDirectorySnippet.astro'; التدقيق (Linting) هو عملية تحليل ساكن للكود لاكتشاف الأخطاء المحتملة، إلى جانب المشكلات البرمجية ومشكلات الأسلوب. توفّر مكتبة Bloc أداة تدقيق مدمجة يمكن استخدامها عبر بيئة التطوير (IDE)، أو من خلال [`أدوات سطر أوامر bloc`](https://pub.dev/packages/bloc_tools) باستخدام الأمر `bloc lint`. بمساعدة أداة التدقيق في bloc، يمكنك رفع جودة قاعدة الأكواد البرمجية وفرض الاتساق دون تنفيذ أي سطر من الكود. على سبيل المثال، قد تستورد بالخطأ اعتمادًا خاصًا بـ Flutter داخل الـ cubit: إذا كانت الأداة مُعدّة بشكل صحيح، فستشير إلى هذا الاستيراد وتُظهر التحذير التالي: في الأقسام التالية، سنشرح كيفية تثبيت أداة التدقيق في bloc وإعدادها وتخصيصها، حتى تستفيد من التحليل الساكن في مشروعك. ## البدء السريع (Quick Start) ابدأ باستخدام أداة التدقيق في bloc عبر خطوات سريعة وبسيطة. :::note لبدء استخدام bloc، يجب أن تكون [Dart SDK](https://dart.dev/get-dart) مثبتة على جهازك. ::: 1. ثبّت [أدوات سطر أوامر bloc](https://pub.dev/packages/bloc_tools) 2. ثبّت حزمة [bloc_lint](https://pub.dev/packages/bloc_lint) 3. أضف ملف `analysis_options.yaml` إلى جذر مشروعك مع القواعد الموصى بها 4. شغّل أداة التدقيق وهذا كل ما تحتاج إليه 🎉 تابع القراءة للحصول على شرح أعمق لإعداد أداة التدقيق في bloc وتخصيصها. ================================================ FILE: docs/src/content/docs/ar/lint/installation.mdx ================================================ --- title: تثبيت أداة التدقيق (Linter Installation) description: كيفية تثبيت أداة التدقيق الخاصة بـ bloc. sidebar: order: 2 --- import { CardGrid } from '@astrojs/starlight/components'; import Card from '~/components/landing/Card.astro'; import InstallBlocToolsSnippet from '~/components/lint/InstallBlocToolsSnippet.astro'; import BlocToolsLintHelpOutputSnippet from '~/components/lint/BlocToolsLintHelpOutputSnippet.astro'; import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import BlocLintMultipleRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintMultipleRecommendedAnalysisOptionsSnippet.astro'; ## أدوات سطر الأوامر لاستخدام أداة التدقيق من سطر الأوامر، ثبّت حزمة [`package:bloc_tools`](https://pub.dev/packages/bloc_tools) عبر الأمر التالي: بعد تثبيت أدوات سطر أوامر bloc، يمكنك تشغيل أداة التدقيق عبر الأمر `bloc lint`: ## مجموعة القواعد الموصى بها لتثبيت مجموعة قواعد التدقيق الموصى بها، ثبّت حزمة [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) كاعتماد تطوير (`dev dependency`) باستخدام الأمر التالي: ثم أضف ملف `analysis_options.yaml` إلى جذر المشروع مع مجموعة القواعد الموصى بها: عند الحاجة، يمكنك تضمين أكثر من مجموعة قواعد عبر تعريفها كقائمة: ## تكاملات بيئات التطوير المتكاملة (IDE Integrations) تدعم بيئات التطوير المتكاملة (IDEs) التالية رسميًا أداة التدقيق في bloc وخادم اللغة (language server) لتوفير تشخيصات فورية مباشرة داخل بيئة التطوير الخاصة بك. يتوفر دعم [إضافة Bloc لـ VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) ابتداءً من الإصدار `v6.8.0`. يتوفر دعم [إضافة Bloc لـ IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) ابتداءً من الإصدار `v4.1.0`. ================================================ FILE: docs/src/content/docs/ar/lint-rules/avoid_build_context_extensions.mdx ================================================ --- title: تجنب امتدادات BuildContext (Avoid BuildContext Extensions) description: قاعدة `avoid_build_context_extensions`. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_build_context_extensions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_build_context_extensions/GoodSnippet.astro';
تجنب استخدام امتدادات `BuildContext` للوصول إلى instances من `Bloc` أو `Cubit`. :::note تم تقديم قاعدة التدقيق (lint rule) هذه في الإصدار `0.3.0` من [`package:bloc_lint`](https://pub.dev/packages/bloc_lint). ::: ## المبرر (Rationale) لتحقيق الاتساق والوضوح، يفضَّل استخدام الطرق الأساسية (methods) بشكل مباشر بدلًا من امتدادات `BuildContext`. وهذا مفيد أيضًا في الاختبارات، لأنه **لا يمكن** إنشاء محاكاة (`mock`) لـ `extension method`. | الامتداد (extension) | الطريقة الصريحة (explicit method) | | -------------------- | ------------------------------------------------------------------- | | `context.read` | `BlocProvider.of(context, listen: false)` | | `context.watch` | `BlocBuilder(...)` أو `BlocProvider.of(context)` | | `context.select` | `BlocSelector(...)` | ## أمثلة (Examples) **تجنب** استخدام امتدادات `BuildContext` للتعامل مع نسخ (instances) من `Bloc` أو `Cubit`. **مثال سيئ (BAD)**: **مثال جيد (GOOD)**: ## التفعيل (Enable) لتفعيل قاعدة `avoid_build_context_extensions`، أضفها إلى `analysis_options.yaml` ضمن `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ar/lint-rules/avoid_flutter_imports.mdx ================================================ --- title: تجنب استيراد Flutter (Avoid Flutter Imports) description: قاعدة `avoid_flutter_imports` الخاصة بـ Bloc. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_flutter_imports/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_flutter_imports/GoodSnippet.astro';
تجنب إضافة تبعيات (dependencies) على Flutter داخل مكونات منطق الأعمال (Business Logic Components) (نسخ (instances) من `Bloc` أو `Cubit`). ## المبرر (Rationale) يعد تقسيم التطبيق إلى طبقات (Layering an application) جزءًا أساسيًا لبناء قاعدة أكواد برمجية قابلة للصيانة (maintainable codebase)، كما يساعد المطورين على التطوير بسرعة وثقة. يجب أن تمتلك كل طبقة مسؤولية واحدة (single responsibility) وأن تكون قابلة للعمل والاختبار بشكل مستقل (in isolation). هذا يسهّل حصر التغييرات داخل طبقات محددة ويقلل تأثيرها على التطبيق بالكامل. بناءً على ذلك، من الأفضل أن تركز مكونات منطق الأعمال (Business Logic Components) على إدارة حالة الميزة (`feature state`) فقط، مع بقائها مفصولة عن طبقة واجهة المستخدم (UI layer). يجب أن تتدفق الأحداث (`events`) من طبقة واجهة المستخدم (UI layer) إلى مكونات منطق الأعمال (Business Logic Components)، بينما تتدفق الحالة (`state`) من طبقة منطق الأعمال (Business Logic layer) إلى طبقة واجهة المستخدم (UI layer). فصل مكونات منطق الأعمال (Business Logic Components) عن Flutter يسهّل إعادة استخدام منطق الأعمال عبر منصات أو أطر عمل (frameworks) متعددة (مثل Flutter وAngularDart وJaspr وغيرها). ## أمثلة (Examples) **لا تقم** باستيراد Flutter ضمن مكونات منطق الأعمال الخاصة بك. **مثال سيئ (BAD)**: **مثال جيد (GOOD)**: ## التفعيل (Enable) لتفعيل قاعدة `avoid_flutter_imports`، أضفها إلى `analysis_options.yaml` ضمن `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ar/lint-rules/avoid_public_bloc_methods.mdx ================================================ --- title: تجنب استخدام دوال Bloc العامة description: قاعدة `avoid_public_bloc_methods`. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_public_bloc_methods/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_public_bloc_methods/GoodSnippet.astro';
تجنب كشف الدوال العامة (public methods) على instances من `Bloc`. ## المبرر (Rationale) يتفاعل الـ Bloc مع الأحداث الواردة (incoming events) ويُصدر الحالات الصادرة (outgoing states). لذلك، الطريقة الموصى بها للتواصل مع instance من `Bloc` هي عبر الدالة `add`. في معظم الحالات، لا حاجة لبناء تجريدات إضافية (additional abstractions) فوق واجهة `add` (API). ![هيكلية Bloc](~/assets/concepts/bloc_architecture_full.png) ## أمثلة (Examples) **لا تقم** بكشف الدوال العامة على instances من `Bloc`. **مثال سيئ (BAD)**: **مثال جيد (GOOD)**: ## التفعيل (Enable) لتفعيل قاعدة `avoid_public_bloc_methods`، أضفها إلى `analysis_options.yaml` ضمن `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ar/lint-rules/avoid_public_fields.mdx ================================================ --- title: تجنب استخدام الحقول العامة description: قاعدة `avoid_public_fields`. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_public_fields/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_public_fields/GoodSnippet.astro';
تجنب كشف الحقول العامة (public fields) على instances من `Bloc` و`Cubit`. ## المبرر (Rationale) تحافظ مكونات منطق الأعمال (Business Logic Components) على `state` الخاص بها وتُصدر تغييرات الحالة عبر واجهة `emit` (API). لذلك، يجب كشف كل الحالة الظاهرة للعامة (public-facing state) من خلال كائن `state`. ## أمثلة (Examples) **لا تقم** بكشف الحقول العامة على instances من `Bloc` و`Cubit`. **مثال سيئ (BAD)**: **مثال جيد (GOOD)**: ## التفعيل (Enable) لتفعيل قاعدة `avoid_public_fields`، أضفها إلى `analysis_options.yaml` ضمن `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ar/lint-rules/prefer_bloc.mdx ================================================ --- title: تفضيل Bloc description: قاعدة `prefer_bloc`. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_bloc/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_bloc/GoodSnippet.astro';
يفضَّل استخدام instances من `Bloc` بدلًا من instances من `Cubit`. ## المبرر (Rationale) هذه قاعدة أسلوبية (stylistic rule) بحتة. في بعض الحالات، قد تفضّل الفرق توحيد استخدام instances من `Bloc` فقط على مستوى التطبيق بالكامل لتحقيق الاتساق (consistency). :::tip تعرّف على المزيد حول مزايا `Bloc` في [المفاهيم الأساسية](/ar/bloc-concepts/#مزايا-bloc). ::: ## أمثلة (Examples) **تجنب** استخدام instances من `Cubit`. **مثال سيئ (BAD)**: **مثال جيد (GOOD)**: ## التفعيل (Enable) لتفعيل قاعدة `prefer_bloc`، أضفها إلى `analysis_options.yaml` ضمن `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ar/lint-rules/prefer_build_context_extensions.mdx ================================================ --- title: تفضيل استخدام امتدادات BuildContext description: قاعدة `prefer_build_context_extensions`. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_build_context_extensions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_build_context_extensions/GoodSnippet.astro';
يفضَّل استخدام امتدادات `BuildContext` للوصول إلى instance من `Bloc` أو `Repository`. :::note تم تقديم قاعدة التدقيق (lint rule) هذه في الإصدار `0.3.2` من [`package:bloc_lint`](https://pub.dev/packages/bloc_lint). ::: ## المبرر (Rationale) لتحقيق الاتساق (consistency)، يفضَّل استخدام امتدادات `BuildContext` مثل `context.read` و`context.watch` و`context.select` بدلًا من `BlocProvider.of` و `RepositoryProvider.of` و`BlocBuilder` و`BlocSelector`. | الطريقة الصريحة (explicit method) | الامتداد (extension) | | ------------------------------------------------------------------- | --------------------- | | `BlocProvider.of(context, listen: false)` | `context.read` | | `BlocBuilder(...)` أو `BlocProvider.of(context)` | `context.watch` | | `BlocSelector(...)` | `context.select` | ## أمثلة (Examples) **تجنب** استخدام `BlocProvider.of(context)` للوصول إلى instance من `Bloc`. **مثال سيئ (BAD)**: **مثال جيد (GOOD)**: ## التفعيل (Enable) لتفعيل قاعدة `prefer_build_context_extensions`، أضفها إلى `analysis_options.yaml` ضمن `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ar/lint-rules/prefer_cubit.mdx ================================================ --- title: تفضيل Cubit description: قاعدة `prefer_cubit`. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_cubit/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_cubit/GoodSnippet.astro';
يفضَّل استخدام instances من `Cubit` بدلًا من instances من `Bloc`. ## المبرر (Rationale) هذه قاعدة أسلوبية (stylistic rule) بحتة. في بعض الحالات، قد تفضّل الفرق توحيد استخدام instances من `Cubit` فقط على مستوى التطبيق بالكامل لتحقيق الاتساق (consistency). :::tip تعرّف على المزيد حول مزايا `Cubit` في [المفاهيم الأساسية](/ar/bloc-concepts/#مزايا-cubit). ::: ## أمثلة (Examples) **تجنب** استخدام instances من `Bloc`. **مثال سيئ (BAD)**: **مثال جيد (GOOD)**: ## التفعيل (Enable) لتفعيل قاعدة `prefer_cubit`، أضفها إلى `analysis_options.yaml` ضمن `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ar/lint-rules/prefer_file_naming_conventions.mdx ================================================ --- title: تفضيل اصطلاحات تسمية الملفات description: قاعدة `prefer_file_naming_conventions`. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_file_naming_conventions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_file_naming_conventions/GoodSnippet.astro';
يفضَّل اتباع اصطلاحات تسمية الملفات. :::note تم تقديم قاعدة التدقيق (lint rule) هذه في الإصدار `0.3.0` من حزمة [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## المبرر (Rationale) لتحقيق الاتساق وسهولة الصيانة وفصل الاهتمامات (separation of concerns)، يفضَّل تعريف instances من `bloc` و`cubit` في ملفات Dart الخاصة بها بدلًا من تضمينها مباشرة (inlining). :::tip فكر في استخدام الأمر `bloc new ` من حزمة [package:bloc_tools](https://pub.dev/packages/bloc_tools) لإنشاء instances جديدة من bloc/cubit بسرعة وبشكل متسق. ::: ## أمثلة (Examples) **يفضَّل** التصريح عن instances من bloc/cubit في ملفاتها الخاصة. **مثال جيد (GOOD)**: **مثال سيئ (BAD)**: ## التفعيل (Enable) لتفعيل قاعدة `prefer_file_naming_conventions`، أضفها إلى ملف `analysis_options.yaml` ضمن `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ar/lint-rules/prefer_void_public_cubit_methods.mdx ================================================ --- title: تفضيل الدوال العامة ذات القيمة المرتجعة Void في Cubit description: قاعدة `prefer_void_public_cubit_methods`. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_void_public_cubit_methods/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_void_public_cubit_methods/GoodSnippet.astro';
يفضَّل أن تكون الدوال العامة (public methods) في instances من `Cubit` ذات قيمة مرتجعة `void`. :::note تم تقديم قاعدة التدقيق (lint rule) هذه في الإصدار `0.2.0-dev.2` من حزمة [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## المبرر (Rationale) يجب استخدام الدوال العامة (public methods) في instances من `Cubit` لإخطار `Cubit` وبدء تغييرات الحالة عبر الدالة `emit`. وإذا احتاج المستدعي (caller) إلى أي معلومات عن الحالة، فيفترض أن يحصل عليها من `state` بدلًا من ذلك. :::note القواعد التالية مرتبطة، وغالبًا ما تُفعّل مع قاعدة `prefer_void_public_cubit_methods`: - [`avoid_public_bloc_methods`](/ar/lint-rules/avoid_public_bloc_methods) - [`avoid_public_fields`](/ar/lint-rules/avoid_public_fields) ::: ## أمثلة (Examples) **تجنب** الدوال العامة (public methods) ذات القيمة المرتجعة غير `void` في instances من `Cubit`. **مثال سيئ (BAD)**: **مثال جيد (GOOD)**: ## التفعيل (Enable) لتفعيل قاعدة `prefer_void_public_cubit_methods`، أضفها إلى ملف `analysis_options.yaml` ضمن `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ar/migration.mdx ================================================ --- title: دليل الترقية (Migration Guide) description: الترقية إلى أحدث إصدار مستقر من Bloc. --- import { Code, Tabs, TabItem } from '@astrojs/starlight/components'; :::tip يرجى الرجوع إلى [سجل الإصدارات](https://github.com/felangel/bloc/releases) للاطلاع على تفاصيل التغييرات في كل إصدار. ::: ## v10.0.0 ### `package:bloc_test` #### ❗✨ فصل `blocTest` عن `BlocBase` :::note[ما الذي تغير؟] في إصدار bloc_test v10.0.0، لم يعد واجهة برمجة التطبيقات (API) لـ `blocTest` مرتبطة بشكل وثيق بـ `BlocBase`. ::: ##### الأسباب (Rationale) يجب أن يستخدم `blocTest` واجهات bloc الأساسية كلما أمكن ذلك لزيادة المرونة وقابلية إعادة الاستخدام. سابقاً، لم يكن هذا ممكناً لأن `BlocBase` كان يطبق `StateStreamableSource` وهو ما لم يكن كافياً لـ `blocTest` بسبب الاعتماد الداخلي على API الـ `emit`. ### `package:hydrated_bloc` #### ❗✨ دعم WebAssembly :::note[ما الذي تغير؟] في إصدار hydrated_bloc v10.0.0، تمت إضافة دعم للترجمة البرمجية (compiling) إلى WebAssembly (wasm). ::: ##### الأسباب (Rationale) سابقاً، لم يكن من الممكن ترجمة التطبيقات إلى wasm عند استخدام `hydrated_bloc`. في الإصدار v10.0.0، تمت إعادة هيكلة الحزمة للسماح بالترجمة إلى wasm. **v9.x.x** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` **v10.x.x** ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorageDirectory.web : HydratedStorageDirectory((await getTemporaryDirectory()).path), ); runApp(const App()); } ``` ## v9.0.0 ### `package:bloc` #### ❗🧹 إزالة الواجهات المهجورة :::note[ما الذي تغير؟] في الإصدار bloc v9.0.0، تمت إزالة جميع الواجهات التي تم الإعلان عنها سابقًا كـ "مهجورة" (deprecated). ::: ##### ملخص - تمت إزالة `BlocOverrides` واستبداله بـ `Bloc.observer` و `Bloc.transformer`. #### ❗✨ تقديم واجهة `EmittableStateStreamableSource` :::note[ما الذي تغير؟] في هذا الإصدار، تم تقديم واجهة أساسية جديدة باسم `EmittableStateStreamableSource`. ::: ##### السبب كان `package:bloc_test` مرتبطًا بشكل مباشر بـ `BlocBase`. تم تقديم هذه الواجهة لفصل `blocTest` عن التنفيذ الفعلي لـ `BlocBase`. ### `package:hydrated_bloc` #### ✨ إعادة تقديم `HydratedBloc.storage` :::note[ما الذي تغير؟] في الإصدار hydrated_bloc v9.0.0، تمت إزالة `HydratedBlocOverrides` واستبداله باستخدام `HydratedBloc.storage`. ::: ##### السبب راجع: [سبب إعادة تقديم Bloc.observer و Bloc.transformer](/ar/migration/#-إعادة-تقديم-واجهات-برمجة-التطبيقات-blocobserver-و-bloctransformer) **v8.x.x** ```dart Future main() async { final storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); HydratedBlocOverrides.runZoned( () => runApp(App()), storage: storage, ); } ``` **v9.0.0** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` ## v8.1.0 ### `package:bloc` #### ✨ إعادة تقديم واجهات برمجة التطبيقات `Bloc.observer` و `Bloc.transformer` :::note[ما الذي تغير؟] في إصدار bloc v8.1.0، تم وضع علامة "مهجور" على `BlocOverrides` لصالح واجهات `Bloc.observer` و `Bloc.transformer`. ::: ##### الأسباب (Rationale) تم تقديم API الـ `BlocOverrides` في الإصدار v8.0.0 في محاولة لدعم نطاق (scoping) إعدادات الـ bloc المحددة مثل `BlocObserver` و `EventTransformer` و `HydratedStorage`. في تطبيقات Dart الصرفة، عملت التغييرات بشكل جيد؛ ومع ذلك، في تطبيقات Flutter، تسبب API الجديد في مشاكل أكثر مما حلها. تم استلهام API الـ `BlocOverrides` من واجهات مماثلة في Flutter/Dart: - [HttpOverrides](https://api.flutter.dev/flutter/dart-io/HttpOverrides-class.html) - [IOOverrides](https://api.flutter.dev/flutter/dart-io/IOOverrides-class.html) **المشاكل** على الرغم من أنه لم يكن السبب الرئيسي لهذه التغييرات، إلا أن API الـ `BlocOverrides` أضاف تعقيداً إضافياً للمطورين. فبالإضافة إلى زيادة مقدار التداخل (nesting) وعدد أسطر الكود اللازمة لتحقيق نفس التأثير، تطلب API الـ `BlocOverrides` من المطورين أن يكون لديهم فهم قوي للـ [Zones](https://api.dart.dev/stable/2.17.6/dart-async/Zone-class.html) في Dart. الـ `Zones` ليست مفهوماً سهلاً للمبتدئين، والفشل في فهم كيفية عملها قد يؤدي إلى ظهور أخطاء (مثل عدم تهيئة المراقبين، المحولات، أو مثيلات التخزين). على سبيل المثال، كان لدى العديد من المطورين كود مثل: ```dart void main() { WidgetsFlutterBinding.ensureInitialized(); BlocOverrides.runZoned(...); } ``` الكود أعلاه قد يبدو بسيطًا وغير ضار، لكنه قد يؤدي فعليًا إلى أخطاء يصعب تتبعها. الـ Zone التي يتم استدعاء `WidgetsFlutterBinding.ensureInitialized` منها في البداية ستكون هي نفسها المستخدمة لمعالجة أحداث الواجهة (مثل `onTap` و `onPressed`) وذلك بسبب `GestureBinding.initInstances`. وهذا مجرد مثال واحد من العديد من المشاكل الناتجة عن استخدام `zoneValues`. بالإضافة إلى ذلك، يقوم Flutter بالعديد من العمليات خلف الكواليس التي تتضمن إنشاء أو تعديل الـ Zones (خصوصًا أثناء تشغيل الاختبارات)، مما قد يؤدي إلى سلوك غير متوقع، وفي بعض الحالات سلوك خارج عن سيطرة المطور (انظر المشاكل أدناه). بسبب استخدام [runZoned](https://api.flutter.dev/flutter/dart-async/runZoned.html)، أدى الانتقال إلى `BlocOverrides` إلى اكتشاف عدة مشاكل وقيود في Flutter، خاصة في اختبارات Widget و Integration: - https://github.com/flutter/flutter/issues/96939 - https://github.com/flutter/flutter/issues/94123 - https://github.com/flutter/flutter/issues/93676 كما أثرت هذه المشاكل على العديد من مستخدمي مكتبة bloc: - https://github.com/felangel/bloc/issues/3394 - https://github.com/felangel/bloc/issues/3350 - https://github.com/felangel/bloc/issues/3319 **v8.0.x** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` **v8.1.0** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` ## v8.0.0 ### `package:bloc` #### ❗✨ تقديم `BlocOverrides` :::note[ما الذي تغير؟] في الإصدار bloc v8.0.0، تمت إزالة `Bloc.observer` و `Bloc.transformer` واستبدالهما بـ `BlocOverrides`. ::: ##### السبب كان النهج السابق لتجاوز `BlocObserver` و `EventTransformer` يعتمد على singleton عام لكل منهما. ونتيجة لذلك، لم يكن من الممكن: - استخدام عدة تطبيقات مختلفة من `BlocObserver` أو `EventTransformer` ضمن أجزاء مختلفة من التطبيق - حصر (scoping) هذه الإعدادات داخل حزمة (package) معينة - في حال قامت حزمة تعتمد على `package:bloc` بتسجيل `BlocObserver` خاص بها، سيُجبر المستخدم إما على استبداله أو التعامل معه كما هو كما أن هذا النهج جعل عملية الاختبار أكثر صعوبة بسبب وجود حالة عامة مشتركة بين الاختبارات. يقدم الإصدار v8.0.0 فئة `BlocOverrides` التي تتيح للمطورين تخصيص `BlocObserver` و/أو `EventTransformer` ضمن `Zone` محددة، بدلاً من الاعتماد على singleton عام قابل للتغيير. **v7.x.x** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` **v8.0.0** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` ستستخدم كائنات `Bloc` كلًا من `BlocObserver` و/أو `EventTransformer` الخاصة بالـ `Zone` الحالية عبر `BlocOverrides.current`. في حال عدم وجود `BlocOverrides` لهذه الـ `Zone`، سيتم استخدام القيم الافتراضية الداخلية (دون أي تغيير في السلوك أو الوظائف). يسمح ذلك لكل `Zone` بالعمل بشكل مستقل باستخدام `BlocOverrides` الخاصة بها. ```dart BlocOverrides.runZoned( () { // BlocObserverA and eventTransformerA final overrides = BlocOverrides.current; // Blocs in this zone report to BlocObserverA // and use eventTransformerA as the default transformer. // ... // Later... BlocOverrides.runZoned( () { // BlocObserverB and eventTransformerB final overrides = BlocOverrides.current; // Blocs in this zone report to BlocObserverB // and use eventTransformerB as the default transformer. // ... }, blocObserver: BlocObserverB(), eventTransformer: eventTransformerB(), ); }, blocObserver: BlocObserverA(), eventTransformer: eventTransformerA(), ); ``` #### ❗✨ تحسين معالجة الأخطاء والإبلاغ عنها :::note[ما الذي تغير؟] في الإصدار bloc v8.0.0، تمت إزالة `BlocUnhandledErrorException`. كما يتم الآن دائمًا الإبلاغ عن أي استثناء غير مُعالج إلى `onError` ثم إعادة رميه (بغض النظر عن وضع debug أو release). أما `addError`، فيقوم بالإبلاغ عن الأخطاء إلى `onError`، لكنه لا يعتبر هذه الأخطاء استثناءات غير مُعالجة. ::: ##### السبب تهدف هذه التغييرات إلى: - جعل الاستثناءات غير المُعالجة واضحة بشكل أكبر مع الحفاظ على استقرار bloc - دعم استخدام `addError` دون التأثير على تدفق التنفيذ سابقًا، كان سلوك معالجة الأخطاء يختلف بين وضعي debug و release. كما أن الأخطاء المُبلغ عنها عبر `addError` كانت تُعامل كاستثناءات غير مُعالجة في وضع debug، مما كان يؤدي إلى تجربة غير جيدة للمطورين (خصوصًا عند كتابة اختبارات الوحدة). في الإصدار v8.0.0، يمكن استخدام `addError` بأمان للإبلاغ عن الأخطاء، كما يمكن استخدام `blocTest` للتحقق من ذلك. لا تزال جميع الأخطاء تُرسل إلى `onError`، ولكن يتم إعادة رمي الاستثناءات غير المُعالجة فقط (بغض النظر عن وضع التشغيل). #### ❗🧹 جعل `BlocObserver` فئة مجردة :::note[ما الذي تغير؟] في الإصدار bloc v8.0.0، تم تحويل `BlocObserver` إلى فئة `abstract`، مما يعني أنه لا يمكن إنشاء instance منها مباشرة. ::: ##### السبب تم تصميم `BlocObserver` ليكون بمثابة واجهة (interface). وبما أن التنفيذ الافتراضي لا يحتوي على أي سلوك (no-op)، فقد تم تحويله إلى فئة `abstract` لتوضيح أنه مخصص للتوسيع (extension) وليس للاستخدام المباشر. ```dart void main() { // كان من الممكن إنشاء مثيل من الفئة الأساسية. final observer = BlocObserver(); } ``` **v8.0.0** ```dart class MyBlocObserver extends BlocObserver {...} void main() { // لا يمكن إنشاء مثيل من الفئة الأساسية. final observer = BlocObserver(); // خطأ (ERROR) // قم بتوسيع `BlocObserver` بدلاً من ذلك. final observer = MyBlocObserver(); // مقبول (OK) } ``` #### ❗✨ استدعاء `add` يرمي `StateError` عند إغلاق الـ Bloc :::note[ما الذي تغير؟] في الإصدار bloc v8.0.0، يؤدي استدعاء `add` على Bloc مغلق إلى رمي `StateError`. ::: ##### السبب سابقًا، كان من الممكن استدعاء `add` على Bloc مغلق، وكان الخطأ الداخلي يتم تجاهله، مما يجعل من الصعب معرفة سبب عدم معالجة الحدث. لجعل هذا السيناريو أكثر وضوحًا، في v8.0.0، يؤدي استدعاء `add` على Bloc مغلق إلى رمي `StateError`، ويتم التعامل معه كاستثناء غير مُعالج ويتم تمريره إلى `onError`. #### ❗✨ استدعاء `emit` يرمي `StateError` عند إغلاق الـ Bloc :::note[ما الذي تغير؟] في الإصدار bloc v8.0.0، يؤدي استدعاء `emit` داخل Bloc مغلق إلى رمي `StateError`. ::: ##### السبب سابقًا، كان من الممكن استدعاء `emit` داخل Bloc مغلق دون حدوث أي تغيير في الحالة، ودون وجود أي مؤشر على الخطأ، مما يجعل عملية التصحيح صعبة. لجعل هذا السلوك أكثر وضوحًا، في v8.0.0، يؤدي استدعاء `emit` داخل Bloc مغلق إلى رمي `StateError`، ويتم التعامل معه كاستثناء غير مُعالج ويتم تمريره إلى `onError`. #### ❗🧹 إزالة الواجهات المهجورة :::note[ما الذي تغير؟] في الإصدار bloc v8.0.0، تمت إزالة جميع الواجهات التي تم الإعلان عنها سابقًا كـ مهجورة (deprecated). ::: ##### ملخص - تمت إزالة `mapEventToState` واستبداله بـ `on` - تمت إزالة `transformEvents` واستبداله بـ `EventTransformer` - تمت إزالة `TransitionFunction` واستبداله بـ `EventTransformer` - تمت إزالة `listen` واستبداله بـ `stream.listen` ### `package:bloc_test` #### ✨ لم يعد `MockBloc` و `MockCubit` يتطلبان `registerFallbackValue` :::note[ما الذي تغير؟] في الإصدار bloc_test v9.0.0، لم يعد من الضروري استدعاء `registerFallbackValue` عند استخدام `MockBloc` أو `MockCubit`. ::: ##### ملخص يُستخدم `registerFallbackValue` فقط عند استخدام `any()` من `package:mocktail` مع أنواع مخصصة. سابقًا، كان يجب استدعاؤه لكل `Event` و `State` عند استخدام `MockBloc` أو `MockCubit`. **v8.x.x** ```dart class FakeMyEvent extends Fake implements MyEvent {} class FakeMyState extends Fake implements MyState {} class MyMockBloc extends MockBloc implements MyBloc {} void main() { setUpAll(() { registerFallbackValue(FakeMyEvent()); registerFallbackValue(FakeMyState()); }); // الاختبارات... } ``` **v9.0.0** ```dart class MyMockBloc extends MockBloc implements MyBloc {} void main() { // الاختبارات... } ``` ### `package:hydrated_bloc` #### ❗✨ تقديم `HydratedBlocOverrides` :::note[ما الذي تغير؟] في الإصدار hydrated_bloc v8.0.0، تمت إزالة `HydratedBloc.storage` واستبداله بـ `HydratedBlocOverrides`. ::: ##### السبب سابقًا، كان يتم الاعتماد على singleto عام لتخصيص تنفيذ `Storage`. ونتيجة لذلك، لم يكن من الممكن استخدام أكثر من تنفيذ لـ `Storage` ضمن أجزاء مختلفة من التطبيق. كما أن هذا النهج جعل الاختبارات أكثر صعوبة بسبب وجود حالة عامة مشتركة بين الاختبارات. يقدم الإصدار v8.0.0 فئة `HydratedBlocOverrides` التي تتيح للمطورين تخصيص `Storage` ضمن `Zone` محددة، بدلاً من الاعتماد على singleton عام قابل للتغيير. **v7.x.x** ```dart void main() async { HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); // ... } ``` **v8.0.0** ```dart void main() { final storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); HydratedBlocOverrides.runZoned( () { // ... }, storage: storage, ); } ``` ستستخدم كائنات `HydratedBloc` الـ `Storage` الخاصة بالـ `Zone` الحالية عبر `HydratedBlocOverrides.current`. يسمح ذلك لكل `Zone` بالعمل بشكل مستقل باستخدام الإعدادات الخاصة بها. ## v7.2.0 ### `package:bloc` #### ✨ تقديم `on` :::note[ما الذي تغير؟] في الإصدار bloc v7.2.0، تم إيقاف استخدام `mapEventToState` (deprecated) واستبداله بـ `on`. وسيتم إزالة `mapEventToState` في الإصدار v8.0.0. ::: ##### السبب تم تقديم `on` كجزء من: [مقترح: استبدال mapEventToState بـ `on` في Bloc](https://github.com/felangel/bloc/issues/2526). بسبب [مشكلة في Dart](https://github.com/dart-lang/sdk/issues/44616)، قد لا يكون من الواضح دائمًا ما ستكون عليه قيمة `state` عند التعامل مع مولدات غير متزامنة متداخلة (`async*`). على الرغم من وجود حلول بديلة، إلا أن أحد المبادئ الأساسية لمكتبة bloc هو القابلية للتنبؤ. لذلك، تم تقديم `on` لجعل استخدام المكتبة أكثر أمانًا وتقليل أي غموض متعلق بتغيّر الحالة. :::tip لمزيد من التفاصيل: [اقرأ المقترح الكامل](https://github.com/felangel/bloc/issues/2526) ::: ##### ملخص يتيح لك `on` تسجيل معالج أحداث لجميع الأحداث من النوع `E`. بشكل افتراضي، تتم معالجة الأحداث بشكل متوازي (concurrently) عند استخدام `on`، على عكس `mapEventToState` الذي يعالج الأحداث بشكل تسلسلي (sequentially). **v7.1.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0); @override Stream mapEventToState(CounterEvent event) async* { if (event is Increment) { yield state + 1; } } } ``` **v7.2.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } } ``` :::note يعمل كل `EventHandler` بشكل مستقل، لذلك من المهم تسجيل معالجات الأحداث بناءً على نوع الـ transformer الذي ترغب في استخدامه. ::: إذا كنت ترغب في الحفاظ على نفس السلوك تمامًا كما في v7.1.0، يمكنك تسجيل معالج أحداث واحد لجميع الأحداث وتطبيق transformer تسلسلي (`sequential`). ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; class MyBloc extends Bloc { MyBloc() : super(MyState()) { on(_onEvent, transformer: sequential()) } FutureOr _onEvent(MyEvent event, Emitter emit) async { // TODO: المنطق البرمجي يوضع هنا... } } ``` يمكنك أيضًا تجاوز `EventTransformer` الافتراضي لجميع الـ blocs في تطبيقك: ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; void main() { Bloc.transformer = sequential(); ... } ``` #### ✨ تقديم `EventTransformer` :::note[ما الذي تغير؟] في الإصدار bloc v7.2.0، تم إيقاف استخدام `transformEvents` (deprecated) واستبداله بـ `EventTransformer`. وسيتم إزالة `transformEvents` في الإصدار v8.0.0. ::: ##### السبب أتاح `on` إمكانية تخصيص event transformer لكل معالج أحداث (event handler). تم تقديم typedef جديد باسم `EventTransformer`، والذي يتيح للمطورين تحويل تدفق الأحداث الواردة لكل معالج بشكل مستقل، بدلاً من استخدام transformer واحد لكافة الأحداث. ##### ملخص يقوم `EventTransformer` باستقبال تدفق الأحداث الواردة مع `EventMapper` (معالج الأحداث)، وإرجاع تدفق جديد من الأحداث. ```dart typedef EventTransformer = Stream Function(Stream events, EventMapper mapper) ``` يقوم الـ `EventTransformer` الافتراضي بمعالجة جميع الأحداث بشكل متزامن ويبدو كالتالي: ```dart EventTransformer concurrent() { return (events, mapper) => events.flatMap(mapper); } ``` :::tip تحقق من [package:bloc_concurrency](https://pub.dev/packages/bloc_concurrency) للحصول على مجموعة مختارة من محولات الأحداث المخصصة. ::: **v7.1.0** ```dart @override Stream> transformEvents(events, transitionFn) { return events .debounceTime(const Duration(milliseconds: 300)) .flatMap(transitionFn); } ``` **v7.2.0** ```dart /// تعريف EventTransformer مخصص EventTransformer debounce(Duration duration) { return (events, mapper) => events.debounceTime(duration).flatMap(mapper); } MyBloc() : super(MyState()) { /// تطبيق EventTransformer المخصص على معالج الحدث on(_onEvent, transformer: debounce(const Duration(milliseconds: 300))) } ``` #### ⚠️ إيقاف استخدام `transformTransitions` :::note[ما الذي تغير؟] في الإصدار bloc v7.2.0، تم إيقاف استخدام `transformTransitions` (deprecated) واستبداله بتخصيص `stream`. وسيتم إزالة `transformTransitions` في الإصدار v8.0.0. ::: ##### السبب يتيح getter الخاص بـ `stream` في `Bloc` تخصيص تدفق الحالات (states) الخارجة بسهولة، لذلك لم تعد هناك حاجة للاحتفاظ بواجهة منفصلة مثل `transformTransitions`. ##### ملخص **v7.1.0** ```dart @override Stream> transformTransitions( Stream> transitions, ) { return transitions.debounceTime(const Duration(milliseconds: 42)); } ``` **v7.2.0** ```dart @override Stream get stream => super.stream.debounceTime(const Duration(milliseconds: 42)); ``` ## v7.0.0 ### `package:bloc` #### ❗ أصبح كل من `Bloc` و `Cubit` يعتمدان على `BlocBase` ##### السبب كمطور، كانت العلاقة بين blocs و cubits غير واضحة إلى حدٍ ما. عند تقديم Cubit لأول مرة، كان يُستخدم كفئة أساسية (base class) لـ blocs، وهو ما كان منطقيًا لأنه يوفر جزءًا من الوظائف، بينما تقوم blocs بتوسيعه وإضافة المزيد من الـ APIs. لكن هذا التصميم أدى إلى بعض المشاكل: - كان من الضروري إما إعادة تسمية APIs لتدعم cubit بشكل دقيق، أو الإبقاء على تسميات bloc للحفاظ على الاتساق، رغم أن ذلك غير دقيق من الناحية الهيكلية ([#1708](https://github.com/felangel/bloc/issues/1708)، [#1560](https://github.com/felangel/bloc/issues/1560)). - كان على Cubit أن يرث من `Stream` ويطبق `EventSink` لتوفير قاعدة مشتركة يمكن أن تعتمد عليها مكونات مثل `BlocBuilder` و `BlocListener` ([#1429](https://github.com/felangel/bloc/issues/1429)). --- لاحقًا، تم تجربة عكس العلاقة وجعل bloc هو الفئة الأساسية. هذا الحل عالج جزئيًا المشكلة الأولى، لكنه تسبب في مشاكل أخرى: - أصبح API الخاص بـ cubit معقدًا بسبب وراثة APIs من bloc مثل `mapEventToState` و `add` وغيرها ([#2228](https://github.com/felangel/bloc/issues/2228)) - ويمكن للمطورين استخدام هذه APIs بشكل غير صحيح مما يؤدي إلى مشاكل - استمرت مشكلة كشف cubit لكامل واجهة `Stream` كما كانت سابقًا ([#1429](https://github.com/felangel/bloc/issues/1429)) لحل هذه التحديات، تم تقديم فئة أساسية جديدة مشتركة بين `Bloc` و `Cubit` باسم `BlocBase`. يسمح ذلك للمكونات الأعلى في التطبيق بالتعامل مع bloc و cubit بطريقة موحدة، دون الحاجة إلى كشف كامل واجهات `Stream` و `EventSink`. ##### ملخص **BlocObserver** **v6.1.x** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(Cubit cubit) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(Cubit cubit, Object event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(Cubit cubit, Object error, StackTrace stackTrace) {...} @override void onClose(Cubit cubit) {...} } ``` **v7.0.0** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(BlocBase bloc) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(BlocBase bloc, Object? event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) {...} @override void onClose(BlocBase bloc) {...} } ``` **Bloc/Cubit** **v6.1.x** ```dart final bloc = MyBloc(); bloc.listen((state) {...}); final cubit = MyCubit(); cubit.listen((state) {...}); ``` **v7.0.0** ```dart final bloc = MyBloc(); bloc.stream.listen((state) {...}); final cubit = MyCubit(); cubit.stream.listen((state) {...}); ``` ### `package:bloc_test` #### ❗ `seed` تُعيد دالة لدعم القيم الديناميكية ##### السبب لدعم استخدام قيمة seed قابلة للتغيير ويمكن تحديثها ديناميكيًا داخل `setUp`، أصبحت `seed` تُعيد دالة بدلاً من قيمة ثابتة. ##### ملخص **v7.x.x** ```dart blocTest( '...', seed: MyState(), ... ); ``` **v8.0.0** ```dart blocTest( '...', seed: () => MyState(), ... ); ``` #### ❗ `expect` تُعيد دالة لدعم القيم الديناميكية مع دعم `Matchers` ##### السبب لدعم استخدام توقع (expectation) قابل للتغيير ويمكن تحديثه ديناميكيًا داخل `setUp`، أصبحت `expect` تُعيد دالة بدلاً من قيمة ثابتة. كما تدعم `expect` استخدام `Matchers`. ##### ملخص **v7.x.x** ```dart blocTest( '...', expect: [MyStateA(), MyStateB()], ... ); ``` **v8.0.0** ```dart blocTest( '...', expect: () => [MyStateA(), MyStateB()], ... ); // يمكن أن يكون أيضاً `Matcher` blocTest( '...', expect: () => contains(MyStateA()), ... ); ``` #### ❗ `errors` تُعيد دالة لدعم القيم الديناميكية مع دعم `Matchers` ##### السبب لدعم استخدام قائمة أخطاء (errors) قابلة للتغيير ويمكن تحديثها ديناميكيًا داخل `setUp`، أصبحت `errors` تُعيد دالة بدلاً من قيمة ثابتة. كما تدعم `errors` استخدام `Matchers`. ##### ملخص **v7.x.x** ```dart blocTest( '...', errors: [MyError()], ... ); ``` **v8.0.0** ```dart blocTest( '...', errors: () => [MyError()], ... ); // يمكن أن يكون أيضاً `Matcher` blocTest( '...', errors: () => contains(MyError()), ... ); ``` #### ❗ `MockBloc` و `MockCubit` ##### السبب لدعم محاكاة (stubbing) واجهات برمجة التطبيقات الأساسية بشكل أفضل، تم توفير `MockBloc` و `MockCubit` ضمن حزمة `bloc_test`. سابقًا، كان يتم استخدام `MockBloc` لكل من `Bloc` و `Cubit`، وهو ما لم يكن واضحًا أو بديهيًا للمطورين. ##### ملخص **v7.x.x** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockBloc implements MyBloc {} ``` **v8.0.0** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockCubit implements MyCubit {} ``` #### ❗ التكامل مع `Mocktail` ##### السبب بسبب القيود المختلفة في الحزمة الداعمة لـ null-safety [package:mockito](https://pub.dev/packages/mockito) والمُوضحة [هنا](https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#problems-with-typical-mocking-and-stubbing)، تم اعتماد [package:mocktail](https://pub.dev/packages/mocktail) في `MockBloc` و `MockCubit`. يتيح ذلك للمطورين استخدام واجهة محاكاة مألوفة دون الحاجة إلى كتابة stubs يدويًا أو الاعتماد على توليد الكود. ##### ملخص **v7.x.x** ```dart import 'package:mockito/mockito.dart'; ... when(bloc.state).thenReturn(MyState()); verify(bloc.add(any)).called(1); ``` **v8.0.0** ```dart import 'package:mocktail/mocktail.dart'; ... when(() => bloc.state).thenReturn(MyState()); verify(() => bloc.add(any())).called(1); ``` > يرجى الرجوع إلى [#347](https://github.com/dart-lang/mockito/issues/347) > بالإضافة إلى > [توثيق mocktail](https://github.com/felangel/mocktail/tree/main/packages/mocktail) > لمزيد من التفاصيل. ### `package:flutter_bloc` #### ❗ إعادة تسمية المعامل `cubit` إلى `bloc` ##### السبب نتيجة لإعادة الهيكلة في `package:bloc` وتقديم `BlocBase` الذي يتم توريثه من قبل كلٍ من `Bloc` و `Cubit`، تم تغيير اسم المعامل في `BlocBuilder` و `BlocConsumer` و `BlocListener` من `cubit` إلى `bloc`، لأن هذه الواجهات تعمل على النوع `BlocBase`. كما أن هذا التغيير يتماشى بشكل أفضل مع اسم المكتبة، ويساهم في تحسين وضوح وقابلية قراءة الكود. ##### ملخص **v6.1.x** ```dart BlocBuilder( cubit: myBloc, ... ) BlocListener( cubit: myBloc, ... ) BlocConsumer( cubit: myBloc, ... ) ``` **v7.0.0** ```dart BlocBuilder( bloc: myBloc, ... ) BlocListener( bloc: myBloc, ... ) BlocConsumer( bloc: myBloc, ... ) ``` ### `package:hydrated_bloc` #### ❗ أصبح `storageDirectory` مطلوبًا عند استدعاء `HydratedStorage.build` ##### السبب لجعل `package:hydrated_bloc` حزمة Dart صِرفة، تم إزالة الاعتماد على [package:path_provider](https://pub.dev/packages/path_provider). وبالتالي، أصبح تمرير المعامل `storageDirectory` عند استدعاء `HydratedStorage.build` إلزاميًا، ولم يعد يتم تعيينه تلقائيًا إلى `getTemporaryDirectory`. ##### ملخص **v6.x.x** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` **v7.0.0** ```dart import 'package:path_provider/path_provider.dart'; ... HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getTemporaryDirectory(), ); ``` ## v6.1.0 ### `package:flutter_bloc` #### ❗ تم وضع علامة "مهجور" على `context.bloc` و `context.repository` لصالح `context.read` و `context.watch` ##### السبب تم تقديم `context.read` و `context.watch` و `context.select` لتتوافق مع واجهة برمجة التطبيقات الخاصة بـ [provider](https://pub.dev/packages/provider)، والتي يألفها العديد من المطورين، وكذلك لمعالجة المشكلات التي تم طرحها من قبل المجتمع. ولتحسين أمان الكود والحفاظ على الاتساق، تم وضع علامة "مهجور" على `context.bloc`، حيث يمكن استبداله بـ `context.read` أو `context.watch` اعتمادًا على ما إذا كان يتم استخدامه مباشرة داخل `build`. ##### `context.watch` يوفر `context.watch` بديلاً عمليًا لطلب [MultiBlocBuilder](https://github.com/felangel/bloc/issues/538)، حيث يمكن مراقبة عدة blocs داخل `Builder` واحد من أجل بناء واجهة المستخدم بناءً على حالات متعددة. ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // إرجاع Widget يعتمد على حالة BlocA و BlocB و BlocC } ); ``` ##### `context.select` يتيح `context.select` للمطورين إعادة بناء (rebuild) واجهة المستخدم بناءً على جزء محدد من حالة الـ bloc، كما يوفر بديلاً أبسط لاستخدام [buildWhen](https://github.com/felangel/bloc/issues/1521). ```dart final name = context.select((UserBloc bloc) => bloc.state.user.name); ``` تسمح لنا القطعة البرمجية أعلاه بالوصول إلى الـ widget وإعادة بنائه فقط عندما يتغير اسم المستخدم الحالي. ##### `context.read` على الرغم من أن `context.read` يبدو مشابهًا لـ `context.bloc`، إلا أن هناك اختلافات دقيقة لكنها مهمة. كلاهما يتيح لك الوصول إلى الـ bloc باستخدام `BuildContext` دون التسبب في إعادة بناء (rebuild)؛ ومع ذلك، لا يمكن استخدام `context.read` مباشرة داخل دالة `build`. كان هناك سببان رئيسيان لاستخدام `context.bloc` داخل `build`: 1. **للوصول إلى حالة الـ bloc** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` يُعد الاستخدام أعلاه عرضة للأخطاء، لأن عنصر `Text` لن تتم إعادة بنائه عند تغيّر حالة الـ bloc. في هذه الحالة، يُنصح باستخدام `BlocBuilder` أو `context.watch`. `` ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` أو ```dart @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) => Text('$state'), ); } ``` :::note سيؤدي استخدام `context.watch` في جذر دالة `build` إلى إعادة بناء الـ widget بالكامل عند تغيّر حالة الـ bloc. إذا لم تكن هناك حاجة لإعادة بناء الـ widget بالكامل، يمكنك استخدام `BlocBuilder` لتغليف الأجزاء التي تحتاج إلى إعادة بناء، أو استخدام `Builder` مع `context.watch` لتحديد نطاق إعادة البناء، أو تقسيم الـ widget إلى مكونات أصغر. ::: 2. **للوصول إلى الـ bloc من أجل إضافة حدث** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` الاستخدام أعلاه غير فعال لأنه يؤدي إلى البحث عن الـ bloc في كل عملية إعادة بناء بينما لا تكون هناك حاجة للـ bloc إلا عندما ينقر المستخدم على الـ `ElevatedButton`. في هذا السيناريو، يفضل استخدام `context.read` للوصول إلى الـ bloc مباشرة حيث تبرز الحاجة إليه (في هذه الحالة، في استدعاء `onPressed`). ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` ##### ملخص **v6.0.x** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` ?> إذا كنت بحاجة للوصول إلى bloc لإضافة حدث، فاستخدم `context.read` داخل الـ callback في المكان الذي تحتاجه فيه. **v6.0.x** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` ?> استخدم `context.watch` عند الوصول إلى حالة الـ bloc لضمان إعادة بناء الـ widget عند تغيّر الحالة. ## v6.0.0 ### `package:bloc` #### ❗ أصبح `BlocObserver.onError` يستقبل `Cubit` ##### السبب بسبب دمج `Cubit`، أصبحت `onError` مشتركة بين كلٍ من `Bloc` و `Cubit`. وبما أن `Cubit` هو الفئة الأساسية، فإن `BlocObserver` يستقبل نوع `Cubit` بدلاً من `Bloc` عند تجاوز `onError`. **v5.x.x** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Bloc bloc, Object error, StackTrace stackTrace) { super.onError(bloc, error, stackTrace); } } ``` **v6.0.0** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Cubit cubit, Object error, StackTrace stackTrace) { super.onError(cubit, error, stackTrace); } } ``` #### ❗ لا يقوم `Bloc` بإصدار الحالة الأخيرة عند الاشتراك ##### السبب تم إجراء هذا التغيير لمواءمة سلوك `Bloc` و `Cubit` مع سلوك `Stream` المدمج في Dart. بالإضافة إلى ذلك، أدى الالتزام بالسلوك السابق في `Cubit` إلى ظهور العديد من الآثار الجانبية غير المقصودة، كما زاد من تعقيد التنفيذ الداخلي لحزم أخرى مثل `flutter_bloc` و `bloc_test` دون داعٍ (مثل الحاجة إلى استخدام `skip(1)` وغيرها). **v5.x.x** ```dart final bloc = MyBloc(); bloc.listen(print); ``` سابقًا، كان الكود أعلاه يقوم بإخراج (output) الحالة الأولية للـ bloc، تليها تغييرات الحالة اللاحقة. **v6.x.x** في الإصدار v6.0.0، لم يعد الكود أعلاه يخرج الحالة الأولية، وإنما يقتصر على إخراج تغييرات الحالة اللاحقة فقط. يمكن استعادة السلوك السابق باستخدام ما يلي: ```dart final bloc = MyBloc(); print(bloc.state); bloc.listen(print); ``` ?> **ملاحظة**: يؤثر هذا التغيير فقط على الكود الذي يعتمد على الاشتراك المباشر في الـ bloc. عند استخدام `BlocBuilder` أو `BlocListener` أو `BlocConsumer`، لن يكون هناك أي تغيير ملحوظ في السلوك. ### `package:bloc_test` #### ❗ `MockBloc` يتطلب نوع الحالة (`State`) فقط ##### السبب لم يعد من الضروري تحديد أنواع إضافية، مما يقلل من الكود الزائد، كما يجعل `MockBloc` متوافقًا مع `Cubit`. **v5.x.x** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` **v6.0.0** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` #### ❗ `whenListen` يتطلب نوع الحالة (`State`) فقط ##### السبب لم يعد من الضروري تحديد أنواع إضافية، مما يقلل من الكود الزائد، كما يجعل `whenListen` متوافقًا مع `Cubit`. **v5.x.x** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` **v6.0.0** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` #### ❗ `blocTest` لا يتطلب نوع الحدث (`Event`) ##### السبب لم يعد من الضروري تحديد نوع الحدث، مما يقلل من الكود الزائد، كما يجعل `blocTest` متوافقًا مع `Cubit`. **v5.x.x** ```dart blocTest( 'emits [1] when increment is called', build: () async => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` **v6.0.0** ```dart blocTest( 'emits [1] when increment is called', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` #### ❗ القيمة الافتراضية لـ `skip` في `blocTest` أصبحت 0 ##### السبب بما أن مثيلات `bloc` و `cubit` لم تعد تُصدر الحالة الأخيرة عند الاشتراك الجديد، لم يعد من الضروري تعيين القيمة الافتراضية لـ `skip` إلى `1`. **v5.x.x** ```dart blocTest( 'emits [0] when skip is 0', build: () async => CounterBloc(), skip: 0, expect: const [0], ); ``` **v6.0.0** ```dart blocTest( 'emits [] when skip is 0', build: () => CounterBloc(), skip: 0, expect: const [], ); ``` يمكن اختبار الحالة الأولية لـ bloc أو cubit بما يلي: ```dart test('initial state is correct', () { expect(MyBloc().state, InitialState()); }); ``` #### ❗ جعل `build` في `blocTest` متزامنًا (synchronous) ##### السبب سابقًا، كان يتم جعل `build` غير متزامن (`async`) لإتاحة تنفيذ بعض التحضيرات من أجل وضع الـ bloc تحت الاختبار في حالة محددة. لم يعد ذلك ضروريًا، كما أنه يساهم في حل عدة مشكلات ناتجة عن التأخير بين مرحلة البناء (build) وبدء الاشتراك داخليًا. بدلًا من الاعتماد على التحضير غير المتزامن للوصول إلى حالة معينة، يمكن الآن تعيين حالة الـ bloc مباشرة باستخدام `emit` مع الحالة المطلوبة. **v5.x.x** ```dart blocTest( 'emits [2] when increment is added', build: () async { final bloc = CounterBloc(); bloc.add(CounterEvent.increment); await bloc.take(2); return bloc; } act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` **v6.0.0** ```dart blocTest( 'emits [2] when increment is added', build: () => CounterBloc()..emit(1), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` :::note تُستخدم `emit` لأغراض الاختبار فقط، ولا ينبغي استخدامها خارج نطاق الاختبارات. ::: ### `package:flutter_bloc` #### ❗ إعادة تسمية المعامل `bloc` في `BlocBuilder` إلى `cubit` ##### السبب لجعل `BlocBuilder` يعمل مع كلٍ من مثيلات `bloc` و `cubit`، تم تغيير اسم المعامل من `bloc` إلى `cubit` (نظرًا لأن `Cubit` هو الفئة الأساسية). **v5.x.x** ```dart BlocBuilder( bloc: myBloc, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocBuilder( cubit: myBloc, builder: (context, state) {...} ) ``` #### ❗ إعادة تسمية المعامل `bloc` في `BlocListener` إلى `cubit` ##### السبب لجعل `BlocListener` يعمل مع كلٍ من مثيلات `bloc` و `cubit`، تم تغيير اسم المعامل من `bloc` إلى `cubit` (نظرًا لأن `Cubit` هو الفئة الأساسية). **v5.x.x** ```dart BlocListener( bloc: myBloc, listener: (context, state) {...} ) ``` **v6.0.0** ```dart BlocListener( cubit: myBloc, listener: (context, state) {...} ) ``` #### ❗ إعادة تسمية المعامل `bloc` في `BlocConsumer` إلى `cubit` ##### السبب لجعل `BlocConsumer` يعمل مع كلٍ من مثيلات `bloc` و `cubit`، تم تغيير اسم المعامل من `bloc` إلى `cubit` (نظرًا لأن `Cubit` هو الفئة الأساسية). **v5.x.x** ``` BlocConsumer( cubit: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocConsumer( cubit: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` --- ## v5.0.0 ### `package:bloc` #### ❗ تمت إزالة `initialState` ##### السبب كمطور، كان الاضطرار إلى تجاوز `initialState` عند إنشاء bloc يسبب مشكلتين رئيسيتين: - يمكن أن تكون `initialState` ديناميكية، كما يمكن الوصول إليها لاحقًا (حتى من خارج الـ bloc نفسه)، مما قد يُعد تسريبًا لبعض تفاصيله الداخلية إلى طبقة واجهة المستخدم. - يتطلب ذلك كتابة كود إضافي وغير ضروري (verbose). **v4.x.x** ```dart class CounterBloc extends Bloc { @override int get initialState => 0; ... } ``` **v5.0.0** ```dart class CounterBloc extends Bloc { CounterBloc() : super(0); ... } ``` ?> لمزيد من المعلومات، راجع [#1304](https://github.com/felangel/bloc/issues/1304) #### ❗ إعادة تسمية `BlocDelegate` إلى `BlocObserver` ##### السبب لم يكن اسم `BlocDelegate` يعكس بدقة الدور الحقيقي لهذه الفئة. إذ يوحي الاسم بأن لها دورًا نشطًا، بينما في الواقع كان الهدف منها أن تكون مكونًا سلبيًا يقوم فقط بمراقبة جميع الـ blocs داخل التطبيق. :::note من المفترض ألا يحتوي `BlocObserver` على أي منطق أو وظائف موجهة للمستخدم، بل يقتصر دوره على المراقبة فقط. ::: **v4.x.x** ```dart class MyBlocDelegate extends BlocDelegate { ... } ``` **v5.0.0** ```dart class MyBlocObserver extends BlocObserver { ... } ``` #### ❗ تمت إزالة `BlocSupervisor` ##### السبب كان `BlocSupervisor` مكونًا إضافيًا يتعين على المطورين التعرف عليه والتعامل معه فقط من أجل تعيين `BlocDelegate` مخصص. مع الانتقال إلى `BlocObserver`، أصبح من الأبسط والأفضل من ناحية تجربة المطور تعيين المراقب مباشرة على الـ bloc نفسه. ?> كما أتاح هذا التغيير فصل بعض إضافات bloc الأخرى مثل `HydratedStorage` عن `BlocObserver`. **v4.x.x** ```dart BlocSupervisor.delegate = MyBlocDelegate(); ``` **v5.0.0** ```dart Bloc.observer = MyBlocObserver(); ``` ### `package:flutter_bloc` #### ❗ إعادة تسمية `condition` في `BlocBuilder` إلى `buildWhen` ##### السبب عند استخدام `BlocBuilder`، كان بالإمكان سابقًا تحديد `condition` للتحكم في ما إذا كان يجب على `builder` إعادة البناء أم لا. ```dart BlocBuilder( condition: (previous, current) { // إرجاع true/false لتحديد ما إذا كان سيتم استدعاء الـ builder }, builder: (context, state) {...} ) ``` لم يكن الاسم `condition` واضحًا أو معبّرًا بشكل كافٍ، والأهم من ذلك أنه أدى إلى عدم اتساق في واجهة الاستخدام (API) عند التعامل مع `BlocConsumer`، حيث يمكن للمطورين تحديد شرطين مختلفين (أحدهما لـ `builder` والآخر لـ `listener`). ونتيجة لذلك، أصبحت واجهة `BlocConsumer` توفر `buildWhen` و `listenWhen`. ```dart BlocConsumer( listenWhen: (previous, current) { // إرجاع true/false لتحديد ما إذا كان سيتم استدعاء الـ listener }, listener: (context, state) {...}, buildWhen: (previous, current) { // إرجاع true/false لتحديد ما إذا كان سيتم استدعاء الـ builder }, builder: (context, state) {...}, ) ``` لتحقيق اتساق أكبر في واجهة الاستخدام (API) وتوفير تجربة تطوير أكثر سلاسة، تمت إعادة تسمية `condition` إلى `buildWhen`. **v4.x.x** ```dart BlocBuilder( condition: (previous, current) { // إرجاع true/false لتحديد ما إذا كان سيتم استدعاء الـ builder }, builder: (context, state) {...} ) ``` **v5.0.0** ```dart BlocBuilder( buildWhen: (previous, current) { // إرجاع true/false لتحديد ما إذا كان سيتم استدعاء الـ builder }, builder: (context, state) {...} ) ``` #### ❗ إعادة تسمية `condition` في `BlocListener` إلى `listenWhen` ##### السبب لنفس الأسباب المذكورة أعلاه، تم أيضًا إعادة تسمية `condition` في `BlocListener` إلى `listenWhen`. **v4.x.x** ```dart BlocListener( condition: (previous, current) { // إرجاع true/false لتحديد ما إذا كان سيتم استدعاء الـ listener }, listener: (context, state) {...} ) ``` **v5.0.0** ```dart BlocListener( listenWhen: (previous, current) { // إرجاع true/false لتحديد ما إذا كان سيتم استدعاء الـ listener }, listener: (context, state) {...} ) ``` ### `package:hydrated_bloc` #### ❗ إعادة تسمية `HydratedStorage` و `HydratedBlocStorage` ##### السبب لتحسين إعادة استخدام الكود بين [hydrated_bloc](https://pub.dev/packages/hydrated_bloc) و [hydrated_cubit](https://pub.dev/packages/hydrated_cubit)، تم تغيير اسم تنفيذ التخزين الافتراضي من `HydratedBlocStorage` إلى `HydratedStorage`. بالإضافة إلى ذلك، تمت إعادة تسمية واجهة `HydratedStorage` إلى `Storage`. **v4.0.0** ```dart class MyHydratedStorage implements HydratedStorage { ... } ``` **v5.0.0** ```dart class MyHydratedStorage implements Storage { ... } ``` #### ❗ فصل `HydratedStorage` عن `BlocDelegate` ##### السبب كما ذُكر سابقًا، تمت إعادة تسمية `BlocDelegate` إلى `BlocObserver`، وأصبح يتم تعيينه مباشرة ضمن إعدادات الـ bloc كما يلي: ```dart Bloc.observer = MyBlocObserver(); ``` تم إجراء هذا التغيير من أجل: - الحفاظ على الاتساق مع واجهة `BlocObserver` الجديدة - إبقاء التخزين (`Storage`) محصورًا ضمن `HydratedBloc` فقط - فصل `BlocObserver` عن `Storage` **v4.0.0** ```dart BlocSupervisor.delegate = await HydratedBlocDelegate.build(); ``` **v5.0.0** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` #### ❗ تبسيط عملية التهيئة (Initialization) ##### السبب سابقًا، كان على المطورين استدعاء `super.initialState ?? DefaultInitialState()` يدويًا من أجل تهيئة مثيلات `HydratedBloc`. كان هذا الأسلوب معقدًا ومطولًا، كما أنه غير متوافق مع التغييرات الجوهرية على `initialState` في `bloc`. ونتيجة لذلك، أصبحت تهيئة `HydratedBloc` في الإصدار v5.0.0 مماثلة تمامًا لتهيئة `Bloc` العادية. **v4.0.0** ```dart class CounterBloc extends HydratedBloc { @override int get initialState => super.initialState ?? 0; } ``` **v5.0.0** ```dart class CounterBloc extends HydratedBloc { CounterBloc() : super(0); ... } ``` ================================================ FILE: docs/src/content/docs/ar/modeling-state.mdx ================================================ --- title: نمذجة الحالة (Modeling State) description: نظرة عامة على عدة طرق لنمذجة الحالات عند استخدام حزمة bloc. --- import ConcreteClassAndStatusEnumSnippet from '~/components/modeling-state/ConcreteClassAndStatusEnumSnippet.astro'; import SealedClassAndSubclassesSnippet from '~/components/modeling-state/SealedClassAndSubclassesSnippet.astro'; هناك العديد من الأساليب المختلفة عندما يتعلق الأمر بهيكلة حالة التطبيق. لكل أسلوب مزاياه وعيوبه. في هذا القسم، سنستعرض عدة أساليب، مع إيجابياتها وسلبياتها، ومتى يُفضّل استخدام كل منها. الأساليب التالية هي مجرد توصيات وهي اختيارية بالكامل. لا تتردد في استخدام أي أسلوب تفضله. قد تلاحظ أن بعض الأمثلة/التوثيقات لا تتبع هذه الأساليب بشكل صارم، وذلك لأسباب تتعلق بالتبسيط/الإيجاز. :::tip تركز مقتطفات الكود التالية على هيكل الحالة. من الناحية العملية، قد ترغب أيضًا في: - توسيع `Equatable` من حزمة [`package:equatable`](https://pub.dev/packages/equatable) - استخدام التعليق التوضيحي `@Data()` للفئة من حزمة [`package:data_class`](https://pub.dev/packages/data_class) - استخدام التعليق التوضيحي `@immutable` للفئة من حزمة [`package:meta`](https://pub.dev/packages/meta) - تنفيذ دالة `copyWith` - استخدام الكلمة المفتاحية `const` للمُنشئات (constructors) ::: ## الفئة الملموسة وتعداد الحالة (Concrete Class and Status Enum) يتكون هذا الأسلوب من **فئة ملموسة واحدة** لجميع الحالات، إلى جانب `enum` يمثل حالات/Statuses مختلفة. يتم جعل الخصائص قابلة للقيمة الفارغة (nullable) ويتم التعامل معها بناءً على الحالة الحالية. يعمل هذا الأسلوب بشكل أفضل للحالات التي ليست حصرية بشكل صارم و/أو تحتوي على الكثير من الخصائص المشتركة. #### الإيجابيات - **بسيط**: سهل إدارة فئة واحدة وتعداد حالة (status enum)، وجميع الخصائص متاحة بسهولة. - **موجز**: يتطلب غالبًا عددًا أقل من أسطر الكود مقارنة بالأساليب الأخرى. #### السلبيات - **غير آمن من حيث النوع (Not Type Safe)**: يتطلب التحقق من `status` قبل الوصول إلى الخصائص. من الممكن إصدار حالة غير صحيحة (`emit` a malformed state) مما قد يؤدي إلى أخطاء. كما أن خصائص حالات معينة تكون nullable، وقد يكون التعامل معها مرهقًا ويتطلب إما فك تغليف قسري (force unwrapping) أو إجراء فحوصات null. يمكن التخفيف من بعض هذه السلبيات عبر اختبارات الوحدة (unit tests) أو إنشاء مُنشئات (constructors) مخصصة ومُسماة. - **متضخم**: ينتج عنه State واحدة قد تصبح متضخمة مع مرور الوقت بسبب كثرة الخصائص. #### الحكم يعمل هذا الأسلوب بشكل أفضل للحالات البسيطة أو عندما تتطلب المتطلبات حالات ليست حصرية (مثل إظهار Snackbar عند حدوث خطأ مع الاستمرار في عرض البيانات القديمة من آخر حالة نجاح). يوفر مرونة وإيجازًا على حساب أمان النوع. ## الفئة المختومة والفئات الفرعية (Sealed Class and Subclasses) يتكون هذا الأسلوب من **فئة مختومة (sealed class)** تحتوي على الخصائص المشتركة، مع عدة فئات فرعية تمثل الحالات المنفصلة. هذا الأسلوب مناسب جدًا للحالات المنفصلة والحصرية. #### الإيجابيات - **آمن من حيث النوع (Type Safe)**: الكود آمن وقت التجميع (compile-safe) ولا يمكن الوصول بالخطأ إلى خاصية غير صالحة. كل فئة فرعية تحتوي خصائصها الخاصة، مما يجعل من الواضح ما الذي ينتمي لأي حالة. - **صريح (Explicit)**: يفصل الخصائص المشتركة عن الخصائص الخاصة بكل حالة. - **شامل (Exhaustive)**: يمكنك استخدام `switch` لفحوصات الشمول (exhaustiveness checks) لضمان التعامل مع كل حالة بشكل صريح. - إذا كنت لا تريد [التبديل الشامل](https://dart.dev/language/branches#exhaustiveness-checking) أو تريد إضافة أنواع فرعية لاحقًا دون كسر واجهة الـ API، فاستخدم [final](https://dart.dev/language/class-modifiers#final). - راجع [توثيق sealed class](https://dart.dev/language/class-modifiers#sealed) لمزيد من التفاصيل. #### السلبيات - **مطوّل (Verbose)**: يتطلب كودًا أكثر (فئة أساسية + فئة فرعية لكل حالة). وقد يتطلب تكرارًا للخصائص المشتركة عبر الفئات الفرعية. - **أكثر تعقيدًا (Complex)**: إضافة خصائص جديدة قد تتطلب تحديث الفئة الأساسية وجميع الفئات الفرعية، مما يزيد التعقيد. وقد يؤدي أيضًا إلى فحوصات نوع إضافية للوصول إلى خصائص معينة. #### الحكم يعمل هذا الأسلوب بشكل أفضل للحالات المحددة جيدًا والحصرية ذات الخصائص الفريدة. يوفر أمان النوع وفحوصات الشمول، ويؤكد على السلامة أكثر من الإيجاز والبساطة. ================================================ FILE: docs/src/content/docs/ar/naming-conventions.mdx ================================================ --- title: اصطلاحات التسمية description: نظرة عامة على اصطلاحات التسمية الموصى بها عند استخدام bloc. --- import EventExamplesGood1 from '~/components/naming-conventions/EventExamplesGood1Snippet.astro'; import EventExamplesBad1 from '~/components/naming-conventions/EventExamplesBad1Snippet.astro'; import StateExamplesGood1Snippet from '~/components/naming-conventions/StateExamplesGood1Snippet.astro'; import SingleStateExamplesGood1Snippet from '~/components/naming-conventions/SingleStateExamplesGood1Snippet.astro'; import StateExamplesBad1Snippet from '~/components/naming-conventions/StateExamplesBad1Snippet.astro'; اصطلاحات التسمية التالية هي مجرد توصيات وهي اختيارية بالكامل. لا تتردد في استخدام أي أسلوب تسمية تفضله. قد تلاحظ أن بعض الأمثلة أو أجزاء التوثيق لا تلتزم بهذه الاصطلاحات، وذلك غالبًا من أجل البساطة أو الإيجاز. ومع ذلك، يُوصى بشدة بهذه الاصطلاحات في المشاريع الكبيرة التي يعمل عليها عدة مطورين. ## اصطلاحات الأحداث (Event Conventions) يجب أن تُسمّى الأحداث بصيغة **الماضي**، لأن الحدث من منظور الـ bloc هو شيء حدث بالفعل. ### البنية (Anatomy) `BlocSubject` + `Noun (اختياري)` + `Verb (الحدث)` يجب أن تتبع أحداث التحميل الأولي الاصطلاح التالي: `BlocSubject` + `Started` :::note يجب أن يكون اسم فئة الحدث الأساسية (base event class) بالشكل التالي: `BlocSubject` + `Event`. ::: ### أمثلة ✅ **جيد** ❌ **سيئ** ## اصطلاحات الحالات (State Conventions) يجب أن تكون الحالات أسماء (Nouns)، لأن الحالة تمثل مجرد لقطة (snapshot) في نقطة زمنية محددة. هناك طريقتان شائعتان لتمثيل الحالة: باستخدام فئات فرعية (subclasses) أو باستخدام فئة واحدة (single class). ### البنية (Anatomy) #### الفئات الفرعية (Subclasses) `BlocSubject` + `Verb (الإجراء)` + `State` عند تمثيل الحالة باستخدام فئات فرعية متعددة، يجب أن تكون `State` واحدة من القيم التالية: `Initial` | `Success` | `Failure` | `InProgress` :::note يجب أن تتبع الحالات الأولية الاصطلاح التالي: `BlocSubject` + `Initial`. ::: #### الفئة الواحدة (Single Class) `BlocSubject` + `State` عند تمثيل الحالة كفئة أساسية واحدة، يجب استخدام تعداد (enum) باسم: `BlocSubject` + `Status` لتمثيل حالة الـ State: `initial` | `success` | `failure` | `loading` :::note يجب أن يكون اسم فئة الحالة الأساسية (base state class) دائمًا: `BlocSubject` + `State`. ::: ### أمثلة ✅ **جيد** ##### الفئات الفرعية (Subclasses) ##### الفئة الواحدة (Single Class) ❌ **سيئ** ================================================ FILE: docs/src/content/docs/ar/testing.mdx ================================================ --- title: الاختبار (Testing) description: أساسيات كيفية كتابة اختبارات للـ blocs الخاصة بك. --- import CounterBlocSnippet from '~/components/testing/CounterBlocSnippet.astro'; import AddDevDependenciesSnippet from '~/components/testing/AddDevDependenciesSnippet.astro'; import CounterBlocTestImportsSnippet from '~/components/testing/CounterBlocTestImportsSnippet.astro'; import CounterBlocTestMainSnippet from '~/components/testing/CounterBlocTestMainSnippet.astro'; import CounterBlocTestSetupSnippet from '~/components/testing/CounterBlocTestSetupSnippet.astro'; import CounterBlocTestInitialStateSnippet from '~/components/testing/CounterBlocTestInitialStateSnippet.astro'; import CounterBlocTestBlocTestSnippet from '~/components/testing/CounterBlocTestBlocTestSnippet.astro'; تم تصميم Bloc ليكون سهل الاختبار إلى حدٍ كبير. في هذا القسم، سنستعرض كيفية كتابة اختبارات وحدة (unit tests) للـ bloc. لأغراض التبسيط، سنقوم بكتابة اختبارات لـ `CounterBloc` الذي أنشأناه في [المفاهيم الأساسية](/ar/bloc-concepts). للتذكير، فإن تنفيذ `CounterBloc` يبدو كالتالي: ## الإعداد (Setup) قبل البدء في كتابة الاختبارات، نحتاج إلى إضافة إطار عمل للاختبار إلى تبعيات المشروع. نحتاج إلى إضافة حزمتَي [test](https://pub.dev/packages/test) و [bloc_test](https://pub.dev/packages/bloc_test) إلى مشروعنا. ## الاختبار (Testing) لنبدأ بإنشاء ملف لاختبارات `CounterBloc` باسم `counter_bloc_test.dart` واستيراد حزمة الاختبار. بعد ذلك، نحتاج إلى إنشاء دالة `main` بالإضافة إلى مجموعة اختبارات (test group). :::note تُستخدم المجموعات (Groups) لتنظيم الاختبارات الفردية، وكذلك لإنشاء سياق يمكن من خلاله مشاركة دوال `setUp` و `tearDown` بين جميع الاختبارات داخل المجموعة. ::: لنبدأ بإنشاء مثيل من `CounterBloc` سيتم استخدامه عبر جميع اختباراتنا. الآن يمكننا البدء في كتابة اختباراتنا الفردية. :::note يمكن تشغيل جميع الاختبارات باستخدام الأمر `dart test`. ::: في هذه المرحلة، يجب أن يكون لدينا أول اختبار ناجح! الآن دعونا نكتب اختبارًا أكثر تقدمًا باستخدام حزمة [bloc_test](https://pub.dev/packages/bloc_test). يجب أن نتمكن من تشغيل الاختبارات والتأكد من أن جميعها ناجحة. بهذا نكون قد انتهينا. ينبغي أن يكون الاختبار أمرًا سهلًا، وأن نشعر بالثقة عند إجراء التعديلات أو إعادة هيكلة الكود. يمكنك الرجوع إلى [تطبيق Weather App](https://github.com/felangel/bloc/tree/master/examples/flutter_weather) للاطلاع على مثال لتطبيق مختبَر بالكامل. ================================================ FILE: docs/src/content/docs/ar/tutorials/flutter-counter.mdx ================================================ --- title: Flutter Counter description: دليل متعمّق لبناء تطبيق عدّاد (Counter) في Flutter باستخدام مكتبة Bloc. sidebar: order: 1 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-counter/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) في هذا الدليل التعليمي، سنبني تطبيق **عداد (Counter)** في Flutter باستخدام مكتبة **Bloc**. ![demo](~/assets/tutorials/flutter-counter.gif) ## المواضيع الرئيسية (Key Topics) - مراقبة تغييرات الحالة باستخدام [`BlocObserver`](/ar/bloc-concepts#blocobserver). - [`BlocProvider`](/ar/flutter-bloc-concepts#blocprovider)، وهي Widget في Flutter توفّر Bloc للأبناء. - [`BlocBuilder`](/ar/flutter-bloc-concepts#blocbuilder)، وهي Widget في Flutter تتولّى إعادة البناء استجابةً للحالات الجديدة. - استخدام Cubit بدلاً من Bloc. [ما هو الفرق؟](/ar/bloc-concepts/#cubit-مقابل-bloc) - إضافة الأحداث باستخدام [`context.read`](/ar/flutter-bloc-concepts#contextread). ## الإعداد (Setup) سنبدأ بإنشاء مشروع Flutter جديد بالكامل: بعد ذلك، يمكننا استبدال محتويات ملف `pubspec.yaml` بما يلي: ثم نثبّت جميع التبعيات (dependencies): ## هيكل المشروع (Project Structure) ``` ├── lib │ ├── app.dart │ ├── counter │ │ ├── counter.dart │ │ ├── cubit │ │ │ └── counter_cubit.dart │ │ └── view │ │ ├── counter_page.dart │ │ ├── counter_view.dart │ │ └── view.dart │ ├── counter_observer.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` يستخدم التطبيق هيكل مجلدات يعتمد على الميزات (feature-driven directory structure). هذا النمط يساعدنا على توسيع المشروع عبر ميزات مستقلة بذاتها. في هذا المثال لدينا ميزة واحدة فقط (العداد نفسه)، لكن في التطبيقات الأكثر تعقيدًا قد نمتلك مئات الميزات المختلفة. ## BlocObserver أول ما سنراجعه هو إنشاء `BlocObserver` الذي يساعدنا على مراقبة جميع تغييرات الحالة في التطبيق. لننشئ الملف `lib/counter_observer.dart`: في هذه الحالة، نقوم فقط بعمل override للدالة `onChange` لمتابعة جميع تغييرات الحالة. :::note تعمل الدالة `onChange` بالطريقة نفسها في instances من `Bloc` و`Cubit`. ::: ## main.dart بعد ذلك، استبدل محتويات الملف `lib/main.dart` بما يلي: هنا نهيّئ `CounterObserver` الذي أنشأناه للتو، ثم نستدعي `runApp` باستخدام Widget `CounterApp` التي سنراجعها الآن. ## تطبيق العداد (Counter App) لننشئ الملف `lib/app.dart`: `CounterApp` هو `MaterialApp` ويحدد `home` على أنه `CounterPage`. :::note نقوم بعمل extend لـ `MaterialApp` لأن `CounterApp` **هو** `MaterialApp`. في أغلب الحالات سننشئ instances من `StatelessWidget` أو `StatefulWidget` ونركّب Widgets داخل `build`، لكن في هذا المثال لا توجد Widgets تحتاج إلى تركيب، لذا فإن extend المباشر لـ `MaterialApp` أبسط. ::: لننتقل الآن إلى `CounterPage`. ## صفحة العداد (Counter Page) لننشئ الملف `lib/counter/view/counter_page.dart`: تتولى Widget `CounterPage` إنشاء `CounterCubit` (الذي سنراجعه بعد قليل) وتوفيره إلى `CounterView`. :::note من المهم فصل إنشاء `Cubit` عن استهلاكه للحصول على أكواد برمجية أكثر قابلية للاختبار وإعادة الاستخدام. ::: ## Counter Cubit لننشئ الملف `lib/counter/cubit/counter_cubit.dart`: تعرض class `CounterCubit` طريقتين (methods): - `increment`: تضيف 1 إلى الحالة الحالية - `decrement`: تطرح 1 من الحالة الحالية نوع الحالة الذي يديره `CounterCubit` هو `int` فقط، والحالة الأولية هي `0`. :::tip استخدم [إضافة VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) أو [Plugin لـ IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) لإنشاء Cubits جديدة تلقائيًا. ::: بعد ذلك، لنراجع `CounterView`، وهي المسؤولة عن استهلاك الحالة والتفاعل مع `CounterCubit`. ## عرض العداد (Counter View) لننشئ الملف `lib/counter/view/counter_view.dart`: `CounterView` مسؤولة عن عرض قيمة العداد الحالية، وإظهار زري `FloatingActionButton` لزيادة/إنقاص العداد. نستخدم `BlocBuilder` لتغليف Widget `Text` بهدف تحديث النص كلما تغيّرت حالة `CounterCubit`. بالإضافة إلى ذلك، نستخدم `context.read()` للعثور على أقرب instance من `CounterCubit`. :::note تم تغليف Widget `Text` فقط داخل `BlocBuilder` لأنها الوحيدة التي تحتاج إعادة بناء عند تغيّر حالة `CounterCubit`. تجنّب تغليف Widgets لا تحتاج لإعادة البناء عند تغيّر الحالة. ::: ## Barrel (تجميع الصادرات) لننشئ الملف `lib/counter/view/view.dart`: أضف `view.dart` لتصدير جميع الأجزاء العامة (public) الخاصة بعرض العداد. لننشئ الملف `lib/counter/counter.dart`: أضف `counter.dart` لتصدير جميع الأجزاء العامة (public) الخاصة بميزة العداد. انتهينا! قمنا بفصل طبقة العرض (presentation layer) عن طبقة منطق الأعمال (business logic layer). لا تعرف `CounterView` ماذا يحدث عند ضغط المستخدم على الزر؛ هي فقط تُخطر `CounterCubit`. وفي المقابل، لا يعرف `CounterCubit` شيئًا عن طريقة عرض الحالة (قيمة العداد)، بل يصدر حالات جديدة استجابةً لاستدعاء methods. يمكننا تشغيل التطبيق بالأمر `flutter run` وعرضه على الجهاز أو simulator/emulator. يمكن العثور على المصدر الكامل (بما في ذلك اختبارات الوحدة واختبارات Widgets) لهذا المثال [هنا](https://github.com/felangel/Bloc/tree/master/examples/flutter_counter). ================================================ FILE: docs/src/content/docs/ar/tutorials/flutter-firebase-login.mdx ================================================ --- title: تسجيل الدخول باستخدام Flutter وFirebase description: دليل متعمق لبناء تدفق تسجيل دخول (Login Flow) في Flutter باستخدام Bloc وFirebase. sidebar: order: 7 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-firebase-login/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) في هذا الدليل، سنبني تدفق تسجيل دخول (Firebase Login Flow) في Flutter باستخدام مكتبة Bloc. ![demo](~/assets/tutorials/flutter-firebase-login.gif) ## المواضيع الرئيسية - [BlocProvider](/ar/flutter-bloc-concepts#blocprovider)، وهو `widget` من Flutter يوفّر Bloc للأبناء. - استخدام Cubit بدلًا من Bloc. [ما الفرق؟](/ar/bloc-concepts/#cubit-مقابل-bloc) - إضافة الأحداث (events) باستخدام [context.read](/ar/flutter-bloc-concepts#contextread). - تجنب إعادة البناء غير الضرورية باستخدام [Equatable](/ar/faqs/#متى-يجب-استخدام-equatable). - [RepositoryProvider](/ar/flutter-bloc-concepts#repositoryprovider)، وهو `widget` من Flutter يوفّر Repository للأبناء. - [BlocListener](/ar/flutter-bloc-concepts#bloclistener)، وهو `widget` من Flutter يستدعي منطق `listener` عند تغيّر الحالة (state) في Bloc. - قراءة جزء محدد من الحالة باستخدام [context.select](/ar/flutter-bloc-concepts#contextselect). ## الإعداد سنبدأ بإنشاء مشروع Flutter جديد تمامًا. وكما فعلنا في [دليل تسجيل الدخول](/ar/tutorials/flutter-login)، سننشئ حزمًا داخلية لتقسيم معمارية التطبيق (application architecture) إلى طبقات أوضح، مع الحفاظ على حدود واضحة بين الطبقات، وتحسين قابلية إعادة الاستخدام (reusability) والاختبار (testability). في هذا المثال، ستكون حزمتا [firebase_auth](https://pub.dev/packages/firebase_auth) و [google_sign_in](https://pub.dev/packages/google_sign_in) هما طبقة البيانات (data layer). لذلك سننشئ `AuthenticationRepository` فقط لدمج البيانات من عميلَي واجهات البرمجة (API clients). ## مستودع المصادقة (Authentication Repository) سيكون `AuthenticationRepository` مسؤولًا عن إخفاء تفاصيل التنفيذ الداخلية (implementation details) لطريقة مصادقة المستخدم وجلب بياناته. في هذا المثال سيتكامل مع Firebase، لكن يمكننا لاحقًا تغيير التنفيذ الداخلي دون التأثير على باقي التطبيق. ### الإعداد سنبدأ بإنشاء `packages/authentication_repository` وملف `pubspec.yaml` في جذر المشروع. بعد ذلك، يمكننا تثبيت dependencies عبر تشغيل: داخل مجلد `authentication_repository`. مثل أغلب الحزم، يعرّف `authentication_repository` واجهته العامة (API surface) عبر الملف `packages/authentication_repository/lib/authentication_repository.dart`. :::note حزمة `authentication_repository` توفّر `AuthenticationRepository` بالإضافة إلى models. ::: الخطوة التالية هي مراجعة models. ### المستخدم (User) نموذج `User` يصف المستخدم ضمن نطاق المصادقة (authentication domain). وفي هذا المثال، يتكوّن المستخدم من `email` و`id` و`name` و`photo`. :::note شكل `User` النهائي يعتمد بالكامل على متطلبات نطاق عملك (domain). ::: [user.dart](https://raw.githubusercontent.com/felangel/bloc/master/examples/flutter_firebase_login/packages/authentication_repository/lib/src/models/user.dart ':include') :::note الصنف `User` يرث من [equatable](https://pub.dev/packages/equatable) لتجاوز مقارنات التساوي (equality comparisons)، حتى نتمكن من مقارنة كائنات `User` المختلفة بالقيمة (by value). ::: :::tip من المفيد تعريف `User.empty` بشكل `static` لتجنب التعامل مع `null User`، والتعامل دائمًا مع كائن `User` فعلي. ::: ### المستودع (Repository) `AuthenticationRepository` مسؤول عن تجريد (abstraction) تنفيذ المصادقة الفعلي وكذلك آلية جلب بيانات المستخدم. يوفّر `AuthenticationRepository` تدفقًا `Stream` يمكننا الاشتراك فيه للحصول على إشعار عند تغيّر `User`. كما يوفّر methods مثل `signUp` و `logInWithGoogle` و`logInWithEmailAndPassword` و`logOut`. :::note `AuthenticationRepository` مسؤول أيضًا عن التعامل مع أخطاء طبقة البيانات (data layer) منخفضة المستوى، ويقدّم مجموعة أخطاء أبسط وأنظف ومتوافقة مع النطاق (domain). ::: هذا كل ما نحتاجه في `AuthenticationRepository`. الخطوة التالية هي دمجه في مشروع Flutter الذي أنشأناه. ## إعداد Firebase نحتاج إلى اتباع [تعليمات استخدام firebase_auth](https://pub.dev/packages/firebase_auth#usage) لتوصيل التطبيق مع Firebase وتفعيل [google_sign_in](https://pub.dev/packages/google_sign_in). :::caution تأكد من تحديث `google-services.json` على Android، و`GoogleService-Info.plist` و`Info.plist` على iOS، وإلا سيتعطل التطبيق. ::: ## تبعيات المشروع (Project Dependencies) يمكننا استبدال ملف `pubspec.yaml` المُولّد في جذر المشروع بما يلي: لاحظ أننا نحدد مجلد `assets` لكل الأصول المحلية (local assets) الخاصة بالتطبيق. أنشئ مجلد `assets` في جذر المشروع، ثم أضف أصل شعار bloc [bloc logo](https://github.com/felangel/bloc/blob/master/examples/flutter_firebase_login/assets/bloc_logo_small.png) (سنستخدمه لاحقًا). بعد ذلك ثبّت جميع dependencies: :::note نحن نستخدم حزمة `authentication_repository` عبر `path`، وهذا يتيح لنا التطوير بسرعة مع الحفاظ على فصل واضح بين الطبقات. ::: ## main.dart يمكن استبدال ملف `main.dart` بما يلي: الملف يضبط إعدادات عامة للتطبيق، ثم يستدعي `runApp` مع نسخة من `App`. :::note نحقن (inject) نسخة واحدة من `AuthenticationRepository` داخل `App` كاعتمادية صريحة في المُنشئ (constructor dependency). ::: ## التطبيق (App) كما في [دليل تسجيل الدخول](/ar/tutorials/flutter-login)، يوفّر `app.dart` نسخة `AuthenticationRepository` للتطبيق عبر `RepositoryProvider`، كما ينشئ نسخة من `AuthenticationBloc` ويجعلها متاحة. بعد ذلك، تتعامل `AppView` مع `AuthenticationBloc` وتحدّث المسار الحالي (current route) بناءً على `AuthenticationState`. ## App Bloc `AppBloc` مسؤول عن إدارة الحالة العامة (global state) للتطبيق. وهو يعتمد على `AuthenticationRepository`، ويشترك في تدفق `user` لإصدار حالات جديدة عند تغيّر المستخدم الحالي. ### الحالة (State) تتكوّن `AppState` من `AppStatus` و`User`. يقبل المُنشئ الافتراضي `User` اختياريًا، ثم يعيد التوجيه إلى المُنشئ الخاص مع حالة المصادقة المناسبة. ### الحدث (Event) يحتوي `AppEvent` على فئتين فرعيتين (subclasses): - `AppUserSubscriptionRequested` لإعلام الـ Bloc ببدء الاشتراك في تدفق المستخدم. - `AppLogoutPressed` لإعلام الـ Bloc بأن المستخدم طلب تسجيل الخروج. ### Bloc داخل المُنشئ (constructor body)، يتم ربط الفئات الفرعية من `AppEvent` مع معالجات الأحداث (event handlers) المقابلة لها. داخل معالج `_onUserSubscriptionRequested`، يستخدم `AppBloc` الدالة `emit.onEach` للاشتراك في تدفق المستخدم الخاص بـ `AuthenticationRepository`، ثم إصدار حالة لكل `User` جديد. `emit.onEach` تنشئ اشتراكًا داخليًا في التدفق (stream subscription)، وتتكفل بإلغائه عند إغلاق `AppBloc` أو إغلاق تدفق المستخدم. إذا أصدر تدفق المستخدم خطأً، تقوم `addError` بتمرير الخطأ و`stack trace` إلى أي `BlocObserver` يستمع. :::caution إذا لم يتم تمرير `onError`، فسيتم اعتبار أي خطأ في تدفق المستخدم غير مُعالَج، وسيتم رميه من `onEach`. ونتيجة لذلك سيتم إلغاء الاشتراك في تدفق المستخدم. ::: :::tip [`BlocObserver`](/ar/bloc-concepts/#blocobserver) مفيد جدًا لتسجيل أحداث Bloc والأخطاء وتغيّرات الحالة، خصوصًا في سياق التحليلات (analytics) وتقارير الأعطال (crash reporting). ::: ## النماذج (Models) نماذج إدخال `Email` و`Password` مفيدة لتغليف منطق التحقق (validation logic)، وسيتم استخدامها في `LoginForm` و`SignUpForm` (لاحقًا في هذا الدليل). كلا النموذجين مبنيّ باستخدام حزمة [formz](https://pub.dev/packages/formz)، وهذا يسمح لنا بالتعامل مع كائن مُتحقَّق منه (validated object) بدلًا من نوع بدائي (primitive type) مثل `String`. ### البريد الإلكتروني (Email) ### كلمة المرور (Password) ## صفحة تسجيل الدخول (Login Page) `LoginPage` مسؤولة عن إنشاء نسخة من `LoginCubit` وتوفيرها إلى `LoginForm`. :::tip من المهم جدًا فصل مكان إنشاء blocs/cubits عن مكان استهلاكها. هذا يجعل حقن نسخ وهمية (mock instances) أسهل، ويسهّل اختبار الواجهة (view) بشكل مستقل (in isolation). ::: ## Login Cubit `LoginCubit` مسؤول عن إدارة `LoginState` الخاص بالنموذج. وهو يوفّر APIs مثل `logInWithCredentials` و`logInWithGoogle`، كما يتلقى إشعارات عند تحديث البريد الإلكتروني/كلمة المرور. ### الحالة (State) تتكوّن `LoginState` من `Email` و`Password` و`FormzStatus`. ويرث نموذجا `Email` و`Password` من `FormzInput` في حزمة [formz](https://pub.dev/packages/formz). ### Cubit يعتمد `LoginCubit` على `AuthenticationRepository` لتسجيل دخول المستخدم إما عبر بيانات الاعتماد (credentials) أو عبر Google Sign-In. :::note استخدمنا `Cubit` بدلًا من `Bloc` هنا لأن `LoginState` بسيط ومحصور في نطاق صغير. حتى بدون events، يمكن فهم ما حدث من خلال الانتقال بين الحالات، مع كود أبسط وأكثر اختصارًا. ::: ## نموذج تسجيل الدخول (Login Form) `LoginForm` مسؤول عن عرض النموذج بناءً على `LoginState`، ويستدعي methods على `LoginCubit` استجابةً لتفاعلات المستخدم. كما يعرض `LoginForm` زر "إنشاء حساب" (Create Account) الذي ينقل المستخدم إلى `SignUpPage` لإنشاء حساب جديد. ## صفحة إنشاء الحساب (Sign Up Page) بنية `SignUp` تعكس بنية `Login`، وتتكوّن من `SignUpPage` و`SignUpView` و `SignUpCubit`. `SignUpPage` مسؤولة فقط عن إنشاء نسخة من `SignUpCubit` وتوفيرها إلى `SignUpForm` (تمامًا كما في `LoginPage`). :::note كما في `LoginCubit`، يعتمد `SignUpCubit` على `AuthenticationRepository` لإنشاء حسابات مستخدمين جديدة. ::: ## Sign Up Cubit يدير `SignUpCubit` حالة `SignUpForm`، ويتواصل مع `AuthenticationRepository` لإنشاء حسابات جديدة. ### الحالة (State) تُعيد `SignUpState` استخدام نموذجي إدخال `Email` و`Password` لأن منطق التحقق (validation logic) هو نفسه. ### Cubit `SignUpCubit` قريب جدًا من `LoginCubit`، والفرق الأساسي أنه يوفّر API لإرسال النموذج (submit) بدلًا من تسجيل الدخول. ## نموذج إنشاء الحساب (Sign Up Form) `SignUpForm` مسؤول عن عرض النموذج بناءً على `SignUpState`، ويستدعي methods على `SignUpCubit` استجابةً لتفاعلات المستخدم. ## الصفحة الرئيسية (Home Page) بعد أن يسجل المستخدم الدخول أو ينشئ حسابًا بنجاح، يتم تحديث تدفق `user`، ما يؤدي إلى تغيّر الحالة في `AuthenticationBloc`، ثم تدفع `AppView` صفحة `HomePage` إلى مكدس التنقل (navigation stack). من `HomePage` يمكن للمستخدم عرض معلومات ملفه الشخصي وتسجيل الخروج عبر النقر على أيقونة الخروج في `AppBar`. :::note تم إنشاء مجلد `widgets` بجانب مجلد `view` داخل ميزة `home` للمكونات القابلة لإعادة الاستخدام الخاصة بهذه الميزة. في هذا المثال، يتم تصدير `Avatar` بسيط واستخدامه داخل `HomePage`. ::: :::note عند النقر على `IconButton` الخاص بتسجيل الخروج، يُضاف الحدث `AuthenticationLogoutRequested` إلى `AuthenticationBloc`، فيتم تسجيل خروج المستخدم وإعادته إلى `LoginPage`. ::: بهذه المرحلة أصبح لدينا تنفيذ قوي لتسجيل الدخول باستخدام Firebase، وتمكّنا من فصل طبقة العرض (presentation layer) عن طبقة منطق الأعمال (business logic layer) باستخدام مكتبة Bloc. يمكنك الاطلاع على المصدر الكامل لهذا المثال من [هنا](https://github.com/felangel/bloc/tree/master/examples/flutter_firebase_login). ================================================ FILE: docs/src/content/docs/ar/tutorials/flutter-infinite-list.mdx ================================================ --- title: قائمة Flutter اللانهائية (Infinite List) description: دليل متعمّق لبناء قائمة Flutter لانهائية باستخدام مكتبة Bloc. sidebar: order: 3 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-infinite-list/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/flutter-infinite-list/FlutterPubGetSnippet.astro'; import PostsJsonSnippet from '~/components/tutorials/flutter-infinite-list/PostsJsonSnippet.astro'; import PostBlocInitialStateSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocInitialStateSnippet.astro'; import PostBlocOnPostFetchedSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocOnPostFetchedSnippet.astro'; import PostBlocTransformerSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocTransformerSnippet.astro'; ![intermediate](https://img.shields.io/badge/level-intermediate-orange.svg) في هذا الدليل، سنبني تطبيقًا يجلب البيانات عبر الشبكة ويحمّلها تدريجيًا أثناء تمرير المستخدم، باستخدام Flutter ومكتبة Bloc. ![demo](~/assets/tutorials/flutter-infinite-list.gif) ## المواضيع الرئيسية - مراقبة تغييرات الحالة باستخدام [BlocObserver](/ar/bloc-concepts#blocobserver). - [BlocProvider](/ar/flutter-bloc-concepts#blocprovider)، وهي Widget في Flutter توفّر Bloc للأبناء. - [BlocBuilder](/ar/flutter-bloc-concepts#blocbuilder)، وهي Widget في Flutter تتولّى إعادة البناء استجابةً للحالات الجديدة. - إضافة الأحداث باستخدام [context.read](/ar/flutter-bloc-concepts#contextread). - منع إعادة البناء غير الضرورية باستخدام [Equatable](/ar/faqs/#متى-يجب-استخدام-equatable). - استخدام `transformEvents` مع Rx. ## الإعداد سنبدأ بإنشاء مشروع Flutter جديد بالكامل. بعد ذلك، يمكننا استبدال محتويات `pubspec.yaml` بما يلي: ثم نثبّت جميع التبعيات (dependencies): ## هيكل المشروع ``` ├── lib | ├── posts │ │ ├── bloc │ │ │ └── post_bloc.dart | | | └── post_event.dart | | | └── post_state.dart | | └── models | | | └── models.dart* | | | └── post.dart │ │ └── view │ │ | ├── posts_page.dart │ │ | └── posts_list.dart | | | └── view.dart* | | └── widgets | | | └── bottom_loader.dart | | | └── post_list_item.dart | | | └── widgets.dart* │ │ ├── posts.dart* │ ├── app.dart │ ├── simple_bloc_observer.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` يستخدم التطبيق هيكل مجلدات يعتمد على الميزات (feature-driven directory structure). يتيح لنا هذا النمط توسيع المشروع عبر ميزات مستقلة بذاتها. في هذا المثال، سيكون لدينا ميزة واحدة فقط (ميزة المنشورات `post feature`) وهي مقسمة إلى مجلدات خاصة بها مع ملفات تجميع (barrel files)، المشار إليها بعلامة النجمة (\*). ## واجهة برمجة تطبيقات REST في هذا التطبيق التجريبي، سنستخدم [jsonplaceholder](http://jsonplaceholder.typicode.com) كمصدر للبيانات. :::note `jsonplaceholder` هي REST API عبر الإنترنت تقدم بيانات وهمية؛ وهي مفيدة جدًا لبناء النماذج الأولية. ::: افتح علامة تبويب جديدة في المتصفح وزر الرابط التالي: `https://jsonplaceholder.typicode.com/posts?_start=0&_limit=2` لترى ما ترجعه واجهة برمجة التطبيقات. :::note في URL هذا، حددنا `start` و`limit` كمعاملات استعلام (query parameters) في طلب GET. ::: ممتاز، بعد أن عرفنا شكل البيانات، لننشئ الـ model. ## نموذج البيانات (Data Model) أنشئ `post.dart` ولنبدأ ببناء model كائن المنشور (`Post`). `Post` عبارة عن class بسيط يحتوي على `id` و`title` و`body`. :::note نحن نرث من [`Equatable`](https://pub.dev/packages/equatable) حتى نتمكن من مقارنة كائنات `Posts`. بدون ذلك سنحتاج لتعديل الـ class يدويًا وعمل override لـ `equality` و`hashCode` حتى نميّز بين كائنين مختلفين من `Posts`. راجع [الحزمة](https://pub.dev/packages/equatable) لمزيد من التفاصيل. ::: الآن بعد أن أصبح لدينا model كائن `Post`، لنبدأ العمل على مكوّن منطق الأعمال (Business Logic Component) أي `bloc`. ## أحداث المنشور (Post Events) قبل التعمّق في التنفيذ، نحتاج إلى تحديد ما الذي سيفعله `PostBloc`. على مستوى عام، سيستجيب لإدخال المستخدم (التمرير) ويجلب المزيد من المنشورات حتى تتمكن طبقة العرض من عرضها. لنبدأ بإنشاء `Event`. سيستجيب `PostBloc` لحدث واحد فقط هو `PostFetched`، والذي تضيفه طبقة العرض كلما احتاجت إلى مزيد من المنشورات. وبما أن `PostFetched` نوع من `PostEvent`، يمكننا إنشاء `bloc/post_event.dart` وتنفيذه كالتالي. باختصار، سيتلقى `PostBloc` أحداث `PostEvents` ويحوّلها إلى حالات `PostStates`. بعد تعريف `PostEvents` (`PostFetched`)، ننتقل لتعريف `PostState`. ## حالات المنشور (Post States) تحتاج طبقة العرض إلى عدة معلومات لتتمكن من البناء بشكل صحيح: - `PostInitial`: ستخبر طبقة العرض بأنها بحاجة إلى عرض مؤشر تحميل أثناء تحميل الدفعة الأولية من المنشورات. - `PostSuccess`: تخبر طبقة العرض أن لديها محتوى جاهزًا للعرض. - `posts`: ستكون قائمة (`List`) سيتم عرضها. - `hasReachedMax`: ستخبر طبقة العرض بما إذا كانت قد وصلت إلى الحد الأقصى لعدد المنشورات أم لا. - `PostFailure`: ستخبر طبقة العرض بحدوث خطأ أثناء جلب المنشورات. يمكننا الآن إنشاء `bloc/post_state.dart` وتنفيذه بالشكل التالي. :::note قمنا بتنفيذ `copyWith` حتى نتمكن من نسخ instance من `PostSuccess` وتحديث صفر أو أكثر من الخصائص بسهولة (وسيكون ذلك مفيدًا لاحقًا). ::: الآن بعد تنفيذ `Events` و`States`، يمكننا إنشاء `PostBloc`. ## Post Bloc للتبسيط، سيعتمد `PostBloc` مباشرة على `http client`. لكن في تطبيق إنتاجي يُفضل حقن API client واستخدام نمط المستودع (repository pattern). راجع [الوثائق](/ar/architecture). لننشئ `post_bloc.dart` ونبدأ بـ `PostBloc` فارغ. :::note من class declaration فقط، يمكننا معرفة أن `PostBloc` يستقبل `PostEvents` كمدخلات ويخرج `PostStates`. ::: بعد ذلك، نحتاج لتسجيل event handler لمعالجة أحداث `PostFetched` الواردة. استجابةً لهذا الحدث، سنستدعي `_fetchPosts` لجلب المنشورات من API. سيقوم `PostBloc` بإصدار (`emit`) حالات جديدة عبر `Emitter` الممرر إلى event handler. راجع [المفاهيم الأساسية](/ar/bloc-concepts/#التدفقات-streams) لمزيد من المعلومات. الآن، في كل مرة يُضاف فيها `PostEvent` وكان الحدث هو `PostFetched` وما زالت هناك منشورات متاحة، سيقوم `PostBloc` بجلب 20 منشورًا إضافيًا. ستعيد API مصفوفة فارغة إذا حاولنا الجلب بعد الحد الأقصى (100 منشور). لذلك إذا حصلنا على مصفوفة فارغة، فسيقوم bloc بإصدار (`emit`) الحالة الحالية مع تعيين `hasReachedMax` إلى `true`. إذا لم نتمكن من استرداد المنشورات، فإننا نصدر `PostStatus.failure`. إذا تمكنا من استرداد المنشورات، فإننا نصدر `PostStatus.success` والقائمة الكاملة للمنشورات. أحد التحسينات الممكنة هو عمل **throttle** لحدث `PostFetched` لتجنب إرسال طلبات غير ضرورية إلى API. ويمكن تنفيذ ذلك باستخدام معامل `transform` عند تسجيل معالج الحدث `_onFetched`. :::note يسمح لنا تمرير `transformer` إلى `on` بتخصيص كيفية معالجة الأحداث. ::: :::note تأكد من استيراد [`package:stream_transform`](https://pub.dev/packages/stream_transform) لاستخدام API الخاصة بـ `throttle`. ::: يجب أن يبدو `PostBloc` النهائي لدينا الآن كما يلي: ممتاز! بعد الانتهاء من منطق الأعمال، المتبقي هو تنفيذ طبقة العرض. ## طبقة العرض (Presentation Layer) في `main.dart`، نبدأ بتنفيذ الدالة الرئيسية واستدعاء `runApp` لعرض Widget الجذر. وهنا يمكننا أيضًا تضمين `bloc observer` لتسجيل الانتقالات وأي أخطاء. في ويدجت `App`، وهو جذر مشروعنا، يمكننا بعد ذلك تعيين الصفحة الرئيسية إلى `PostsPage`. في Widget `PostsPage`، نستخدم `BlocProvider` لإنشاء وتوفير instance من `PostBloc` للشجرة الفرعية. كما نضيف حدث `PostFetched` بحيث يطلب التطبيق الدفعة الأولية من المنشورات عند التحميل. بعد ذلك، نحتاج إلى تنفيذ `PostsList` التي ستعرض المنشورات وتتصل بـ `PostBloc`. :::note `PostsList` هي `StatefulWidget` لأنها تحتاج للاحتفاظ بـ `ScrollController`. في `initState`، نضيف listener إلى `ScrollController` حتى نستجيب لأحداث التمرير. كما نصل إلى instance من `PostBloc` عبر `context.read()`. ::: في خطوة البناء، تعيد `build method` لدينا `BlocBuilder`. و`BlocBuilder` هي Widget من Flutter ضمن [حزمة flutter_bloc](https://pub.dev/packages/flutter_bloc) تتولى بناء ويدجت استجابةً لحالات bloc الجديدة. وعند تغيّر حالة `PostBloc`، سيتم استدعاء `builder` بالحالة الجديدة `PostState`. :::caution يجب أن نتذكر تنظيف الموارد والتخلّص من `ScrollController` عند التخلص من `StatefulWidget`. ::: عند تمرير المستخدم، نحسب المسافة التي وصل إليها داخل الصفحة. وإذا بلغت المسافة `>= 90%` من `maxScrollExtent` نضيف حدث `PostFetched` لتحميل مزيد من المنشورات. بعد ذلك، نحتاج لتنفيذ Widget `BottomLoader` التي توضح للمستخدم أننا نحمّل المزيد من المنشورات. أخيرًا، نحتاج إلى تنفيذ `PostListItem` التي تعرض عنصر `Post` واحدًا. في هذه المرحلة يفترض أن يعمل التطبيق بشكل صحيح، لكن ما زال هناك تحسين إضافي. من مزايا مكتبة bloc أننا نستطيع الوصول إلى جميع الانتقالات (`Transitions`) في مكان واحد. التغيير من حالة إلى أخرى يسمى انتقالًا (`Transition`). :::note يتكون الانتقال (`Transition`) من الحالة الحالية، والحدث، والحالة التالية. ::: على الرغم من أن لدينا في هذا التطبيق كتلة واحدة فقط، فمن الشائع جدًا في التطبيقات الأكبر أن يكون لدينا العديد من الكتل التي تدير أجزاء مختلفة من حالة التطبيق. إذا أردنا تنفيذ منطق معيّن استجابةً لكل الانتقالات (`Transitions`)، يمكننا ببساطة إنشاء `BlocObserver` خاص بنا. :::note كل ما نحتاجه هو التوسيع (extend) من `BlocObserver` وعمل override للدالة `onTransition`. ::: الآن، في كل مرة يحدث فيها انتقال (`Transition`) داخل Bloc، يمكننا رؤية تفاصيله مطبوعة في terminal. :::note عمليًا، يمكنك إنشاء `BlocObservers` مختلفة. وبما أن كل تغيّر في الحالة يُسجّل، يمكننا بسهولة فحص التطبيق وتتبع تفاعلات المستخدم وتغييرات الحالة في مكان واحد. ::: هذا كل ما في الأمر! نجحنا الآن في تنفيذ قائمة لانهائية في Flutter باستخدام حزمتي [bloc](https://pub.dev/packages/bloc) و [flutter_bloc](https://pub.dev/packages/flutter_bloc)، مع فصل طبقة العرض عن منطق الأعمال. لا تعرف `PostsPage` من أين تأتي `Posts` أو كيف يتم جلبها. وفي المقابل، لا يعرف `PostBloc` كيف تُعرض `State`؛ هو فقط يحوّل الأحداث إلى حالات. يمكن العثور على المصدر الكامل لهذا المثال [هنا](https://github.com/felangel/Bloc/tree/master/examples/flutter_infinite_list). ================================================ FILE: docs/src/content/docs/ar/tutorials/flutter-login.mdx ================================================ --- title: تسجيل الدخول في Flutter description: دليل متعمّق لبناء مسار تسجيل الدخول في Flutter باستخدام مكتبة Bloc. sidebar: order: 4 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-login/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![intermediate](https://img.shields.io/badge/level-intermediate-orange.svg) في هذا الدليل، سنقوم ببناء مسار تسجيل دخول في Flutter باستخدام مكتبة Bloc. ![demo](~/assets/tutorials/flutter-login.gif) ## المواضيع الرئيسية - [BlocProvider](/ar/flutter-bloc-concepts#blocprovider)، وهي Widget في Flutter توفّر bloc للأبناء (subtree). - إضافة الأحداث باستخدام [context.read](/ar/flutter-bloc-concepts#contextread). - تجنب إعادة البناء غير الضرورية باستخدام [Equatable](/ar/faqs/#متى-يجب-استخدام-equatable). - [RepositoryProvider](/ar/flutter-bloc-concepts#repositoryprovider)، وهي Widget في Flutter توفّر repository للأبناء. - [BlocListener](/ar/flutter-bloc-concepts#bloclistener)، وهي Widget في Flutter تنفّذ كود المستمع استجابةً لتغيّرات حالة الـ bloc. - تحديث واجهة المستخدم بناءً على جزء من حالة الـ bloc باستخدام [context.select](/ar/flutter-bloc-concepts#contextselect). ## إعداد المشروع سنبدأ بإنشاء مشروع Flutter جديد بالكامل. بعد ذلك، يمكننا تثبيت جميع التبعيات (dependencies). ## مستودع المصادقة (Authentication Repository) أول شيء سنقوم به هو إنشاء حزمة `authentication_repository` والتي ستكون مسؤولة عن إدارة مجال المصادقة. سنبدأ بإنشاء مجلد `packages/authentication_repository` في جذر المشروع والذي سيحتوي على جميع الحزم الداخلية. على مستوى عالٍ، يجب أن يبدو هيكل المجلدات بالشكل التالي: ``` ├── android ├── ios ├── lib ├── packages │ └── authentication_repository └── test ``` بعد ذلك، يمكننا إنشاء ملف `pubspec.yaml` لحزمة `authentication_repository`: :::note حزمة `package:authentication_repository` ستكون حزمة Dart نقية بدون أي تبعيات خارجية. ::: بعد ذلك، نحتاج إلى تنفيذ الفئة `AuthenticationRepository` نفسها والتي ستكون في `packages/authentication_repository/lib/src/authentication_repository.dart`. توفّر `AuthenticationRepository` تدفق `Stream` من تحديثات `AuthenticationStatus`، ويُستخدم هذا التدفق لإبلاغ التطبيق عند تسجيل المستخدم الدخول أو الخروج. بالإضافة إلى ذلك، هناك دالتا `logIn` و`logOut` مبسّطتان للشرح، لكن يمكن بسهولة توسيعها للمصادقة باستخدام `FirebaseAuth` مثلاً أو أي مزود مصادقة آخر. :::note بما أننا ندير `StreamController` داخليًا، تم توفير دالة `dispose` لإغلاق controller عندما لا يعود مطلوبًا. ::: أخيرًا، نحتاج إلى إنشاء الملف `packages/authentication_repository/lib/authentication_repository.dart` والذي سيحتوي على الصادرات العامة (public exports): هذا كل شيء بالنسبة لـ `AuthenticationRepository`، في الخطوة التالية سنعمل على `UserRepository`. ## مستودع المستخدم تمامًا كما فعلنا مع `AuthenticationRepository`، سنقوم بإنشاء حزمة `user_repository` داخل مجلد `packages`. ``` ├── android ├── ios ├── lib ├── packages │ ├── authentication_repository │ └── user_repository └── test ``` بعد ذلك، سنقوم بإنشاء ملف `pubspec.yaml` الخاص بـ `user_repository`: حزمة `user_repository` مسؤولة عن نطاق المستخدم، وتوفّر APIs للتفاعل مع المستخدم الحالي. أول شيء سنحدده هو نموذج المستخدم في الملف `packages/user_repository/lib/src/models/user.dart`: لأجل البساطة، يحتوي المستخدم على خاصية `id` فقط، لكن في التطبيق العملي قد تكون هناك خصائص إضافية مثل `firstName`، `lastName`، `avatarUrl` وغيرها... :::note يتم استخدام [`package:equatable`](https://pub.dev/packages/equatable) لتمكين مقارنة القيم داخل كائن `User`. ::: بعد ذلك، يمكننا إنشاء ملف `models.dart` داخل `packages/user_repository/lib/src/models` ليقوم بتصدير كل النماذج، بحيث يمكننا استخدام استيراد واحد لاستدعاء نماذج متعددة. الآن بعد تعريف النماذج، يمكننا تنفيذ فئة `UserRepository` في `packages/user_repository/lib/src/user_repository.dart`. في هذا المثال البسيط، توفّر `UserRepository` دالة واحدة فقط هي `getUser` والتي تسترجع المستخدم الحالي. نحن هنا نستخدم stubbing، لكن في التطبيق الفعلي ستكون هذه الدالة هي التي تستعلم المستخدم الحالي من الخادم (backend). لقد اقتربنا من الانتهاء من حزمة `user_repository`، والشيء الوحيد المتبقي هو إنشاء ملف `user_repository.dart` في المسار `packages/user_repository/lib` والذي يعرّف الصادرات العامة (public exports): الآن بعد أن أتممنا حزمتي `authentication_repository` و `user_repository`، يمكننا الانتقال للتركيز على تطبيق Flutter. ## تثبيت التبعيات لنبدأ بتحديث ملف `pubspec.yaml` المُولد في جذر مشروعنا: يمكننا تثبيت التبعيات عن طريق تشغيل الأمر: ## Authentication Bloc سيتولى `AuthenticationBloc` مسؤولية الاستجابة لتغيرات حالة المصادقة (التي يعرضها `AuthenticationRepository`) وسيصدر حالات يمكننا التفاعل معها في طبقة العرض. تم تنفيذ `AuthenticationBloc` داخل مجلد `lib/authentication` لأننا نعتبر المصادقة كميزة في طبقة التطبيق الخاصة بنا. ``` ├── lib │ ├── app.dart │ ├── authentication │ │ ├── authentication.dart │ │ └── bloc │ │ ├── authentication_bloc.dart │ │ ├── authentication_event.dart │ │ └── authentication_state.dart │ ├── main.dart ``` :::tip استخدم [امتداد VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) أو [إضافة IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) لإنشاء blocs تلقائيًا. ::: ### authentication_event.dart تمثل مثيلات `AuthenticationEvent` المدخلات إلى `AuthenticationBloc`، وسيتم معالجتها لاستخدامها في إصدار مثيلات جديدة من `AuthenticationState`. في هذا التطبيق، سيستجيب `AuthenticationBloc` لحدثين مختلفين: - `AuthenticationSubscriptionRequested`: الحدث الأولي الذي يُبلغ الـ bloc بالاشتراك في تدفق `AuthenticationStatus`. - `AuthenticationLogoutPressed`: يُعلم الـ bloc بحدوث تسجيل خروج من قِبل المستخدم. لننتقل الآن للنظر في `AuthenticationState`. ### authentication_state.dart تمثل مثيلات `AuthenticationState` نواتج `AuthenticationBloc` وسيتم استهلاكها من قبل طبقة العرض. لفئة `AuthenticationState` ثلاث مُنشئين مسمّين: - `AuthenticationState.unknown()`: الحالة الافتراضية التي تدل على أن الـ bloc لا يعرف بعد ما إذا كان المستخدم الحالي مصدقًا أم لا. - `AuthenticationState.authenticated()`: الحالة التي تشير إلى أن المستخدم حالياً مصدق عليه. - `AuthenticationState.unauthenticated()`: الحالة التي تدل على أن المستخدم حالياً غير مصدق عليه. بعد أن اطلعنا على تنفيذ `AuthenticationEvent` و`AuthenticationState`، دعونا نلقي نظرة على `AuthenticationBloc`. ### authentication_bloc.dart يدير `AuthenticationBloc` حالة المصادقة في التطبيق، والتي تُستخدم لاتخاذ قرارات مثل بدء المستخدم في صفحة تسجيل الدخول أو الصفحة الرئيسية. يعتمد `AuthenticationBloc` على كل من `AuthenticationRepository` و`UserRepository`، ويحدد الحالة الابتدائية كـ `AuthenticationState.unknown()`. في مُنشئ الـ bloc، يتم ربط فئات الأحداث المشتقة من `AuthenticationEvent` بمعالجيها المناسبين. في معالج الحدث `_onSubscriptionRequested`، يستخدم `AuthenticationBloc` `emit.onEach` للاشتراك في تدفق `status` الخاص بـ `AuthenticationRepository` وإصدار حالة استجابةً لكل حالة من `AuthenticationStatus`. `emit.onEach` يقوم بإنشاء اشتراك داخلي في التدفق ويتولى إلغاءه تلقائيًا عند إغلاق `AuthenticationBloc` أو تدفق `status`. إذا أصدر تدفق `status` خطأً، فإن `addError` يمرّر الخطأ مع `stackTrace` لأي `BlocObserver` يستمع. :::caution إذا تم حذف `onError`، تُعتبر أي أخطاء في تدفق `status` غير معالجة، وسيتم رميها بواسطة `onEach`، مما يؤدي إلى إلغاء الاشتراك في التدفق. ::: :::tip يُعد [`BlocObserver`](/ar/bloc-concepts/#blocobserver) أداة ممتازة لتسجيل أحداث الـ Bloc والأخطاء وتغيرات الحالة، خصوصًا في سياق التحليلات وتقرير الأعطال. ::: عندما يصدر تدفق `status` الحالة `AuthenticationStatus.unknown` أو `unauthenticated`، يتم إصدار الحالة المطابقة في `AuthenticationState`. عندما يُصدر التدفق `AuthenticationStatus.authenticated`، يقوم `AuthenticationBloc` باستعلام بيانات المستخدم عبر `UserRepository`. ## main.dart بعد ذلك، يمكننا استبدال ملف `main.dart` الافتراضي بالنص التالي: ## التطبيق `app.dart` يحتوي على ويدجت الجذر `App` الخاص بالتطبيق بأكمله. :::note تم تقسيم `app.dart` إلى جزأين: `App` و`AppView`. يتحمل `App` مسؤولية إنشاء/توفير `AuthenticationBloc` الذي سيتم استهلاكه من قبل `AppView`. هذا الفصل (decoupling) يسمح لنا باختبار كل من Widget `App` و`AppView` بسهولة لاحقًا. ::: :::note يُستخدم `RepositoryProvider` لتوفير instance واحدة من `AuthenticationRepository` لكامل التطبيق، وهو ما سيكون مفيدًا لاحقًا. ::: افتراضيًا، `BlocProvider` يكون lazy ولا يستدعي `create` إلا عند أول وصول إلى الـ Bloc. وبما أن `AuthenticationBloc` يجب أن يشترك دائمًا في stream `AuthenticationStatus` فورًا (عبر الحدث `AuthenticationSubscriptionRequested`)، يمكننا تجاوز هذا السلوك صراحةً عن طريق ضبط `lazy: false`. `AppView` هو `StatefulWidget` لأنه يحتفظ بـ `GlobalKey` الذي يُستخدم للوصول إلى حالة الـ `Navigator`. بشكل افتراضي، يقوم `AppView` بعرض `SplashPage` (التي سنراها لاحقًا) ويستخدم `BlocListener` للتنقل بين الصفحات المختلفة بناءً على التغيرات في حالة `AuthenticationState`. ## شاشة البداية ميزة شاشة البداية ستتكون من عرض بسيط يُعرض فور إطلاق التطبيق بينما يحدد التطبيق ما إذا كان المستخدم مصادقًا عليه. ``` lib └── splash ├── splash.dart └── view └── splash_page.dart ``` :::tip `SplashPage` توفّر مسارًا (`Route`) ثابتًا، مما يجعل التنقل إليها سهلًا باستخدام `Navigator.of(context).push(SplashPage.route())`; ::: ## تسجيل الدخول يحتوي مسار تسجيل الدخول على `LoginPage` و `LoginForm` و `LoginBloc`، ويسمح للمستخدمين بإدخال اسم المستخدم وكلمة المرور لتسجيل الدخول إلى التطبيق. ``` ├── lib │ ├── login │ │ ├── bloc │ │ │ ├── login_bloc.dart │ │ │ ├── login_event.dart │ │ │ └── login_state.dart │ │ ├── login.dart │ │ ├── models │ │ │ ├── models.dart │ │ │ ├── password.dart │ │ │ └── username.dart │ │ └── view │ │ ├── login_form.dart │ │ ├── login_page.dart │ │ └── view.dart ``` ### نماذج تسجيل الدخول نستخدم [`package:formz`](https://pub.dev/packages/formz) لإنشاء نماذج قابلة لإعادة الاستخدام وموحدة لـ `username` و`password`. #### اسم المستخدم لأجل البساطة، نحن نتحقق فقط من أن اسم المستخدم ليس فارغًا، ولكن في التطبيق العملي يمكنك فرض قواعد استخدام الأحرف الخاصة، الطول، وغيرها... #### كلمة المرور مرة أخرى، نحن نُجري فحصًا بسيطًا للتأكد من أن كلمة المرور ليست فارغة. #### ملف التجميع للنماذج (Models Barrel) كما في السابق، هناك ملف `models.dart` لتسهيل استيراد نماذج `Username` و `Password` عبر استيراد واحد فقط. ### Login Bloc يقوم `LoginBloc` بإدارة حالة `LoginForm` ويتولى التحقق من صحة إدخالات اسم المستخدم وكلمة المرور بالإضافة إلى حالة النموذج. #### login_event.dart في هذا التطبيق، هناك ثلاثة أنواع مختلفة من `LoginEvent`: - `LoginUsernameChanged`: يخطر الـ bloc بأنه تم تعديل اسم المستخدم. - `LoginPasswordChanged`: يخطر الـ bloc بأنه تم تعديل كلمة المرور. - `LoginSubmitted`: يخطر الـ bloc بأنه تم تقديم النموذج. #### login_state.dart يحتوي الـ `LoginState` على حالة النموذج بالإضافة إلى حالات إدخال اسم المستخدم وكلمة المرور. :::note نماذج `Username` و `Password` تُستخدم كجزء من `LoginState`، والحالة (status) هي أيضًا جزء من [package:formz](https://pub.dev/packages/formz). ::: #### login_bloc.dart يتولى `LoginBloc` التفاعل مع تفاعلات المستخدم داخل الـ `LoginForm` والتعامل مع التحقق من صحة النموذج وتقديمه. يعتمد `LoginBloc` على `AuthenticationRepository` لأنه عند تقديم النموذج يستدعي `logIn`. الحالة الابتدائية للـ bloc هي `pure`، ما يعني أن الحقول والنموذج لم يتم التفاعل معهما بعد. عندما يتغير اسم المستخدم أو كلمة المرور، يقوم الـ bloc بإنشاء نسخة "متسخة" (dirty) من نموذج `Username` أو `Password` ويُحدّث حالة النموذج عبر واجهة برمجة التطبيقات `Formz.validate`. عند إضافة حدث `LoginSubmitted`، إذا كانت حالة النموذج الحالية صالحة، يقوم الـ bloc باستدعاء `logIn` ويُحدّث الحالة بناءً على نتيجة الطلب. بعد ذلك، سنلقي نظرة على `LoginPage` و `LoginForm`. ### صفحة تسجيل الدخول تتولى `LoginPage` مسؤولية توفير الـ `Route` بالإضافة إلى إنشاء وتوفير `LoginBloc` لـ `LoginForm`. :::note يُستخدم `context.read()` للعثور على instance من `AuthenticationRepository` عبر `BuildContext`. ::: ### نموذج تسجيل الدخول يتولى `LoginForm` إخطار `LoginBloc` بأحداث المستخدم ويستجيب أيضًا للتغيرات في الحالة باستخدام `BlocBuilder` و `BlocListener`. يُستخدم `BlocListener` لعرض `SnackBar` في حال فشل تقديم بيانات تسجيل الدخول. بالإضافة إلى ذلك، يُستخدم `context.select` للوصول بكفاءة إلى أجزاء محددة من `LoginState` لكل Widget، مما يمنع عمليات البناء غير الضرورية. تُستخدم دالة `onChanged` لإخطار `LoginBloc` بالتغييرات التي تطرأ على اسم المستخدم أو كلمة المرور. يتم تفعيل Widget `_LoginButton` فقط إذا كانت حالة النموذج صالحة، ويُعرض مؤشر تحميل دائري `CircularProgressIndicator` مكانه أثناء تقديم النموذج. ## الصفحة الرئيسية عند نجاح طلب `logIn`، ستتغير حالة `AuthenticationBloc` إلى `authenticated` وسيتم توجيه المستخدم إلى صفحة `HomePage` حيث نعرض معرف المستخدم (`id`) بالإضافة إلى زر لتسجيل الخروج. ``` ├── lib │ ├── home │ │ ├── home.dart │ │ └── view │ │ └── home_page.dart ``` ### الصفحة الرئيسية يمكن لصفحة `HomePage` الوصول إلى معرف المستخدم الحالي عبر `context.select((AuthenticationBloc bloc) => bloc.state.user.id)` وعرضه باستخدام Widget `Text`. بالإضافة إلى ذلك، عند الضغط على زر تسجيل الخروج يتم إضافة حدث `AuthenticationLogoutPressed` إلى الـ `AuthenticationBloc`. :::note `context.select((AuthenticationBloc bloc) => bloc.state.user.id)` يُحدث تحديثات تلقائية في حال تغيّر معرف المستخدم. ::: في هذه المرحلة لدينا تنفيذ قوي لمسار تسجيل الدخول وقد قمنا بفصل طبقة العرض عن طبقة منطق الأعمال باستخدام Bloc. يمكن العثور على الأكواد البرمجية المصدرية الكاملة لهذا المثال (بما في ذلك اختبارات الوحدة واختبارات Widgets) [هنا](https://github.com/felangel/Bloc/tree/master/examples/flutter_login). ================================================ FILE: docs/src/content/docs/ar/tutorials/flutter-timer.mdx ================================================ --- title: مؤقت Flutter (Flutter Timer) description: دليل متعمّق لبناء تطبيق مؤقّت (Timer) في Flutter باستخدام مكتبة Bloc. sidebar: order: 2 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-timer/FlutterCreateSnippet.astro'; import TimerBlocEmptySnippet from '~/components/tutorials/flutter-timer/TimerBlocEmptySnippet.astro'; import TimerBlocInitialStateSnippet from '~/components/tutorials/flutter-timer/TimerBlocInitialStateSnippet.astro'; import TimerBlocTickerSnippet from '~/components/tutorials/flutter-timer/TimerBlocTickerSnippet.astro'; import TimerBlocOnStartedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnStartedSnippet.astro'; import TimerBlocOnTickedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnTickedSnippet.astro'; import TimerBlocOnPausedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnPausedSnippet.astro'; import TimerBlocOnResumedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnResumedSnippet.astro'; import TimerPageSnippet from '~/components/tutorials/flutter-timer/TimerPageSnippet.astro'; import ActionsSnippet from '~/components/tutorials/flutter-timer/ActionsSnippet.astro'; import BackgroundSnippet from '~/components/tutorials/flutter-timer/BackgroundSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) في هذا الدليل، سنتعلّم كيفية بناء تطبيق مؤقّت باستخدام مكتبة Bloc. يفترض أن يبدو التطبيق النهائي بهذا الشكل: ![demo](~/assets/tutorials/flutter-timer.gif) ## المواضيع الرئيسية - مراقبة تغييرات الحالة باستخدام [BlocObserver](/ar/bloc-concepts#blocobserver). - [BlocProvider](/ar/flutter-bloc-concepts#blocprovider)، وهي Widget في Flutter توفّر Bloc للأبناء. - [BlocBuilder](/ar/flutter-bloc-concepts#blocbuilder)، وهي Widget في Flutter تتولّى إعادة البناء استجابةً للحالات الجديدة. - منع إعادة البناء غير الضرورية باستخدام [Equatable](/ar/faqs/#متى-يجب-استخدام-equatable). - تعلم استخدام `StreamSubscription` داخل Bloc. - منع إعادة البناء غير الضرورية باستخدام `buildWhen`. ## الإعداد سنبدأ بإنشاء مشروع Flutter جديد بالكامل: بعد ذلك، يمكننا استبدال محتويات `pubspec.yaml` بما يلي: :::note سنستخدم حزمتي [flutter_bloc](https://pub.dev/packages/flutter_bloc) و [equatable](https://pub.dev/packages/equatable) في هذا التطبيق. ::: ثم شغّل `flutter pub get` لتثبيت جميع التبعيات (dependencies). ## هيكل المشروع ``` ├── lib | ├── timer │ │ ├── bloc │ │ │ └── timer_bloc.dart | | | └── timer_event.dart | | | └── timer_state.dart │ │ └── view │ │ | ├── timer_page.dart │ │ ├── timer.dart │ ├── app.dart │ ├── ticker.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` ## المؤقت (Ticker) سيكون `Ticker` هو مصدر البيانات لتطبيق المؤقّت. إذ يوفّر stream من النبضات (`ticks`) يمكننا الاشتراك فيه والتفاعل معه. ابدأ بإنشاء الملف `ticker.dart`. كل ما يفعله class `Ticker` هو توفير الدالة `tick` التي تستقبل عدد النبضات (الثواني) المطلوب، ثم ترجع stream يصدر الثواني المتبقية كل ثانية. بعد ذلك، نحتاج إلى إنشاء `TimerBloc` الذي سيستهلك `Ticker`. ## Timer Bloc ### حالة المؤقت (TimerState) سنبدأ بتعريف `TimerStates` التي يمكن أن تكون عليها `TimerBloc`. يمكن أن تكون حالة `TimerBloc` الخاصة بنا واحدة مما يلي: - `TimerInitial`: جاهز لبدء العد التنازلي من المدة المحددة. - `TimerRunInProgress`: يعد تنازليًا بنشاط من المدة المحددة. - `TimerRunPause`: متوقف مؤقتًا عند مدة متبقية معينة. - `TimerRunComplete`: اكتمل بمدة متبقية 0. كل حالة من هذه الحالات تؤثر على واجهة المستخدم والإجراءات المتاحة للمستخدم. على سبيل المثال: - إذا كانت الحالة هي `TimerInitial`، فسيتمكن المستخدم من بدء المؤقت. - إذا كانت الحالة هي `TimerRunInProgress`، فسيتمكن المستخدم من إيقاف المؤقت مؤقتًا وإعادة تعيينه، بالإضافة إلى رؤية المدة المتبقية. - إذا كانت الحالة هي `TimerRunPause`، فسيتمكن المستخدم من استئناف المؤقت وإعادة تعيينه. - إذا كانت الحالة هي `TimerRunComplete`، فسيتمكن المستخدم من إعادة تعيين المؤقت. للحفاظ على جميع ملفات bloc معًا، لننشئ مجلد `bloc` ونضيف `bloc/timer_state.dart`. :::tip يمكنك استخدام إضافات [IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc-code-generator) أو [VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) لإنشاء ملفات bloc التالية تلقائيًا. ::: لاحظ أن جميع `TimerStates` ترث من abstract base class `TimerState` الذي يحتوي على الخاصية `duration`. السبب هو أننا نريد معرفة الوقت المتبقي مهما كانت حالة `TimerBloc`. بالإضافة إلى ذلك، يرث `TimerState` من `Equatable` لتحسين الأكواد البرمجية ومنع إعادة البناء عندما تتكرر الحالة نفسها. بعد ذلك، لنحدّد وننفّذ `TimerEvents` التي سيعالجها `TimerBloc`. ### حدث المؤقت (TimerEvent) يحتاج `TimerBloc` إلى معرفة كيفية معالجة الأحداث التالية: - `TimerStarted`: يُعلم `TimerBloc` بضرورة بدء المؤقت. - `TimerPaused`: يُعلم `TimerBloc` بضرورة إيقاف المؤقت مؤقتًا. - `TimerResumed`: يُعلم `TimerBloc` بضرورة استئناف المؤقت. - `TimerReset`: يُعلم `TimerBloc` بضرورة إعادة تعيين المؤقت إلى حالته الأصلية. - `_TimerTicked`: يُعلم `TimerBloc` بحدوث نبضة (`tick`) وبضرورة تحديث حالته وفقًا لذلك. إذا لم تستخدم إضافات [IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc-code-generator) أو [VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc)، فأنشئ `bloc/timer_event.dart` ونفّذ هذه الأحداث. والآن لننفّذ `TimerBloc`. ### TimerBloc إذا لم تكن قد فعلت ذلك بعد، فأنشئ `bloc/timer_bloc.dart` وأنشئ `TimerBloc` فارغًا. أول خطوة هي تحديد الحالة الأولية لـ `TimerBloc`. هنا نريد أن يبدأ `TimerBloc` في حالة `TimerInitial` بمدة مضبوطة مسبقًا تبلغ دقيقة واحدة (60 ثانية). بعد ذلك، نحتاج إلى تحديد dependency على `Ticker`. كما نعرّف `StreamSubscription` لـ `Ticker` وسنعود إليها بعد قليل. في هذه المرحلة، المتبقي هو تنفيذ event handlers. ولتحسين قابلية القراءة، نفصل كل معالج في helper function مستقلة. سنبدأ بحدث `TimerStarted`. إذا استقبل `TimerBloc` حدث `TimerStarted`، فإنه يصدر حالة `TimerRunInProgress` بمدة البداية. وإذا كانت `_tickerSubscription` مفتوحة مسبقًا، فنحتاج إلى إلغائها لتحرير الذاكرة. كما نحتاج إلى عمل override للدالة `close` في `TimerBloc` حتى نلغي `_tickerSubscription` عند إغلاق الـ bloc. أخيرًا، نستمع إلى stream `_ticker.tick`، ومع كل نبضة نضيف حدث `_TimerTicked` بالمدة المتبقية. بعد ذلك، دعنا ننفذ معالج حدث `_TimerTicked`. في كل مرة نستقبل فيها حدث `_TimerTicked`، إذا كانت مدة النبضة أكبر من 0 فنحن بحاجة إلى إصدار حالة `TimerRunInProgress` محدثة بالمدة الجديدة. أما إذا كانت مدة النبضة تساوي 0 فقد انتهى المؤقّت ونحتاج إلى إصدار حالة `TimerRunComplete`. الآن دعنا ننفذ معالج حدث `TimerPaused`. في `_onPaused`، إذا كانت `state` الخاصة بـ `TimerBloc` هي `TimerRunInProgress` فيمكننا إيقاف `_tickerSubscription` مؤقتًا وإصدار حالة `TimerRunPause` بالمدة الحالية. بعد ذلك، لننفذ معالج حدث `TimerResumed` حتى نستأنف المؤقّت. معالج حدث `TimerResumed` مشابه جدًا لمعالج حدث `TimerPaused`. إذا كانت حالة `TimerBloc` من نوع `TimerRunPause` ووصل حدث `TimerResumed`، فإنه يستأنف `_tickerSubscription` ويصدر حالة `TimerRunInProgress` بالمدة الحالية. أخيرًا، نحتاج إلى تنفيذ معالج حدث `TimerReset`. إذا استقبل `TimerBloc` حدث `TimerReset`، فإنه يحتاج إلى إلغاء `_tickerSubscription` الحالية حتى لا يتلقى أي نبضات إضافية، ثم يصدر حالة `TimerInitial` بالمدة الأصلية. هذا كل ما يخص `TimerBloc`. والمتبقي الآن هو تنفيذ واجهة المستخدم (UI) للتطبيق. ## واجهة مستخدم التطبيق (Application UI) ### MyApp يمكننا البدء بحذف محتويات `main.dart` واستبدالها بما يلي. بعد ذلك، لننشئ Widget التطبيق في `app.dart`، والتي ستكون جذر التطبيق. بعد ذلك، نحتاج إلى تنفيذ Widget `Timer`. ### المؤقت (Timer) Widget `Timer` في (`lib/timer/view/timer_page.dart`) مسؤولة عن عرض الوقت المتبقي مع الأزرار المناسبة التي تمكّن المستخدم من بدء المؤقّت وإيقافه مؤقتًا وإعادة تعيينه. حتى الآن، نستخدم `BlocProvider` فقط للوصول إلى instance من `TimerBloc`. بعد ذلك، سننفّذ Widget `Actions` والتي ستحتوي على الإجراءات المناسبة (بدء، إيقاف مؤقت، وإعادة تعيين). ### ملف التجميع (Barrel) لتنظيم عمليات الاستيراد من قسم `Timer`، نحتاج إلى إنشاء ملف تجميعي (`barrel file`) باسم `timer/timer.dart`. ### الإجراءات (Actions) Widget `Actions` هي `StatelessWidget` تستخدم `BlocBuilder` لإعادة بناء واجهة المستخدم كلما حصلنا على `TimerState` جديدة. تستخدم `Actions` الدالة `context.read()` للوصول إلى instance من `TimerBloc`، وتُرجع أزرار `FloatingActionButton` مختلفة حسب الحالة الحالية لـ `TimerBloc`. كل زر من أزرار `FloatingActionButton` يضيف event داخل callback `onPressed` لإخطار `TimerBloc`. إذا أردت تحكمًا أدق في توقيت استدعاء `builder`، يمكنك تمرير `buildWhen` اختياريًا إلى `BlocBuilder`. تستقبل `buildWhen` الحالة السابقة والحالة الحالية للـ bloc وتعيد قيمة منطقية (`boolean`). إذا أعادت `true` فسيُستدعى `builder` بالحالة وتحدث إعادة البناء. وإذا أعادت `false` فلن يُستدعى `builder` ولن تحدث إعادة بناء. في هذه الحالة، لا نريد إعادة بناء Widget `Actions` في كل نبضة لأن ذلك غير فعّال. بدلًا من ذلك، نريد إعادة بناء `Actions` فقط إذا تغيّر `runtimeType` لـ `TimerState` (على سبيل المثال: TimerInitial => TimerRunInProgress، TimerRunInProgress => TimerRunPause، إلخ...). نتيجةً لذلك، إذا قمنا بتلوين الـ Widgets عشوائيًا عند كل إعادة بناء، فسيبدو الأمر كما يلي: ![BlocBuilder buildWhen demo](https://cdn-images-1.medium.com/max/1600/1*YyjpH1rcZlYWxCX308l_Ew.gif) :::note على الرغم من أن Widget `Text` تُعاد بناؤها في كل نبضة، فإننا نعيد بناء `Actions` فقط إذا كانت بحاجة إلى إعادة بناء. ::: ### الخلفية (Background) أخيرًا، أضف Widget الخلفية كما يلي: ### تجميع كل شيء معًا هذا كل ما في الأمر! في هذه المرحلة أصبح لدينا تطبيق مؤقّت جيد يعيد بناء Widgets التي تحتاج فقط إلى إعادة البناء بكفاءة. يمكن العثور على المصدر الكامل لهذا المثال [هنا](https://github.com/felangel/Bloc/tree/master/examples/flutter_timer). ================================================ FILE: docs/src/content/docs/ar/tutorials/flutter-todos.mdx ================================================ --- title: Flutter Todos description: دليل متعمّق لبناء تطبيق مهام (Todos) في Flutter باستخدام مكتبة Bloc. sidebar: order: 6 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-todos/FlutterCreateSnippet.astro'; import ActivateVeryGoodCLISnippet from '~/components/tutorials/flutter-todos/ActivateVeryGoodCLISnippet.astro'; import FlutterCreatePackagesSnippet from '~/components/tutorials/flutter-todos/FlutterCreatePackagesSnippet.astro'; import ProjectStructureSnippet from '~/components/tutorials/flutter-todos/ProjectStructureSnippet.astro'; import VeryGoodPackagesGetSnippet from '~/components/tutorials/flutter-todos/VeryGoodPackagesGetSnippet.astro'; import HomePageTreeSnippet from '~/components/tutorials/flutter-todos/HomePageTreeSnippet.astro'; import TodosOverviewPageTreeSnippet from '~/components/tutorials/flutter-todos/TodosOverviewPageTreeSnippet.astro'; import StatsPageTreeSnippet from '~/components/tutorials/flutter-todos/StatsPageTreeSnippet.astro'; import EditTodosPageTreeSnippet from '~/components/tutorials/flutter-todos/EditTodosPageTreeSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) في هذا الدليل التعليمي، سنبني تطبيق مهام (Todos) في Flutter باستخدام مكتبة Bloc. ![demo](~/assets/tutorials/flutter-todos.gif) ## المواضيع الرئيسية - [Bloc و Cubit](/ar/bloc-concepts/#cubit-مقابل-bloc) لإدارة حالات الميزات المختلفة. - [الهيكلية الطبقية](/ar/architecture) لفصل الاهتمامات/المسؤوليات وتسهيل إعادة الاستخدام. - [BlocObserver](/ar/bloc-concepts#blocobserver) لمراقبة تغييرات الحالة. - [BlocProvider](/ar/flutter-bloc-concepts#blocprovider)، وهي Widget في Flutter توفر Bloc للأبناء. - [BlocBuilder](/ar/flutter-bloc-concepts#blocbuilder)، وهي Widget في Flutter تتولى إعادة البناء استجابةً للحالات الجديدة. - [BlocListener](/ar/flutter-bloc-concepts#bloclistener)، وهي Widget في Flutter تنفذ تأثيرات جانبية استجابةً لتغيرات الحالة. - [RepositoryProvider](/ar/flutter-bloc-concepts#repositoryprovider)، وهي Widget في Flutter توفر مستودع (repository) للأبناء. - [Equatable](/ar/faqs/#متى-يجب-استخدام-equatable) لمنع عمليات إعادة البناء غير الضرورية. - [MultiBlocListener](/ar/flutter-bloc-concepts#multibloclistener)، وهي Widget في Flutter تقلل التعشيش عند استخدام عدة BlocListeners. ## الإعداد سنبدأ بإنشاء مشروع Flutter جديد كليًا باستخدام [very_good_cli](https://pub.dev/packages/very_good_cli). :::note قم بتثبيت `very_good_cli` باستخدام الأمر التالي: ::: بعد ذلك، سنقوم بإنشاء الحزم `todos_api`، `local_storage_todos_api`، و `todos_repository` باستخدام `very_good_cli`: بإمكاننا بعد ذلك استبدال محتويات ملف `pubspec.yaml` بـ: وأخيرًا، ثبّت جميع التبعيات (dependencies): ## هيكلية المشروع يجب أن تبدو هيكلية مشروع التطبيق لدينا على النحو التالي: نقسم المشروع إلى عدة حزم للحفاظ على تبعيات واضحة وصريحة لكل حزمة، مع حدود واضحة تفرض [مبدأ المسؤولية الواحدة](https://en.wikipedia.org/wiki/Single-responsibility_principle). تؤدي هذه الطريقة في تنظيم المشروع إلى فوائد عديدة تشمل، ولكن لا تقتصر على: - سهولة إعادة استخدام الحزم عبر مشاريع متعددة - تحسينات في الـ CI/CD من حيث الكفاءة (تشغيل الفحوصات فقط على الكود الذي تم تغييره) - سهولة صيانة الحزم بمعزل عن بعضها مع مجموعات اختبار مخصصة، وإصدار نسخ دلالية، ودورة/إيقاع إصدار واضحة ## البنية المعمارية ![مخطط بنية تطبيق المهام](~/assets/tutorials/todos-architecture.png) تنظيم الكود على شكل طبقات أمر بالغ الأهمية ويساعدنا على تطوير التطبيق بسرعة وثقة. كل طبقة تتحمل مسؤولية واحدة فقط، ويمكن استخدامها واختبارها بشكل مستقل. هذا يسمح لنا بحصر التعديلات داخل طبقة معينة لتقليل تأثيرها على كامل التطبيق. بالإضافة إلى ذلك، فصل الطبقات في التطبيق يسمح لنا بإعادة استخدام المكتبات بسهولة عبر مشاريع متعددة، خاصة فيما يتعلق بطبقة البيانات. يتكون تطبيقنا من ثلاث طبقات رئيسية: - طبقة البيانات - طبقة المجال (Domain) - طبقة المميزات - طبقة العرض/UI (widgets) - منطق الأعمال (blocs/cubits) **طبقة البيانات** هذه الطبقة هي الأدنى في الهيكل، وهي مسؤولة عن جلب البيانات الخام من مصادر خارجية مثل قواعد البيانات، واجهات برمجة التطبيقات (APIs)، وغيرها. الحزم (packages) في طبقة البيانات عادة لا تعتمد على أي جزء من طبقة العرض، ويمكن إعادة استخدامها أو حتى نشرها على [pub.dev](https://pub.dev) كحزمة مستقلة. في هذا المثال، تتضمن طبقة البيانات لدينا حزمتَي `todos_api` و `local_storage_todos_api`. **طبقة المجال** تجمع هذه الطبقة بين مزود بيانات أو أكثر وتطبق قواعد الأعمال على البيانات. كل مكون في هذه الطبقة يسمى repository وتدير كل repository مجالًا واحدًا عادةً. الحزم في طبقة الـ repository ينبغي أن تتعامل فقط مع طبقة البيانات. في هذا المثال، تتكون طبقة الـ repository لدينا من حزمة `todos_repository`. **طبقة المميزات** تحتوي هذه الطبقة على جميع الميزات وحالات الاستخدام الخاصة بالتطبيق. تتضمن كل ميزة عادةً بعض عناصر الواجهة (UI) ومنطق الأعمال. يجب أن تكون الميزات مستقلة عن بعضها البعض لتسهيل إضافتها أو إزالتها دون التأثير على بقية قاعدة الكود. ضمن كل ميزة، يتم إدارة حالة الميزة ومنطق الأعمال بواسطة blocs. تتفاعل الـ blocs مع صفر أو أكثر من المستودعات (repositories). تستجيب الـ blocs للأحداث (events) وتصدر الحالات (states) التي تؤدي إلى تغيير واجهة المستخدم. الأدوات (widgets) في كل ميزة تعتمد عادةً على الـ bloc المناظر وتقوم بعرض واجهة المستخدم بناءً على الحالة الحالية. يمكن لواجهة المستخدم إعلام الـ bloc بإدخالات المستخدم عبر الأحداث. في هذا المثال، يتكون تطبيقنا من الميزات التالية: `home`، `todos_overview`، `stats`، و `edit_todos`. الآن بعد أن استعرضنا الطبقات على مستوى عالٍ، لنبدأ ببناء تطبيقنا بدءًا من طبقة البيانات! ## طبقة البيانات طبقة البيانات هي الطبقة الأدنى في تطبيقنا وتتكون من موفري البيانات الخام. الحزم الموجودة في هذه الطبقة تُعنى أساساً بمصدر البيانات وكيفية الحصول عليها. في حالتنا، ستتكون طبقة البيانات من `TodosApi`، وهو واجهة (interface)، و`LocalStorageTodosApi`، وهو تنفيذ لـ `TodosApi` يعتمد على `shared_preferences`. ### TodosApi حزمة `todos_api` ستصدر واجهة عامة للتفاعل مع وإدارة المهام (todos). لاحقًا سنقوم بتنفيذ `TodosApi` باستخدام `shared_preferences`. وجود تجريد (abstraction) يجعل من السهل دعم تنفيذات أخرى دون الحاجة لتغيير أي جزء آخر من التطبيق. على سبيل المثال، يمكننا لاحقًا إضافة `FirestoreTodosApi` التي تستخدم `cloud_firestore` بدلاً من `shared_preferences` مع تغييرات برمجية طفيفة في باقي التطبيق. #### نموذج Todo بعد ذلك سنُعرّف نموذج `Todo`. أول ما يجب ملاحظته هو أن نموذج `Todo` لا يعيش داخل تطبيقنا مباشرة — بل هو جزء من حزمة `todos_api`. السبب في ذلك هو أن `TodosApi` تُعرّف واجهات برمجية تُرجع أو تستقبل كائنات `Todo`. النموذج هو تمثيل دُرتي (Dart) للكائن الخام `Todo` الذي سيتم تخزينه واسترجاعه. يستخدم نموذج `Todo` مكتبة [json_serializable](https://pub.dev/packages/json_serializable) لمعالجة السيريالايزيشن (serialization) وفكه (deserialization) من وإلى JSON. إذا كنت تتبع هذا الشرح، سيتوجب عليك تشغيل [خطوة توليد الأكواد](https://pub.dev/packages/json_serializable#running-the-code-generator) لحل أخطاء المترجم. يوفر `json_map.dart` تعريف نوع (typedef) للمساعدة في مراجعة الأكواد والتنقيح (linting). نموذج `Todo` مُعرّف في `todos_api/models/todo.dart` ويتم تصديره عبر `package:todos_api/todos_api.dart`. #### تحديث التصديرات نموذج `Todo` و `TodosApi` يتم تصديرهما عبر ملفات البرميل (barrel files). لاحظ كيف أننا لا نستورد النموذج مباشرة، بل نستوردها في `lib/src/todos_api.dart` مع إشارة إلى ملف البرميل الخاص بالحزمة: `import 'package:todos_api/todos_api.dart';`. حدّث ملفات البرميل هذه لحل أي أخطاء استيراد متبقية: #### Streams مقابل Futures في نسخة سابقة من هذا الدرس، كانت `TodosApi` تعتمد على `Future` وليست على `Stream`. كمثال على API مبني على `Future`، راجع [تنفيذ براين إيجان في عينات عمارة التطبيقات](https://github.com/brianegan/flutter_architecture_samples/tree/master/todos_repository_core). يمكن أن يتكون التنفيذ القائم على `Future` من طريقين: `loadTodos` و`saveTodos` (لاحظ الجمع). هذا يعني أنه يجب توفير قائمة كاملة من المهام في كل مرة يتم فيها استدعاء الطريقة. - إحدى القيود في هذا الأسلوب هي أن عمليات CRUD القياسية (إنشاء، قراءة، تحديث، حذف) تتطلب إرسال القائمة الكاملة للمهام مع كل استدعاء. على سبيل المثال، في شاشة إضافة مهمة، لا يمكننا فقط إرسال المهمة المضافة، بل يجب تتبع القائمة الكاملة وإرسال القائمة الجديدة كاملة عند حفظ التغييرات. - قيد آخر هو أن `loadTodos` تقدم البيانات لمرة واحدة فقط. لذلك يجب أن يحتوي التطبيق على منطق لطلب التحديثات بشكل دوري. في التنفيذ الحالي، تتيح `TodosApi` دفق `Stream>` عبر `getTodos()` والذي يقوم بإبلاغ جميع المشتركين بالتحديثات في الوقت الحقيقي عند حدوث تغييرات في قائمة المهام. بالإضافة إلى ذلك، يمكن إنشاء المهام وحذفها وتحديثها بشكل منفرد. على سبيل المثال، يتم حذف المهمة أو حفظها بتمرير المهمة وحدها فقط كوسيط. لا يلزم توفير القائمة الكاملة المحدثة في كل مرة. ### LocalStorageTodosApi تقوم هذه الحزمة بتنفيذ `todos_api` باستخدام حزمة [`shared_preferences`](https://pub.dev/packages/shared_preferences). ## طبقة المستودع (Repository Layer) يُعدّ [المستودع](/ar/architecture/#المستودع-repository) جزءًا من طبقة منطق العمل/الأعمال. يعتمد المستودع على واحد أو أكثر من مزودي البيانات الذين لا يمتلكون قيمة منطقية للأعمال، ويجمع واجهات برمجة التطبيقات (APIs) الخاصة بهم في واجهات توفر قيمة أعمال. بالإضافة إلى ذلك، يساعد وجود طبقة المستودع في تجريد عملية الحصول على البيانات عن بقية التطبيق، مما يتيح لنا تغيير مكان أو طريقة تخزين البيانات دون التأثير على باقي أجزاء التطبيق. ### TodosRepository يتطلب إنشاء نسخة من المستودع تحديد `TodosApi`، الذي ناقشناه سابقًا في هذا الدليل، لذا قمنا بإضافته كاعتماد في ملف `pubspec.yaml` الخاص بنا: #### تصدير المكتبة بالإضافة إلى تصدير فصل `TodosRepository`، نقوم أيضًا بتصدير نموذج `Todo` من حزمة `todos_api`. تساهم هذه الخطوة في منع الربط الوثيق بين التطبيق ومزودي البيانات. قررنا إعادة تصدير نفس نموذج `Todo` من `todos_api` بدلاً من تعريف نموذج منفصل في `todos_repository`، لأننا في هذه الحالة نمتلك السيطرة الكاملة على نموذج البيانات. في العديد من الحالات، لن يكون مزود البيانات شيئًا يمكنك التحكم فيه. في هذه الحالات، يصبح من الضروري بشكل متزايد الحفاظ على تعريفات النماذج الخاصة بك داخل طبقة المستودع للحفاظ على سيطرة كاملة على الواجهة وعقد الـ API. ## طبقة الخاصية (Feature Layer) ### نقطة الدخول (Entrypoint) نقطة الدخول لتطبيقنا هي `main.dart`. في هذه الحالة، هناك ثلاث نسخ: أبرز ما يميز هذه النسخ هو أن التنفيذ الملموس لـ `local_storage_todos_api` يتم إنشاؤه داخل كل نقطة دخول. ### البوتستراب (Bootstrapping) `bootstrap.dart` يقوم بتحميل مراقب الـ `BlocObserver` الخاص بنا وينشئ نسخة من `TodosRepository`. ### التطبيق (App) `App` يغلف ويدير Widget `RepositoryProvider` الذي يوفر الـ repository لجميع الأبناء. بما أن شجرتي الـ widgets الخاصة بصفحة `EditTodoPage` وصفحة `HomePage` هي منحدرات لـ `App`، فيمكن لجميع الـ blocs والـ cubits الوصول إلى الـ repository. `AppView` يقوم بإنشاء تطبيق `MaterialApp` ويضبط الثيم والتعريب (الترجمة). ### الثيم (Theme) يوفر هذا تعريف الثيم للوضع الفاتح والغامق. ### الصفحة الرئيسية (Home) الميزة الرئيسية مسؤولة عن إدارة حالة التبويب المختار حالياً وعرض الشجرة الفرعية المناسبة. #### HomeState هناك حالتان فقط مرتبطتان بالشاشتين: `todos` و `stats`. :::note `EditTodo` هي مسار منفصل لذلك ليست جزءًا من `HomeState`. ::: #### HomeCubit الـ cubit مناسب هنا بسبب بساطة منطق الأعمال. لدينا دالة واحدة `setTab` لتغيير التبويب. #### HomeView الملف `view.dart` هو ملف تجميعي (barrel file) يصدر كل مكونات واجهة المستخدم المتعلقة بميزة الصفحة الرئيسية. الملف `home_page.dart` يحتوي على واجهة المستخدم للصفحة الجذرية التي يراها المستخدم عند إطلاق التطبيق. تمثيل مبسط لشجرة Widget الخاصة بـ `HomePage` هو: تقدم `HomePage` نسخة من `HomeCubit` إلى `HomeView`. يستخدم `HomeView` الدالة `context.select` لإعادة البناء بشكل انتقائي كلما تغير التبويب. هذا يسمح لنا باختبار Widget `HomeView` بسهولة عن طريق توفير نسخة Mock من `HomeCubit` وتمثيل الحالة (stubbing state). شريط التطبيق السفلي (`BottomAppBar`) يحتوي على أزرار `HomeTabButton` التي تستدعي `setTab` على `HomeCubit`. يتم الحصول على نسخة الـ cubit عبر `context.read` ويتم استدعاء الدالة المناسبة على هذه النسخة. :::caution `context.read` لا يستمع للتغييرات، يستخدم فقط للوصول إلى `HomeCubit` واستدعاء `setTab`. ::: ### ملخص المهام (TodosOverview) ميزة ملخص المهام تتيح للمستخدمين إدارة مهامهم من خلال الإنشاء، التعديل، الحذف، والتصفية. #### أحداث TodosOverviewEvent لننشئ الملف `todos_overview/bloc/todos_overview_event.dart` ونعرف فيه الأحداث. - `TodosOverviewSubscriptionRequested`: هذا هو الحدث الذي يُطلق عند بدء التشغيل. عند استقباله، يقوم الـ bloc بالاشتراك في تيار المهام من `TodosRepository`. - `TodosOverviewTodoDeleted`: هذا الحدث يحذف مهمة. - `TodosOverviewTodoCompletionToggled`: يقوم بتبديل حالة إكمال المهمة. - `TodosOverviewToggleAllRequested`: يبدل حالة إكمال كل المهام. - `TodosOverviewClearCompletedRequested`: يحذف كل المهام المكتملة. - `TodosOverviewUndoDeletionRequested`: يتراجع عن حذف مهمة، على سبيل المثال في حالة الحذف بالخطأ. - `TodosOverviewFilterChanged`: يأخذ مرشح العرض `TodosViewFilter` كوسيط ويغير العرض بتطبيق الفلتر. #### الحالة TodosOverviewState لننشئ الملف `todos_overview/bloc/todos_overview_state.dart` ونعرف فيه الحالة. `TodosOverviewState` يحتفظ بقائمة المهام، المرشح النشط، المهمة المحذوفة الأخيرة (`lastDeletedTodo`)، والحالة العامة. :::note إلى جانب الـ getters والـ setters الافتراضية، لدينا getter مخصص يسمى `filteredTodos`. يستخدم واجهة المستخدم `BlocBuilder` للوصول إما إلى `state.filteredTodos` أو `state.todos`. ::: #### الـ Bloc - TodosOverviewBloc لننشئ الملف `todos_overview/bloc/todos_overview_bloc.dart`. :::note الـ bloc لا ينشئ نسخة من `TodosRepository` داخليًا. بدلاً من ذلك، يعتمد على حقن نسخة الـ repository عبر المُنشئ (constructor). ::: ##### عند طلب الاشتراك (onSubscriptionRequested) عند إضافة حدث `TodosOverviewSubscriptionRequested`، يبدأ الـ bloc بإصدار حالة `loading`. في المقابل، يمكن لواجهة المستخدم عرض مؤشر تحميل. بعد ذلك، نستخدم `emit.forEach>( ... )` التي تنشئ اشتراكًا على تيار المهام من `TodosRepository`. :::caution `emit.forEach()` ليست نفسها `forEach()` المستخدمة في القوائم. هذه الدالة تسمح للـ bloc بالاشتراك في `Stream` وإصدار حالة جديدة مع كل تحديث. ::: :::note `stream.listen` لا يتم استدعاؤه مباشرةً في هذا الدليل. استخدام `await emit.forEach()` هو نمط أحدث للاشتراك في تيارات يسمح للـ bloc بإدارة الاشتراك داخليًا. ::: بمجرد التعامل مع الاشتراك، سيتم التعامل مع الأحداث الأخرى مثل إضافة، تعديل، وحذف المهام. ##### عند حفظ المهمة (onTodoSaved) `_onTodoSaved` ببساطة يستدعي `_todosRepository.saveTodo(event.todo)`. :::note `emit` لا يتم استدعاؤه من داخل `onTodoSaved` والعديد من معالجات الأحداث الأخرى. بدلاً من ذلك، تقوم هذه المعالجات بإعلام الـ repository الذي يصدر قائمة محدثة عبر تيار المهام. راجع قسم [تدفق البيانات](#تدفق-البيانات-data-flow) لمزيد من التفاصيل. ::: ##### التراجع (Undo) ميزة التراجع تتيح للمستخدمين استعادة آخر عنصر تم حذفه. `_onTodoDeleted` يقوم بشيئين: أولاً، يصدر حالة جديدة مع المهمة التي سيتم حذفها. ثم يحذف المهمة عن طريق استدعاء الـ repository. `_onUndoDeletionRequested` يُنفذ عند استقبال حدث طلب التراجع من واجهة المستخدم. يقوم `_onUndoDeletionRequested` بما يلي: - يحفظ مؤقتًا نسخة من المهمة المحذوفة الأخيرة. - يحدث الحالة بإزالة `lastDeletedTodo`. - يعكس عملية الحذف. ##### التصفية (Filtering) `_onFilterChanged` يصدر حالة جديدة مع الفلتر الجديد. #### النماذج (Models) يوجد ملف نموذج واحد يتعامل مع تصفية العرض. `todos_view_filter.dart` هو enum يمثل مرشحات العرض الثلاثة بالإضافة إلى الدوال لتطبيق الفلتر. `models.dart` هو ملف تجميعي لإعادة التصدير. بعد ذلك، لنلقي نظرة على `TodosOverviewPage`. #### صفحة ملخص المهام TodosOverviewPage تمثيل مبسط لشجرة Widget الخاصة بـ `TodosOverviewPage` هو: تمامًا مثل ميزة الصفحة الرئيسية، تقوم `TodosOverviewPage` بتوفير نسخة من `TodosOverviewBloc` للشجرة الفرعية عبر `BlocProvider`، مما يقتصر نطاق هذا الـ bloc على Widgets الموجودة تحت `TodosOverviewPage` فقط. هناك ثلاث ويدجتات تستمع للتغييرات في `TodosOverviewBloc`. 1. الأولى هي `BlocListener` يستمع للأخطاء. الدالة `listener` تُستدعى فقط حين يعيد `listenWhen` القيمة `true`. إذا كانت الحالة `TodosOverviewStatus.failure`، يتم عرض `SnackBar`. 2. لدينا أيضًا `BlocListener` آخر يستمع لعمليات الحذف. عند حذف مهمة، يعرض `SnackBar` مع زر التراجع. إذا ضغط المستخدم على التراجع، يتم إضافة حدث `TodosOverviewUndoDeletionRequested` إلى الـ bloc. 3. وأخيرًا نستخدم `BlocBuilder` لبناء قائمة تظهر المهام. شريط التطبيق (`AppBar`) يحتوي على عمليتين كرُفع قوائم (dropdowns) للتصفية والتعديل على المهام. :::note الأحداث `TodosOverviewTodoCompletionToggled` و `TodosOverviewTodoDeleted` تمت إضافتها إلى الـ bloc باستخدام `context.read`. ::: الملف `view.dart` هو ملف تجميعي يصدر `todos_overview_page.dart`. #### Widgets `widgets.dart` هو ملف تجميعي آخر يصدر جميع المكونات المستخدمة ضمن ميزة `todos_overview`. `todo_list_tile.dart` هو `ListTile` لكل عنصر مهمة. `todos_overview_options_button.dart` يعرض خيارين للتحكم بالمهام: - `toggleAll` - `clearCompleted` `todos_overview_filter_button.dart` يعرض ثلاثة خيارات للتصفية: - `all` - `activeOnly` - `completedOnly` ### الإحصائيات (Stats) ميزة الإحصائيات تعرض معلومات عن المهام النشطة والمكتملة. #### StatsState `StatsState` يحتفظ بمعلومات ملخصة وحالة `StatsStatus` الحالية. #### StatsEvent لدى `StatsEvent` حدث وحيد هو `StatsSubscriptionRequested`: #### StatsBloc `StatsBloc` يعتمد على `TodosRepository` كما هو الحال مع `TodosOverviewBloc`. يشترك في تيار المهام عبر `_todosRepository.getTodos`. #### واجهة الإحصائيات (Stats View) `view.dart` هو الملف التجميعي الخاص بصفحة الإحصائيات `stats_page`. `stats_page.dart` يحتوي على واجهة المستخدم للصفحة التي تعرض إحصاءات المهام. تمثيل مبسط لشجرة Widget الخاصة بـ `StatsPage` هو: :::caution كل من `TodosOverviewBloc` و `StatsBloc` يتواصلان مع `TodosRepository`, لكن من المهم ملاحظة أنه لا يوجد تواصل مباشر بين الـ blocs. راجع قسم [تدفق البيانات](#تدفق-البيانات-data-flow) لمزيد من التفاصيل. ::: ### تعديل المهمة (EditTodo) ميزة `EditTodo` تتيح للمستخدمين تعديل مهمة موجودة وحفظ التغييرات. #### EditTodoState `EditTodoState` يحتفظ بالمعلومات اللازمة أثناء تعديل مهمة. #### أحداث EditTodoEvent الأحداث التي يستجيب لها الـ bloc هي: - `EditTodoTitleChanged` - `EditTodoDescriptionChanged` - `EditTodoSubmitted` #### EditTodoBloc `EditTodoBloc` يعتمد على `TodosRepository` مثل `TodosOverviewBloc` و `StatsBloc`. :::caution على عكس الـ blocs الأخرى، `EditTodoBloc` لا يشترك في `_todosRepository.getTodos`. إنه bloc "للكتابة فقط" يعني أنه لا يحتاج لقراءة المعلومات من الـ repository. ::: ##### تدفق البيانات (Data Flow) رغم وجود العديد من الميزات التي تعتمد على نفس قائمة المهام، لا يوجد تواصل مباشر بين الـ blocs. بدلاً من ذلك، كل ميزة مستقلة وتعتمد على `TodosRepository` للاستماع للتغييرات في قائمة المهام، بالإضافة لأداء التحديثات عليها. على سبيل المثال، ميزة `EditTodos` لا تعرف شيئًا عن ميزات `TodosOverview` أو `Stats`. عند إرسال واجهة المستخدم حدث `EditTodoSubmitted`: - `EditTodoBloc` يتولى منطق الأعمال بتحديث الـ `TodosRepository`. - `TodosRepository` يُبلغ كل من `TodosOverviewBloc` و `StatsBloc`. - `TodosOverviewBloc` و `StatsBloc` يُبلغان الواجهة التي تقوم بالتحديث وفقًا للحالة الجديدة. #### صفحة تعديل المهمة (EditTodoPage) تمامًا مثل الميزات السابقة، توفر `EditTodosPage` نسخة من `EditTodosBloc` عبر `BlocProvider`. على عكس الميزات الأخرى، صفحة `EditTodosPage` هي مسار منفصل ولهذا توفر طريقة `static` اسمها `route`. هذا يسهل دفع الصفحة على مكدس التنقل عبر `Navigator.of(context).push(...)`. تمثيل مبسط لشجرة Widget الخاصة بـ `EditTodosPage` هو: ## الملخص هذا كل شيء، لقد أكملنا الدرس التعليمي! 🎉 يمكنك العثور على الأكواد البرمجية المصدرية الكاملة لهذا المثال، بما في ذلك اختبارات الوحدة واختبارات Widgets، [هنا](https://github.com/felangel/bloc/tree/master/examples/flutter_todos). ================================================ FILE: docs/src/content/docs/ar/tutorials/flutter-weather.mdx ================================================ --- title: تطبيق الطقس Flutter description: دليل متعمّق لبناء تطبيق طقس في Flutter باستخدام مكتبة Bloc. sidebar: order: 5 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-weather/FlutterCreateSnippet.astro'; import FeatureTreeSnippet from '~/components/tutorials/flutter-weather/FeatureTreeSnippet.astro'; import FlutterCreateApiClientSnippet from '~/components/tutorials/flutter-weather/FlutterCreateApiClientSnippet.astro'; import OpenMeteoModelsTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoModelsTreeSnippet.astro'; import LocationJsonSnippet from '~/components/tutorials/flutter-weather/LocationJsonSnippet.astro'; import LocationDartSnippet from '~/components/tutorials/flutter-weather/LocationDartSnippet.astro'; import WeatherJsonSnippet from '~/components/tutorials/flutter-weather/WeatherJsonSnippet.astro'; import WeatherDartSnippet from '~/components/tutorials/flutter-weather/WeatherDartSnippet.astro'; import OpenMeteoModelsBarrelTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoModelsBarrelTreeSnippet.astro'; import OpenMeteoLibrarySnippet from '~/components/tutorials/flutter-weather/OpenMeteoLibrarySnippet.astro'; import BuildRunnerBuildSnippet from '~/components/tutorials/flutter-weather/BuildRunnerBuildSnippet.astro'; import OpenMeteoApiClientTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoApiClientTreeSnippet.astro'; import LocationSearchMethodSnippet from '~/components/tutorials/flutter-weather/LocationSearchMethodSnippet.astro'; import GetWeatherMethodSnippet from '~/components/tutorials/flutter-weather/GetWeatherMethodSnippet.astro'; import FlutterTestCoverageSnippet from '~/components/tutorials/flutter-weather/FlutterTestCoverageSnippet.astro'; import FlutterCreateRepositorySnippet from '~/components/tutorials/flutter-weather/FlutterCreateRepositorySnippet.astro'; import RepositoryModelsBarrelTreeSnippet from '~/components/tutorials/flutter-weather/RepositoryModelsBarrelTreeSnippet.astro'; import WeatherRepositoryLibrarySnippet from '~/components/tutorials/flutter-weather/WeatherRepositoryLibrarySnippet.astro'; import WeatherCubitTreeSnippet from '~/components/tutorials/flutter-weather/WeatherCubitTreeSnippet.astro'; import WeatherBarrelDartSnippet from '~/components/tutorials/flutter-weather/WeatherBarrelDartSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) في هذا الدرس، سنبني تطبيق طقس في Flutter يوضح كيفية إدارة عدة cubits لتنفيذ التخصيص الديناميكي للثيمات، والسحب للتحديث، والعديد من الميزات الأخرى. سيعتمد تطبيق الطقس على جلب بيانات الطقس الحية من واجهة برمجة التطبيقات العامة OpenMeteo، وسنشرح كيفية فصل التطبيق إلى طبقات (البيانات، المستودع، منطق الأعمال، وطبقة العرض). ![demo](~/assets/tutorials/flutter-weather.gif) ## متطلبات المشروع يجب أن يتيح تطبيقنا للمستخدمين ما يلي: - البحث عن مدينة من خلال صفحة مخصصة للبحث - عرض تمثيل بصري مريح لبيانات الطقس التي يتم جلبها من [Open Meteo API](https://open-meteo.com) - تغيير وحدات القياس المعروضة (متري مقابل إمبيريال) بالإضافة إلى ذلك، - يجب أن يعكس موضوع التطبيق حالة الطقس للمدينة المختارة - يجب أن تستمر حالة التطبيق عبر الجلسات: أي يجب أن يتذكر التطبيق حالته بعد الإغلاق وإعادة الفتح (باستخدام [HydratedBloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc)) ## المفاهيم الرئيسية - مراقبة تغييرات الحالة باستخدام [BlocObserver](/ar/bloc-concepts#blocobserver). - [BlocProvider](/ar/flutter-bloc-concepts#blocprovider)، وهي Widget في Flutter توفر bloc لأبنائها. - [BlocBuilder](/ar/flutter-bloc-concepts#blocbuilder)، وهي Widget في Flutter تتولى إعادة البناء استجابةً للحالات الجديدة. - تجنب عمليات إعادة البناء غير الضرورية باستخدام [Equatable](/ar/faqs/#متى-يجب-استخدام-equatable). - [RepositoryProvider](/ar/flutter-bloc-concepts#repositoryprovider)، وهي Widget في Flutter توفر repository لأبنائها. - [BlocListener](/ar/flutter-bloc-concepts#bloclistener)، وهي Widget في Flutter تستدعي كود المستمع استجابةً لتغيرات الحالة في الـ bloc. - [MultiBlocProvider](/ar/flutter-bloc-concepts#multiblocprovider)، وهي Widget في Flutter التي تجمع عدة `BlocProvider` في واحد. - [BlocConsumer](/ar/flutter-bloc-concepts#blocconsumer)، وهي Widget في Flutter توفر builder و listener للاشتراك في حالات جديدة. - [HydratedBloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) لإدارة الحالة وحفظها بشكل مستمر. ## الإعداد لبداية العمل، أنشئ مشروع Flutter جديدًا. ### هيكل المشروع سيتكون تطبيقنا من ميزات منفصلة في مجلدات مخصصة لها. هذا يسمح لنا بالتوسع مع زيادة عدد الميزات ويسمح للمطورين بالعمل على ميزات مختلفة بشكل متوازي. يمكن تقسيم تطبيقنا إلى أربع ميزات رئيسية: **search, settings, theme, weather**. دعنا ننشئ هذه المجلدات. ### البنية المعمارية وفقًا لإرشادات [bloc architecture](/ar/architecture)، سيتكون تطبيقنا من عدة طبقات. في هذا الدرس، تقوم كل طبقة بالمهام التالية: - **Data**: جلب بيانات الطقس الخام من واجهة برمجة التطبيقات (API) - **Repository**: تجريد طبقة البيانات وتوفير نماذج نطاق العمل (domain models) ليتم استهلاكها داخل التطبيق - **Business Logic**: إدارة حالة كل ميزة (معلومات الوحدة، تفاصيل المدينة، السمات، إلخ) - **Presentation**: عرض معلومات الطقس وجمع مدخلات المستخدمين (صفحة الإعدادات، صفحة البحث، إلخ) ## طبقة البيانات في هذا التطبيق، سنقوم بالوصول إلى [Open Meteo API](https://open-meteo.com). سنركز على نقطتي نهاية (Endpoints) الأساسية: - `https://geocoding-api.open-meteo.com/v1/search?name=$city&count=1` للحصول على موقع جغرافي لاسم مدينة معين - `https://api.open-meteo.com/v1/forecast?latitude=$latitude&longitude=$longitude¤t_weather=true` للحصول على حالة الطقس لموقع محدد افتح [https://geocoding-api.open-meteo.com/v1/search?name=chicago&count=1](https://geocoding-api.open-meteo.com/v1/search?name=chicago&count=1) في متصفحك للاطلاع على الاستجابة لمدينة شيكاغو. سنستخدم قيمتي `latitude` و `longitude` في الاستجابة للوصول إلى نقطة نهاية الطقس. قيم `latitude`/`longitude` لشيكاغو هي `41.85003`/`-87.65005`. يمكنك التوجه إلى [https://api.open-meteo.com/v1/forecast?latitude=43.0389&longitude=-87.90647¤t_weather=true](https://api.open-meteo.com/v1/forecast?latitude=43.0389&longitude=-87.90647¤t_weather=true) في متصفحك لترى الاستجابة الخاصة بحالة الطقس في شيكاغو والتي تحتوي على جميع البيانات التي سنحتاجها لتطبيقنا. ### عميل واجهة برمجة التطبيقات OpenMeteo API عميل واجهة برمجة التطبيقات OpenMeteo مستقل عن تطبيقنا. لذلك، سنقوم بإنشائه كحزمة داخلية (ويمكننا حتى نشره على [pub.dev](https://pub.dev)). بعد ذلك، نستطيع استخدام الحزمة بإضافتها إلى `pubspec.yaml` في طبقة repository، التي ستتولى طلبات البيانات لتطبيق الطقس الرئيسي لدينا. أنشئ مجلدًا جديدًا على مستوى المشروع باسم `packages`. هذا المجلد سيحتوي جميع حزمنا الداخلية. داخل هذا المجلد، نفذ أمر `flutter create` المدمج لإنشاء حزمة جديدة باسم `open_meteo_api` لعميل واجهة برمجة التطبيقات. ### نموذج بيانات الطقس بعد ذلك، لنقم بإنشاء ملفي `location.dart` و `weather.dart` الذي سيحتويان النماذج (Models) لاستجابات نقاط نهاية الـ API الخاصة بالموقع والطقس. #### نموذج الموقع يجب أن يخزن نموذج `location.dart` البيانات التي تعود من API الموقع، والتي تبدو كما يلي: وهذا ملف `location.dart` قيد التقدم والذي يخزن الاستجابة أعلاه: #### نموذج الطقس بعدها، ننتقل إلى العمل على `weather.dart`. يجب أن يخزن نموذج الطقس البيانات التي تعود من API الطقس، الواردة كما يلي: وهذا ملف `weather.dart` قيد التقدم الذي يخزن الاستجابة أعلاه: ### ملفات البرميل (Barrel Files) وأثناء تواجدنا هنا، دعونا نُنشئ بسرعة [barrel file](https://adrianfaciu.dev/posts/barrel-files/) لتنظيم وتقليل تعقيد الاستيرادات لدينا مستقبلاً. أنشئ ملف `models.dart` كملف برميل (barrel) وصدّر فيه النموذجان: لنقم أيضاً بإنشاء ملف برميل على مستوى الحزمة باسم `open_meteo_api.dart` في الملف الأعلى مستوى `open_meteo_api.dart`، قمنا بتصدير النماذج: ### الإعداد نحتاج لأن نتمكن من [التسلسل والتفكيك التسلسلي (serialization/deserialization)](https://en.wikipedia.org/wiki/Serialization) لنماذجنا لكي نتمكن من التعامل مع بيانات الـ API. للقيام بذلك، سنضيف طرق `toJson` و `fromJson` لنماذجنا. وبالإضافة لذلك، نحتاج طريقة لإجراء [طلبات الشبكة عبر HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) لجلب البيانات من API. لحسن الحظ، هناك العديد من الحزم الشعبية للقيام بهذه المهمة. سنستخدم حزم: [json_annotation](https://pub.dev/packages/json_annotation)، [json_serializable](https://pub.dev/packages/json_serializable)، و [build_runner](https://pub.dev/packages/build_runner) لتوليد طرق `toJson` و`fromJson` تلقائيًا. وفي خطوة مستقبلية، سنستخدم أيضاً حزمة [http](https://pub.dev/packages/http) لإرسال طلبات الشبكة لواجهة الطقس ليتمكن تطبيقنا من عرض بيانات الطقس الحالية. لنُضِف هذه التبعيات إلى `pubspec.yaml`. :::note تذكر تشغيل الأمر `flutter pub get` بعد إضافة التبعيات. ::: ### (إعادة)التسلسل Serialization/Deserialization لكي يعمل توليد الأكواد، نحتاج إلى تمييز الكود لدينا باستخدام ما يلي: - `@JsonSerializable` لتعليم الكلاسات القابلة للتسلسل - `@JsonKey` لتوفير تمثيلات نصية لأسماء الحقول - `@JsonValue` لتوفير تمثيلات نصية لقيم الحقول - تنفيذ `JSONConverter` لتحويل تمثيلات الكائن إلى تمثيلات JSON لكل ملف نحتاج أيضاً إلى: - استيراد `json_annotation` - تضمين الكود المُولد باستخدام كلمة المفتاح [part](https://dart.dev/tools/pub/create-packages#organizing-a-package) - تضمين طرق `fromJson` لعملية التفكيك التسلسلي (deserialization) #### نموذج الموقع هذا هو ملف نموذج `location.dart` كاملاً: #### نموذج الطقس وهذا هو ملف نموذج `weather.dart` كاملاً: #### إنشاء ملف البناء (Build File) في مجلد `open_meteo_api`، أنشئ ملفًا باسم `build.yaml`. هدف هذا الملف هو التعامل مع الفروقات في تسميات الحقول داخل `json_serializable`. #### توليد الأكواد (Code Generation) لنستخدم `build_runner` لتوليد الأكواد المطلوبة. من المفترض أن يولّد `build_runner` الملفات `location.g.dart` و `weather.g.dart`. ### عميل OpenMeteo API دعونا ننشئ عميل الـ API الخاص بنا في ملف `open_meteo_api_client.dart` داخل مجلد `src`. يجب أن تبدو بنية مشروعنا كما يلي: الآن يمكننا استخدام حزمة [http](https://pub.dev/packages/http) التي أضفناها سابقًا إلى ملف `pubspec.yaml` لإجراء طلبات HTTP إلى واجهة الطقس واستخدام هذه المعلومات في تطبيقنا. سيُوفر عميل API الخاص بنا طريقتين: - `locationSearch` التي تعيد `Future` - `getWeather` التي تعيد `Future` #### البحث عن الموقع تصل طريقة `locationSearch` إلى API الموقع وتطرح أخطاء من النوع `LocationRequestFailure` عند الحاجة. الطرح النهائي للطريقة كما يلي: #### الحصول على الطقس بنفس الطريقة، تصل طريقة `getWeather` إلى API الطقس وتطرح أخطاء من النوع `WeatherRequestFailure`. الشكل النهائي للطريقة كالتالي: الملف المكتمل يبدو بالشكل التالي: #### تحديثات ملفات البرميل لنُنهي هذه الحزمة بإضافة عميل واجهة برمجة التطبيقات إلى ملف البرميل. ### اختبارات الوحدة (Unit Tests) من المهم بشكل خاص كتابة اختبارات وحدة لطبقة البيانات لأنها أساس تطبيقنا. اختبارات الوحدة ستمنحنا الثقة أن الحزمة تعمل كما هو متوقع. #### الإعداد في السابق، أضفنا حزمة [test](https://pub.dev/packages/test) إلى ملف pubspec.yaml والتي تتيح كتابة اختبارات الوحدة بسهولة. سوف نقوم بإنشاء ملف اختبار لعميل API بالإضافة إلى ملفات اختبار لكل نموذج من النموذجين. #### اختبارات الموقع #### اختبارات الطقس #### اختبارات عميل API بعدها، لنختبر عميل API الخاص بنا. يجب أن نتحقق من أن عميل API يتعامل بشكل صحيح مع كل طلبات الـ API، بما في ذلك السيناريوهات الحدية (Edge Cases). :::note لا نرغب في أن تستخدم اختباراتنا طلبات API حقيقية لأن هدفنا هو اختبار منطق عميل الـ API (بما في ذلك جميع السيناريوهات الحدية) وليس الـ API نفسه. ولضمان بيئة اختبار متسقة ومتحكم بها، سنستخدم [mocktail](https://github.com/felangel/mocktail) (والتي أضفناها مسبقًا في `pubspec.yaml`) لمحاكاة عميل الـ `http`. ::: #### تغطية الاختبارات (Test Coverage) وأخيرًا، دعونا نجمع تغطية الاختبارات لنتأكد من أننا غطينا كل سطر من الكود على الأقل بحالة اختبار واحدة. ## طبقة المستودع (Repository Layer) الهدف من طبقة المستودع هو تجريد طبقة البيانات وتسهيل الاتصال مع طبقة الـ bloc. عند القيام بذلك، يعتمد بقية الكود الأساسي لدينا فقط على الدوال المعروضة من خلال طبقة المستودع بدلًا من الاعتماد على تنفيذات مزود البيانات المحددة. هذا يسمح لنا بتغيير مزودي البيانات دون التأثير على أي كود في مستوى التطبيق. على سبيل المثال، إذا قررنا الانتقال بعيدًا عن واجهة برمجة التطبيقات الخاصة بالطقس التي نستخدمها حالياً، يجب أن نتمكن من إنشاء عميل API جديد واستبداله دون الحاجة لتعديل واجهة API العامة لطبقة المستودع أو طبقات التطبيق. ### الإعداد داخل مجلد الحزم `packages`، نفذ الأمر التالي: سنستخدم نفس الحزم الموجودة في حزمة `open_meteo_api` بما في ذلك حزمة `open_meteo_api` من الخطوة السابقة. حدّث ملف `pubspec.yaml` ثم نفذ الأمر `flutter pub get`. :::note نستخدم مسار `path` لتحديد موقع `open_meteo_api` مما يسمح لنا بالتعامل معها كما لو كانت حزمة خارجية من `pub.dev`. ::: ### نماذج Weather Repository سنقوم بإنشاء ملف جديد `weather.dart` لتعريف نموذج الطقس الخاص بالمجال (domain-specific). هذا النموذج سيحتوي فقط على البيانات ذات الصلة بحالات العمل لدينا — بمعنى آخر، يجب أن يكون مفصولًا تمامًا عن عميل الـ API وصيغة البيانات الخام. كما هو معتاد، سننشئ أيضًا ملف برميل `models.dart` لتجميع النماذج. هذه المرة، سيحتوي نموذج الطقس لدينا فقط على الخصائص `location, temperature, condition`. سنستمر أيضًا في ترميز كودنا للسماح بعمليات التسلسل والتسلسل العكسي (serialization & deserialization). قم بتحديث ملف البرميل الذي أنشأناه سابقًا ليشمل النماذج. #### إنشاء ملف Build كما في السابق، نحتاج إلى إنشاء ملف `build.yaml` بالمحتويات التالية: #### توليد الكود كما فعلنا في السابق، نفذ الأمر التالي لتوليد تنفيذ عمليات (de)serialization. #### ملف البرميل (Barrel File) لننشئ أيضًا ملف برميل على مستوى الحزمة باسم `packages/weather_repository/lib/weather_repository.dart` لتصدير النماذج: ### Weather Repository الهدف الأساسي من `WeatherRepository` هو توفير واجهة تجريدية لمزود البيانات. في هذه الحالة، ستعتمد `WeatherRepository` على `WeatherApiClient` وستعرض دالة عامة واحدة فقط، وهي `getWeather(String city)`. :::note مستهلكو `WeatherRepository` غير مطلعين على تفاصيل التنفيذ الداخلية مثل حقيقة إجراء طلبين شبكيين إلى واجهة الطقس. الهدف من `WeatherRepository` هو فصل الـ "ماذا" عن الـ "كيف" — بمعنى أننا نريد طريقة لجلب الطقس لمدينة معينة، لكننا لا نهتم بكيفية أو من أين تأتي البيانات. ::: #### الإعداد لنقم بإنشاء ملف `weather_repository.dart` داخل مجلد `src` في الحزمة، ونعمل على تنفيذ الريبو. الدالة الرئيسية التي سنركز عليها هي `getWeather(String city)`. يمكننا تنفيذها باستخدام مكالمتين إلى عميل الـ API كما يلي: #### ملف البرميل (Barrel File) قم بتحديث ملف البرميل الذي أنشأناه سابقًا. ### اختبارات الوحدة تمامًا كما هو الحال مع طبقة البيانات، من الضروري اختبار طبقة المستودع لضمان صحة منطق المجال. لاختبار `WeatherRepository`، سنستخدم مكتبة [mocktail](https://github.com/felangel/mocktail). سنقوم بمحاكاة عميل الـ API الأساسي لاختبار منطق `WeatherRepository` ضمن بيئة مختبرية ومعزولة. ## طبقة منطق الأعمال في طبقة منطق الأعمال، سنستخدم نموذج مجال الطقس من `WeatherRepository` وسنعرض نموذجًا خاصًا بالمزايا على مستوى الميزة ليتم عرضه للمستخدم عبر واجهة المستخدم. :::note هذا هو النوع الثالث المختلف من نموذج الطقس الذي نطبقه. في عميل API، كان نموذج الطقس لدينا يحتوي على كل المعلومات التي ترجعها الـ API. في طبقة المستودع، كان نموذج الطقس يحتوي فقط على النموذج المجرد بناءً على حالة العمل/الأعمال لدينا. في هذه الطبقة، سيكون نموذج الطقس لدينا يحتوي فقط على المعلومات ذات الصلة اللازمة خصيصًا لمجموعة الميزات الحالية. ::: ### الإعداد بما أن طبقة منطق الأعمال تقع داخل التطبيق الرئيسي، نحتاج إلى تعديل ملف `pubspec.yaml` لمشروع `flutter_weather` بأكمله وإضافة جميع الحزم التي سنستخدمها. - استخدام [equatable](https://pub.dev/packages/equatable) يتيح لمثيلات حالة التطبيق إمكانية المقارنة باستخدام معامل المساواة `==`. في الخلفية، يقوم bloc بمقارنة الحالات ليرى إذا ما كانت متساوية، وإذا لم تكن كذلك، سيؤدي ذلك إلى إعادة بناء. هذا يضمن أن شجرة Widget ستُعاد بناؤها فقط عند الضرورة للحفاظ على الأداء سريعًا ومتجاوبًا. - يمكننا تحسين واجهة المستخدم باستخدام حزمة [google_fonts](https://pub.dev/packages/google_fonts). - توفر [HydratedBloc](https://pub.dev/packages/hydrated_bloc) إمكانية حفظ حالة التطبيق عند إغلاقه وإعادة فتحه. - سنقوم بإضافة حزمة `weather_repository` التي أنشأناها للتو لتمكيننا من جلب بيانات الطقس الحالية! لأغراض الاختبار، سنضيف الحزم المعتادة `test`، بالإضافة إلى `mocktail` لتقليد التبعيات، و [bloc_test](https://pub.dev/packages/bloc_test) لتسهيل اختبار وحدات منطق الأعمال أو الـ blocs! بعدها، سنعمل على طبقة التطبيق ضمن دليل ميزة `weather`. ### نموذج الطقس الهدف من نموذج الطقس هو متابعة بيانات الطقس التي يعرضها تطبيقنا، بالإضافة إلى إعدادات درجة الحرارة (سلسيوس أو فهرنهايت). أنشئ الملف `flutter_weather/lib/weather/models/weather.dart`: ### إنشاء ملف البناء أنشئ ملف `build.yaml` لطبقة منطق الأعمال. ### توليد الكود شغّل الأمر `build_runner` لتوليد تطبيقات التسلسل العكسي (de)serialization. ### ملف البرميل (Barrel File) دعنا نصدر نماذجنا من ملف البرميل (`flutter_weather/lib/weather/models/models.dart`): ثم لننشئ ملف برميل رئيسي لميزة الطقس (`flutter_weather/lib/weather/weather.dart`); ### الطقس سنستخدم `HydratedCubit` لتمكين تطبيقنا من حفظ حالة التطبيق واستعادتها، حتى بعد إغلاق التطبيق وإعادة فتحه. :::note `HydratedCubit` هو امتداد لـ `Cubit` يتولى حفظ واستعادة الحالة عبر الجلسات. ::: #### حالة الطقس باستخدام [Bloc VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) أو [Bloc IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) افتح القائمة السياقية على مجلد `weather` وأنشئ cubit جديد اسمه `Weather`. يجب أن يكون هيكل المشروع كما يلي: هناك أربع حالات يمكن أن يكون عليها تطبيق الطقس: - `initial` قبل تحميل أي شيء - `loading` أثناء طلب البيانات من الـ API - `success` إذا نجح طلب الـ API - `failure` إذا فشل طلب الـ API سيُمثل هذا الـ enum المسمى `WeatherStatus` الحالات السابقة. يجب أن تبدو حالة الطقس الكاملة كما يلي: #### Weather Cubit بعد تعريف `WeatherState`، فلنكتب `WeatherCubit` الذي يعرض الطرق التالية: - `fetchWeather(String? city)` يستخدم مستودع الطقس لمحاولة جلب كائن الطقس للمدينة المعطاة - `refreshWeather()` يجلب كائن طقس جديد باستخدام مستودع الطقس اعتمادًا على الحالة الحالية - `toggleUnits()` يبدّل الوحدات بين سلسيوس وفهرنهايت - `fromJson(Map json)`، `toJson(WeatherState state)` تُستخدم للحفظ والاستعادة :::note لا تنسَ توليد كود (de)serialization عبر: ::: ### اختبارات الوحدة مثل طبقات البيانات والمستودع، من الضروري اختبار وحدة طبقة منطق الأعمال لضمان أن منطق الميزة يعمل كما هو متوقع. سنعتمد على [bloc_test](https://pub.dev/packages/bloc_test) إلى جانب `mocktail` و`test`. دعونا نضيف الحزم `test`، `bloc_test`، و`mocktail` إلى قسم `dev_dependencies`. :::note تتيح حزمة [bloc_test](https://pub.dev/packages/bloc_test) تجهيز الـ blocs بسهولة للاختبار، مع التعامل مع تغيرات الحالة والتحقق من النتائج بطريقة موحدة. ::: #### اختبارات Weather Cubit ## طبقة العرض ### صفحة الطقس سنبدأ بـ `WeatherPage` التي تستخدم `BlocProvider` لتوفير instance من `WeatherCubit` لشجرة Widget. ستلاحظ أن هذه الصفحة تعتمد على Widget `SettingsPage` و`SearchPage`، اللتين سننشئهما بعد ذلك. ### صفحة الإعدادات تتيح صفحة الإعدادات للمستخدمين تحديث تفضيلاتهم لوحدات درجة الحرارة. ### صفحة البحث تمكن صفحة البحث المستخدمين من إدخال اسم المدينة المرغوبة وتعيد نتيجة البحث إلى المسار السابق عبر `Navigator.of(context).pop`. ### Widgets الطقس سيعرض التطبيق شاشات مختلفة اعتمادًا على الحالات الأربع المحتملة لـ `WeatherCubit`. #### حالة WeatherEmpty تعرض هذه الشاشة عندما لا تكون هناك بيانات للعرض لأن المستخدم لم يختَر مدينة بعد. #### حالة WeatherError تعرض هذه الشاشة في حال حدوث خطأ. #### حالة WeatherLoading تعرض هذه الشاشة أثناء جلب التطبيق للبيانات. #### حالة WeatherPopulated تعرض هذه الشاشة بعد أن يختار المستخدم مدينة ويتم استرجاع البيانات. ### ملف البرميل (Barrel File) لنقم بإضافة هذه الحالات إلى ملف برميل لتنظيم عمليات الاستيراد وجعلها أنظف. ### نقطة الدخول ينبغي أن يقوم ملف `main.dart` بتهيئة تطبيق `WeatherApp` و`BlocObserver` (لأغراض التصحيح)، بالإضافة إلى إعداد `HydratedStorage` للحفاظ على الحالة عبر الجلسات. Widget `app.dart` تتولى بناء عرض `WeatherPage` الذي أنشأناه سابقًا وتستخدم `BlocProvider` لحقن الـ `WeatherCubit`. ### اختبارات Widgets توفّر مكتبة [`bloc_test`](https://pub.dev/packages/bloc_test) أيضًا `MockBlocs` و `MockCubits` التي تجعل من السهل اختبار واجهة المستخدم. يمكننا محاكاة حالات الـ cubits المختلفة والتأكد من استجابة واجهة المستخدم بشكل صحيح. :::note نستخدم `MockWeatherCubit` مع واجهة برمجة التطبيقات `when` من مكتبة `mocktail` من أجل محاكاة الحالة الخاصة بـ cubit في كل حالة اختبار. هذا يسمح لنا بمحاكاة جميع الحالات والتحقق من أن واجهة المستخدم تتصرف بشكل صحيح تحت كل الظروف. ::: ## الملخص هذا كل شيء، لقد أنهينا الدرس التعليمي! 🎉 يمكننا تشغيل التطبيق النهائي باستخدام الأمر `flutter run`. يمكن العثور على الأكواد البرمجية المصدرية الكاملة لهذا المثال، بما في ذلك اختبارات الوحدة واختبارات Widgets، [هنا](https://github.com/felangel/bloc/tree/master/examples/flutter_weather). ================================================ FILE: docs/src/content/docs/ar/tutorials/github-search.mdx ================================================ --- title: بحث GitHub description: دليل متعمق لبناء تطبيق بحث GitHub باستخدام Flutter وAngularDart مع bloc. sidebar: order: 9 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import SetupSnippet from '~/components/tutorials/github-search/SetupSnippet.astro'; import DartPubGetSnippet from '~/components/tutorials/github-search/DartPubGetSnippet.astro'; import FlutterCreateSnippet from '~/components/tutorials/github-search/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; import StagehandSnippet from '~/components/tutorials/github-search/StagehandSnippet.astro'; import ActivateStagehandSnippet from '~/components/tutorials/github-search/ActivateStagehandSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) في هذا الدليل، سنبني تطبيق بحث GitHub باستخدام Flutter وAngularDart لشرح كيف يمكننا مشاركة طبقة البيانات (data layer) وطبقة منطق الأعمال (business logic layer) بين المشروعين. ![demo](~/assets/tutorials/flutter-github-search.gif) ![demo](~/assets/tutorials/ngdart-github-search.gif) ## المواضيع الرئيسية - [BlocProvider](/ar/flutter-bloc-concepts#blocprovider)، وهو `widget` في Flutter يوفّر bloc للأبناء. - [BlocBuilder](/ar/flutter-bloc-concepts#blocbuilder)، وهو `widget` في Flutter يتولى بناء الواجهة استجابةً للحالات الجديدة. - استخدام Cubit بدلًا من Bloc. [ما الفرق؟](/ar/bloc-concepts/#cubit-مقابل-bloc) - تجنب إعادة البناء غير الضرورية باستخدام [Equatable](/ar/faqs/#متى-يجب-استخدام-equatable). - استخدام `EventTransformer` مخصص مع [`bloc_concurrency`](https://pub.dev/packages/bloc_concurrency). - تنفيذ طلبات الشبكة عبر حزمة `http`. ## مكتبة GitHub Search المشتركة ستتضمن مكتبة GitHub Search المشتركة النماذج (models)، ومزوّد البيانات (data provider)، وRepository، بالإضافة إلى bloc الذي سنعيد استخدامه بين AngularDart وFlutter. ### الإعداد سنبدأ بإنشاء مجلد جديد لتطبيقنا. :::note سيحتوي مجلد `common_github_search` على المكتبة المشتركة. ::: نحتاج إلى إنشاء `pubspec.yaml` يتضمن dependencies المطلوبة. أخيرًا، نحتاج إلى تثبيت dependencies. انتهى إعداد المشروع. الآن يمكننا البدء في بناء حزمة `common_github_search`. ### Github Client `GithubClient` سيكون مسؤولًا عن توفير البيانات الخام من [GitHub API](https://developer.github.com/v3/). :::note يمكنك رؤية مثال لشكل البيانات العائدة من API [هنا](https://api.github.com/search/repositories?q=dartlang). ::: لننشئ `github_client.dart`. :::note `GithubClient` يرسل طلب شبكة إلى Repository Search API في GitHub، ثم يحوّل النتيجة إلى `SearchResult` أو `SearchResultError` ضمن `Future`. ::: :::note تنفيذ `GithubClient` يعتمد على `SearchResult.fromJson`، ولم نطبّقه بعد. ::: الخطوة التالية هي تعريف نموذجي `SearchResult` و`SearchResultError`. #### نموذج نتيجة البحث أنشئ `search_result.dart`، وهو يمثل قائمة `SearchResultItems` بناءً على استعلام المستخدم: :::note تنفيذ `SearchResult` يعتمد على `SearchResultItem.fromJson`، ولم نطبّقه بعد. ::: :::note لا نضيف الخصائص التي لن نستخدمها داخل النموذج. ::: #### نموذج عنصر نتيجة البحث الآن سننشئ `search_result_item.dart`. :::note مرة أخرى، تنفيذ `SearchResultItem` يعتمد على `GithubUser.fromJson`، ولم نطبّقه بعد. ::: #### نموذج مستخدم GitHub الآن سننشئ `github_user.dart`. بهذا نكون انتهينا من تنفيذ `SearchResult` واعتمادياته، وننتقل الآن إلى `SearchResultError`. #### نموذج خطأ نتيجة البحث أنشئ `search_result_error.dart`. انتهينا من `GithubClient`، والخطوة التالية هي `GithubCache`، والذي سيكون مسؤولًا عن [memoization](https://en.wikipedia.org/wiki/Memoization) كتحسين للأداء. ### GitHub Cache سيكون `GithubCache` مسؤولًا عن تذكّر جميع الاستعلامات السابقة لتجنب تنفيذ طلبات شبكة غير ضرورية إلى GitHub API. هذا يساعد أيضًا في تحسين أداء التطبيق. أنشئ `github_cache.dart`. الآن أصبحنا جاهزين لإنشاء `GithubRepository`. ### GitHub Repository `GithubRepository` مسؤول عن إنشاء طبقة تجريد (abstraction) بين طبقة البيانات (`GithubClient`) وطبقة منطق الأعمال (`Bloc`). وهنا أيضًا سنستخدم `GithubCache`. أنشئ `github_repository.dart`. :::note `GithubRepository` يعتمد على `GithubCache` و`GithubClient`، ويخفي تفاصيل التنفيذ الداخلية. التطبيق لا يحتاج لمعرفة كيفية جلب البيانات أو مصدرها. يمكننا تغيير طريقة عمل الـ repository في أي وقت، وما دمنا لم نغيّر الواجهة (interface)، فلن نحتاج لتعديل كود العملاء (client code). ::: بهذا نكون أكملنا طبقة مزوّد البيانات وطبقة الـ repository، وأصبحنا جاهزين للانتقال إلى طبقة منطق الأعمال. ### حدث GitHub Search سيتم إشعار Bloc عندما يكتب المستخدم اسم repository، وسنمثل ذلك عبر حدث `TextChanged` من نوع `GithubSearchEvent`. أنشئ `github_search_event.dart`. :::note نرث من [`Equatable`](https://pub.dev/packages/equatable) حتى نتمكن من مقارنة نسخ `GithubSearchEvent`. افتراضيًا، عامل المساواة يعيد `true` فقط إذا كان الكائنان نفس النسخة. ::: ### حالة GitHub Search طبقة العرض تحتاج عدة حالات لتتمكن من رسم الواجهة بشكل صحيح: - `SearchStateEmpty` لإبلاغ طبقة العرض بعدم وجود إدخال من المستخدم. - `SearchStateLoading` لإبلاغ طبقة العرض بضرورة إظهار مؤشر تحميل. - `SearchStateSuccess` لإبلاغ طبقة العرض بوجود بيانات جاهزة للعرض. - `items` ستكون `List` التي ستُعرض. - `SearchStateError` لإبلاغ طبقة العرض بحدوث خطأ أثناء جلب repositories. - `error` يمثل الخطأ الفعلي الذي حدث. يمكننا الآن إنشاء `github_search_state.dart` وتنفيذه كالتالي. :::note نرث من [`Equatable`](https://pub.dev/packages/equatable) حتى نتمكن من مقارنة نسخ `GithubSearchState`. افتراضيًا، عامل المساواة يعيد `true` فقط إذا كان الكائنان نفس النسخة. ::: بعد تنفيذ Events وStates، يمكننا إنشاء `GithubSearchBloc`. ### GitHub Search Bloc أنشئ `github_search_bloc.dart`: :::note `GithubSearchBloc` يحوّل `GithubSearchEvent` إلى `GithubSearchState`، ويعتمد على `GithubRepository`. ::: :::note ننشئ `EventTransformer` مخصصًا لتنفيذ [debounce](https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/debounce.html) على `GithubSearchEvents`. وأحد أسباب اختيار Bloc بدل Cubit هو الاستفادة من stream transformers. ::: ممتاز. انتهينا من حزمة `common_github_search`. الناتج النهائي يجب أن يبدو مثل [هذا](https://github.com/felangel/bloc/tree/master/examples/github_search/common_github_search). الخطوة التالية: تنفيذ Flutter. ## بحث GitHub باستخدام Flutter Flutter GitHub Search سيكون تطبيق Flutter يعيد استخدام النماذج، ومزوّدي البيانات، والـ repositories، والـ blocs من `common_github_search` لتنفيذ ميزة البحث في GitHub. ### الإعداد نبدأ بإنشاء مشروع Flutter جديد داخل مجلد `github_search` في نفس مستوى `common_github_search`. بعد ذلك نحدّث `pubspec.yaml` ليتضمن كل dependencies المطلوبة. :::note نضيف مكتبة `common_github_search` التي أنشأناها كاعتمادية. ::: الآن نثبت dependencies. انتهى إعداد المشروع. بما أن `common_github_search` تحتوي طبقة البيانات وطبقة منطق الأعمال، فكل ما نحتاج لبنائه هو طبقة العرض. ### نموذج البحث سنحتاج إلى إنشاء نموذج يحتوي `widget` باسم `_SearchBar` و`widget` باسم `_SearchBody`. - `_SearchBar` سيكون مسؤولًا عن استقبال إدخال المستخدم. - `_SearchBody` سيكون مسؤولًا عن عرض نتائج البحث ومؤشرات التحميل والأخطاء. لننشئ `search_form.dart`. `SearchForm` سيكون `StatelessWidget` يعرض `_SearchBar` و`_SearchBody`. `_SearchBar` سيكون أيضًا `StatefulWidget` لأنه يحتاج لإدارة `TextEditingController` خاص به حتى نتتبع مدخلات المستخدم. `_SearchBody` هو `StatelessWidget` مسؤول عن عرض نتائج البحث والأخطاء ومؤشرات التحميل. وهو المستهلك لـ `GithubSearchBloc`. إذا كانت الحالة `SearchStateSuccess`، سنعرض `_SearchResults` الذي سننفذه لاحقًا. `_SearchResults` هو `StatelessWidget` يستقبل `List` ويعرضها كقائمة من `_SearchResultItems`. `_SearchResultItem` هو `StatelessWidget` مسؤول عن عرض بيانات نتيجة بحث واحدة. وهو أيضًا مسؤول عن التعامل مع تفاعل المستخدم والتنقل إلى رابط repository عند النقر. :::note `_SearchBar` يصل إلى `GithubSearchBloc` عبر `context.read()` ويُشعِر bloc بأحداث `TextChanged`. ::: :::note `_SearchBody` يستخدم `BlocBuilder` لإعادة البناء استجابةً لتغيّر الحالة. وبما أن وسيط bloc في `BlocBuilder` تم حذفه، سيقوم `BlocBuilder` تلقائيًا بالبحث عن instance مناسبة عبر `BlocProvider` و`BuildContext` الحالي. اقرأ المزيد [هنا.](/ar/flutter-bloc-concepts#blocbuilder) ::: :::note نستخدم `ListView.builder` لبناء قائمة قابلة للتمرير من `_SearchResultItem`. ::: :::note نستخدم حزمة [url_launcher](https://pub.dev/packages/url_launcher) لفتح الروابط الخارجية. ::: ### تجميع كل شيء كل ما تبقى هو تنفيذ التطبيق الرئيسي في `main.dart`. :::note يتم إنشاء `GithubRepository` في `main` وحقنه داخل `App`. ثم يتم تغليف `SearchForm` داخل `BlocProvider` المسؤول عن إنشاء instance من `GithubSearchBloc` وإغلاقها وإتاحتها لـ `SearchForm` والأبناء. ::: بهذا نكون نفذنا تطبيق بحث GitHub في Flutter بنجاح باستخدام حزمتَي [bloc](https://pub.dev/packages/bloc) و [flutter_bloc](https://pub.dev/packages/flutter_bloc)، وتمكنا من فصل طبقة العرض عن طبقة منطق الأعمال. يمكنك العثور على المصدر الكامل [هنا](https://github.com/felangel/bloc/tree/master/examples/github_search/flutter_github_search). الآن سننتقل إلى بناء تطبيق GitHub Search باستخدام AngularDart. ## بحث GitHub باستخدام AngularDart AngularDart GitHub Search سيكون تطبيق AngularDart يعيد استخدام النماذج، ومزوّدي البيانات، والـ repositories، والـ blocs من `common_github_search` لتنفيذ ميزة البحث في GitHub. ### الإعداد نبدأ بإنشاء مشروع AngularDart جديد داخل مجلد `github_search` في نفس مستوى `common_github_search`. :::note يمكنك تثبيت `stagehand` عبر: ::: بعدها يمكننا استبدال محتوى `pubspec.yaml` بما يلي: ### نموذج البحث كما في تطبيق Flutter، نحتاج إلى `SearchForm` يحتوي على مكوّني `SearchBar` و `SearchBody`. مكوّن `SearchForm` سيطبّق `OnInit` و`OnDestroy` لأنه يحتاج لإنشاء `GithubSearchBloc` ثم إغلاقه. - `SearchBar` مسؤول عن استقبال إدخال المستخدم. - `SearchBody` مسؤول عن عرض نتائج البحث ومؤشرات التحميل والأخطاء. لننشئ `search_form_component.dart`. :::note يتم حقن `GithubRepository` داخل `SearchFormComponent`. ::: :::note `SearchFormComponent` هو المسؤول عن إنشاء `GithubSearchBloc` وإغلاقه. ::: وسيكون القالب (`search_form_component.html`) كالتالي: الآن سننفذ مكوّن `SearchBar`. ### Search Bar `SearchBar` هو مكوّن مسؤول عن استقبال إدخال المستخدم، وإشعار `GithubSearchBloc` بتغيّر النص. أنشئ `search_bar_component.dart`. :::note `SearchBarComponent` يعتمد على `GithubSearchBloc` لأنه مسؤول عن إرسال أحداث `TextChanged` إلى bloc. ::: بعدها ننشئ `search_bar_component.html`. انتهينا من `SearchBar`، والآن ننتقل إلى `SearchBody`. ### Search Body `SearchBody` هو مكوّن مسؤول عن عرض نتائج البحث والأخطاء ومؤشرات التحميل. وهو المستهلك لـ `GithubSearchBloc`. أنشئ `search_body_component.dart`. :::note `SearchBodyComponent` يعتمد على `GithubSearchState` التي يتم توفيرها من `GithubSearchBloc` باستخدام bloc pipe الخاصة بـ `angular_bloc`. ::: أنشئ `search_body_component.html`. إذا كانت الحالة `isSuccess`، سنعرض `SearchResults`، وسننفيذه الآن. ### Search Results `SearchResults` هو مكوّن يستقبل `List` ويعرضها كقائمة من `SearchResultItems`. أنشئ `search_results_component.dart`. بعد ذلك ننشئ `search_results_component.html`. :::note نستخدم `ngFor` لبناء قائمة من مكونات `SearchResultItem`. ::: حان الوقت لتنفيذ `SearchResultItem`. ### Search Result Item `SearchResultItem` هو مكوّن مسؤول عن عرض معلومات نتيجة بحث واحدة، كما يتعامل مع تفاعل المستخدم وينتقل إلى رابط repository عند النقر. أنشئ `search_result_item_component.dart`. ثم أنشئ القالب المقابل `search_result_item_component.html`. ### تجميع كل شيء لدينا الآن جميع المكونات، وحان وقت تجميعها داخل `app_component.dart`. :::note ننشئ `GithubRepository` داخل `AppComponent` ثم نحقنه في مكوّن `SearchForm`. ::: بهذا نكون نفذنا تطبيق بحث GitHub في AngularDart بنجاح باستخدام حزمتَي `bloc` و `angular_bloc`، وتمكّنا من فصل طبقة العرض عن طبقة منطق الأعمال. يمكنك العثور على المصدر الكامل [هنا](https://github.com/felangel/bloc/tree/master/examples/github_search/angular_github_search). ## الملخص في هذا الدليل، أنشأنا تطبيق Flutter وتطبيق AngularDart مع مشاركة جميع النماذج، ومزوّدي البيانات، والـ blocs بين المشروعين. الجزء الوحيد الذي احتجنا لكتابته مرتين فعليًا هو طبقة العرض (UI)، وهذا ممتاز من ناحية الكفاءة وسرعة التطوير. وبما أن تطبيقات الويب وتطبيقات الجوال غالبًا ما تملك تجارب استخدام وأنماط تصميم مختلفة، فهذا النهج يوضح مدى سهولة بناء تطبيقين بواجهتين مختلفتين تمامًا مع مشاركة نفس طبقة البيانات وطبقة منطق الأعمال. يمكنك العثور على المصدر الكامل [هنا](https://github.com/felangel/bloc/tree/master/examples/github_search). ================================================ FILE: docs/src/content/docs/ar/tutorials/ngdart-counter.mdx ================================================ --- title: AngularDart Counter description: دليل متعمق لبناء تطبيق عدّاد (Counter) باستخدام AngularDart ومكتبة Bloc. sidebar: order: 8 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import ActivateStagehandSnippet from '~/components/tutorials/ngdart-counter/ActivateStagehandSnippet.astro'; import StagehandSnippet from '~/components/tutorials/ngdart-counter/StagehandSnippet.astro'; import InstallDependenciesSnippet from '~/components/tutorials/ngdart-counter/InstallDependenciesSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) في هذا الدليل، سنبني تطبيق عدّاد (Counter) باستخدام AngularDart ومكتبة Bloc. ![demo](~/assets/tutorials/ngdart-counter.gif) ## الإعداد (Setup) سنبدأ بإنشاء مشروع AngularDart جديد باستخدام [`stagehand`](https://github.com/dart-lang/stagehand). إذا لم يكن `stagehand` مثبتًا لديك، فعِّله عبر: ثم أنشئ مشروعًا جديدًا عبر: بعد ذلك، استبدل محتوى ملف `pubspec.yaml` بما يلي: ثم ثبّت جميع dependencies: سيحتوي تطبيق العداد على زرين لزيادة/إنقاص قيمة العداد، وعنصر لعرض القيمة الحالية. لنبدأ بتصميم `CounterEvents`. ## Counter Bloc بما أن حالة العداد يمكن تمثيلها بعدد صحيح (`integer`)، فلا حاجة لإنشاء class مخصصة، ويمكننا وضع events وBloc في نفس المكان. :::note من تعريف class فقط، نعرف أن `CounterBloc` يستقبل `CounterEvents` كمدخلات، ويُنتج أعدادًا صحيحة (`integers`). ::: ## تطبيق العداد (Counter App) بعد اكتمال تنفيذ `CounterBloc`، يمكننا البدء في إنشاء مكوّن تطبيق AngularDart (`App Component`). يجب أن يكون `app.component.dart` كالتالي: ويجب أن يكون `app.component.html` كالتالي: ## صفحة العداد (Counter Page) أخيرًا، يتبقى بناء مكوّن صفحة العداد (`Counter Page Component`). يجب أن يكون `counter_page_component.dart` كالتالي: :::note يمكن الوصول إلى نسخة `CounterBloc` عبر نظام حقن الاعتماديات (`dependency injection system`) في AngularDart. وبما أننا سجلناه كـ `Provider`، يستطيع AngularDart إجراء `resolve` لـ `CounterBloc` بشكل صحيح. ::: :::note نغلق `CounterBloc` داخل `ngOnDestroy`. ::: :::note نستورد `BlocPipe` حتى نتمكن من استخدامه في القالب (`template`). ::: أخيرًا، يجب أن يكون `counter_page_component.html` كالتالي: :::note نستخدم `BlocPipe` لعرض حالة `CounterBloc` مع كل تحديث. ::: هذا كل شيء. قمنا بفصل طبقة العرض (`presentation layer`) عن طبقة منطق الأعمال (`business logic layer`). لا يعرف `CounterPageComponent` ماذا يحدث عند ضغط المستخدم على الزر؛ هو فقط يضيف حدثًا لإخطار `CounterBloc`. وبالمثل، لا يعرف `CounterBloc` تفاصيل العرض الخاصة بالحالة (قيمة العداد)؛ بل يحوّل `CounterEvents` إلى أعداد صحيحة. يمكن تشغيل التطبيق باستخدام `webdev serve` ثم عرضه محليًا. يمكنك العثور على المصدر الكامل لهذا المثال [هنا](https://github.com/felangel/bloc/tree/master/examples/angular_counter). ================================================ FILE: docs/src/content/docs/ar/why-bloc.mdx ================================================ --- title: لماذا Bloc؟ description: نظرة عامة على ما يجعل Bloc حلاً متينًا لإدارة الحالة (State Management). sidebar: order: 1 --- يسهّل Bloc فصل طبقة العرض (Presentation) عن منطق العمل (Business Logic)، مما يجعل كودك سريعًا، سهل الاختبار، وقابلًا لإعادة الاستخدام. عند بناء تطبيقات بجودة إنتاجية، تصبح إدارة الحالة أمرًا بالغ الأهمية. كمطورين، نرغب في: - معرفة الحالة التي يكون عليها تطبيقنا في أي لحظة. - اختبار كل سيناريو بسهولة للتأكد من أن التطبيق يستجيب بالشكل الصحيح. - تسجيل كل تفاعل يقوم به المستخدم داخل التطبيق لاتخاذ قرارات مبنية على البيانات. - العمل بأعلى قدر ممكن من الكفاءة وإعادة استخدام المكونات داخل التطبيق نفسه أو عبر تطبيقات أخرى. - تمكين عدة مطورين من العمل بسلاسة ضمن قاعدة كود واحدة باتباع نفس الأنماط والاتفاقيات. - تطوير تطبيقات سريعة وتفاعلية. تم تصميم Bloc لتلبية جميع هذه الاحتياجات وأكثر من ذلك. هناك العديد من حلول إدارة الحالة، وقد يكون اختيار الحل المناسب مهمة صعبة. لا يوجد حل مثالي واحد! المهم هو اختيار الحل الذي يناسب فريقك ومشروعك بالشكل الأفضل. تم تصميم Bloc مع التركيز على ثلاث قيم أساسية: - **بسيط (Simple):** سهل الفهم ويمكن استخدامه من قبل مطورين بمستويات مهارية مختلفة. - **قوي (Powerful):** يساعد على بناء تطبيقات مذهلة ومعقدة من خلال تكوينها من مكونات أصغر. - **قابل للاختبار (Testable):** يسهّل اختبار جميع جوانب التطبيق لنتمكن من التطوير بثقة. بشكل عام، يسعى Bloc إلى جعل تغييرات الحالة قابلة للتنبؤ (Predictable) من خلال تنظيم توقيت حدوث التغيير وفرض أسلوب موحد لتغيير الحالة في كامل التطبيق. ================================================ FILE: docs/src/content/docs/architecture.mdx ================================================ --- title: Architecture description: Overview of the recommended architecture patterns when using bloc. --- import DataProviderSnippet from '~/components/architecture/DataProviderSnippet.astro'; import RepositorySnippet from '~/components/architecture/RepositorySnippet.astro'; import BusinessLogicComponentSnippet from '~/components/architecture/BusinessLogicComponentSnippet.astro'; import BlocTightCouplingSnippet from '~/components/architecture/BlocTightCouplingSnippet.astro'; import BlocLooseCouplingPresentationSnippet from '~/components/architecture/BlocLooseCouplingPresentationSnippet.astro'; import AppIdeasRepositorySnippet from '~/components/architecture/AppIdeasRepositorySnippet.astro'; import AppIdeaRankingBlocSnippet from '~/components/architecture/AppIdeaRankingBlocSnippet.astro'; import PresentationComponentSnippet from '~/components/architecture/PresentationComponentSnippet.astro'; ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) Using the bloc library allows us to separate our application into three layers: - Presentation - Business Logic - Data - Repository - Data Provider We're going to start at the lowest level layer (farthest from the user interface) and work our way up to the presentation layer. ## Data Layer The data layer's responsibility is to retrieve/manipulate data from one or more sources. The data layer can be split into two parts: - Repository - Data Provider This layer is the lowest level of the application and interacts with databases, network requests, and other asynchronous data sources. ### Data Provider The data provider's responsibility is to provide raw data. The data provider should be generic and versatile. The data provider will usually expose simple APIs to perform [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) operations. We might have a `createData`, `readData`, `updateData`, and `deleteData` method as part of our data layer. ### Repository The repository layer is a wrapper around one or more data providers with which the Bloc Layer communicates. As you can see, our repository layer can interact with multiple data providers and perform transformations on the data before handing the result to the business logic layer. ## Business Logic Layer The business logic layer's responsibility is to respond to input from the presentation layer with new states. This layer can depend on one or more repositories to retrieve data needed to build up the application state. Think of the business logic layer as the bridge between the user interface (presentation layer) and the data layer. The business logic layer is notified of events/actions from the presentation layer and then communicates with repository in order to build a new state for the presentation layer to consume. ### Bloc-to-Bloc Communication Because blocs expose streams, it may be tempting to make a bloc which listens to another bloc. You should **not** do this. There are better alternatives than resorting to the code below: While the code above is error free (and even cleans up after itself), it has a bigger problem: it creates a dependency between two blocs. Generally, sibling dependencies between two entities in the same architectural layer should be avoided at all costs, as it creates tight-coupling which is hard to maintain. Since blocs reside in the business logic architectural layer, no bloc should know about any other bloc. ![Application Architecture Layers](~/assets/architecture/architecture.png) A bloc should only receive information through events and from injected repositories (i.e., repositories given to the bloc in its constructor). If you're in a situation where a bloc needs to respond to another bloc, you have two other options. You can push the problem up a layer (into the presentation layer), or down a layer (into the domain layer). #### Connecting Blocs through Presentation You can use a `BlocListener` to listen to one bloc and add an event to another bloc whenever the first bloc changes. The code above prevents `SecondBloc` from needing to know about `FirstBloc`, encouraging loose-coupling. The [flutter_weather](/tutorials/flutter-weather) application [uses this technique](https://github.com/felangel/bloc/blob/b4c8db938ad71a6b60d4a641ec357905095c3965/examples/flutter_weather/lib/weather/view/weather_page.dart#L38-L42) to change the app's theme based on the weather information that is received. In some situations, you may not want to couple two blocs in the presentation layer. Instead, it can often make sense for two blocs to share the same source of data and update whenever the data changes. #### Connecting Blocs through Domain Two blocs can listen to a stream from a repository and update their states independent of each other whenever the repository data changes. Using reactive repositories to keep state synchronized is common in large-scale enterprise applications. First, create or use a repository which provides a data `Stream`. For example, the following repository exposes a never-ending stream of the same few app ideas: The same repository can be injected into each bloc that needs to react to new app ideas. Below is an `AppIdeaRankingBloc` which yields a state out for each incoming app idea from the repository above: For more about using streams with Bloc, see [How to use Bloc with streams and concurrency](https://verygood.ventures/blog/how-to-use-bloc-with-streams-and-concurrency). ## Presentation Layer The presentation layer's responsibility is to figure out how to render itself based on one or more bloc states. In addition, it should handle user input and application lifecycle events. Most applications flows will start with a `AppStart` event which triggers the application to fetch some data to present to the user. In this scenario, the presentation layer would add an `AppStart` event. In addition, the presentation layer will have to figure out what to render on the screen based on the state from the bloc layer. So far, even though we've had some code snippets, all of this has been fairly high level. In the tutorial section we're going to put all this together as we build several different example apps. ================================================ FILE: docs/src/content/docs/bloc-concepts.mdx ================================================ --- title: Bloc Concepts description: An overview of the core concepts for package:bloc. sidebar: order: 1 --- import CountStreamSnippet from '~/components/concepts/bloc/CountStreamSnippet.astro'; import SumStreamSnippet from '~/components/concepts/bloc/SumStreamSnippet.astro'; import StreamsMainSnippet from '~/components/concepts/bloc/StreamsMainSnippet.astro'; import CounterCubitSnippet from '~/components/concepts/bloc/CounterCubitSnippet.astro'; import CounterCubitInitialStateSnippet from '~/components/concepts/bloc/CounterCubitInitialStateSnippet.astro'; import CounterCubitInstantiationSnippet from '~/components/concepts/bloc/CounterCubitInstantiationSnippet.astro'; import CounterCubitIncrementSnippet from '~/components/concepts/bloc/CounterCubitIncrementSnippet.astro'; import CounterCubitBasicUsageSnippet from '~/components/concepts/bloc/CounterCubitBasicUsageSnippet.astro'; import CounterCubitStreamUsageSnippet from '~/components/concepts/bloc/CounterCubitStreamUsageSnippet.astro'; import CounterCubitOnChangeSnippet from '~/components/concepts/bloc/CounterCubitOnChangeSnippet.astro'; import CounterCubitOnChangeUsageSnippet from '~/components/concepts/bloc/CounterCubitOnChangeUsageSnippet.astro'; import CounterCubitOnChangeOutputSnippet from '~/components/concepts/bloc/CounterCubitOnChangeOutputSnippet.astro'; import SimpleBlocObserverOnChangeSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeSnippet.astro'; import SimpleBlocObserverOnChangeUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeUsageSnippet.astro'; import SimpleBlocObserverOnChangeOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeOutputSnippet.astro'; import CounterCubitOnErrorSnippet from '~/components/concepts/bloc/CounterCubitOnErrorSnippet.astro'; import SimpleBlocObserverOnErrorSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnErrorSnippet.astro'; import CounterCubitOnErrorOutputSnippet from '~/components/concepts/bloc/CounterCubitOnErrorOutputSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/bloc/CounterBlocSnippet.astro'; import CounterBlocEventHandlerSnippet from '~/components/concepts/bloc/CounterBlocEventHandlerSnippet.astro'; import CounterBlocIncrementSnippet from '~/components/concepts/bloc/CounterBlocIncrementSnippet.astro'; import CounterBlocUsageSnippet from '~/components/concepts/bloc/CounterBlocUsageSnippet.astro'; import CounterBlocStreamUsageSnippet from '~/components/concepts/bloc/CounterBlocStreamUsageSnippet.astro'; import CounterBlocOnChangeSnippet from '~/components/concepts/bloc/CounterBlocOnChangeSnippet.astro'; import CounterBlocOnChangeUsageSnippet from '~/components/concepts/bloc/CounterBlocOnChangeUsageSnippet.astro'; import CounterBlocOnChangeOutputSnippet from '~/components/concepts/bloc/CounterBlocOnChangeOutputSnippet.astro'; import CounterBlocOnTransitionSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionSnippet.astro'; import CounterBlocOnTransitionOutputSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionOutputSnippet.astro'; import SimpleBlocObserverOnTransitionSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionSnippet.astro'; import SimpleBlocObserverOnTransitionUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionUsageSnippet.astro'; import SimpleBlocObserverOnTransitionOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionOutputSnippet.astro'; import CounterBlocOnEventSnippet from '~/components/concepts/bloc/CounterBlocOnEventSnippet.astro'; import SimpleBlocObserverOnEventSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventSnippet.astro'; import SimpleBlocObserverOnEventOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventOutputSnippet.astro'; import CounterBlocOnErrorSnippet from '~/components/concepts/bloc/CounterBlocOnErrorSnippet.astro'; import CounterBlocOnErrorOutputSnippet from '~/components/concepts/bloc/CounterBlocOnErrorOutputSnippet.astro'; import CounterCubitFullSnippet from '~/components/concepts/bloc/CounterCubitFullSnippet.astro'; import CounterBlocFullSnippet from '~/components/concepts/bloc/CounterBlocFullSnippet.astro'; import AuthenticationStateSnippet from '~/components/concepts/bloc/AuthenticationStateSnippet.astro'; import AuthenticationTransitionSnippet from '~/components/concepts/bloc/AuthenticationTransitionSnippet.astro'; import AuthenticationChangeSnippet from '~/components/concepts/bloc/AuthenticationChangeSnippet.astro'; import DebounceEventTransformerSnippet from '~/components/concepts/bloc/DebounceEventTransformerSnippet.astro'; :::note Please make sure to carefully read the following sections before working with [`package:bloc`](https://pub.dev/packages/bloc). ::: There are several core concepts that are critical to understanding how to use the bloc package. In the upcoming sections, we're going to discuss each of them in detail as well as work through how they would apply to a counter app. ## Streams :::note Check out the official [Dart Documentation](https://dart.dev/tutorials/language/streams) for more information about `Streams`. ::: A stream is a sequence of asynchronous data. In order to use the bloc library, it is critical to have a basic understanding of `Streams` and how they work. If you're unfamiliar with `Streams` just think of a pipe with water flowing through it. The pipe is the `Stream` and the water is the asynchronous data. We can create a `Stream` in Dart by writing an `async*` (async generator) function. By marking a function as `async*` we are able to use the `yield` keyword and return a `Stream` of data. In the above example, we are returning a `Stream` of integers up to the `max` integer parameter. Every time we `yield` in an `async*` function we are pushing that piece of data through the `Stream`. We can consume the above `Stream` in several ways. If we wanted to write a function to return the sum of a `Stream` of integers it could look something like: By marking the above function as `async` we are able to use the `await` keyword and return a `Future` of integers. In this example, we are awaiting each value in the stream and returning the sum of all integers in the stream. We can put it all together like so: Now that we have a basic understanding of how `Streams` work in Dart we're ready to learn about the core component of the bloc package: a `Cubit`. ## Cubit A `Cubit` is a class which extends `BlocBase` and can be extended to manage any type of state. ![Cubit Architecture](~/assets/concepts/cubit_architecture_full.png) A `Cubit` can expose functions which can be invoked to trigger state changes. States are the output of a `Cubit` and represent a part of your application's state. UI components can be notified of states and redraw portions of themselves based on the current state. :::note For more information about the origins of `Cubit` checkout [the following issue](https://github.com/felangel/cubit/issues/69). ::: ### Creating a Cubit We can create a `CounterCubit` like: When creating a `Cubit`, we need to define the type of state which the `Cubit` will be managing. In the case of the `CounterCubit` above, the state can be represented via an `int` but in more complex cases it might be necessary to use a `class` instead of a primitive type. The second thing we need to do when creating a `Cubit` is specify the initial state. We can do this by calling `super` with the value of the initial state. In the snippet above, we are setting the initial state to `0` internally but we can also allow the `Cubit` to be more flexible by accepting an external value: This would allow us to instantiate `CounterCubit` instances with different initial states like: ### Cubit State Changes Each `Cubit` has the ability to output a new state via `emit`. In the above snippet, the `CounterCubit` is exposing a public method called `increment` which can be called externally to notify the `CounterCubit` to increment its state. When `increment` is called, we can access the current state of the `Cubit` via the `state` getter and `emit` a new state by adding 1 to the current state. :::caution The `emit` method is protected, meaning it should only be used inside of a `Cubit`. ::: ### Using a Cubit We can now take the `CounterCubit` we've implemented and put it to use! #### Basic Usage In the above snippet, we start by creating an instance of the `CounterCubit`. We then print the current state of the cubit which is the initial state (since no new states have been emitted yet). Next, we call the `increment` function to trigger a state change. Finally, we print the state of the `Cubit` again which went from `0` to `1` and call `close` on the `Cubit` to close the internal state stream. #### Stream Usage `Cubit` exposes a `Stream` which allows us to receive real-time state updates: In the above snippet, we are subscribing to the `CounterCubit` and calling print on each state change. We are then invoking the `increment` function which will emit a new state. Lastly, we are calling `cancel` on the `subscription` when we no longer want to receive updates and closing the `Cubit`. :::note `await Future.delayed(Duration.zero)` is added for this example to avoid canceling the subscription immediately. ::: :::caution Only subsequent state changes will be received when calling `listen` on a `Cubit`. ::: ### Observing a Cubit When a `Cubit` emits a new state, a `Change` occurs. We can observe all changes for a given `Cubit` by overriding `onChange`. We can then interact with the `Cubit` and observe all changes output to the console. The above example would output: :::note A `Change` occurs just before the state of the `Cubit` is updated. A `Change` consists of the `currentState` and the `nextState`. ::: #### BlocObserver One added bonus of using the bloc library is that we can have access to all `Changes` in one place. Even though in this application we only have one `Cubit`, it's fairly common in larger applications to have many `Cubits` managing different parts of the application's state. If we want to be able to do something in response to all `Changes` we can simply create our own `BlocObserver`. :::note All we need to do is extend `BlocObserver` and override the `onChange` method. ::: In order to use the `SimpleBlocObserver`, we just need to tweak the `main` function: The above snippet would then output: :::note The internal `onChange` override is called first, which calls `super.onChange` notifying the `onChange` in the `BlocObserver`. ::: :::tip In `BlocObserver` we have access to the `Cubit` instance in addition to the `Change` itself. ::: ### Cubit Error Handling Every `Cubit` has an `addError` method which can be used to indicate that an error has occurred. :::note `onError` can be overridden within the `Cubit` to handle all errors for a specific `Cubit`. ::: `onError` can also be overridden in `BlocObserver` to handle all reported errors globally. If we run the same program again we should see the following output: ## Bloc A `Bloc` is a more advanced class which relies on `events` to trigger `state` changes rather than functions. `Bloc` also extends `BlocBase` which means it has a similar public API as `Cubit`. However, rather than calling a `function` on a `Bloc` and directly emitting a new `state`, `Blocs` receive `events` and convert the incoming `events` into outgoing `states`. ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) ### Creating a Bloc Creating a `Bloc` is similar to creating a `Cubit` except in addition to defining the state that we'll be managing, we must also define the event that the `Bloc` will be able to process. Events are the input to a Bloc. They are commonly added in response to user interactions such as button presses or lifecycle events like page loads. Just like when creating the `CounterCubit`, we must specify an initial state by passing it to the superclass via `super`. ### Bloc State Changes `Bloc` requires us to register event handlers via the `on` API, as opposed to functions in `Cubit`. An event handler is responsible for converting any incoming events into zero or more outgoing states. :::tip An `EventHandler` has access to the added event as well as an `Emitter` which can be used to emit zero or more states in response to the incoming event. ::: We can then update the `EventHandler` to handle the `CounterIncrementPressed` event: In the above snippet, we have registered an `EventHandler` to manage all `CounterIncrementPressed` events. For each incoming `CounterIncrementPressed` event we can access the current state of the bloc via the `state` getter and `emit(state + 1)`. :::note Since the `Bloc` class extends `BlocBase`, we have access to the current state of the bloc at any point in time via the `state` getter just like in `Cubit`. ::: :::caution Blocs should never directly `emit` new states. Instead every state change must be output in response to an incoming event within an `EventHandler`. ::: :::caution Both blocs and cubits will ignore duplicate states. If we emit `State nextState` where `state == nextState`, then no state change will occur. ::: ### Using a Bloc At this point, we can create an instance of our `CounterBloc` and put it to use! #### Basic Usage In the above snippet, we start by creating an instance of the `CounterBloc`. We then print the current state of the `Bloc` which is the initial state (since no new states have been emitted yet). Next, we add the `CounterIncrementPressed` event to trigger a state change. Finally, we print the state of the `Bloc` again which went from `0` to `1` and call `close` on the `Bloc` to close the internal state stream. :::note `await Future.delayed(Duration.zero)` is added to ensure we wait for the next event-loop iteration (allowing the `EventHandler` to process the event). ::: #### Stream Usage Just like with `Cubit`, a `Bloc` is a special type of `Stream`, which means we can also subscribe to a `Bloc` for real-time updates to its state: In the above snippet, we are subscribing to the `CounterBloc` and calling print on each state change. We are then adding the `CounterIncrementPressed` event which triggers the `on` `EventHandler` and emits a new state. Lastly, we are calling `cancel` on the subscription when we no longer want to receive updates and closing the `Bloc`. :::note `await Future.delayed(Duration.zero)` is added for this example to avoid canceling the subscription immediately. ::: ### Observing a Bloc Since `Bloc` extends `BlocBase`, we can observe all state changes for a `Bloc` using `onChange`. We can then update `main.dart` to: Now if we run the above snippet, the output will be: One key differentiating factor between `Bloc` and `Cubit` is that because `Bloc` is event-driven, we are also able to capture information about what triggered the state change. We can do this by overriding `onTransition`. The change from one state to another is called a `Transition`. A `Transition` consists of the current state, the event, and the next state. If we then rerun the same `main.dart` snippet from before, we should see the following output: :::note `onTransition` is invoked before `onChange` and contains the event which triggered the change from `currentState` to `nextState`. ::: #### BlocObserver Just as before, we can override `onTransition` in a custom `BlocObserver` to observe all transitions that occur from a single place. We can initialize the `SimpleBlocObserver` just like before: Now if we run the above snippet, the output should look like: :::note `onTransition` is invoked first (local before global) followed by `onChange`. ::: Another unique feature of `Bloc` instances is that they allow us to override `onEvent` which is called whenever a new event is added to the `Bloc`. Just like with `onChange` and `onTransition`, `onEvent` can be overridden locally as well as globally. We can run the same `main.dart` as before and should see the following output: :::note `onEvent` is called as soon as the event is added. The local `onEvent` is invoked before the global `onEvent` in `BlocObserver`. ::: ### Bloc Error Handling Just like with `Cubit`, each `Bloc` has an `addError` and `onError` method. We can indicate that an error has occurred by calling `addError` from anywhere inside our `Bloc`. We can then react to all errors by overriding `onError` just as with `Cubit`. If we rerun the same `main.dart` as before, we can see what it looks like when an error is reported: :::note The local `onError` is invoked first followed by the global `onError` in `BlocObserver`. ::: :::note `onError` and `onChange` work the exact same way for both `Bloc` and `Cubit` instances. ::: :::caution Any unhandled exceptions that occur within an `EventHandler` are also reported to `onError`. ::: ## Cubit vs. Bloc Now that we've covered the basics of the `Cubit` and `Bloc` classes, you might be wondering when you should use `Cubit` and when you should use `Bloc`. ### Cubit Advantages #### Simplicity One of the biggest advantages of using `Cubit` is simplicity. When creating a `Cubit`, we only have to define the state as well as the functions which we want to expose to change the state. In comparison, when creating a `Bloc`, we have to define the states, events, and the `EventHandler` implementation. This makes `Cubit` easier to understand and there is less code involved. Now let's take a look at the two counter implementations: ##### CounterCubit ##### CounterBloc The `Cubit` implementation is more concise and instead of defining events separately, the functions act like events. In addition, when using a `Cubit`, we can simply call `emit` from anywhere in order to trigger a state change. ### Bloc Advantages #### Traceability One of the biggest advantages of using `Bloc` is knowing the sequence of state changes as well as exactly what triggered those changes. For state that is critical to the functionality of an application, it might be very beneficial to use a more event-driven approach in order to capture all events in addition to state changes. A common use case might be managing `AuthenticationState`. For simplicity, let's say we can represent `AuthenticationState` via an `enum`: There could be many reasons as to why the application's state could change from `authenticated` to `unauthenticated`. For example, the user might have tapped a logout button and requested to be signed out of the application. On the other hand, maybe the user's access token was revoked and they were forcefully logged out. When using `Bloc` we can clearly trace how the application state got to a certain state. The above `Transition` gives us all the information we need to understand why the state changed. If we had used a `Cubit` to manage the `AuthenticationState`, our logs would look like: This tells us that the user was logged out but it doesn't explain why which might be critical to debugging and understanding how the state of the application is changing over time. #### Advanced Event Transformations Another area in which `Bloc` excels over `Cubit` is when we need to take advantage of reactive operators such as `buffer`, `debounceTime`, `throttle`, etc. :::tip See [`package:stream_transform`](https://pub.dev/packages/stream_transform) and [`package:rxdart`](https://pub.dev/packages/rxdart) for stream transformers. ::: `Bloc` has an event sink that allows us to control and transform the incoming flow of events. For example, if we were building a real-time search, we would probably want to debounce the requests to the backend in order to avoid getting rate-limited as well as to cut down on cost/load on the backend. With `Bloc` we can provide a custom `EventTransformer` to change the way incoming events are processed by the `Bloc`. With the above code, we can easily debounce the incoming events with very little additional code. :::tip Check out [`package:bloc_concurrency`](https://pub.dev/packages/bloc_concurrency) for an opinionated set of event transformers. ::: If you are unsure about which to use, start with `Cubit` and you can later refactor or scale-up to a `Bloc` as needed. ================================================ FILE: docs/src/content/docs/bn/architecture.mdx ================================================ --- title: আর্কিটেকচার description: Bloc ব্যবহার করার সময় সুপারিশকৃত আর্কিটেকচার প্যাটার্নগুলোর একটি ওভারভিউ। --- import DataProviderSnippet from '~/components/architecture/DataProviderSnippet.astro'; import RepositorySnippet from '~/components/architecture/RepositorySnippet.astro'; import BusinessLogicComponentSnippet from '~/components/architecture/BusinessLogicComponentSnippet.astro'; import BlocTightCouplingSnippet from '~/components/architecture/BlocTightCouplingSnippet.astro'; import BlocLooseCouplingPresentationSnippet from '~/components/architecture/BlocLooseCouplingPresentationSnippet.astro'; import AppIdeasRepositorySnippet from '~/components/architecture/AppIdeasRepositorySnippet.astro'; import AppIdeaRankingBlocSnippet from '~/components/architecture/AppIdeaRankingBlocSnippet.astro'; import PresentationComponentSnippet from '~/components/architecture/PresentationComponentSnippet.astro'; ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) Bloc লাইব্রেরি ব্যবহার করলে আমরা আমাদের অ্যাপ্লিকেশনকে তিনটি স্তরে ভাগ করতে পারি: - প্রেজেন্টেশন - বিজনেস লজিক - ডেটা - রিপোজিটরি - ডেটা প্রোভাইডার আমরা ব্যবহারকারীর ইন্টারফেস থেকে সবচেয়ে দূরের (সবচেয়ে নিচের) স্তর থেকে শুরু করব এবং প্রেজেন্টেশন লেয়ার পর্যন্ত উপরের দিকে এগোব। ## ডেটা লেয়ার ডেটা লেয়ারের দায়িত্ব হলো এক বা একাধিক সোর্স থেকে ডেটা উদ্ধার/ম্যানিপুলেট করা। ডেটা লেয়ার দুইটি অংশে বিভক্ত হতে পারে: - রিপোজিটরি - ডেটা প্রোভাইডার এই লেয়ারটি অ্যাপ্লিকেশনের সবচেয়ে নিচের স্তর এবং এটি ডাটাবেস, নেটওয়ার্ক রিকোয়েস্ট এবং অন্যান্য অ্যাসিঙ্ক্রোনাস ডেটা সোর্সের সাথে ইন্টারঅ্যাক্ট করে। ### ডেটা প্রোভাইডার ডেটা প্রোভাইডারের দায়িত্ব হলো র’ ডেটা সরবরাহ করা। ডেটা প্রোভাইডারটি জেনেরিক এবং বহুমুখী হওয়া উচিত। ডেটা প্রোভাইডার সাধারণত [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) অপারেশন সম্পাদনের জন্য সরল API প্রদান করবে। উদাহরণস্বরূপ, আমাদের `createData`, `readData`, `updateData`, এবং `deleteData` মেথড থাকতে পারে। ### রিপোজিটরি রিপোজিটরি লেয়ার হলো এক বা একাধিক ডেটা প্রোভাইডারের একটি র‍্যাপার, যার সাথে Bloc লেয়ার যোগাযোগ করে। যেমনটি দেখানো হয়েছে, আমাদের রিপোজিটরি লেয়ার একাধিক ডেটা প্রোভাইডারের সাথে ইন্টারঅ্যাক্ট করতে পারে এবং বিজনেস লজিক লেয়ারকে ফলাফল দেওয়ার আগে ডেটা ট্রান্সফর্ম করতে পারে। ## বিজনেস লজিক লেয়ার বিজনেস লজিক লেয়ারের দায়িত্ব হলো প্রেজেন্টেশন লেয়ার থেকে ইনপুট নিয়ে নতুন স্টেট তৈরি করা। এই লেয়ার এক বা একাধিক রিপোজিটরির উপর নির্ভর করতে পারে প্রয়োজনীয় ডেটা উদ্ধারের জন্য। বিজনেস লজিক লেয়ারকে প্রেজেন্টেশন লেয়ার এবং ডেটা লেয়ারের মধ্যে একটি ব্রিজ হিসেবে ভাবুন। এটি প্রেজেন্টেশন লেয়ার থেকে ইভেন্ট/অ্যাকশন নোটিফাই হয় এবং নতুন স্টেট তৈরি করতে রিপোজিটরির সাথে যোগাযোগ করে যা প্রেজেন্টেশন লেয়ার ব্যবহার করবে। ### Bloc-to-Bloc কমিউনিকেশন কারণ ব্লকগুলি স্ট্রিম প্রকাশ করে, তাই একটি ব্লক অন্য ব্লককে লিসেন করে তৈরি করা প্রলোভন হতে পারে। তবে এটি **করবেন না**। নিচের কোডের চেয়ে ভালো বিকল্প রয়েছে: উপরের কোডটি ত্রুটিমুক্ত হলেও, এতে একটি বড় সমস্যা রয়েছে: এটি দুইটি ব্লকের মধ্যে নির্ভরতা তৈরি করে। সাধারণভাবে, একই আর্কিটেকচার স্তরের দুটি সিবলিং এন্টিটির মধ্যে নির্ভরতা এড়ানো উচিত, কারণ এটি কঠিন এবং রক্ষণাবেক্ষণে অসুবিধাজনক। যেহেতু ব্লকগুলি বিজনেস লজিক আর্কিটেকচার লেয়ারে থাকে, কোন ব্লকই অন্য ব্লকের সম্পর্কে জানবে না। একটি ব্লককে অন্য ব্লকের পরিবর্তনের উপর রেসপন্ড করতে হলে, আপনার দুটি বিকল্প রয়েছে। আপনি সমস্যা উপরের স্তরে (প্রেজেন্টেশন লেয়ারে) বা নিচের স্তরে (ডোমেইন লেয়ারে) ঠেলতে পারেন। #### প্রেজেন্টেশন লেয়ারের মাধ্যমে ব্লক সংযোগ `BlocListener` ব্যবহার করে একটি ব্লককে লিসেন করতে এবং প্রথম ব্লক পরিবর্তিত হলে অন্য ব্লকে ইভেন্ট যোগ করতে পারেন। উপরের কোডটি `SecondBloc`-কে `FirstBloc` সম্পর্কে জানার প্রয়োজন থেকে মুক্ত রাখে, এবং লুজ-কাপলিংকে উৎসাহিত করে। [flutter_weather](/bn/tutorials/flutter-weather) অ্যাপ্লিকেশনটি এই কৌশল ব্যবহার করে [এখানে](https://github.com/felangel/bloc/blob/b4c8db938ad71a6b60d4a641ec357905095c3965/examples/flutter_weather/lib/weather/view/weather_page.dart#L38-L42) আবহাওয়ার উপর ভিত্তি করে অ্যাপের থিম পরিবর্তন করে। কিছু পরিস্থিতিতে, প্রেজেন্টেশন লেয়ারে দুইটি ব্লক কাপল করতে চাইতে নাও পারেন। বরং, দুটি ব্লক একই ডেটা সোর্স ভাগ করতে পারে এবং ডেটা পরিবর্তনের সাথে সাথে স্টেট আপডেট করতে পারে। #### ডোমেইন লেয়ারের মাধ্যমে ব্লক সংযোগ দুটি ব্লক রিপোজিটরির স্ট্রিম শুনতে পারে এবং রিপোজিটরি ডেটা পরিবর্তিত হলে স্বাধীনভাবে তাদের স্টেট আপডেট করে। বড় স্কেলের এন্টারপ্রাইজ অ্যাপ্লিকেশনে স্টেট সিঙ্ক্রোনাইজ করার জন্য রিয়েক্টিভ রিপোজিটরি ব্যবহার করা সাধারণ। প্রথমে একটি রিপোজিটরি তৈরি করুন বা ব্যবহার করুন যা একটি ডেটা `Stream` প্রদান করে। উদাহরণস্বরূপ, নিচের রিপোজিটরিটি কিছু অ্যাপ আইডিয়ার একটি অবিরাম স্ট্রিম প্রকাশ করে: একই রিপোজিটরিটি প্রতিটি ব্লকে ইঞ্জেক্ট করা যেতে পারে যা নতুন অ্যাপ আইডিয়ার জন্য রেসপন্ড করতে চায়। নিচে একটি `AppIdeaRankingBloc` দেখানো হলো যা রিপোজিটরি থেকে প্রতিটি আসা আইডিয়ার জন্য স্টেট প্রকাশ করে: ব্লকের স্ট্রিম এবং কনকারেন্সি ব্যবহার করার বিষয়ে আরও জানার জন্য দেখুন [How to use Bloc with streams and concurrency](https://verygood.ventures/blog/how-to-use-bloc-with-streams-and-concurrency)। ## প্রেজেন্টেশন লেয়ার প্রেজেন্টেশন লেয়ারের দায়িত্ব হলো এক বা একাধিক ব্লক স্টেটের উপর ভিত্তি করে নিজেকে রেন্ডার করা। এছাড়াও এটি ইউজার ইনপুট এবং অ্যাপ্লিকেশন লাইফসাইকেল ইভেন্ট হ্যান্ডেল করে। অধিকাংশ অ্যাপ্লিকেশন ফ্লো শুরু হয় একটি `AppStart` ইভেন্ট দিয়ে, যা ব্যবহারকারীর জন্য প্রদর্শনের জন্য কিছু ডেটা ফেচ করতে ট্রিগার করে। এই পরিস্থিতিতে, প্রেজেন্টেশন লেয়ার `AppStart` ইভেন্ট যোগ করবে। এছাড়াও, প্রেজেন্টেশন লেয়ারকে ব্লক লেয়ারের স্টেট অনুযায়ী স্ক্রিনে কী রেন্ডার করতে হবে তা নির্ধারণ করতে হবে। এখন পর্যন্ত, যদিও কিছু কোড স্নিপেট দেখানো হয়েছে, সবকিছু এখনও উচ্চ-স্তরের। টিউটোরিয়াল সেকশনে আমরা একাধিক উদাহরণ অ্যাপ বানিয়ে সবকিছু একত্রে দেখাবো। ================================================ FILE: docs/src/content/docs/bn/bloc-concepts.mdx ================================================ --- title: ব্লক কনসেপ্টস description: package:bloc এর প্রধান ধারণাগুলোর একটি ওভারভিউ sidebar: order: 1 --- import CountStreamSnippet from '~/components/concepts/bloc/CountStreamSnippet.astro'; import SumStreamSnippet from '~/components/concepts/bloc/SumStreamSnippet.astro'; import StreamsMainSnippet from '~/components/concepts/bloc/StreamsMainSnippet.astro'; import CounterCubitSnippet from '~/components/concepts/bloc/CounterCubitSnippet.astro'; import CounterCubitInitialStateSnippet from '~/components/concepts/bloc/CounterCubitInitialStateSnippet.astro'; import CounterCubitInstantiationSnippet from '~/components/concepts/bloc/CounterCubitInstantiationSnippet.astro'; import CounterCubitIncrementSnippet from '~/components/concepts/bloc/CounterCubitIncrementSnippet.astro'; import CounterCubitBasicUsageSnippet from '~/components/concepts/bloc/CounterCubitBasicUsageSnippet.astro'; import CounterCubitStreamUsageSnippet from '~/components/concepts/bloc/CounterCubitStreamUsageSnippet.astro'; import CounterCubitOnChangeSnippet from '~/components/concepts/bloc/CounterCubitOnChangeSnippet.astro'; import CounterCubitOnChangeUsageSnippet from '~/components/concepts/bloc/CounterCubitOnChangeUsageSnippet.astro'; import CounterCubitOnChangeOutputSnippet from '~/components/concepts/bloc/CounterCubitOnChangeOutputSnippet.astro'; import SimpleBlocObserverOnChangeSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeSnippet.astro'; import SimpleBlocObserverOnChangeUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeUsageSnippet.astro'; import SimpleBlocObserverOnChangeOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeOutputSnippet.astro'; import CounterCubitOnErrorSnippet from '~/components/concepts/bloc/CounterCubitOnErrorSnippet.astro'; import SimpleBlocObserverOnErrorSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnErrorSnippet.astro'; import CounterCubitOnErrorOutputSnippet from '~/components/concepts/bloc/CounterCubitOnErrorOutputSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/bloc/CounterBlocSnippet.astro'; import CounterBlocEventHandlerSnippet from '~/components/concepts/bloc/CounterBlocEventHandlerSnippet.astro'; import CounterBlocIncrementSnippet from '~/components/concepts/bloc/CounterBlocIncrementSnippet.astro'; import CounterBlocUsageSnippet from '~/components/concepts/bloc/CounterBlocUsageSnippet.astro'; import CounterBlocStreamUsageSnippet from '~/components/concepts/bloc/CounterBlocStreamUsageSnippet.astro'; import CounterBlocOnChangeSnippet from '~/components/concepts/bloc/CounterBlocOnChangeSnippet.astro'; import CounterBlocOnChangeUsageSnippet from '~/components/concepts/bloc/CounterBlocOnChangeUsageSnippet.astro'; import CounterBlocOnChangeOutputSnippet from '~/components/concepts/bloc/CounterBlocOnChangeOutputSnippet.astro'; import CounterBlocOnTransitionSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionSnippet.astro'; import CounterBlocOnTransitionOutputSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionOutputSnippet.astro'; import SimpleBlocObserverOnTransitionSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionSnippet.astro'; import SimpleBlocObserverOnTransitionUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionUsageSnippet.astro'; import SimpleBlocObserverOnTransitionOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionOutputSnippet.astro'; import CounterBlocOnEventSnippet from '~/components/concepts/bloc/CounterBlocOnEventSnippet.astro'; import SimpleBlocObserverOnEventSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventSnippet.astro'; import SimpleBlocObserverOnEventOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventOutputSnippet.astro'; import CounterBlocOnErrorSnippet from '~/components/concepts/bloc/CounterBlocOnErrorSnippet.astro'; import CounterBlocOnErrorOutputSnippet from '~/components/concepts/bloc/CounterBlocOnErrorOutputSnippet.astro'; import CounterCubitFullSnippet from '~/components/concepts/bloc/CounterCubitFullSnippet.astro'; import CounterBlocFullSnippet from '~/components/concepts/bloc/CounterBlocFullSnippet.astro'; import AuthenticationStateSnippet from '~/components/concepts/bloc/AuthenticationStateSnippet.astro'; import AuthenticationTransitionSnippet from '~/components/concepts/bloc/AuthenticationTransitionSnippet.astro'; import AuthenticationChangeSnippet from '~/components/concepts/bloc/AuthenticationChangeSnippet.astro'; import DebounceEventTransformerSnippet from '~/components/concepts/bloc/DebounceEventTransformerSnippet.astro'; :::note অনুগ্রহ করে package:bloc সম্পর্কিত নীচের অংশগুলো মনোযোগ দিয়ে পড়ুন। ::: কয়েকটি মূল ধারণা আছে যেগুলো bloc প্যাকেজ ব্যবহার করার সময় বোঝা খুবই জরুরি। নিচের সেকশনে আমরা এগুলো বিস্তারিতভাবে আলোচনা করব এবং একটি কাউন্টার অ্যাপ দিয়ে উদাহরণ দেখাব। ## স্ট্রিমস (Streams) :::note আরও তথ্যের জন্য অফিসিয়াল Dart ডকুমেন্টেশন দেখুন: https://dart.dev/tutorials/language/streams ::: স্ট্রিম হল অ্যাসিঙ্ক্রোনাস ডেটার একটি ক্রম। bloc লাইব্রেরি ব্যবহার করার জন্য স্ট্রিমগুলোর একটি বেসিক ধারণা থাকা জরুরি। যদি আপনি স্ট্রিম সম্পর্কে অপরিচিত হন, তাহলে একটি পাইপে পানি প্রবাহের কল্পনা করুন — পাইপ হচ্ছে স্ট্রিম এবং পানি হচ্ছে অ্যাসিঙ্ক্রোনাস ডেটা। আমরা Dart-এ একটি স্ট্রিম তৈরি করতে পারি একটি `async*` (async generator) ফাংশন লিখে। `async*` ফাংশনে `yield` ব্যবহার করে আমরা স্ট্রিমে ডেটা পুশ করতে পারি। উপরের উদাহরণে আমরা `max` পর্যন্ত পূর্ণসংখ্যার একটি স্ট্রিম রিটার্ন করছি। স্ট্রিমের প্রতিটি `yield` কল একটি নতুন ডেটা আইটেম পাঠায়। উপরের স্ট্রিমটি আমরা বিভিন্নভাবে কনজিউম করতে পারি। যদি আমরা স্ট্রিমের সব সংখ্যার যোগফল ফেরত দিতে চাই, তাহলে একটি ফাংশন হতে পারে: উপরের ফাংশনটি `async` হওয়ায় আমরা `await` ব্যবহার করে স্ট্রিম থেকে প্রতিটি মান সংগ্রহ করে একটি `Future` রিটার্ন করছি। সবকিছু একসাথে করা যেতে পারে এইভাবে: স্ট্রিম সম্পর্কে এই মৌলিক ধারণা হলে আমরা bloc প্যাকেজের মূল উপাদান — Cubit — শেখার জন্য প্রস্তুত। ## কিউবিট (Cubit) Cubit হল একটি ক্লাস যা `BlocBase` থেকে এক্সটেন্ড করে এবং যেকোনো ধরনের state হ্যান্ডেল করতে ব্যবহৃত হয়। ![Cubit Architecture](~/assets/concepts/cubit_architecture_full.png) Cubit এমন ফাংশন এক্সপোজ করতে পারে যা কল করলে state পরিবর্তন ট্রিগার হয়। State হল Cubit-র আউটপুট এবং আপনার অ্যাপ্লিকেশনের অংশবিশেষকে প্রতিনিধিত্ব করে। UI কম্পোনেন্টগুলো current state অনুযায়ী রিল-রেন্ডার করতে পারে। :::note Cubit-এর উত্স সম্পর্কে আরও জানতে এই ইস্যুটি পড়তে পারেন: https://github.com/felangel/cubit/issues/69 ::: ### Cubit তৈরি করা আমরা একটি CounterCubit তৈরি করতে পারি এভাবে: Cubit তৈরি করার সময় আমাদের state-এর টাইপ নির্ধারণ করতে হবে। CounterCubit-এর ক্ষেত্রে state একটি `int`, কিন্তু জটিল পরিস্থিতিতে একটি `class` ব্যবহার করা যেতে পারে। আরেকটি গুরুত্বপূর্ণ বিষয় হল initial state নির্ধারণ করা। এটা আমরা `super`-এ ভ্যালু দিয়ে করতে পারি। উপরের উদাহরণে initial state ভিতর থেকেই `0` সেট করা হয়েছে, অথবা Cubit কে বাহ্যিকভাবে আরও ফ্লেক্সিবল করা যায়: এভাবে আমরা ভিন্ন initial state দিয়ে CounterCubit ইনস্ট্যানশিয়েট করতে পারি: ### Cubit state পরিবর্তন প্রতিটি Cubit নতুন state আউটপুট করার জন্য `emit` ব্যবহার করে। উপরের উদাহরণে CounterCubit একটি public method `increment` এক্সপোজ করেছে। `increment` কল করলে আমরা current state (`state` getter) পড়ে `emit(state + 1)` করি। :::caution `emit` মেথডটি প্রোটেক্টেড — অর্থাৎ এটি শুধুমাত্র Cubit-এর ভিতরে ব্যবহার করবেন। ::: ### Cubit ব্যবহার করা এখন আমরা CounterCubit তৈরি করে ব্যবহার করব। #### বেসিক ব্যবহার উপরের উদাহরণে আমরা প্রথমে CounterCubit ইনস্ট্যানশিয়েট করি। তারপর বর্তমান state প্রিন্ট করি (এটি initial state)। এরপর `increment` কল করে state পরিবর্তন করি এবং আবার state প্রিন্ট করি। শেষে `Cubit.close()` করে স্ট্রিম বন্ধ করি। #### স্ট্রিম ব্যবহার Cubit একটি স্ট্রিম এক্সপোজ করে যাতে রিয়েল-টাইম আপডেট নেওয়া যায়: উপরের উদাহরণে আমরা Cubit-এ subscribe করে প্রতিটি state পরিবর্তনে print করছি, তারপর `increment` কল করছি এবং শেষে `subscription.cancel()` করে সাবস্ক্রিপশন বন্ধ করছি। `Cubit.close()` করে Cubit বন্ধ করা হয়। :::note উদাহরণে `await Future.delayed(Duration.zero)` দেওয়া আছে যাতে সাবস্ক্রিপশন একেবারেই তাড়াতাড়ি ক্যানসেল না হয়ে পরের ইভেন্ট-লুপ মিলানো যায়। ::: :::caution Cubit-এ listen করলে শুধুমাত্র পরবর্তী state পরিবর্তনগুলোই পাবেন (past states নয়)। ::: ### Cubit পর্যবেক্ষণ (Observing a Cubit) Cubit একটি নতুন state emit করলে সেটা একটি `Change` হিসেবে ধরা হয়। Cubit-এর `onChange` override করে আমরা সব চেঞ্জ পর্যবেক্ষণ করতে পারি: পরে Cubit চালিয়ে আমরা কনসোলে সব চেঞ্জ দেখতে পারি: উপরের উদাহরণ আউটপুট হবে: :::note `Change` ঘটে ঠিক আগে যখন Cubit-এর state আপডেট হয়। একটি `Change`-এ `currentState` এবং `nextState` থাকে। ::: #### BlocObserver bloc লাইব্রেরির একটি সুবিধা হচ্ছে আমরা সব `Changes` এক জায়গায় দেখতে পারি। বড় অ্যাপে বহু Cubit থাকতে পারে, তাই সব Changes কেন্দ্রিয়ভাবে হ্যান্ডল করতে BlocObserver বানানো যেতে পারে। :::note শুধু `BlocObserver` এক্সটেন্ড করে `onChange` override করলেই হয়। ::: SimpleBlocObserver ব্যবহার করতে main-এ একটু পরিবর্তন করতে হবে: তারপর আউটপুট হবে: :::note লোকাল (internal) `onChange` আগে কল হয় এবং `super.onChange` ডাকে যাতে global BlocObserver-ও নোটিফাই হয়। ::: :::tip BlocObserver-এ আপনার কাছে Cubit ইনস্ট্যান্সও অ্যাক্সেস থাকে, change ছাড়াও। ::: ### Cubit ত্রুটি হ্যান্ডলিং প্রতি Cubit-এর `addError` মেথড আছে যা কোনো ত্রুটি ইঙ্গিত করতে ব্যবহার করা যায়। :::note Cubit-এর ভিতরে `onError` ওভাররাইড করে নির্দিষ্ট Cubit-এর সব এরর হ্যান্ডল করা যায়। ::: গ্লোবালি সব রিপোর্টেড এরর হ্যান্ডল করতে BlocObserver-এ `onError` ওভাররাইড করা যায়। উপরের প্রোগ্রাম চালালে আমরা দেখতে পাব: ## Bloc Bloc একটি উন্নত ক্লাস যা ইভেন্ট-ভিত্তিকভাবে state পরিবর্তন করে; এটি `BlocBase`-ও এক্সটেন্ড করে। Cubit-এর তুলনায় Bloc-এ সরাসরি ফাংশন কল করে state পরিবর্তন না করে, পরিবর্তে ইভেন্ট যোগ করে Bloc সেই ইভেন্টগুলোকে state-এ রূপান্তর করে। ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) ### Bloc তৈরি করা Bloc তৈরি করা Cubit-এর মতোই, বরং অতিরিক্তভাবে আপনাকে কোন ইভেন্টগুলি Bloc প্রক্রিয়াকরণ করবে তা নির্ধারণ করতে হবে। ইভেন্ট হল Bloc-এর ইনপুট। সাধারণত ইউজার অ্যাকশন (বাটন ট্যাপ) বা লাইফসাইকেল ইভেন্টের ফলে ইভেন্ট যোগ করা হয়। Cubit-এর মতোই Bloc-এও initial state `super`-এ দিয়ে নির্ধারণ করতে হবে। ### Bloc state পরিবর্তন Bloc-এ ইভেন্ট-হ্যান্ডলার অনুজায়ীভাবে `on` API দিয়ে রেজিস্টার করা হয়। ইভেন্ট-হ্যান্ডলার ইনকামিং ইভেন্ট থেকে শূন্য বা একাধিক স্টেট ইমিট করে। :::tip EventHandler-এ আপনার কাছে event এবং একটি `Emitter` থাকবে, যেটি ব্যবহার করে শূন্য বা একাধিক state ইমিট করা যায়। ::: আমরা এখন CounterIncrementPressed ইভেন্ট হ্যান্ডল করতে পারি: উপরের উদাহরণে `on` রেজিস্টার করা হয়েছে। প্রতিটি ইভেন্টে current state পড়ে `emit(state + 1)` করা হচ্ছে। :::note Bloc ক্লাস `BlocBase` এক্সটেন্ড করে, তাই current state যেকোনো সময় `state` গেটারের মাধ্যমে অ্যাক্সেস করা যাবে — ঠিক Cubit-এর মতোই। ::: :::caution Blocs সরাসরি `emit` করা উচিত নয়; প্রতিটি state change অবশ্যই ইভেন্ট-হ্যান্ডলারের মধ্যে ইমিট করা উচিত। ::: :::caution Blocs এবং Cubits উভয়ই duplicate states উপেক্ষা করে। যদি emit করা nextState পূর্ববর্তী state এর সমান হয়, তাহলে কোন স্টেট পরিবর্তন হবে না। ::: ### Bloc ব্যবহার করা এখন আমরা CounterBloc ইনস্ট্যানশিয়েট করে ব্যবহার করব। #### বেসিক ব্যবহার উপরের উদাহরণে আমরা Bloc তৈরি করে current state প্রিন্ট করেছি, তারপর `CounterIncrementPressed` ইভেন্ট add করে state পরিবর্তন করেছি এবং শেষেও `close()` করে Bloc বন্ধ করেছি। :::note ইভেন্ট-হ্যান্ডলারকে প্রক্রিয়া করার জন্য `await Future.delayed(Duration.zero)` যোগ করা হয়েছে যাতে পরের ইভেন্ট-লুপের ইটারেশন পর্যন্ত অপেক্ষা করা যায়। ::: #### স্ট্রিম ব্যবহার Cubit-এর মতোই Bloc-ও স্ট্রিম; আপনি Bloc সাবস্ক্রাইব করে রিয়েল-টাইম state আপডেট পেতে পারেন: উপরের উদাহরণে আমরা Bloc সাবস্ক্রাইব করে প্রতিটি state পরিবর্তন print করছি, তারপর ইভেন্ট add করছি এবং শেষে `subscription.cancel()` ও `Bloc.close()` করেছি। :::note উদাহরণে `await Future.delayed(Duration.zero)` দেওয়া হয়েছে যাতে সাবস্ক্রিপশন সরাসরি ক্যানসেল না হয়। ::: ### Bloc পর্যবেক্ষণ Cubit-এর মতো Bloc-এও `onChange` override করে state পরিবর্তন পর্যবেক্ষণ করা যায়। main.dart আপডেট করলে: এরপর আউটপুট হবে: Bloc-এর একটি গুরুত্বপূর্ণ পার্থক্য হল এটি ইভেন্ট-চৌকশ হওয়ায় আমরা ঠিক কী ইভেন্ট ট্রিগার করেছে তা ট্র্যাক করতে পারি। এজন্য `onTransition` override করে আমরা transition সম্পর্কিত তথ্য পেতে পারি। Transition হচ্ছে current state, event এবং next state — এই তিনটির মিশ্রণ। উপরের main-টি আবার চালালে আমরা পাব: :::note `onTransition` `onChange`-এর আগে invoked হয় এবং এতে কোন ইভেন্টে `currentState` থেকে `nextState`-এ পরিবর্তন ঘটল তা থাকে। ::: #### BlocObserver ওইভাবে BlocObserver-এ `onTransition` override করলে সব Bloc-এ ঘটা transition এক জায়গায় দেখা যাবে। SimpleBlocObserver initialization ঠিক আগের মতোই করা যাবে: এরপর আউটপুট হবে: :::note `onTransition` প্রথমে (লোকাল আগে, গ্লোবাল পরে) কল হয়ে থাকে, এরপর `onChange` চলে। ::: আরেকটি ইউনিক ফিচার হল Bloc-এ `onEvent` override করা যায় — যেটা তখন কল হয় যখন নতুন ইভেন্ট যোগ করা হয়। লোকাল `onEvent` প্রথমে, তারপর global `BlocObserver.onEvent` কল হয়। তারপর আউটপুট হবে: :::note `onEvent` ইভেন্ট যোগ করার সঙ্গে সঙ্গেই কল হয়। লোকাল `onEvent` আগে, গ্লোবাল পরে কল হয়। ::: ### Bloc ত্রুটি হ্যান্ডলিং Cubit-ও Bloc-ও `addError` এবং `onError` মেথড প্রদান করে। Bloc-এর ভেতর কোথাও `addError` কল করলে that error রিপোর্ট হবে এবং local অথবা global `onError`-এ হ্যান্ডল করা যাবে। উপরের main-টি আবার চালালে error রিপোর্ট কেমন লাগে তা দেখা যাবে: :::note লোকাল `onError` আগে, তারপর গ্লোবাল `BlocObserver.onError` কল হয়। ::: :::note `onError` এবং `onChange` উভয়ই Bloc ও Cubit-এ একইভাবে কাজ করে। ::: :::caution EventHandler-এ কোনো unhandled exception হলে সেটাও `onError`-এ রিপোর্ট করা হয়। ::: ## Cubit vs Bloc Cubit এবং Bloc-এর মৌলিক পার্থক্য এখন covered হয়েছে; আপনি হয়তো চাইবেন কখন Cubit ব্যবহার করবেন এবং কখন Bloc। ### Cubit সুবিধা #### সরলতা Cubit-এর প্রধান সুবিধা হল সরলতা। Cubit তৈরি করতে কেবল state এবং সেই state পরিবর্তনের জন্য যে ফাংশনগুলো দরকার সেগুলোই লিখতে হয়। Bloc-এ events, states এবং EventHandler লিখতে হয় — ফলে Cubit কোড কম এবং বোঝা সহজ। নিচে দুইটি কাউন্টার ইমপ্লিমেন্টেশন: ##### CounterCubit ##### CounterBloc Cubit ইমপ্লিমেন্টেশন সংক্ষিপ্ত; এখানে আলাদা ইভেন্ট ডিফাইন করার দরকার পড়ে না — ফাংশনগুলো ইভেন্টের কাজ করে। Cubit-এ `emit` যেখানেই প্রয়োজন সেখানেই করা যায়। ### Bloc সুবিধা #### ট্রেসযোগ্যতা (Traceability) Bloc-এর বড় সুবিধা হল state পরিবর্তনের ক্রম এবং ঠিক কী কারণে পরিবর্তন ঘটল সেটা ট্র্যাক করা যায়। `AuthenticationState` এর মত গুরুত্বপূর্ণ state-এ Bloc-এর মাধ্যমে ইভেন্ট-ভিত্তিক লগ রাখা বেশ কার্যকর। সরলতার জন্য `AuthenticationState` কে enum ধরে নেওয়া যাক: state পরিবর্তনের অনেক কারণ থাকতে পারে — উদাহরণ: ইউজার লগআউট ট্যাপ করেছে অথবা টোকেন বাতিল হয়েছে। Bloc-এ Transition আপনাকে জানায় কোন ইভেন্টের ফলে state বদলেছে: এই Transition আমাদের প্রয়োজনীয় তথ্য দেয়। Cubit ব্যবহার করলে আমরা শুধুমাত্র Change দেখতে পারি: এই লোগে বলা থাকে যে ব্যবহারকারী লগআউট হয়েছে, কিন্তু কেন তা স্পষ্ট থাকে না — যা ডিবাগিং-এ অসুবিধা করতে পারে। #### অ্যাডভান্সড ইভেন্ট ট্রান্সফর্মেশন আরেকটি জায়গা যেখানে Bloc এগিয়ে তা হলো reactive অপারেটর (`buffer`, `debounceTime`, `throttle` ইত্যাদি) ব্যবহার করে ইনকামিং ইভেন্টগুলো কিভাবে ট্রান্সফর্ম করা হবে তা নিয়ন্ত্রণ করা যায়। :::tip stream_transform এবং rxdart প্যাকেজগুলো দেখুন: https://pub.dev/packages/stream_transform , https://pub.dev/packages/rxdart ::: Bloc-এ ইভেন্ট সিংক থাকে যা ইনকামিং ইভেন্টের ফ্লো কন্ট্রোল ও ট্রান্সফর্ম করতে দেয়। উদাহরণস্বরূপ, রিয়েল-টাইম সার্চে অনুরোধগুলো debounce করলে ব্যাকেন্ড-এ রেট লিমিটিং বা অতিরিক্ত লোড কমানো যায়। Bloc-এ কাস্টম `EventTransformer` ব্যবহার করে ইনকামিং ইভেন্ট ডিবাউন্স করা যায়: উপরের কোড দিয়ে ইনকামিং ইভেন্ট সহজে ডিবাউন্স করা যায়। :::tip bloc_concurrency প্যাকেজ দেখুন; এটি কিছু প্রচলিত EventTransformer প্রদান করে: https://pub.dev/packages/bloc_concurrency ::: নিশ্চয় না হলে Cubit দিয়ে শুরু করুন; পরে প্রয়োজনে Bloc-এ রিফ্যাক্টর করতে পারবেন। ================================================ FILE: docs/src/content/docs/bn/faqs.mdx ================================================ --- title: FAQs description: bloc লাইব্রেরি সম্পর্কিত প্রায়শই জিজ্ঞাসিত প্রশ্নগুলোর উত্তর। --- import StateNotUpdatingGood1Snippet from '~/components/faqs/StateNotUpdatingGood1Snippet.astro'; import StateNotUpdatingGood2Snippet from '~/components/faqs/StateNotUpdatingGood2Snippet.astro'; import StateNotUpdatingGood3Snippet from '~/components/faqs/StateNotUpdatingGood3Snippet.astro'; import StateNotUpdatingBad1Snippet from '~/components/faqs/StateNotUpdatingBad1Snippet.astro'; import StateNotUpdatingBad2Snippet from '~/components/faqs/StateNotUpdatingBad2Snippet.astro'; import StateNotUpdatingBad3Snippet from '~/components/faqs/StateNotUpdatingBad3Snippet.astro'; import EquatableEmitSnippet from '~/components/faqs/EquatableEmitSnippet.astro'; import EquatableBlocTestSnippet from '~/components/faqs/EquatableBlocTestSnippet.astro'; import NoEquatableBlocTestSnippet from '~/components/faqs/NoEquatableBlocTestSnippet.astro'; import SingleStateSnippet from '~/components/faqs/SingleStateSnippet.astro'; import SingleStateUsageSnippet from '~/components/faqs/SingleStateUsageSnippet.astro'; import BlocProviderGood1Snippet from '~/components/faqs/BlocProviderGood1Snippet.astro'; import BlocProviderGood2Snippet from '~/components/faqs/BlocProviderGood2Snippet.astro'; import BlocProviderBad1Snippet from '~/components/faqs/BlocProviderBad1Snippet.astro'; import BlocInternalAddEventSnippet from '~/components/faqs/BlocInternalAddEventSnippet.astro'; import BlocInternalEventSnippet from '~/components/faqs/BlocInternalEventSnippet.astro'; import BlocExternalForEachSnippet from '~/components/faqs/BlocExternalForEachSnippet.astro'; ## State Not Updating ❔ **প্রশ্ন**: আমি আমার bloc-এ state emit করছি কিন্তু UI আপডেট হচ্ছে না। আমি কী ভুল করছি? 💡 **উত্তর**: আপনি যদি Equatable ব্যবহার করেন তাহলে props getter-এ অবশ্যই সমস্ত প্রপার্টি পাস করছেন কিনা নিশ্চিত করুন। ✅ **ভাল** ❌ **খারাপ** এছাড়াও, bloc-এর ভিতরে আপনি একটি নতুন state instance emit করছেন তা নিশ্চিত করুন। ✅ **ভাল** ❌ **খারাপ** :::caution `Equatable` প্রপার্টিগুলো কখনও modify করা উচিত নয়, বরং সবসময় copy করা উচিত। যদি কোনো `Equatable` ক্লাসে `List` বা `Map` থাকে, তবে রেফারেন্সের উপর ভিত্তি না করে মানের উপর ভিত্তি করে equality নির্ধারণ করতে যথাক্রমে `List.of` বা `Map.of` ব্যবহার করুন। ::: ## Equatable কবে ব্যবহার করবেন ❔**প্রশ্ন**: কবে আমি Equatable ব্যবহার করব? 💡**উত্তর**: উপরের ক্ষেত্রে যদি `StateA` `Equatable` এক্সটেন্ড করে তবে শুধুমাত্র একটি state change হবে (দ্বিতীয় emit উপেক্ষা করা হবে)। সাধারণভাবে, আপনি যদি rebuild কমিয়ে অপ্টিমাইজ করতে চান তাহলে `Equatable` ব্যবহার করা উচিত। আপনি যদি একই state back-to-back হলেও একাধিক transition চান তাহলে `Equatable` ব্যবহার করা উচিত নয়। এছাড়াও, `Equatable` ব্যবহারে bloc টেস্ট করা সহজ হয় কারণ আমরা নির্দিষ্ট স্টেট instance আশা করতে পারি `Matchers` বা `Predicates` ব্যবহারের পরিবর্তে। `Equatable` ছাড়া উপরের টেস্ট ব্যর্থ হবে এবং এভাবে পুনঃলিখন করতে হবে: ## ত্রুটি (Error) হ্যান্ডলিং ❔ **প্রশ্ন**: আগের ডেটা দেখানো অবস্থায় কিভাবে আমি error হ্যান্ডেল করব? 💡 **উত্তর**: এটি bloc-এর state কিভাবে মডেল করা হয়েছে তার উপর অনেকটাই নির্ভর করে। এমন ক্ষেত্রে যেখানে error থাকলেও ডেটা রেখে দেওয়া উচিত, একটি single state class ব্যবহার করার কথা বিবেচনা করুন। এতে widget গুলো একসাথে `data` এবং `error` পাওয়ার সুবিধা পাবে এবং bloc `state.copyWith` ব্যবহার করে error থাকলেও পুরনো ডেটা ধরে রাখতে পারবে। ## Bloc বনাম Redux ❔ **প্রশ্ন**: Bloc এবং Redux-এর মধ্যে পার্থক্য কী? 💡 **উত্তর**: BLoC একটি ডিজাইন প্যাটার্ন যা নিম্নলিখিত নিয়ম দ্বারা সংজ্ঞায়িত: 1. BLoC-এর Input এবং Output শুধু Streams এবং Sinks। 2. Dependencies injectable এবং প্ল্যাটফর্ম-অ্যাগনস্টিক হতে হবে। 3. কোনো প্ল্যাটফর্ম branching অনুমোদিত নয়। 4. উপরের নিয়ম মেনে চললে যেকোনো implementation ব্যবহার করা যায়। UI নির্দেশিকা: 1. প্রতিটি “যথেষ্ট জটিল” কম্পোনেন্টের একটি BLoC থাকে। 2. কম্পোনেন্টগুলো ইনপুট পাঠাবে “as is”। 3. কম্পোনেন্টগুলো আউটপুট দেখাবে যতটা সম্ভব “as is”। 4. সকল branching হবে সহজ BLoC boolean আউটপুটের উপর ভিত্তি করে। Bloc লাইব্রেরি BLoC ডিজাইন প্যাটার্ন বাস্তবায়ন করে এবং RxDart abstraction দিয়ে developer experience সহজ করে। Redux-এর তিনটি নীতি: 1. Single source of truth 2. State read-only 3. পরিবর্তন pure functions দিয়ে করা হয় bloc লাইব্রেরি প্রথম নীতি লঙ্ঘন করে; bloc state বহু bloc-এ বিতরণ করা হয়। এছাড়াও bloc-এ middleware-এর ধারণা নেই এবং async state পরিবর্তন খুব সহজ, যেখানে একটি event-এর জন্য একাধিক state emit করা যায়। ## Bloc বনাম Provider ❔ **প্রশ্ন**: Bloc আর Provider-এর মধ্যে পার্থক্য কী? 💡 **উত্তর**: `provider` dependency injection-এর জন্য তৈরি (এটি `InheritedWidget` কে wrap করে)। আপনাকে এখনও state ম্যানেজমেন্ট বেছে নিতে হবে (`ChangeNotifier`, `Bloc`, `Mobx`, ইত্যাদি)। Bloc লাইব্রেরি অভ্যন্তরীণভাবে `provider` ব্যবহার করে widget tree তে bloc সরবরাহ ও ব্যবহারের সুবিধার জন্য। ## BlocProvider.of() Bloc খুঁজে পাচ্ছে না ❔ **প্রশ্ন**: `BlocProvider.of(context)` ব্যবহার করলে bloc খুঁজে পায় না। আমি কীভাবে এটি ঠিক করব? 💡 **উত্তর**: একই context থেকে bloc অ্যাক্সেস করা যায় না যেখানে এটি provide করা হয়েছে। তাই নিশ্চিত করুন `BlocProvider.of()` একটি child `BuildContext` এর ভিতরে কল হচ্ছে। ✅ **ভাল** ❌ **খারাপ** ## Project Structure ❔ **প্রশ্ন**: আমি আমার প্রজেক্ট কীভাবে গঠন করব? 💡 **উত্তর**: এই প্রশ্নের আসলে ঠিক/ভুল বলে কিছু নেই, কিছু সুপারিশকৃত উদাহরণ হলো: - [I/O Photobooth](https://github.com/flutter/photobooth) - [I/O Pinball](https://github.com/flutter/pinball) - [Flutter News Toolkit](https://github.com/flutter/news_toolkit) সবচেয়ে গুরুত্বপূর্ণ হলো **একটি সুনির্দিষ্ট** এবং **উদ্দেশ্যপূর্ণ** project structure বজায় রাখা। ## Bloc-এর ভিতরে Events যোগ করা ❔ **প্রশ্ন**: একটি bloc-এর ভিতরে event যোগ করা কি ঠিক? 💡 **উত্তর**: বেশিরভাগ ক্ষেত্রে event বাইরে থেকে যোগ করা উচিত, তবে কিছু ক্ষেত্রে ভিতরে যোগ করা যুক্তিসঙ্গত। সবচেয়ে সাধারণ অভ্যন্তরীণ event ব্যবহারের পরিস্থিতি হলো যখন repository থেকে real-time আপডেটে state পরিবর্তন করতে হয়। এখানে repository হল state পরিবর্তনের stimulus, কোনো বাহ্যিক ইভেন্ট (যেমন বাটন ট্যাপ) নয়। নিচের উদাহরণে, `MyBloc` এর state নির্ভর করে বর্তমান user-এর উপর, যা `UserRepository` থেকে `Stream` হিসাবে আসে। `MyBloc` user stream শুনে এবং যখনই একটি user emit হয় তখন অভ্যন্তরীণ `_UserChanged` event যোগ করে। অভ্যন্তরীণ event যোগ করে আমরা event-এর জন্য custom `transformer` নির্ধারণ করতে পারি — ডিফল্টভাবে সেগুলো concurrent প্রসেস হয়। Internal event ব্যক্তিগত (private) রাখা অত্যন্ত সুপারিশ করা হয়। এটি স্পষ্টভাবে বোঝায় যে event শুধুমাত্র bloc-এর ভিতর ব্যবহৃত হয় এবং বাইরের কম্পোনেন্ট তা অ্যাক্সেস করতে পারে না। আমরা বিকল্পভাবে একটি বাহ্যিক `Started` event ব্যবহার করতে পারি এবং `emit.forEach` API ব্যবহার করে real-time user update হ্যান্ডেল করতে পারি: এর সুবিধাসমূহ: - আমাদের internal `_UserChanged` event দরকার হয় না - `StreamSubscription` ম্যানুয়ালি ম্যানেজ করতে হয় না - bloc কখন user stream-এ subscribe করবে তার সম্পূর্ণ নিয়ন্ত্রণ থাকে অসুবিধাসমূহ: - subscription সহজে `pause` বা `resume` করা যায় না - একটি public `Started` event প্রকাশ করতে হয় যা বাইরে থেকে add করতে হয় - user update প্রসেস করার উপায় কাস্টমাইজ করার জন্য custom `transformer` ব্যবহার করা যায় না ## Public Methods প্রকাশ করা ❔ **প্রশ্ন**: আমার bloc বা cubit instance-এ public method প্রকাশ করা কি ঠিক? 💡 **উত্তর** Cubit তৈরি করার সময় শুধু state পরিবর্তনের জন্য public method রাখা সুপারিশ করা হয়। সাধারণত cubit-এর সব public method `void` বা `Future` return করা উচিত। Bloc তৈরি করার সময় custom public method প্রকাশ না করে শুধুমাত্র event যোগ করে (`add`) bloc-কে notify করাই সুপারিশ করা হয়। ================================================ FILE: docs/src/content/docs/bn/flutter-bloc-concepts.mdx ================================================ --- title: Flutter Bloc কনসেপ্টস description: package:flutter_bloc এর প্রধান ধারণাগুলোর একটি ওভারভিউ sidebar: order: 2 --- import BlocBuilderSnippet from '~/components/concepts/flutter-bloc/BlocBuilderSnippet.astro'; import BlocBuilderExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocBuilderExplicitBlocSnippet.astro'; import BlocBuilderConditionSnippet from '~/components/concepts/flutter-bloc/BlocBuilderConditionSnippet.astro'; import BlocSelectorSnippet from '~/components/concepts/flutter-bloc/BlocSelectorSnippet.astro'; import BlocProviderSnippet from '~/components/concepts/flutter-bloc/BlocProviderSnippet.astro'; import BlocProviderEagerSnippet from '~/components/concepts/flutter-bloc/BlocProviderEagerSnippet.astro'; import BlocProviderValueSnippet from '~/components/concepts/flutter-bloc/BlocProviderValueSnippet.astro'; import BlocProviderLookupSnippet from '~/components/concepts/flutter-bloc/BlocProviderLookupSnippet.astro'; import NestedBlocProviderSnippet from '~/components/concepts/flutter-bloc/NestedBlocProviderSnippet.astro'; import MultiBlocProviderSnippet from '~/components/concepts/flutter-bloc/MultiBlocProviderSnippet.astro'; import BlocListenerSnippet from '~/components/concepts/flutter-bloc/BlocListenerSnippet.astro'; import BlocListenerExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocListenerExplicitBlocSnippet.astro'; import BlocListenerConditionSnippet from '~/components/concepts/flutter-bloc/BlocListenerConditionSnippet.astro'; import NestedBlocListenerSnippet from '~/components/concepts/flutter-bloc/NestedBlocListenerSnippet.astro'; import MultiBlocListenerSnippet from '~/components/concepts/flutter-bloc/MultiBlocListenerSnippet.astro'; import BlocConsumerSnippet from '~/components/concepts/flutter-bloc/BlocConsumerSnippet.astro'; import BlocConsumerConditionSnippet from '~/components/concepts/flutter-bloc/BlocConsumerConditionSnippet.astro'; import RepositoryProviderSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderSnippet.astro'; import RepositoryProviderLookupSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderLookupSnippet.astro'; import RepositoryProviderDisposeSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderDisposeSnippet.astro'; import NestedRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/NestedRepositoryProviderSnippet.astro'; import MultiRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/MultiRepositoryProviderSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/flutter-bloc/CounterBlocSnippet.astro'; import CounterMainSnippet from '~/components/concepts/flutter-bloc/CounterMainSnippet.astro'; import CounterPageSnippet from '~/components/concepts/flutter-bloc/CounterPageSnippet.astro'; import WeatherRepositorySnippet from '~/components/concepts/flutter-bloc/WeatherRepositorySnippet.astro'; import WeatherMainSnippet from '~/components/concepts/flutter-bloc/WeatherMainSnippet.astro'; import WeatherAppSnippet from '~/components/concepts/flutter-bloc/WeatherAppSnippet.astro'; import WeatherPageSnippet from '~/components/concepts/flutter-bloc/WeatherPageSnippet.astro'; :::note অনুগ্রহ করে package:flutter_bloc সম্পর্কিত নীচের অংশগুলো মনোযোগ দিয়ে পড়ুন। ::: :::note `flutter_bloc` প্যাকেজ থেকে এক্সপোর্ট করা সব widgets `Cubit` এবং `Bloc` উভয় ইনস্ট্যান্সের সাথেই কাজ করে। ::: ## Bloc Widgets ### BlocBuilder **BlocBuilder** হল একটি Flutter widget যার জন্য একটি `Bloc` এবং একটি `builder` ফাংশন প্রয়োজন। `BlocBuilder` নতুন states-এর প্রতিক্রিয়ায় widget তৈরি করে। `BlocBuilder` `StreamBuilder`-এর সাথে খুবই মিল কিন্তু boilerplate কোড কমাতে আরও সহজ API আছে। `builder` ফাংশনটি অনেকবার কল হতে পারে এবং এটি একটি [pure function](https://en.wikipedia.org/wiki/Pure_function) হওয়া উচিত যা state-এর প্রতিক্রিয়ায় একটি widget রিটার্ন করে। State পরিবর্তনের প্রতিক্রিয়ায় কোনো কাজ করতে চাইলে (যেমন navigation, dialog দেখানো ইত্যাদি) `BlocListener` দেখুন। `bloc` প্যারামিটার বাদ দিলে, `BlocBuilder` স্বয়ংক্রিয়ভাবে `BlocProvider` এবং current `BuildContext` ব্যবহার করে lookup করবে। শুধুমাত্র তখনই bloc নির্দিষ্ট করুন যখন আপনি একটি bloc প্রদান করতে চান যা একটি single widget-এ scoped হবে এবং parent `BlocProvider` এবং current `BuildContext` দিয়ে অ্যাক্সেসযোগ্য নয়। `builder` ফাংশন কখন কল হবে তার উপর fine-grained control-এর জন্য একটি optional `buildWhen` প্রদান করা যেতে পারে। `buildWhen` previous bloc state এবং current bloc state নেয় এবং একটি boolean রিটার্ন করে। যদি `buildWhen` true রিটার্ন করে, `builder` `state` সহ কল হবে এবং widget rebuild হবে। যদি `buildWhen` false রিটার্ন করে, `builder` `state` সহ কল হবে না এবং rebuild হবে না। ### BlocSelector **BlocSelector** হল একটি Flutter widget যা `BlocBuilder`-এর অনুরূপ কিন্তু ডেভেলপারদের current bloc state-এর উপর ভিত্তি করে একটি নতুন মান নির্বাচন করে আপডেট ফিল্টার করতে দেয়। নির্বাচিত মান পরিবর্তন না হলে অপ্রয়োজনীয় builds প্রতিরোধ করা হয়। `BlocSelector`-এর জন্য সঠিকভাবে নির্ধারণ করতে যে `builder` আবার কল করা উচিত কিনা, নির্বাচিত মান অবশ্যই immutable হতে হবে। `bloc` প্যারামিটার বাদ দিলে, `BlocSelector` স্বয়ংক্রিয়ভাবে `BlocProvider` এবং current `BuildContext` ব্যবহার করে lookup করবে। ### BlocProvider **BlocProvider** হল একটি Flutter widget যা `BlocProvider.of(context)` এর মাধ্যমে তার children-দের একটি bloc প্রদান করে। এটি dependency injection (DI) widget হিসেবে ব্যবহৃত হয় যাতে একটি subtree-এর মধ্যে একাধিক widgets-এ একটি bloc-এর single instance প্রদান করা যায়। বেশিরভাগ ক্ষেত্রে, `BlocProvider` নতুন blocs তৈরি করতে ব্যবহার করা উচিত যা subtree-এর বাকি অংশে উপলব্ধ করা হবে। এই ক্ষেত্রে, যেহেতু `BlocProvider` bloc তৈরি করার জন্য দায়ী, এটি স্বয়ংক্রিয়ভাবে bloc বন্ধ করার কাজটি handle করবে। ডিফল্টভাবে, `BlocProvider` bloc-টি lazily তৈরি করবে, অর্থাৎ `create` তখনই এক্সিকিউট হবে যখন bloc-টি `BlocProvider.of(context)` এর মাধ্যমে lookup করা হবে। এই আচরণ override করতে এবং `create`-কে অবিলম্বে চালানোর জন্য, `lazy` কে `false` সেট করা যেতে পারে। কিছু ক্ষেত্রে, `BlocProvider` widget tree-এর একটি নতুন অংশে একটি existing bloc প্রদান করতে ব্যবহার করা যেতে পারে। এটি সাধারণত ব্যবহৃত হয় যখন একটি existing bloc-কে একটি নতুন route-এ উপলব্ধ করতে হবে। এই ক্ষেত্রে, `BlocProvider` স্বয়ংক্রিয়ভাবে bloc বন্ধ করবে না যেহেতু এটি এটি তৈরি করেনি। তারপর `ChildA`, অথবা `ScreenA` থেকে আমরা `BlocA` পেতে পারি: ### MultiBlocProvider **MultiBlocProvider** হল একটি Flutter widget যা একাধিক `BlocProvider` widgets-কে একটিতে merge করে। `MultiBlocProvider` readability উন্নত করে এবং একাধিক `BlocProviders` nest করার প্রয়োজন দূর করে। `MultiBlocProvider` ব্যবহার করে আমরা যেতে পারি: থেকে: :::caution যখন একটি `BlocProvider` একটি `MultiBlocProvider`-এর context-এর মধ্যে সংজ্ঞায়িত করা হয়, যেকোনো `child` উপেক্ষা করা হবে। ::: ### BlocListener **BlocListener** হল একটি Flutter widget যা একটি `BlocWidgetListener` এবং একটি optional `Bloc` নেয় এবং bloc-এ state পরিবর্তনের প্রতিক্রিয়ায় `listener` invoke করে। এটি এমন functionality-এর জন্য ব্যবহার করা উচিত যার জন্য প্রতি state পরিবর্তনে একবার ঘটতে হবে যেমন navigation, `SnackBar` দেখানো, `Dialog` দেখানো ইত্যাদি... `listener` প্রতিটি state change-এর জন্য শুধুমাত্র একবার কল হয় (**NOT** initial state সহ) `BlocBuilder`-এ `builder`-এর মতো নয় এবং এটি একটি `void` ফাংশন। `bloc` প্যারামিটার বাদ দিলে, `BlocListener` স্বয়ংক্রিয়ভাবে `BlocProvider` এবং current `BuildContext` ব্যবহার করে lookup করবে। শুধুমাত্র তখনই bloc নির্দিষ্ট করুন যখন আপনি একটি bloc প্রদান করতে চান যা অন্যথায় `BlocProvider` এবং current `BuildContext` দিয়ে অ্যাক্সেসযোগ্য নয়। `listener` ফাংশন কখন কল হবে তার উপর fine-grained control-এর জন্য একটি optional `listenWhen` প্রদান করা যেতে পারে। `listenWhen` previous bloc state এবং current bloc state নেয় এবং একটি boolean রিটার্ন করে। যদি `listenWhen` true রিটার্ন করে, `listener` `state` সহ কল হবে। যদি `listenWhen` false রিটার্ন করে, `listener` `state` সহ কল হবে না। ### MultiBlocListener **MultiBlocListener** হল একটি Flutter widget যা একাধিক `BlocListener` widgets-কে একটিতে merge করে। `MultiBlocListener` readability উন্নত করে এবং একাধিক `BlocListeners` nest করার প্রয়োজন দূর করে। `MultiBlocListener` ব্যবহার করে আমরা যেতে পারি: থেকে: :::caution যখন একটি `BlocListener` একটি `MultiBlocListener`-এর context-এর মধ্যে সংজ্ঞায়িত করা হয়, যেকোনো `child` উপেক্ষা করা হবে। ::: ### BlocConsumer **BlocConsumer** নতুন states-এর প্রতিক্রিয়ায় একটি `builder` এবং `listener` এক্সপোজ করে। `BlocConsumer` একটি nested `BlocListener` এবং `BlocBuilder`-এর অনুরূপ কিন্তু প্রয়োজনীয় boilerplate-এর পরিমাণ কমায়। `BlocConsumer` শুধুমাত্র তখনই ব্যবহার করা উচিত যখন `bloc`-এ state changes-এর জন্য UI rebuild এবং অন্যান্য reactions উভয়ই এক্সিকিউট করা প্রয়োজন। `BlocConsumer` একটি required `BlocWidgetBuilder` এবং `BlocWidgetListener` এবং একটি optional `bloc`, `BlocBuilderCondition`, এবং `BlocListenerCondition` নেয়। `bloc` প্যারামিটার বাদ দিলে, `BlocConsumer` স্বয়ংক্রিয়ভাবে `BlocProvider` এবং current `BuildContext` ব্যবহার করে lookup করবে। `listener` এবং `builder` কখন কল হবে তার উপর আরও granular control-এর জন্য একটি optional `listenWhen` এবং `buildWhen` ইমপ্লিমেন্ট করা যেতে পারে। `listenWhen` এবং `buildWhen` প্রতিটি `bloc` `state` change-এ invoke হবে। তারা প্রতিটি previous `state` এবং current `state` নেয় এবং অবশ্যই একটি `bool` রিটার্ন করতে হবে যা নির্ধারণ করে `builder` এবং/অথবা `listener` ফাংশন invoke হবে কিনা। Previous `state` `bloc`-এর `state`-এ initialize হবে যখন `BlocConsumer` initialize হয়। `listenWhen` এবং `buildWhen` optional এবং যদি তারা ইমপ্লিমেন্ট না করা হয়, তারা `true`-তে default হবে। ### RepositoryProvider **RepositoryProvider** হল একটি Flutter widget যা `RepositoryProvider.of(context)` এর মাধ্যমে তার children-দের একটি repository প্রদান করে। এটি dependency injection (DI) widget হিসেবে ব্যবহৃত হয় যাতে একটি subtree-এর মধ্যে একাধিক widgets-এ একটি repository-এর single instance প্রদান করা যায়। `BlocProvider` blocs প্রদান করতে ব্যবহার করা উচিত যেখানে `RepositoryProvider` শুধুমাত্র repositories-এর জন্য ব্যবহার করা উচিত। তারপর `ChildA` থেকে আমরা `Repository` instance পেতে পারি: যে repositories resources manage করে যা dispose করতে হবে তারা `dispose` callback-এর মাধ্যমে তা করতে পারে: ### MultiRepositoryProvider **MultiRepositoryProvider** হল একটি Flutter widget যা একাধিক `RepositoryProvider` widgets-কে একটিতে merge করে। `MultiRepositoryProvider` readability উন্নত করে এবং একাধিক `RepositoryProvider` nest করার প্রয়োজন দূর করে। `MultiRepositoryProvider` ব্যবহার করে আমরা যেতে পারি: থেকে: :::caution যখন একটি `RepositoryProvider` একটি `MultiRepositoryProvider`-এর context-এর মধ্যে সংজ্ঞায়িত করা হয়, যেকোনো `child` উপেক্ষা করা হবে। ::: ## BlocProvider ব্যবহার আসুন দেখি কিভাবে `BlocProvider` ব্যবহার করে একটি `CounterBloc` একটি `CounterPage`-এ প্রদান করা যায় এবং `BlocBuilder` দিয়ে state changes-এর প্রতিক্রিয়া জানানো যায়। এই মুহূর্তে আমরা আমাদের presentational layer-কে আমাদের business logic layer থেকে সফলভাবে আলাদা করেছি। লক্ষ্য করুন যে `CounterPage` widget-টি buttons-এ tap করলে কী ঘটে সে সম্পর্কে কিছুই জানে না। Widget-টি কেবল `CounterBloc`-কে বলে যে ব্যবহারকারী increment অথবা decrement button চাপেছে। ## RepositoryProvider ব্যবহার আমরা [`flutter_weather`][flutter_weather_link] উদাহরণের context-এ `RepositoryProvider` কিভাবে ব্যবহার করা যায় তা দেখব। আমাদের `main.dart`-এ, আমরা আমাদের `WeatherApp` widget-এর সাথে `runApp` কল করি। আমরা `RepositoryProvider`-এর মাধ্যমে আমাদের `WeatherRepository` instance-কে আমাদের widget tree-তে inject করব। একটি bloc instantiate করার সময়, আমরা `context.read`-এর মাধ্যমে একটি repository-এর instance অ্যাক্সেস করতে পারি এবং constructor-এর মাধ্যমে repository-কে bloc-এ inject করতে পারি। :::tip আপনার যদি একাধিক repository থাকে, আপনি `MultiRepositoryProvider` ব্যবহার করে subtree-তে একাধিক repository instances প্রদান করতে পারেন। ::: :::note `RepositoryProvider` unmount হলে resources মুক্ত করার জন্য `dispose` callback ব্যবহার করুন। ::: [flutter_weather_link]: https://github.com/felangel/bloc/blob/master/examples/flutter_weather ## Extension Methods [Extension methods](https://dart.dev/guides/language/extension-methods), Dart 2.7-এ চালু, existing libraries-এ functionality যোগ করার একটি উপায়। এই সেকশনে, আমরা `package:flutter_bloc`-এ অন্তর্ভুক্ত extension methods এবং সেগুলো কিভাবে ব্যবহার করা যায় তা দেখব। `flutter_bloc`-এর উপর একটি dependency আছে [package:provider](https://pub.dev/packages/provider) যা [`InheritedWidget`](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html)-এর ব্যবহার সহজ করে। অভ্যন্তরীণভাবে, `package:flutter_bloc` `package:provider` ব্যবহার করে implement করে: `BlocProvider`, `MultiBlocProvider`, `RepositoryProvider` এবং `MultiRepositoryProvider` widgets। `package:flutter_bloc` `ReadContext`, `WatchContext` এবং `SelectContext` extensions এক্সপোর্ট করে `package:provider` থেকে। :::note [`package:provider`](https://pub.dev/packages/provider) সম্পর্কে আরও জানুন। ::: ### context.read `context.read()` type `T`-এর closest ancestor instance lookup করে এবং functionally `BlocProvider.of(context)`-এর সমতুল্য। `context.read` সবচেয়ে বেশি সাধারণভাবে একটি bloc instance পেতে ব্যবহৃত হয় যাতে `onPressed` callbacks-এর মধ্যে একটি event add করা যায়। :::note `context.read()` `T`-কে listen করে না -- যদি type `T`-এর প্রদত্ত `Object` পরিবর্তন হয়, `context.read` একটি widget rebuild trigger করবে না। ::: #### ব্যবহার ✅ **করুন** callbacks-এ events add করতে `context.read` ব্যবহার করুন। ```dart onPressed() { context.read().add(CounterIncrementPressed()), } ``` ❌ **এড়িয়ে চলুন** একটি `build` method-এর মধ্যে state পেতে `context.read` ব্যবহার করা। ```dart @override Widget build(BuildContext context) { final state = context.read().state; return Text('$state'); } ``` উপরের ব্যবহার error-prone কারণ `Text` widget rebuild হবে না যদি bloc-এর state পরিবর্তন হয়। :::caution State changes-এর প্রতিক্রিয়ায় rebuild করতে পরিবর্তে `BlocBuilder` অথবা `context.watch` ব্যবহার করুন। ::: ### context.watch `context.read()`-এর মতো, `context.watch()` type `T`-এর closest ancestor instance প্রদান করে, তবে এটি instance-এ changes-ও listen করে। এটি functionally `BlocProvider.of(context, listen: true)`-এর সমতুল্য। যদি type `T`-এর প্রদত্ত `Object` পরিবর্তন হয়, `context.watch` একটি rebuild trigger করবে। :::caution `context.watch` শুধুমাত্র একটি `StatelessWidget` অথবা `State` class-এর `build` method-এর মধ্যে অ্যাক্সেসযোগ্য। ::: #### ব্যবহার ✅ **করুন** rebuilds-কে explicitly scope করতে `context.watch`-এর পরিবর্তে `BlocBuilder` ব্যবহার করুন। ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (context, state) { // Whenever the state changes, only the Text is rebuilt. return Text(state.value); }, ), ), ); } ``` বিকল্পভাবে, rebuilds scope করতে একটি `Builder` ব্যবহার করুন। ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Whenever the state changes, only the Text is rebuilt. final state = context.watch().state; return Text(state.value); }, ), ), ); } ``` ✅ **করুন** `MultiBlocBuilder` হিসেবে `Builder` এবং `context.watch` ব্যবহার করুন। ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // return a Widget which depends on the state of BlocA, BlocB, and BlocC } ); ``` ❌ **এড়িয়ে চলুন** `build` method-এ parent widget state-এর উপর নির্ভর না করলে `context.watch` ব্যবহার করা। ```dart @override Widget build(BuildContext context) { // Whenever the state changes, the MaterialApp is rebuilt // even though it is only used in the Text widget. final state = context.watch().state; return MaterialApp( home: Scaffold( body: Text(state.value), ), ); } ``` :::caution `build` method-এর root-এ `context.watch` ব্যবহার করলে bloc state পরিবর্তন হলে entire widget rebuild হবে। ::: ### context.select `context.watch()`-এর মতোই, `context.select(R function(T value))` type `T`-এর closest ancestor instance প্রদান করে এবং `T`-এ changes listen করে। `context.watch`-এর মতো নয়, `context.select` আপনাকে state-এর একটি ছোট অংশে changes listen করতে দেয়। ```dart Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); } ``` উপরেরটি শুধুমাত্র তখনই widget rebuild করবে যখন `ProfileBloc`-এর state-এর `name` property পরিবর্তন হয়। #### ব্যবহার ✅ **করুন** rebuilds-কে explicitly scope করতে `context.select`-এর পরিবর্তে `BlocSelector` ব্যবহার করুন। ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector( selector: (state) => state.name, builder: (context, name) { // Whenever the state.name changes, only the Text is rebuilt. return Text(name); }, ), ), ); } ``` বিকল্পভাবে, rebuilds scope করতে একটি `Builder` ব্যবহার করুন। ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Whenever state.name changes, only the Text is rebuilt. final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), ); } ``` ❌ **এড়িয়ে চলুন** একটি build method-এ parent widget state-এর উপর নির্ভর না করলে `context.select` ব্যবহার করা। ```dart @override Widget build(BuildContext context) { // Whenever the state.value changes, the MaterialApp is rebuilt // even though it is only used in the Text widget. final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), ); } ``` :::caution `build` method-এর root-এ `context.select` ব্যবহার করলে selection পরিবর্তন হলে entire widget rebuild হবে। ::: ================================================ FILE: docs/src/content/docs/bn/getting-started.mdx ================================================ --- title: শুরু করা description: Bloc দিয়ে শুরু করতে যা যা দরকার তা সবকিছু। --- import InstallationTabs from '~/components/getting-started/InstallationTabs.astro'; import ImportTabs from '~/components/getting-started/ImportTabs.astro'; ## প্যাকেজসমূহ Bloc ইকোসিস্টেমের মধ্যে বিভিন্ন প্যাকেজ রয়েছে, যেগুলো নিচে তালিকাভুক্ত করা হলো: | প্যাকেজ | বর্ণনা | লিংক | | ------------------------------------------------------------------------------------------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | AngularDart কম্পোনেন্টস | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | মূল Dart API | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | ইভেন্ট ট্রান্সফর্মার | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | কাস্টম লিন্টার | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | টেস্টিং API | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | কমান্ড-লাইন টুলস | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | Flutter উইজেটস | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | ক্যাশিং/পারসিস্টেন্স সাপোর্ট | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | Undo/Redo সাপোর্ট | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | ## ইনস্টলেশন :::note Bloc ব্যবহার শুরু করার জন্য আপনার মেশিনে [Dart SDK](https://dart.dev/get-dart) ইনস্টল করা থাকতে হবে। ::: ## ইমপোর্টস এখন যেহেতু আমরা Bloc সফলভাবে ইনস্টল করেছি, আমরা আমাদের `main.dart` তৈরি করতে পারি এবং প্রয়োজনীয় `bloc` প্যাকেজ ইমপোর্ট করতে পারি। ================================================ FILE: docs/src/content/docs/bn/index.mdx ================================================ --- template: splash title: Bloc স্টেট ম্যানেজমেন্ট লাইব্রেরি description: Bloc স্টেট ম্যানেজমেন্ট লাইব্রেরির অফিসিয়াল ডকুমেন্টেশন। Dart, Flutter, এবং AngularDart-এর জন্য সাপোর্ট। উদাহরণ এবং টিউটোরিয়াল অন্তর্ভুক্ত। banner: content: | ✨ ভিজিট করুন Bloc Shop ✨ editUrl: false lastUpdated: false hero: title: Bloc v9.2.0 tagline: Dart-এর জন্য একটি পূর্বানুমানযোগ্য স্টেট ম্যানেজমেন্ট লাইব্রেরি। image: alt: Bloc লোগো file: ~/assets/bloc.svg actions: - text: শুরু করুন link: /bn/getting-started/ variant: primary icon: rocket - text: GitHub-এ দেখুন link: https://github.com/felangel/bloc icon: github variant: secondary --- import { CardGrid } from '@astrojs/starlight/components'; import SponsorsGrid from '~/components/landing/SponsorsGrid.astro'; import Card from '~/components/landing/Card.astro'; import ListCard from '~/components/landing/ListCard.astro'; import SplitCard from '~/components/landing/SplitCard.astro'; import Discord from '~/components/landing/Discord.astro';
```sh # আপনার প্রজেক্টে bloc যুক্ত করুন। dart pub add bloc ``` আমাদের [গেটিং স্টার্টেড গাইড](/bn/getting-started)-এ ধাপে-ধাপে নির্দেশনা রয়েছে যা অনুসরণ করে আপনি কয়েক মিনিটেই Bloc ব্যবহার শুরু করতে পারবেন। [অফিসিয়াল টিউটোরিয়ালগুলো](/bn/tutorials/flutter-counter) সম্পূর্ণ করুন যাতে সেরা প্র্যাকটিস শিখতে পারেন এবং Bloc-চালিত বিভিন্ন ধরনের অ্যাপ তৈরি করতে পারেন। উচ্চমানের, সম্পূর্ণ পরীক্ষিত [স্যাম্পল অ্যাপ](https://github.com/felangel/bloc/tree/master/examples) অনুসন্ধান করুন — যেমন counter, timer, infinite list, weather, todo এবং আরও অনেক কিছু! - [কেন Bloc?](/bn/why-bloc) - [মূল ধারণা](/bn/bloc-concepts) - [আর্কিটেকচার](/bn/architecture) - [টেস্টিং](/bn/testing) - [নেমিং কনভেনশন](/bn/naming-conventions) - [সাধারণ প্রশ্নোত্তর](/bn/faqs) - [VSCode ইন্টিগ্রেশন](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) - [IntelliJ ইন্টিগ্রেশন](https://plugins.jetbrains.com/plugin/12129-bloc) - [Neovim ইন্টিগ্রেশন](https://github.com/wa11breaker/flutter-bloc.nvim) - [Mason CLI ইন্টিগ্রেশন](https://github.com/felangel/bloc/blob/master/bricks/README.md) - [কাস্টম টেমপ্লেট](https://brickhub.dev/search?q=bloc) - [ডেভেলপার টুলস](https://github.com/felangel/bloc/blob/master/packages/bloc_tools/README.md) ================================================ FILE: docs/src/content/docs/bn/migration.mdx ================================================ --- title: মাইগ্রেশন গাইড description: Bloc-এর সর্বশেষ stable ভার্সনে মাইগ্রেট করুন। --- import { Code, Tabs, TabItem } from '@astrojs/starlight/components'; :::tip প্রতিটি release-এ কী পরিবর্তন হয়েছে সে সম্পর্কে আরও তথ্যের জন্য [release log](https://github.com/felangel/bloc/releases) দেখুন। ::: ## v10.0.0 ### `package:bloc_test` #### ❗✨ `blocTest`-কে `BlocBase` থেকে Decouple করা :::note[What Changed?] bloc_test v10.0.0-এ, `blocTest` API আর `BlocBase`-এর সাথে tightly coupled নেই। ::: ##### Rationale `blocTest`-এর উচিত সম্ভব হলে core bloc interfaces ব্যবহার করা flexibility এবং reusability বাড়ানোর জন্য। আগে এটি সম্ভব ছিল না কারণ `BlocBase` `StateStreamableSource` implement করত যা `blocTest`-এর জন্য যথেষ্ট ছিল না `emit` API-এর উপর internal dependency-র কারণে। ### `package:hydrated_bloc` #### ❗✨ WebAssembly সাপোর্ট :::note[What Changed?] hydrated_bloc v10.0.0-এ, WebAssembly (wasm)-এ compile করার সাপোর্ট যোগ করা হয়েছে। ::: ##### Rationale আগে `hydrated_bloc` ব্যবহার করার সময় apps-কে wasm-এ compile করা সম্ভব ছিল না। v10.0.0-এ, প্যাকেজটি refactor করা হয়েছে wasm-এ compile করার অনুমতি দেওয়ার জন্য। **v9.x.x** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` **v10.x.x** ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorageDirectory.web : HydratedStorageDirectory((await getTemporaryDirectory()).path), ); runApp(const App()); } ``` ## v9.0.0 ### `package:bloc` #### ❗🧹 Deprecated APIs সরানো :::note[What Changed?] bloc v9.0.0-এ, আগে থেকে deprecated সব APIs সরানো হয়েছে। ::: ##### Summary - `BlocOverrides` সরানো হয়েছে `Bloc.observer` এবং `Bloc.transformer`-এর পক্ষে #### ❗✨ নতুন `EmittableStateStreamableSource` Interface চালু :::note[What Changed?] bloc v9.0.0-এ, একটি নতুন core interface `EmittableStateStreamableSource` চালু করা হয়েছে। ::: ##### Rationale `package:bloc_test` আগে `BlocBase`-এর সাথে tightly coupled ছিল। `EmittableStateStreamableSource` interface চালু করা হয়েছে যাতে `blocTest`-কে `BlocBase` concrete implementation থেকে decouple করা যায়। ### `package:hydrated_bloc` #### ✨ `HydratedBloc.storage` API পুনরায় চালু :::note[What Changed?] hydrated_bloc v9.0.0-এ, `HydratedBlocOverrides` সরানো হয়েছে `HydratedBloc.storage` API-এর পক্ষে।\*\* ::: ##### Rationale Bloc.observer এবং Bloc.transformer overrides পুনরায় চালু করার [rationale-এর জন্য দেখুন](/bn/migration/#-blocobserver-এবং-bloctransformer-apis-পুনরায়-চালু)। **v8.x.x** ```dart Future main() async { final storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); HydratedBlocOverrides.runZoned( () => runApp(App()), storage: storage, ); } ``` **v9.0.0** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` ## v8.1.0 ### `package:bloc` #### ✨ `Bloc.observer` এবং `Bloc.transformer` APIs পুনরায় চালু :::note[What Changed?] bloc v8.1.0-এ, `BlocOverrides` deprecated করা হয়েছে `Bloc.observer` এবং `Bloc.transformer` APIs-এর পক্ষে। ::: ##### Rationale `BlocOverrides` API v8.0.0-এ চালু করা হয়েছিল `BlocObserver`, `EventTransformer`, এবং `HydratedStorage`-এর মতো bloc-specific configurations scope করার চেষ্টায়। Pure Dart applications-এ, পরিবর্তনগুলো ভালো কাজ করেছিল; তবে, Flutter applications-এ নতুন API যতটা সমস্যা সমাধান করেছে তার চেয়ে বেশি সমস্যা তৈরি করেছে। `BlocOverrides` API Flutter/Dart-এ অনুরূপ APIs দ্বারা অনুপ্রাণিত হয়েছিল: - [HttpOverrides](https://api.flutter.dev/flutter/dart-io/HttpOverrides-class.html) - [IOOverrides](https://api.flutter.dev/flutter/dart-io/IOOverrides-class.html) **সমস্যা** যদিও এটি এই পরিবর্তনগুলোর প্রাথমিক কারণ ছিল না, `BlocOverrides` API ডেভেলপারদের জন্য অতিরিক্ত complexity নিয়ে এসেছে। একই প্রভাব অর্জনের জন্য প্রয়োজনীয় nesting এবং code-এর লাইনের পরিমাণ বাড়ানোর পাশাপাশি, `BlocOverrides` API-এর জন্য ডেভেলপারদের Dart-এ [Zones](https://api.dart.dev/stable/2.17.6/dart-async/Zone-class.html)-এর উপর solid understanding থাকতে হয়েছিল। `Zones` beginner-friendly ধারণা নয় এবং Zones কিভাবে কাজ করে তা বুঝতে ব্যর্থতা bugs (যেমন uninitialized observers, transformers, storage instances) তৈরি করতে পারে। উদাহরণস্বরূপ, অনেক ডেভেলপারের এমন কিছু থাকত: ```dart void main() { WidgetsFlutterBinding.ensureInitialized(); BlocOverrides.runZoned(...); } ``` উপরের কোড, যদিও harmless মনে হয়, আসলে অনেক track করা কঠিন bugs তৈরি করতে পারে। `WidgetsFlutterBinding.ensureInitialized` যে zone থেকে প্রথমে কল হয় সেটিই হবে যে zone-এ gesture events handle করা হয় (যেমন `onTap`, `onPressed` callbacks) `GestureBinding.initInstances`-এর কারণে। এটি `zoneValues` ব্যবহারের কারণে সৃষ্ট অনেক সমস্যার মধ্যে একটি মাত্র। এছাড়াও, Flutter অনেক কিছু behind the scenes করে যা forking/manipulating Zones-কে জড়িত করে (বিশেষ করে tests চালানোর সময়) যা unexpected behaviors তৈরি করতে পারে (এবং অনেক ক্ষেত্রে behaviors যা developer-এর নিয়ন্ত্রণের বাইরে -- নিচের issues দেখুন)। [runZoned](https://api.flutter.dev/flutter/dart-async/runZoned.html) ব্যবহারের কারণে, `BlocOverrides` API-তে transition Flutter-এ বেশ কয়েকটি bugs/limitations আবিষ্কারের দিকে নিয়ে গেছে (বিশেষ করে Widget এবং Integration Tests-এর আশেপাশে): - https://github.com/flutter/flutter/issues/96939 - https://github.com/flutter/flutter/issues/94123 - https://github.com/flutter/flutter/issues/93676 যা bloc library ব্যবহারকারী অনেক ডেভেলপারকে প্রভাবিত করেছে: - https://github.com/felangel/bloc/issues/3394 - https://github.com/felangel/bloc/issues/3350 - https://github.com/felangel/bloc/issues/3319 **v8.0.x** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` **v8.1.0** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` ## v8.0.0 ### `package:bloc` #### ❗✨ নতুন `BlocOverrides` API চালু :::note[What Changed?] bloc v8.0.0-এ, `Bloc.observer` এবং `Bloc.transformer` সরানো হয়েছে `BlocOverrides` API-এর পক্ষে। ::: ##### Rationale আগের API যা default `BlocObserver` এবং `EventTransformer` override করতে ব্যবহৃত হত `BlocObserver` এবং `EventTransformer` উভয়ের জন্য একটি global singleton-এর উপর নির্ভর করত। ফলস্বরূপ, এটি সম্ভব ছিল না: - Application-এর বিভিন্ন অংশে scoped একাধিক `BlocObserver` বা `EventTransformer` implementations থাকা - একটি package-এ scoped `BlocObserver` বা `EventTransformer` overrides থাকা - যদি একটি package `package:bloc`-এর উপর নির্ভর করে এবং তার নিজস্ব `BlocObserver` register করে, package-এর যেকোনো consumer-কে হয় package-এর `BlocObserver` overwrite করতে হবে অথবা package-এর `BlocObserver`-এ report করতে হবে। Tests জুড়ে shared global state-এর কারণে test করাও আরও কঠিন ছিল। Bloc v8.0.0 একটি `BlocOverrides` class চালু করেছে যা ডেভেলপারদের একটি specific `Zone`-এর জন্য `BlocObserver` এবং/অথবা `EventTransformer` override করতে দেয় global mutable singleton-এর উপর নির্ভর করার পরিবর্তে। **v7.x.x** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` **v8.0.0** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` `Bloc` instances `BlocOverrides.current`-এর মাধ্যমে current `Zone`-এর জন্য `BlocObserver` এবং/অথবা `EventTransformer` ব্যবহার করবে। যদি zone-এর জন্য কোনো `BlocOverrides` না থাকে তারা existing internal defaults ব্যবহার করবে (behavior/functionality-তে কোনো পরিবর্তন নেই)। এটি প্রতিটি `Zone`-কে তার নিজস্ব `BlocOverrides`-এর সাথে স্বাধীনভাবে কাজ করতে দেয়। ```dart BlocOverrides.runZoned( () { // BlocObserverA and eventTransformerA final overrides = BlocOverrides.current; // Blocs in this zone report to BlocObserverA // and use eventTransformerA as the default transformer. // ... // Later... BlocOverrides.runZoned( () { // BlocObserverB and eventTransformerB final overrides = BlocOverrides.current; // Blocs in this zone report to BlocObserverB // and use eventTransformerB as the default transformer. // ... }, blocObserver: BlocObserverB(), eventTransformer: eventTransformerB(), ); }, blocObserver: BlocObserverA(), eventTransformer: eventTransformerA(), ); ``` #### ❗✨ Error Handling এবং Reporting উন্নতি :::note[What Changed?] bloc v8.0.0-এ, `BlocUnhandledErrorException` সরানো হয়েছে। এছাড়াও, যেকোনো uncaught exceptions সবসময় `onError`-এ report করা হয় এবং rethrown হয় (debug বা release mode নির্বিশেষে)। `addError` API errors-কে `onError`-এ report করে, কিন্তু reported errors-কে uncaught exceptions হিসেবে treat করে না। ::: ##### Rationale এই পরিবর্তনগুলোর লক্ষ্য হল: - bloc functionality সংরক্ষণ করার সময় internal unhandled exceptions অত্যন্ত স্পষ্ট করা - control flow বিঘ্নিত না করে `addError` support করা আগে, error handling এবং reporting application debug বা release mode-এ চলছে কিনা তার উপর নির্ভর করে পরিবর্তিত হত। এছাড়াও, `addError`-এর মাধ্যমে reported errors debug mode-এ uncaught exceptions হিসেবে treat করা হত যা `addError` API ব্যবহার করার সময় একটি poor developer experience তৈরি করেছিল (বিশেষ করে unit tests লেখার সময়)। v8.0.0-এ, `addError` errors report করতে নিরাপদে ব্যবহার করা যেতে পারে এবং `blocTest` ব্যবহার করা যেতে পারে যাচাই করতে যে errors report করা হয়েছে। সব errors এখনও `onError`-এ report করা হয়, তবে, শুধুমাত্র uncaught exceptions rethrown হয় (debug বা release mode নির্বিশেষে)। #### ❗🧹 `BlocObserver`-কে abstract করা :::note[What Changed?] bloc v8.0.0-এ, `BlocObserver`-কে একটি `abstract` class-এ রূপান্তর করা হয়েছে যার মানে `BlocObserver`-এর একটি instance instantiate করা যায় না। ::: ##### Rationale `BlocObserver` একটি interface হওয়ার উদ্দেশ্যে ছিল। যেহেতু default API implementation no-ops, `BlocObserver` এখন একটি `abstract` class যাতে স্পষ্টভাবে যোগাযোগ করা যায় যে class-টি extend করার জন্য এবং সরাসরি instantiate করার জন্য নয়। **v7.x.x** ```dart void main() { // It was possible to create an instance of the base class. final observer = BlocObserver(); } ``` **v8.0.0** ```dart class MyBlocObserver extends BlocObserver {...} void main() { // Cannot instantiate the base class. final observer = BlocObserver(); // ERROR // Extend `BlocObserver` instead. final observer = MyBlocObserver(); // OK } ``` #### ❗✨ Bloc বন্ধ থাকলে `add` `StateError` throw করে :::note[What Changed?] bloc v8.0.0-এ, একটি closed bloc-এ `add` কল করলে `StateError` হবে। ::: ##### Rationale আগে, একটি closed bloc-এ `add` কল করা সম্ভব ছিল এবং internal error swallow হয়ে যেত, যা debug করা কঠিন করত কেন added event process করা হচ্ছিল না। এই scenario-টি আরও দৃশ্যমান করতে, v8.0.0-এ, একটি closed bloc-এ `add` কল করলে `StateError` throw করবে যা uncaught exception হিসেবে report করা হবে এবং `onError`-এ propagate করা হবে। #### ❗✨ Bloc বন্ধ থাকলে `emit` `StateError` throw করে :::note[What Changed?] bloc v8.0.0-এ, একটি closed bloc-এর মধ্যে `emit` কল করলে `StateError` হবে। ::: ##### Rationale আগে, একটি closed bloc-এর মধ্যে `emit` কল করা সম্ভব ছিল এবং কোনো state change ঘটত না কিন্তু কী ভুল হয়েছে তার কোনো ইঙ্গিতও থাকত না, যা debug করা কঠিন করত। এই scenario-টি আরও দৃশ্যমান করতে, v8.0.0-এ, একটি closed bloc-এর মধ্যে `emit` কল করলে `StateError` throw করবে যা uncaught exception হিসেবে report করা হবে এবং `onError`-এ propagate করা হবে। #### ❗🧹 Deprecated APIs সরানো :::note[What Changed?] bloc v8.0.0-এ, আগে থেকে deprecated সব APIs সরানো হয়েছে। ::: ##### Summary - `mapEventToState` সরানো হয়েছে `on`-এর পক্ষে - `transformEvents` সরানো হয়েছে `EventTransformer` API-এর পক্ষে - `TransitionFunction` typedef সরানো হয়েছে `EventTransformer` API-এর পক্ষে - `listen` সরানো হয়েছে `stream.listen`-এর পক্ষে ### `package:bloc_test` #### ✨ `MockBloc` এবং `MockCubit` আর `registerFallbackValue` প্রয়োজন নেই :::note[What Changed?] bloc_test v9.0.0-এ, ডেভেলপারদের আর `MockBloc` বা `MockCubit` ব্যবহার করার সময় স্পষ্টভাবে `registerFallbackValue` কল করার প্রয়োজন নেই। ::: ##### Summary `registerFallbackValue` শুধুমাত্র একটি custom type-এর জন্য `package:mocktail` থেকে `any()` matcher ব্যবহার করার সময় প্রয়োজন। আগে, `registerFallbackValue` প্রতিটি `Event` এবং `State`-এর জন্য প্রয়োজন ছিল যখন `MockBloc` বা `MockCubit` ব্যবহার করা হত। **v8.x.x** ```dart class FakeMyEvent extends Fake implements MyEvent {} class FakeMyState extends Fake implements MyState {} class MyMockBloc extends MockBloc implements MyBloc {} void main() { setUpAll(() { registerFallbackValue(FakeMyEvent()); registerFallbackValue(FakeMyState()); }); // Tests... } ``` **v9.0.0** ```dart class MyMockBloc extends MockBloc implements MyBloc {} void main() { // Tests... } ``` ### `package:hydrated_bloc` #### ❗✨ নতুন `HydratedBlocOverrides` API চালু :::note[What Changed?] hydrated_bloc v8.0.0-এ, `HydratedBloc.storage` সরানো হয়েছে `HydratedBlocOverrides` API-এর পক্ষে। ::: ##### Rationale আগে, `Storage` implementation override করতে একটি global singleton ব্যবহৃত হত। ফলস্বরূপ, application-এর বিভিন্ন অংশে scoped একাধিক `Storage` implementations থাকা সম্ভব ছিল না। Tests জুড়ে shared global state-এর কারণে test করাও আরও কঠিন ছিল। `HydratedBloc` v8.0.0 একটি `HydratedBlocOverrides` class চালু করেছে যা ডেভেলপারদের একটি specific `Zone`-এর জন্য `Storage` override করতে দেয় global mutable singleton-এর উপর নির্ভর করার পরিবর্তে। **v7.x.x** ```dart void main() async { HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); // ... } ``` **v8.0.0** ```dart void main() { final storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); HydratedBlocOverrides.runZoned( () { // ... }, storage: storage, ); } ``` `HydratedBloc` instances `HydratedBlocOverrides.current`-এর মাধ্যমে current `Zone`-এর জন্য `Storage` ব্যবহার করবে। এটি প্রতিটি `Zone`-কে তার নিজস্ব `BlocOverrides`-এর সাথে স্বাধীনভাবে কাজ করতে দেয়। ## v7.2.0 ### `package:bloc` #### ✨ নতুন `on` API চালু :::note[What Changed?] bloc v7.2.0-এ, `mapEventToState` deprecated করা হয়েছে `on`-এর পক্ষে। `mapEventToState` bloc v8.0.0-এ সরানো হবে। ::: ##### Rationale `on` API চালু করা হয়েছিল [[Proposal] Replace mapEventToState with on\ in Bloc](https://github.com/felangel/bloc/issues/2526)-এর অংশ হিসেবে। [Dart-এ একটি issue](https://github.com/dart-lang/sdk/issues/44616)-এর কারণে nested async generators (`async*`)-এর সাথে deal করার সময় `state`-এর মান কী হবে তা সবসময় স্পষ্ট নয়। যদিও issue-টি work around করার উপায় আছে, bloc library-এর core principles-এর মধ্যে একটি হল predictable হওয়া। `on` API library-টি যতটা সম্ভব নিরাপদে ব্যবহার করার জন্য এবং state changes-এর ক্ষেত্রে যেকোনো uncertainty দূর করার জন্য তৈরি করা হয়েছিল। :::tip আরও তথ্যের জন্য, [full proposal পড়ুন](https://github.com/felangel/bloc/issues/2526)। ::: **Summary** `on` আপনাকে type `E`-এর সব events-এর জন্য একটি event handler register করতে দেয়। ডিফল্টভাবে, events concurrently process হবে যখন `on` ব্যবহার করা হবে `mapEventToState`-এর বিপরীতে যা events `sequentially` process করে। **v7.1.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0); @override Stream mapEventToState(CounterEvent event) async* { if (event is Increment) { yield state + 1; } } } ``` **v7.2.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } } ``` :::note প্রতিটি registered `EventHandler` স্বাধীনভাবে কাজ করে তাই এটি গুরুত্বপূর্ণ যে আপনি যে ধরনের transformer apply করতে চান তার উপর ভিত্তি করে event handlers register করুন। ::: আপনি যদি v7.1.0-এর মতো exact same behavior রাখতে চান আপনি সব events-এর জন্য একটি single event handler register করতে পারেন এবং একটি `sequential` transformer apply করতে পারেন: ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; class MyBloc extends Bloc { MyBloc() : super(MyState()) { on(_onEvent, transformer: sequential()) } FutureOr _onEvent(MyEvent event, Emitter emit) async { // TODO: logic goes here... } } ``` আপনি আপনার application-এ সব blocs-এর জন্য default `EventTransformer` override করতে পারেন: ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; void main() { Bloc.transformer = sequential(); ... } ``` #### ✨ নতুন `EventTransformer` API চালু :::note[What Changed?] bloc v7.2.0-এ, `transformEvents` deprecated করা হয়েছে `EventTransformer` API-এর পক্ষে। `transformEvents` bloc v8.0.0-এ সরানো হবে। ::: ##### Rationale `on` API per event handler একটি custom event transformer প্রদান করার দরজা খুলে দিয়েছে। একটি নতুন `EventTransformer` typedef চালু করা হয়েছে যা ডেভেলপারদের প্রতিটি event handler-এর জন্য incoming event stream transform করতে দেয় সব events-এর জন্য একটি single event transformer specify করার পরিবর্তে। **Summary** একটি `EventTransformer` incoming events-এর stream একটি `EventMapper` (আপনার event handler) সহ নেওয়ার জন্য দায়ী এবং events-এর একটি নতুন stream রিটার্ন করে। ```dart typedef EventTransformer = Stream Function(Stream events, EventMapper mapper) ``` Default `EventTransformer` সব events concurrently process করে এবং দেখতে কিছুটা এমন: ```dart EventTransformer concurrent() { return (events, mapper) => events.flatMap(mapper); } ``` :::tip [package:bloc_concurrency](https://pub.dev/packages/bloc_concurrency) দেখুন custom event transformers-এর একটি opinionated set-এর জন্য ::: **v7.1.0** ```dart @override Stream> transformEvents(events, transitionFn) { return events .debounceTime(const Duration(milliseconds: 300)) .flatMap(transitionFn); } ``` **v7.2.0** ```dart /// Define a custom `EventTransformer` EventTransformer debounce(Duration duration) { return (events, mapper) => events.debounceTime(duration).flatMap(mapper); } MyBloc() : super(MyState()) { /// Apply the custom `EventTransformer` to the `EventHandler` on(_onEvent, transformer: debounce(const Duration(milliseconds: 300))) } ``` #### ⚠️ `transformTransitions` API Deprecate :::note[What Changed?] bloc v7.2.0-এ, `transformTransitions` deprecated করা হয়েছে `stream` API override করার পক্ষে। `transformTransitions` bloc v8.0.0-এ সরানো হবে। ::: ##### Rationale `Bloc`-এ `stream` getter outbound stream of states override করা সহজ করে তোলে তাই একটি separate `transformTransitions` API বজায় রাখা আর মূল্যবান নয়। **Summary** **v7.1.0** ```dart @override Stream> transformTransitions( Stream> transitions, ) { return transitions.debounceTime(const Duration(milliseconds: 42)); } ``` **v7.2.0** ```dart @override Stream get stream => super.stream.debounceTime(const Duration(milliseconds: 42)); ``` ## v7.0.0 ### `package:bloc` #### ❗ Bloc এবং Cubit BlocBase extend করে ##### Rationale একজন ডেভেলপার হিসেবে, blocs এবং cubits-এর মধ্যে সম্পর্ক কিছুটা awkward ছিল। যখন cubit প্রথম চালু করা হয়েছিল এটি blocs-এর base class হিসেবে শুরু হয়েছিল যা যুক্তিসঙ্গত ছিল কারণ এটির functionality-এর একটি subset ছিল এবং blocs কেবল Cubit extend করত এবং additional APIs define করত। এটি কয়েকটি drawback নিয়ে এসেছিল: - সব APIs হয় accuracy-এর জন্য cubit accept করতে rename করতে হবে অথবা consistency-এর জন্য bloc হিসেবে রাখতে হবে যদিও hierarchically এটি inaccurate ([#1708](https://github.com/felangel/bloc/issues/1708), [#1560](https://github.com/felangel/bloc/issues/1560))। - Cubit-এর একটি common base থাকার জন্য Stream extend করতে হবে এবং EventSink implement করতে হবে যাতে BlocBuilder, BlocListener ইত্যাদির মতো widgets implement করা যায় ([#1429](https://github.com/felangel/bloc/issues/1429))। পরে, আমরা relationship-টি invert করে এবং bloc-কে base class করে পরীক্ষা করেছিলাম যা উপরের প্রথম bullet-টি আংশিকভাবে সমাধান করেছিল কিন্তু অন্যান্য issues নিয়ে এসেছিল: - Cubit API bloated underlying bloc APIs-এর কারণে যেমন mapEventToState, add, etc. ([#2228](https://github.com/felangel/bloc/issues/2228)) - ডেভেলপাররা technically এই APIs invoke করতে পারে এবং things break করতে পারে - আমাদের এখনও একই issue আছে cubit entire stream API expose করার আগের মতো ([#1429](https://github.com/felangel/bloc/issues/1429)) এই issues-গুলি সমাধান করতে আমরা `Bloc` এবং `Cubit` উভয়ের জন্য একটি base class চালু করেছি `BlocBase` নামে যাতে upstream components এখনও উভয় bloc এবং cubit instances-এর সাথে interoperate করতে পারে কিন্তু entire `Stream` এবং `EventSink` API সরাসরি expose না করে। **Summary** **BlocObserver** **v6.1.x** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(Cubit cubit) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(Cubit cubit, Object event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(Cubit cubit, Object error, StackTrace stackTrace) {...} @override void onClose(Cubit cubit) {...} } ``` **v7.0.0** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(BlocBase bloc) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(BlocBase bloc, Object? event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) {...} @override void onClose(BlocBase bloc) {...} } ``` **Bloc/Cubit** **v6.1.x** ```dart final bloc = MyBloc(); bloc.listen((state) {...}); final cubit = MyCubit(); cubit.listen((state) {...}); ``` **v7.0.0** ```dart final bloc = MyBloc(); bloc.stream.listen((state) {...}); final cubit = MyCubit(); cubit.stream.listen((state) {...}); ``` ### `package:bloc_test` #### ❗seed returns a function to support dynamic values ##### Rationale In order to support having a mutable seed value which can be updated dynamically in `setUp`, `seed` returns a function. **Summary** **v7.x.x** ```dart blocTest( '...', seed: MyState(), ... ); ``` **v8.0.0** ```dart blocTest( '...', seed: () => MyState(), ... ); ``` #### ❗expect returns a function to support dynamic values and includes matcher support ##### Rationale In order to support having a mutable expectation which can be updated dynamically in `setUp`, `expect` returns a function. `expect` also supports `Matchers`. **Summary** **v7.x.x** ```dart blocTest( '...', expect: [MyStateA(), MyStateB()], ... ); ``` **v8.0.0** ```dart blocTest( '...', expect: () => [MyStateA(), MyStateB()], ... ); // It can also be a `Matcher` blocTest( '...', expect: () => contains(MyStateA()), ... ); ``` #### ❗errors returns a function to support dynamic values and includes matcher support ##### Rationale In order to support having a mutable errors which can be updated dynamically in `setUp`, `errors` returns a function. `errors` also supports `Matchers`. **Summary** **v7.x.x** ```dart blocTest( '...', errors: [MyError()], ... ); ``` **v8.0.0** ```dart blocTest( '...', errors: () => [MyError()], ... ); // It can also be a `Matcher` blocTest( '...', errors: () => contains(MyError()), ... ); ``` #### ❗MockBloc and MockCubit ##### Rationale To support stubbing of various core APIs, `MockBloc` and `MockCubit` are exported as part of the `bloc_test` package. Previously, `MockBloc` had to be used for both `Bloc` and `Cubit` instances which was not intuitive. **Summary** **v7.x.x** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockBloc implements MyBloc {} ``` **v8.0.0** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockCubit implements MyCubit {} ``` #### ❗Mocktail Integration ##### Rationale Due to various limitations of the null-safe [package:mockito](https://pub.dev/packages/mockito) described [here](https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#problems-with-typical-mocking-and-stubbing), [package:mocktail](https://pub.dev/packages/mocktail) is used by `MockBloc` and `MockCubit`. This allows developers to continue using a familiar mocking API without the need to manually write stubs or rely on code generation. **Summary** **v7.x.x** ```dart import 'package:mockito/mockito.dart'; ... when(bloc.state).thenReturn(MyState()); verify(bloc.add(any)).called(1); ``` **v8.0.0** ```dart import 'package:mocktail/mocktail.dart'; ... when(() => bloc.state).thenReturn(MyState()); verify(() => bloc.add(any())).called(1); ``` > Please refer to [#347](https://github.com/dart-lang/mockito/issues/347) as > well as the > [mocktail documentation](https://github.com/felangel/mocktail/tree/main/packages/mocktail) > for more information. ### `package:flutter_bloc` #### ❗ rename `cubit` parameter to `bloc` ##### Rationale As a result of the refactor in `package:bloc` to introduce `BlocBase` which `Bloc` and `Cubit` extend, the parameters of `BlocBuilder`, `BlocConsumer`, and `BlocListener` were renamed from `cubit` to `bloc` because the widgets operate on the `BlocBase` type. This also further aligns with the library name and hopefully improves readability. **Summary** **v6.1.x** ```dart BlocBuilder( cubit: myBloc, ... ) BlocListener( cubit: myBloc, ... ) BlocConsumer( cubit: myBloc, ... ) ``` **v7.0.0** ```dart BlocBuilder( bloc: myBloc, ... ) BlocListener( bloc: myBloc, ... ) BlocConsumer( bloc: myBloc, ... ) ``` ### `package:hydrated_bloc` #### ❗storageDirectory is required when calling HydratedStorage.build ##### Rationale In order to make `package:hydrated_bloc` a pure Dart package, the dependency on [package:path_provider](https://pub.dev/packages/path_provider) was removed and the `storageDirectory` parameter when calling `HydratedStorage.build` is required and no longer defaults to `getTemporaryDirectory`. **Summary** **v6.x.x** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` **v7.0.0** ```dart import 'package:path_provider/path_provider.dart'; ... HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getTemporaryDirectory(), ); ``` ## v6.1.0 ### `package:flutter_bloc` #### ❗context.bloc এবং context.repository deprecated হয়েছে context.read এবং context.watch-এর পক্ষে ##### Rationale `context.read`, `context.watch`, এবং `context.select` যোগ করা হয়েছে existing [provider](https://pub.dev/packages/provider) API-এর সাথে align করতে যা অনেক ডেভেলপার familiar এবং community-এর দ্বারা উত্থাপিত issues-গুলি address করতে। Code-এর safety উন্নত করতে এবং consistency বজায় রাখতে, `context.bloc` deprecated করা হয়েছে কারণ এটি `context.read` বা `context.watch` দিয়ে replace করা যেতে পারে এটি `build`-এর মধ্যে সরাসরি ব্যবহার করা হয় কিনা তার উপর নির্ভর করে। **context.watch** `context.watch` একটি [MultiBlocBuilder](https://github.com/felangel/bloc/issues/538) থাকার request address করে কারণ আমরা একটি single `Builder`-এর মধ্যে একাধিক blocs watch করতে পারি multiple states-এর উপর ভিত্তি করে UI render করার জন্য: ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // return a Widget which depends on the state of BlocA, BlocB, and BlocC } ); ``` **context.select** `context.select` ডেভেলপারদের একটি bloc-এর state-এর একটি অংশের উপর ভিত্তি করে UI render/update করতে দেয় এবং একটি [simpler buildWhen](https://github.com/felangel/bloc/issues/1521) থাকার request address করে। ```dart final name = context.select((UserBloc bloc) => bloc.state.user.name); ``` উপরের snippet আমাদের access করতে এবং widget rebuild করতে দেয় শুধুমাত্র যখন current user-এর name পরিবর্তন হয়। **context.read** যদিও `context.read` `context.bloc`-এর মতো দেখায় সেখানে কিছু subtle কিন্তু significant differences আছে। উভয়ই আপনাকে একটি bloc access করতে দেয় একটি `BuildContext` সহ এবং rebuilds তৈরি করে না; তবে, `context.read` `build` method-এর মধ্যে সরাসরি call করা যায় না। `build`-এর মধ্যে `context.bloc` ব্যবহার করার দুটি main কারণ আছে: 1. **Bloc-এর state access করতে** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` উপরের ব্যবহার error-prone কারণ `Text` widget rebuild হবে না যদি bloc-এর state পরিবর্তন হয়। এই scenario-তে, হয় `BlocBuilder` বা `context.watch` ব্যবহার করা উচিত। ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` অথবা ```dart @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) => Text('$state'), ); } ``` :::note `build` method-এর root-এ `context.watch` ব্যবহার করলে bloc state পরিবর্তন হলে entire widget rebuild হবে। যদি entire widget rebuild করার প্রয়োজন না থাকে, হয় `BlocBuilder` ব্যবহার করুন যে parts-গুলি rebuild করা উচিত সেগুলি wrap করতে, rebuilds scope করতে `Builder` সহ `context.watch` ব্যবহার করুন, অথবা widget-টি smaller widgets-এ decompose করুন। ::: 2. **Bloc access করতে যাতে একটি event add করা যায়** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` উপরের ব্যবহার inefficient কারণ এটি প্রতিটি rebuild-এ bloc lookup তৈরি করে যখন bloc শুধুমাত্র প্রয়োজন যখন user `ElevatedButton` tap করে। এই scenario-তে, bloc সরাসরি access করতে `context.read` ব্যবহার করুন যেখানে এটি প্রয়োজন (এই ক্ষেত্রে, `onPressed` callback-এ)। ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` **Summary** **v6.0.x** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` ?> যদি একটি event add করতে bloc access করা হয়, bloc access করুন `context.read` ব্যবহার করে callback-এ যেখানে এটি প্রয়োজন। **v6.0.x** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` ?> Bloc-এর state access করার সময় `context.watch` ব্যবহার করুন যাতে নিশ্চিত করা যায় state পরিবর্তন হলে widget rebuild হয়। ## v6.0.0 ### `package:bloc` #### ❗BlocObserver onError Cubit নেয় ##### Rationale `Cubit`-এর integration-এর কারণে, `onError` এখন `Bloc` এবং `Cubit` instances উভয়ের মধ্যে shared। যেহেতু `Cubit` base, `BlocObserver` `onError` override-এ `Bloc` type-এর পরিবর্তে একটি `Cubit` type accept করবে। **v5.x.x** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Bloc bloc, Object error, StackTrace stackTrace) { super.onError(bloc, error, stackTrace); } } ``` **v6.0.0** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Cubit cubit, Object error, StackTrace stackTrace) { super.onError(cubit, error, stackTrace); } } ``` #### ❗Bloc subscription-এ last state emit করে না ##### Rationale এই পরিবর্তনটি `Bloc` এবং `Cubit`-কে `Dart`-এ built-in `Stream` behavior-এর সাথে align করার জন্য করা হয়েছিল। এছাড়াও, `Cubit`-এর context-এ এই old behavior conform করা অনেক unintended side-effects তৈরি করেছিল এবং overall অন্যান্য packages যেমন `flutter_bloc` এবং `bloc_test`-এর internal implementations জটিল করেছিল unnecessarily (`skip(1)`, etc... প্রয়োজন)। **v5.x.x** ```dart final bloc = MyBloc(); bloc.listen(print); ``` আগে, উপরোক্ত snippet bloc-এর initial state output করত এর পরে subsequent state changes। **v6.x.x** v6.0.0-এ, উপরোক্ত snippet initial state output করে না এবং শুধুমাত্র outputs subsequent state changes। Previous behavior নিম্নলিখিত দিয়ে অর্জন করা যেতে পারে: ```dart final bloc = MyBloc(); print(bloc.state); bloc.listen(print); ``` ?> **Note**: এই পরিবর্তনটি শুধুমাত্র direct bloc subscriptions-এর উপর নির্ভরশীল code-কে প্রভাবিত করবে। `BlocBuilder`, `BlocListener`, বা `BlocConsumer` ব্যবহার করার সময় behavior-তে কোনো noticeable change থাকবে না। ### `package:bloc_test` #### ❗MockBloc only requires State type ##### Rationale It is not necessary and eliminates extra code while also making `MockBloc` compatible with `Cubit`. **v5.x.x** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` **v6.0.0** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` #### ❗whenListen only requires State type ##### Rationale It is not necessary and eliminates extra code while also making `whenListen` compatible with `Cubit`. **v5.x.x** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` **v6.0.0** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` #### ❗blocTest does not require Event type ##### Rationale It is not necessary and eliminates extra code while also making `blocTest` compatible with `Cubit`. **v5.x.x** ```dart blocTest( 'emits [1] when increment is called', build: () async => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` **v6.0.0** ```dart blocTest( 'emits [1] when increment is called', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` #### ❗blocTest skip defaults to 0 ##### Rationale Since `bloc` and `cubit` instances will no longer emit the latest state for new subscriptions, it was no longer necessary to default `skip` to `1`. **v5.x.x** ```dart blocTest( 'emits [0] when skip is 0', build: () async => CounterBloc(), skip: 0, expect: const [0], ); ``` **v6.0.0** ```dart blocTest( 'emits [] when skip is 0', build: () => CounterBloc(), skip: 0, expect: const [], ); ``` The initial state of a bloc or cubit can be tested with the following: ```dart test('initial state is correct', () { expect(MyBloc().state, InitialState()); }); ``` #### ❗blocTest make build synchronous ##### Rationale Previously, `build` was made `async` so that various preparation could be done to put the bloc under test in a specific state. It is no longer necessary and also resolves several issues due to the added latency between the build and the subscription internally. Instead of doing async prep to get a bloc in a desired state we can now set the bloc state by chaining `emit` with the desired state. **v5.x.x** ```dart blocTest( 'emits [2] when increment is added', build: () async { final bloc = CounterBloc(); bloc.add(CounterEvent.increment); await bloc.take(2); return bloc; } act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` **v6.0.0** ```dart blocTest( 'emits [2] when increment is added', build: () => CounterBloc()..emit(1), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` :::note `emit` is only visible for testing and should never be used outside of tests. ::: ### `package:flutter_bloc` #### ❗BlocBuilder bloc parameter renamed to cubit ##### Rationale In order to make `BlocBuilder` interoperate with `bloc` and `cubit` instances the `bloc` parameter was renamed to `cubit` (since `Cubit` is the base class). **v5.x.x** ```dart BlocBuilder( bloc: myBloc, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocBuilder( cubit: myBloc, builder: (context, state) {...} ) ``` #### ❗BlocListener bloc parameter renamed to cubit ##### Rationale In order to make `BlocListener` interoperate with `bloc` and `cubit` instances the `bloc` parameter was renamed to `cubit` (since `Cubit` is the base class). **v5.x.x** ```dart BlocListener( bloc: myBloc, listener: (context, state) {...} ) ``` **v6.0.0** ```dart BlocListener( cubit: myBloc, listener: (context, state) {...} ) ``` #### ❗BlocConsumer bloc parameter renamed to cubit ##### Rationale In order to make `BlocConsumer` interoperate with `bloc` and `cubit` instances the `bloc` parameter was renamed to `cubit` (since `Cubit` is the base class). **v5.x.x** ```dart BlocConsumer( bloc: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocConsumer( cubit: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` --- ## v5.0.0 ### `package:bloc` #### ❗initialState সরানো হয়েছে ##### Rationale একজন ডেভেলপার হিসেবে, একটি bloc তৈরি করার সময় `initialState` override করতে হলে দুটি main issue দেখা দেয়: - Bloc-এর `initialState` dynamic হতে পারে এবং এটি একটি পরবর্তী সময়ে reference করা যেতে পারে (bloc-এর বাইরেও)। কিছু উপায়ে, এটি internal bloc information UI layer-এ leak করা হিসেবে দেখা যেতে পারে। - এটি verbose। **v4.x.x** ```dart class CounterBloc extends Bloc { @override int get initialState => 0; ... } ``` **v5.0.0** ```dart class CounterBloc extends Bloc { CounterBloc() : super(0); ... } ``` ?> আরও তথ্যের জন্য দেখুন [#1304](https://github.com/felangel/bloc/issues/1304) #### ❗BlocDelegate-কে BlocObserver নামকরণ ##### Rationale `BlocDelegate` নামটি class-টি যে role পালন করত তার একটি accurate description ছিল না। `BlocDelegate` suggests করে যে class-টি একটি active role পালন করে যেখানে বাস্তবে `BlocDelegate`-এর intended role ছিল এটি একটি passive component হওয়া যা কেবল application-এ সব blocs observe করে। :::note `BlocObserver`-এর মধ্যে user-facing functionality বা features handle করা উচিত নয় ideally। ::: **v4.x.x** ```dart class MyBlocDelegate extends BlocDelegate { ... } ``` **v5.0.0** ```dart class MyBlocObserver extends BlocObserver { ... } ``` #### ❗BlocSupervisor সরানো হয়েছে ##### Rationale `BlocSupervisor` আরেকটি component ছিল যা ডেভেলপারদের জানতে এবং interact করতে হয়েছিল একটি custom `BlocDelegate` specify করার একমাত্র উদ্দেশ্যে। `BlocObserver`-এ পরিবর্তনের সাথে আমরা মনে করি এটি developer experience উন্নত করেছে observer সরাসরি bloc-এ set করতে। ?> এই পরিবর্তনটি আমাদের অন্যান্য bloc add-ons যেমন `HydratedStorage`-কে `BlocObserver` থেকে decouple করতেও সক্ষম করেছে। **v4.x.x** ```dart BlocSupervisor.delegate = MyBlocDelegate(); ``` **v5.0.0** ```dart Bloc.observer = MyBlocObserver(); ``` ### `package:flutter_bloc` #### ❗BlocBuilder condition-কে buildWhen নামকরণ ##### Rationale `BlocBuilder` ব্যবহার করার সময়, আমরা আগে একটি `condition` specify করতে পারতাম নির্ধারণ করতে `builder` rebuild করা উচিত কিনা। ```dart BlocBuilder( condition: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...} ) ``` `condition` নামটি খুব self-explanatory বা obvious নয় এবং আরও গুরুত্বপূর্ণ, `BlocConsumer`-এর সাথে interact করার সময় API inconsistent হয়ে গিয়েছিল কারণ ডেভেলপাররা দুটি condition প্রদান করতে পারে (একটি `builder`-এর জন্য এবং একটি `listener`-এর জন্য)। ফলস্বরূপ, `BlocConsumer` API একটি `buildWhen` এবং `listenWhen` expose করেছিল ```dart BlocConsumer( listenWhen: (previous, current) { // return true/false to determine whether to call listener }, listener: (context, state) {...}, buildWhen: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...}, ) ``` API align করতে এবং আরও consistent developer experience প্রদান করতে, `condition`-কে `buildWhen` নামকরণ করা হয়েছে। **v4.x.x** ```dart BlocBuilder( condition: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...} ) ``` **v5.0.0** ```dart BlocBuilder( buildWhen: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...} ) ``` #### ❗BlocListener condition-কে listenWhen নামকরণ ##### Rationale উপরে বর্ণিত একই কারণে, `BlocListener` condition-ও নামকরণ করা হয়েছে। **v4.x.x** ```dart BlocListener( condition: (previous, current) { // return true/false to determine whether to call listener }, listener: (context, state) {...} ) ``` **v5.0.0** ```dart BlocListener( listenWhen: (previous, current) { // return true/false to determine whether to call listener }, listener: (context, state) {...} ) ``` ### `package:hydrated_bloc` #### ❗HydratedStorage এবং HydratedBlocStorage নামকরণ ##### Rationale [hydrated_bloc](https://pub.dev/packages/hydrated_bloc) এবং [hydrated_cubit](https://pub.dev/packages/hydrated_cubit)-এর মধ্যে code reuse উন্নত করতে, concrete default storage implementation-কে `HydratedBlocStorage` থেকে `HydratedStorage` নামকরণ করা হয়েছে। এছাড়াও, `HydratedStorage` interface-কে `HydratedStorage` থেকে `Storage` নামকরণ করা হয়েছে। **v4.0.0** ```dart class MyHydratedStorage implements HydratedStorage { ... } ``` **v5.0.0** ```dart class MyHydratedStorage implements Storage { ... } ``` #### ❗HydratedStorage-কে BlocDelegate থেকে decouple করা ##### Rationale আগে উল্লিখিত, `BlocDelegate`-কে `BlocObserver` নামকরণ করা হয়েছে এবং set করা হয়েছে সরাসরি `bloc`-এর অংশ হিসেবে: ```dart Bloc.observer = MyBlocObserver(); ``` নিম্নলিখিত পরিবর্তন করা হয়েছে: - নতুন bloc observer API-এর সাথে consistent থাকতে - Storage-কে শুধুমাত্র `HydratedBloc`-এ scoped রাখতে - `BlocObserver`-কে `Storage` থেকে decouple করতে **v4.0.0** ```dart BlocSupervisor.delegate = await HydratedBlocDelegate.build(); ``` **v5.0.0** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` #### ❗Simplified Initialization ##### Rationale আগে, ডেভেলপারদের manually call করতে হত `super.initialState ?? DefaultInitialState()` তাদের `HydratedBloc` instances setup করার জন্য। এটি clunky এবং verbose এবং `bloc`-এ `initialState`-এর breaking changes-এর সাথেও incompatible। ফলস্বরূপ, v5.0.0-এ `HydratedBloc` initialization normal `Bloc` initialization-এর মতোই। **v4.0.0** ```dart class CounterBloc extends HydratedBloc { @override int get initialState => super.initialState ?? 0; } ``` **v5.0.0** ```dart class CounterBloc extends HydratedBloc { CounterBloc() : super(0); ... } ``` ================================================ FILE: docs/src/content/docs/bn/modeling-state.mdx ================================================ --- title: স্টেট মডেলিং description: package:bloc ব্যবহার করার সময় স্টেট মডেল করার বিভিন্ন উপায়ের একটি ওভারভিউ। --- import ConcreteClassAndStatusEnumSnippet from '~/components/modeling-state/ConcreteClassAndStatusEnumSnippet.astro'; import SealedClassAndSubclassesSnippet from '~/components/modeling-state/SealedClassAndSubclassesSnippet.astro'; অ্যাপ্লিকেশনের স্টেট সংগঠিত করার ক্ষেত্রে বিভিন্ন ধরনের পদ্ধতি রয়েছে। প্রতিটিরই নিজস্ব সুবিধা ও সীমাবদ্ধতা আছে। এই সেকশনে আমরা কয়েকটি পদ্ধতি, তাদের সুবিধা-অসুবিধা এবং কোন পরিস্থিতিতে কোনটি ব্যবহার করা ভালো — তা দেখব। নিচের পদ্ধতিগুলো সম্পূর্ণই ঐচ্ছিক এবং শুধুই সুপারিশ। আপনি চাইলে আপনার পছন্দমতো যেকোনো পদ্ধতি ব্যবহার করতে পারেন। সংক্ষিপ্ত এবং সহজ রাখার জন্য কিছু উদাহরণ/ডকুমেন্টেশন এই পদ্ধতিগুলো অনুসরণ নাও করতে পারে। :::tip নিচের কোড স্নিপেটগুলো মূলত স্টেট স্ট্রাকচারের উপর ফোকাস করা। বাস্তবে, আপনি চাইলে: - `package:equatable` থেকে `Equatable` প্রসারিত করতে পারেন - `package:data_class` থেকে `@Data()` দিয়ে ক্লাস অ্যানোটেট করতে পারেন - `package:meta` থেকে **@immutable** দিয়ে ক্লাস অ্যানোটেট করতে পারেন - `copyWith` মেথড ইমপ্লিমেন্ট করতে পারেন - কনস্ট্রাক্টরে `const` কীওয়ার্ড ব্যবহার করতে পারেন ::: ## Concrete Class and Status Enum এই পদ্ধতিতে সকল স্টেটের জন্য একটি **সিঙ্গেল কনক্রিট ক্লাস** থাকে এবং একটি `enum` থাকে যা বিভিন্ন স্ট্যাটাস উপস্থাপন করে। সকল প্রপার্টি nullable করা হয় এবং বর্তমান স্ট্যাটাস অনুযায়ী ব্যবহার করা হয়। এই পদ্ধতি সবচেয়ে ভালো কাজ করে যখন স্টেটগুলো একে অপরের থেকে কঠোরভাবে আলাদা নয় বা যখন অনেকগুলো শেয়ারড প্রপার্টি থাকে। #### Pros - **সিম্পল:** একটি ক্লাস এবং একটি status enum পরিচালনা করা সহজ এবং সব প্রপার্টিই সহজে অ্যাক্সেস করা যায়। - **সংক্ষিপ্ত:** অন্যান্য পদ্ধতির তুলনায় সাধারণত কম লাইনের কোড লাগে। #### Cons - **টাইপ সেফ নয়:** প্রপার্টিতে অ্যাক্সেস করার আগে `status` চেক করতে হয়। ভুল স্টেট `emit` করা সম্ভব, যা বাগ তৈরি করতে পারে। নির্দিষ্ট স্টেটের প্রপার্টি nullable হওয়ায় null-check বা force unwrap করতে হয়, যা ঝামেলাযুক্ত হতে পারে। ইউনিট টেস্ট এবং বিশেষায়িত নামযুক্ত কনস্ট্রাক্টর লিখে এগুলোর কিছুটা কমানো যায়। - **Bloated:** সময়ের সাথে সাথে অনেক প্রপার্টি জমে একে ভারী ও জটিল করে ফেলতে পারে। #### Verdict এই পদ্ধতি সাধারণ স্টেট বা এমন পরিস্থিতিতে ভালো কাজ করে যেখানে স্টেটগুলো একে অপরের এক্সক্লুসিভ নয় (যেমন: একটি error snackbar দেখানো হচ্ছে কিন্তু পূর্বের success স্টেটের ডেটা এখনও দেখানো হচ্ছে)। এটি টাইপ সেফটির খরচে বেশি ফ্লেক্সিবিলিটি ও সংক্ষিপ্ততা দেয়। ## Sealed Class and Subclasses এই পদ্ধতিতে একটি **sealed class** থাকে যা শেয়ারড প্রপার্টিগুলো রাখে এবং প্রতিটি আলাদা স্টেটের জন্য আলাদা সাবক্লাস থাকে। এটি আলাদা ও এক্সক্লুসিভ স্টেটগুলোর জন্য অসাধারণভাবে কার্যকর। #### Pros - **টাইপ সেফ:** কম্পাইল-টাইম সেফটি থাকে এবং ভুল প্রপার্টিতে ভুলবশত অ্যাক্সেস করা যায় না। প্রতিটি সাবক্লাস তার নিজস্ব প্রপার্টি রাখে, ফলে কোন প্রপার্টি কোন স্টেটে তা খুব পরিষ্কার। - **স্পষ্ট:** শেয়ারড প্রপার্টি এবং স্টেট-নির্দিষ্ট প্রপার্টিগুলো আলাদা রাখা যায়। - **Exhaustive:** `switch` স্টেটমেন্ট ব্যবহার করে প্রত্যেকটি স্টেট স্পষ্টভাবে হ্যান্ডেল হচ্ছে কিনা তা নিশ্চিত করা যায়। - যদি আপনি [exhaustive switching](https://dart.dev/language/branches#exhaustiveness-checking) না চান অথবা পরে নতুন subtype যোগ করতে চান API ভাঙা ছাড়া, তাহলে [final](https://dart.dev/language/class-modifiers#final) modifier ব্যবহার করুন। - বিস্তারিত জানতে দেখুন: [sealed class documentation](https://dart.dev/language/class-modifiers#sealed) #### Cons - **বিস্তারিত/Verbose:** বেশি কোড লাগে (একটি বেস ক্লাস এবং প্রতিটি স্টেটের জন্য একটি করে সাবক্লাস)। এছাড়াও, সাবক্লাসগুলোতে শেয়ারড প্রপার্টি পুনরাবৃত্তি করতে হতে পারে। - **জটিল:** নতুন প্রপার্টি যোগ করতে চাইলে প্রতিটি সাবক্লাস এবং বেস ক্লাসে আপডেট করতে হয়, যা জটিলতা বাড়াতে পারে। অতিরিক্ত টাইপ-চেকিং লাগতে পারে কিছু ক্ষেত্রে। #### Verdict এই পদ্ধতি সবচেয়ে ভালো কাজ করে যখন স্টেটগুলো সুস্পষ্টভাবে আলাদা ও এক্সক্লুসিভ এবং প্রতিটির নিজস্ব প্রপার্টি থাকে। এটি টাইপ সেফটি, স্পষ্টতা এবং exhaustiveness চেক প্রদান করে এবং সংক্ষিপ্ততার চেয়ে নিরাপত্তাকে বেশি গুরুত্ব দেয়। ================================================ FILE: docs/src/content/docs/bn/naming-conventions.mdx ================================================ --- title: নামকরণ কনভেনশন description: bloc ব্যবহার করার সময় সুপারিশকৃত নামকরণ কনভেনশনগুলোর একটি ওভারভিউ। --- import EventExamplesGood1 from '~/components/naming-conventions/EventExamplesGood1Snippet.astro'; import EventExamplesBad1 from '~/components/naming-conventions/EventExamplesBad1Snippet.astro'; import StateExamplesGood1Snippet from '~/components/naming-conventions/StateExamplesGood1Snippet.astro'; import SingleStateExamplesGood1Snippet from '~/components/naming-conventions/SingleStateExamplesGood1Snippet.astro'; import StateExamplesBad1Snippet from '~/components/naming-conventions/StateExamplesBad1Snippet.astro'; নীচের নামকরণ কনভেনশনগুলো শুধুই সুপারিশ এবং সম্পূর্ণই ঐচ্ছিক। আপনি চাইলে আপনার পছন্দমতো যেকোনো নামকরণ কনভেনশন ব্যবহার করতে পারেন। কিছু উদাহরণ/ডকুমেন্টেশন সংক্ষিপ্ত রাখার সুবিধার্থে এই কনভেনশন অনুসরণ নাও করতে পারে। অনেক ডেভেলপারসহ বড় প্রজেক্টে এই কনভেনশনগুলো অনুসরণ করা বিশেষভাবে সুপারিশ করা হয়। ## Event Conventions ইভেন্টগুলোর নাম **অতীত কাল**-এ হওয়া উচিত কারণ bloc-এর দৃষ্টিকোণ থেকে ইভেন্টগুলো হলো এমন ঘটনা যেগুলো ইতোমধ্যে ঘটেছে। ### Anatomy `BlocSubject` + `Noun (optional)` + `Verb (event)` প্রাথমিক লোড ইভেন্টগুলো এই কনভেনশন অনুসরণ করবে: `BlocSubject` + `Started` :::note বেস ইভেন্ট ক্লাসের নাম হওয়া উচিত: `BlocSubject` + `Event`. ::: ### Examples ✅ **Good** ❌ **Bad** ## State Conventions স্টেটের নাম হওয়া উচিত **noun**, কারণ একটি স্টেট হলো নির্দিষ্ট সময়ে অ্যাপের একটি স্ন্যাপশট। স্টেট উপস্থাপন করার দুটি সাধারণ উপায় আছে: সাবক্লাস ব্যবহার করে বা একটি সিঙ্গেল ক্লাস ব্যবহার করে। ### Anatomy #### Subclasses `BlocSubject` + `Verb (action)` + `State` যখন স্টেটকে একাধিক সাবক্লাস হিসাবে উপস্থাপন করা হয়, তখন `State` নিম্নোক্তগুলোর একটি হওয়া উচিত: `Initial` | `Success` | `Failure` | `InProgress` :::note Initial স্টেটের কনভেনশন হওয়া উচিত: `BlocSubject` + `Initial`. ::: #### Single Class `BlocSubject` + `State` যখন স্টেটকে একটি সিঙ্গেল বেস ক্লাস হিসেবে উপস্থাপন করা হয়, তখন স্টেটের স্ট্যাটাস উপস্থাপন করার জন্য `BlocSubject` + `Status` নামে একটি enum ব্যবহার করা উচিত: `initial` | `success` | `failure` | `loading`. :::note বেস স্টেট ক্লাসের নাম সর্বদা হওয়া উচিত: `BlocSubject` + `State`. ::: ### Examples ✅ **Good** ##### Subclasses ##### Single Class ❌ **Bad** ================================================ FILE: docs/src/content/docs/bn/testing.mdx ================================================ --- title: টেস্টিং description: আপনার blocs-এর জন্য টেস্ট কীভাবে লিখবেন তার মৌলিক বিষয়গুলো। --- import CounterBlocSnippet from '~/components/testing/CounterBlocSnippet.astro'; import AddDevDependenciesSnippet from '~/components/testing/AddDevDependenciesSnippet.astro'; import CounterBlocTestImportsSnippet from '~/components/testing/CounterBlocTestImportsSnippet.astro'; import CounterBlocTestMainSnippet from '~/components/testing/CounterBlocTestMainSnippet.astro'; import CounterBlocTestSetupSnippet from '~/components/testing/CounterBlocTestSetupSnippet.astro'; import CounterBlocTestInitialStateSnippet from '~/components/testing/CounterBlocTestInitialStateSnippet.astro'; import CounterBlocTestBlocTestSnippet from '~/components/testing/CounterBlocTestBlocTestSnippet.astro'; Bloc এমনভাবে ডিজাইন করা হয়েছে যাতে এটি টেস্ট করা অত্যন্ত সহজ হয়। এই সেকশনে আমরা একটি bloc কীভাবে ইউনিট টেস্ট করতে হয় তা দেখে নেব। সহজতার জন্য, চলুন আমরা [Core Concepts](/bn/bloc-concepts)-এ তৈরি করা `CounterBloc`-এর জন্য টেস্ট লিখি। সংক্ষেপে বলতে গেলে, `CounterBloc`-এর ইমপ্লিমেন্টেশনটি এমন দেখায়: ## Setup টেস্ট লেখার আগে আমাদের ডিপেনডেন্সিতে একটি টেস্টিং ফ্রেমওয়ার্ক যোগ করতে হবে। আমাদের প্রজেক্টে [test](https://pub.dev/packages/test) এবং [bloc_test](https://pub.dev/packages/bloc_test) যোগ করতে হবে। ## Testing এখন শুরু করি আমাদের `CounterBloc` টেস্টগুলোর জন্য ফাইল তৈরি করে। ফাইলের নাম হবে `counter_bloc_test.dart` এবং আমরা সেখানে test প্যাকেজ ইমপোর্ট করব। এরপর আমাদের `main` তৈরি করতে হবে এবং টেস্ট গ্রুপ তৈরি করতে হবে। :::note গ্রুপগুলো ব্যবহৃত হয় পৃথক টেস্টগুলো সংগঠিত করতে এবং এমন একটি কনটেক্সট তৈরি করতে যেখানে আপনি সব টেস্টের জন্য সাধারণ `setUp` এবং `tearDown` শেয়ার করতে পারবেন। ::: চলুন শুরু করি আমাদের `CounterBloc`-এর একটি ইনস্ট্যান্স তৈরি করে, যা সব টেস্টেই ব্যবহৃত হবে। এখন আমরা আমাদের পৃথক টেস্টগুলো লেখা শুরু করতে পারি। :::note আমরা `dart test` কমান্ড দিয়ে সব টেস্ট রান করতে পারি। ::: এ পর্যায়ে আমাদের প্রথম টেস্টটি পাশ করা উচিত! এখন চলুন [bloc_test](https://pub.dev/packages/bloc_test) প্যাকেজ ব্যবহার করে আরও জটিল একটি টেস্ট লিখি। আমাদের এখন টেস্টগুলো রান করতে সক্ষম হওয়া উচিত এবং সবগুলো পাস করবে। এতেই সব শেষ — টেস্টিং হওয়া উচিত খুবই সহজ, এবং কোড পরিবর্তন বা রিফ্যাক্টর করার সময় আমাদের আত্মবিশ্বাসী লাগবে। আপনি সম্পূর্ণ টেস্ট করা একটি অ্যাপ্লিকেশনের উদাহরণ হিসেবে দেখতে পারেন [Weather App](https://github.com/felangel/bloc/tree/master/examples/flutter_weather)। ================================================ FILE: docs/src/content/docs/bn/why-bloc.mdx ================================================ --- title: কেন Bloc? description: Bloc কেন একটি শক্তিশালী স্টেট ম্যানেজমেন্ট সমাধান — তার একটি সংক্ষিপ্ত ধারণা। sidebar: order: 1 --- Bloc উপস্থাপনাকে ব্যবসায়িক লজিক থেকে আলাদা করা সহজ করে তোলে, যার ফলে আপনার কোড হয় _দ্রুত_, _সহজে পরীক্ষাযোগ্য_, এবং _পুনঃব্যবহারযোগ্য_। প্রোডাকশন-মানের অ্যাপ্লিকেশন তৈরি করার সময়, স্টেট ম্যানেজমেন্ট অত্যন্ত গুরুত্বপূর্ণ হয়ে ওঠে। ডেভেলপার হিসেবে আমরা চাই: - যেকোনো মুহূর্তে আমাদের অ্যাপ্লিকেশনের স্টেট কী — তা জানতে। - প্রতিটি কেস সহজে পরীক্ষা করতে যাতে নিশ্চিত হওয়া যায় আমাদের অ্যাপ সঠিকভাবে প্রতিক্রিয়া দিচ্ছে। - আমাদের অ্যাপ্লিকেশনের প্রতিটি ইউজার ইন্টারঅ্যাকশন রেকর্ড করতে যাতে ডেটা-চালিত সিদ্ধান্ত নিতে পারি। - যতটা সম্ভব দক্ষভাবে কাজ করতে এবং আমাদের অ্যাপ্লিকেশন ও অন্যান্য অ্যাপ্লিকেশনে কম্পোনেন্টগুলো পুনঃব্যবহার করতে। - একই প্যাটার্ন ও কনভেনশন অনুসরণ করে অনেক ডেভেলপার যেন নির্বিঘ্নে একই কোডবেসে কাজ করতে পারে। - দ্রুত ও রিঅ্যাকটিভ অ্যাপ ডেভেলপ করতে। Bloc এই সব প্রয়োজন এবং আরও অনেক কিছু পূরণ করার জন্য ডিজাইন করা হয়েছে। অনেক স্টেট ম্যানেজমেন্ট সমাধান রয়েছে এবং কোনটি ব্যবহার করবেন তা সিদ্ধান্ত নেওয়া কঠিন হতে পারে। একটিও সমাধান "পারফেক্ট" নয়! গুরুত্বপূর্ণ হলো—আপনার টিম এবং আপনার প্রজেক্টের জন্য যেটি সবচেয়ে উপযোগী, সেটি নির্বাচন করা। Bloc তিনটি মূল মূল্যবোধকে কেন্দ্র করে ডিজাইন করা হয়েছে: - **Simple:** বুঝতে সহজ এবং বিভিন্ন দক্ষতার ডেভেলপাররা ব্যবহার করতে পারে। - **Powerful:** ছোট ছোট কম্পোনেন্ট মিলে জটিল ও চমৎকার অ্যাপ্লিকেশন তৈরি করতে সাহায্য করে। - **Testable:** অ্যাপ্লিকেশনের প্রতিটি অংশ সহজেই পরীক্ষা করা যায় যাতে আমরা আত্মবিশ্বাসের সাথে ইটারেট করতে পারি। সর্বোপরি, Bloc স্টেট পরিবর্তনকে পূর্বানুমানযোগ্য করতে চায়—স্টেট কখন পরিবর্তিত হতে পারে তা নিয়ন্ত্রণ করে এবং পুরো অ্যাপ্লিকেশন জুড়ে স্টেট পরিবর্তনের একটি নির্দিষ্ট পদ্ধতি অনুসরণ করিয়ে। ================================================ FILE: docs/src/content/docs/de/bloc-concepts.mdx ================================================ --- title: Bloc Konzepte description: Ein Überblick über die Kernkonzepte für package:bloc. sidebar: order: 1 --- import CountStreamSnippet from '~/components/concepts/bloc/CountStreamSnippet.astro'; import SumStreamSnippet from '~/components/concepts/bloc/SumStreamSnippet.astro'; import StreamsMainSnippet from '~/components/concepts/bloc/StreamsMainSnippet.astro'; import CounterCubitSnippet from '~/components/concepts/bloc/CounterCubitSnippet.astro'; import CounterCubitInitialStateSnippet from '~/components/concepts/bloc/CounterCubitInitialStateSnippet.astro'; import CounterCubitInstantiationSnippet from '~/components/concepts/bloc/CounterCubitInstantiationSnippet.astro'; import CounterCubitIncrementSnippet from '~/components/concepts/bloc/CounterCubitIncrementSnippet.astro'; import CounterCubitBasicUsageSnippet from '~/components/concepts/bloc/CounterCubitBasicUsageSnippet.astro'; import CounterCubitStreamUsageSnippet from '~/components/concepts/bloc/CounterCubitStreamUsageSnippet.astro'; import CounterCubitOnChangeSnippet from '~/components/concepts/bloc/CounterCubitOnChangeSnippet.astro'; import CounterCubitOnChangeUsageSnippet from '~/components/concepts/bloc/CounterCubitOnChangeUsageSnippet.astro'; import CounterCubitOnChangeOutputSnippet from '~/components/concepts/bloc/CounterCubitOnChangeOutputSnippet.astro'; import SimpleBlocObserverOnChangeSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeSnippet.astro'; import SimpleBlocObserverOnChangeUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeUsageSnippet.astro'; import SimpleBlocObserverOnChangeOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeOutputSnippet.astro'; import CounterCubitOnErrorSnippet from '~/components/concepts/bloc/CounterCubitOnErrorSnippet.astro'; import SimpleBlocObserverOnErrorSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnErrorSnippet.astro'; import CounterCubitOnErrorOutputSnippet from '~/components/concepts/bloc/CounterCubitOnErrorOutputSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/bloc/CounterBlocSnippet.astro'; import CounterBlocEventHandlerSnippet from '~/components/concepts/bloc/CounterBlocEventHandlerSnippet.astro'; import CounterBlocIncrementSnippet from '~/components/concepts/bloc/CounterBlocIncrementSnippet.astro'; import CounterBlocUsageSnippet from '~/components/concepts/bloc/CounterBlocUsageSnippet.astro'; import CounterBlocStreamUsageSnippet from '~/components/concepts/bloc/CounterBlocStreamUsageSnippet.astro'; import CounterBlocOnChangeSnippet from '~/components/concepts/bloc/CounterBlocOnChangeSnippet.astro'; import CounterBlocOnChangeUsageSnippet from '~/components/concepts/bloc/CounterBlocOnChangeUsageSnippet.astro'; import CounterBlocOnChangeOutputSnippet from '~/components/concepts/bloc/CounterBlocOnChangeOutputSnippet.astro'; import CounterBlocOnTransitionSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionSnippet.astro'; import CounterBlocOnTransitionOutputSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionOutputSnippet.astro'; import SimpleBlocObserverOnTransitionSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionSnippet.astro'; import SimpleBlocObserverOnTransitionUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionUsageSnippet.astro'; import SimpleBlocObserverOnTransitionOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionOutputSnippet.astro'; import CounterBlocOnEventSnippet from '~/components/concepts/bloc/CounterBlocOnEventSnippet.astro'; import SimpleBlocObserverOnEventSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventSnippet.astro'; import SimpleBlocObserverOnEventOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventOutputSnippet.astro'; import CounterBlocOnErrorSnippet from '~/components/concepts/bloc/CounterBlocOnErrorSnippet.astro'; import CounterBlocOnErrorOutputSnippet from '~/components/concepts/bloc/CounterBlocOnErrorOutputSnippet.astro'; import CounterCubitFullSnippet from '~/components/concepts/bloc/CounterCubitFullSnippet.astro'; import CounterBlocFullSnippet from '~/components/concepts/bloc/CounterBlocFullSnippet.astro'; import AuthenticationStateSnippet from '~/components/concepts/bloc/AuthenticationStateSnippet.astro'; import AuthenticationTransitionSnippet from '~/components/concepts/bloc/AuthenticationTransitionSnippet.astro'; import AuthenticationChangeSnippet from '~/components/concepts/bloc/AuthenticationChangeSnippet.astro'; import DebounceEventTransformerSnippet from '~/components/concepts/bloc/DebounceEventTransformerSnippet.astro'; :::note Bitte lies die folgenden Abschnitte sorgfältig durch, bevor du mit [`package:bloc`](https://pub.dev/packages/bloc) arbeitest. ::: Es gibt mehrere Kernkonzepte, die entscheidend sind, um zu verstehen, wie man das Bloc-Package verwendet. In den kommenden Abschnitten werden wir jeden davon im Detail besprechen und durcharbeiten, wie sie auf eine Counter-App angewendet werden würden. ## Streams :::note Sieh dir die offizielle [Dart Dokumentation](https://dart.dev/tutorials/language/streams) für weitere Informationen über `Streams` an. ::: Ein Stream ist eine Sequenz von asynchronen Daten. Um die Bloc Library zu verwenden, ist es entscheidend, ein grundlegendes Verständnis von `Streams` und ihrer Funktionsweise zu haben. Wenn du mit `Streams` nicht vertraut bist, denk einfach an ein Rohr mit fließendem Wasser. Das Rohr ist der `Stream` und das Wasser sind die asynchronen Daten. Wir können einen `Stream` in Dart erstellen, indem wir eine `async*` (async generator) Funktion schreiben. Indem wir eine Funktion als `async*` markieren, können wir das `yield` keyword verwenden und einen `Stream` von Daten zurückgeben. Im obigen Beispiel geben wir einen `Stream` von Integers bis zum `max` Integer-Parameter zurück. Jedes Mal, wenn wir in einer `async*` Funktion `yield` verwenden, pushen wir dieses Datenstück durch den `Stream`. Wir können den obigen `Stream` auf verschiedene Weise konsumieren. Wenn wir eine Funktion schreiben wollten, die die Summe eines `Stream` von Integers zurückgibt, könnte sie so aussehen: Indem wir die obige Funktion als `async` markieren, können wir das `await` keyword verwenden und eine `Future` von Integers zurückgeben. In diesem Beispiel warten wir auf jeden Wert im Stream und geben die Summe aller Integers im Stream zurück. Wir können alles zusammenführen: Jetzt, da wir ein grundlegendes Verständnis davon haben, wie `Streams` in Dart funktionieren, sind wir bereit, mehr über die Kernkomponente des Bloc-Packages zu lernen: einen `Cubit`. ## Cubit Ein `Cubit` ist eine Klasse, die `BlocBase` erweitert und erweitert werden kann, um jeden Typ von State zu verwalten. ![Cubit Architecture](~/assets/concepts/cubit_architecture_full.png) Ein `Cubit` kann Funktionen bereitstellen, die aufgerufen werden können, um State-Änderungen auszulösen. States sind die Ausgabe eines `Cubit` und repräsentieren einen Teil des States deiner Anwendung. UI-Komponenten können über States benachrichtigt werden und Teile von sich selbst basierend auf dem aktuellen State neu zeichnen. :::note Für weitere Informationen über die Herkunft von `Cubit` sieh dir [diesen Issue](https://github.com/felangel/cubit/issues/69) an. ::: ### Erstellen eines Cubit Wir können einen `CounterCubit` so erstellen: Beim Erstellen eines `Cubit` müssen wir den Typ des States definieren, den der `Cubit` verwalten wird. Im Fall des obigen `CounterCubit` kann der State durch einen `int` repräsentiert werden, aber in komplexeren Fällen könnte es notwendig sein, eine `class` anstelle eines primitiven Typs zu verwenden. Das zweite, was wir beim Erstellen eines `Cubit` tun müssen, ist den initialen State anzugeben. Wir können dies tun, indem wir `super` mit dem Wert des initialen States aufrufen. Im obigen Snippet setzen wir den initialen State intern auf `0`, aber wir können den `Cubit` auch flexibler machen, indem wir einen externen Wert akzeptieren: Dies würde es uns ermöglichen, `CounterCubit` Instanzen mit verschiedenen initialen States zu instanziieren: ### Cubit State-Änderungen Jeder `Cubit` hat die Fähigkeit, einen neuen State über `emit` auszugeben. Im obigen Snippet stellt der `CounterCubit` eine öffentliche Methode namens `increment` bereit, die extern aufgerufen werden kann, um den `CounterCubit` zu benachrichtigen, seinen State zu erhöhen. Wenn `increment` aufgerufen wird, können wir auf den aktuellen State des `Cubit` über den `state` Getter zugreifen und einen neuen State durch Addition von 1 zum aktuellen State `emit`en. :::caution Die `emit` Methode ist geschützt, was bedeutet, dass sie nur innerhalb eines `Cubit` verwendet werden sollte. ::: ### Verwendung eines Cubit Wir können jetzt den `CounterCubit`, den wir implementiert haben, verwenden! #### Grundlegende Verwendung Im obigen Snippet beginnen wir damit, eine Instanz des `CounterCubit` zu erstellen. Wir drucken dann den aktuellen State des Cubit, der der initiale State ist (da noch keine neuen States emittiert wurden). Als Nächstes rufen wir die `increment` Funktion auf, um eine State-Änderung auszulösen. Schließlich drucken wir den State des `Cubit` erneut, der von `0` auf `1` gegangen ist, und rufen `close` auf dem `Cubit` auf, um den internen State- Stream zu schließen. #### Stream-Verwendung `Cubit` stellt einen `Stream` bereit, der es uns ermöglicht, Echtzeit-State-Updates zu erhalten: Im obigen Snippet abonnieren wir den `CounterCubit` und rufen print bei jeder State-Änderung auf. Wir rufen dann die `increment` Funktion auf, die einen neuen State emittiert. Schließlich rufen wir `cancel` auf dem `subscription` auf, wenn wir keine Updates mehr erhalten möchten, und schließen den `Cubit`. :::note `await Future.delayed(Duration.zero)` wird für dieses Beispiel hinzugefügt, um das Abonnement nicht sofort zu kündigen. ::: :::caution Nur nachfolgende State-Änderungen werden empfangen, wenn `listen` auf einem `Cubit` aufgerufen wird. ::: ### Beobachten eines Cubit Wenn ein `Cubit` einen neuen State emittiert, tritt eine `Change` auf. Wir können alle Änderungen für einen bestimmten `Cubit` beobachten, indem wir `onChange` überschreiben. Wir können dann mit dem `Cubit` interagieren und alle Änderungen in der Konsole ausgeben. Das obige Beispiel würde folgende Ausgabe erzeugen: :::note Eine `Change` tritt kurz vor der Aktualisierung des States des `Cubit` auf. Eine `Change` besteht aus dem `currentState` und dem `nextState`. ::: #### BlocObserver Ein zusätzlicher Vorteil der Verwendung der Bloc Library ist, dass wir Zugriff auf alle `Changes` an einem Ort haben. Obwohl wir in dieser Anwendung nur einen `Cubit` haben, ist es in größeren Anwendungen ziemlich üblich, viele `Cubits` zu haben, die verschiedene Teile des States der Anwendung verwalten. Wenn wir in der Lage sein wollen, auf alle `Changes` zu reagieren, können wir einfach unseren eigenen `BlocObserver` erstellen. :::note Alles, was wir tun müssen, ist `BlocObserver` zu erweitern und die `onChange` Methode zu überschreiben. ::: Um den `SimpleBlocObserver` zu verwenden, müssen wir nur die `main` Funktion anpassen: Das obige Snippet würde dann folgende Ausgabe erzeugen: :::note Die interne `onChange` Überschreibung wird zuerst aufgerufen, die `super.onChange` aufruft und damit `onChange` im `BlocObserver` benachrichtigt. ::: :::tip In `BlocObserver` haben wir Zugriff auf die `Cubit` Instanz zusätzlich zur `Change` selbst. ::: ### Cubit Fehlerbehandlung Jeder `Cubit` hat eine `addError` Methode, die verwendet werden kann, um anzuzeigen, dass ein Fehler aufgetreten ist. :::note `onError` kann innerhalb des `Cubit` überschrieben werden, um alle Fehler für einen spezifischen `Cubit` zu behandeln. ::: `onError` kann auch in `BlocObserver` überschrieben werden, um alle gemeldeten Fehler global zu behandeln. Wenn wir das gleiche Programm erneut ausführen, sollten wir folgende Ausgabe sehen: ## Bloc Ein `Bloc` ist eine fortgeschrittenere Klasse, die sich auf `Events` verlässt, um `State`- Änderungen auszulösen, anstatt auf Funktionen. `Bloc` erweitert auch `BlocBase`, was bedeutet, dass es eine ähnliche öffentliche API wie `Cubit` hat. Anstatt jedoch eine `function` auf einem `Bloc` aufzurufen und direkt einen neuen `state` zu emittieren, empfangen `Blocs` `events` und konvertieren die eingehenden `events` in ausgehende `states`. ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) ### Erstellen eines Bloc Das Erstellen eines `Bloc` ist ähnlich wie das Erstellen eines `Cubit`, außer dass wir zusätzlich zur Definition des States, den wir verwalten werden, auch das Event definieren müssen, das der `Bloc` verarbeiten können wird. Events sind die Eingabe eines Bloc. Sie werden häufig als Reaktion auf Benutzer- interaktionen wie Button-Drücke oder Lifecycle-Events wie Seitenladungen hinzugefügt. Genau wie beim Erstellen des `CounterCubit` müssen wir einen initialen State angeben, indem wir ihn über `super` an die Superklasse übergeben. ### Bloc State-Änderungen `Bloc` erfordert, dass wir Event-Handler über die `on` API registrieren, im Gegensatz zu Funktionen in `Cubit`. Ein Event-Handler ist dafür verantwortlich, alle eingehenden Events in null oder mehr ausgehende States umzuwandeln. :::tip Ein `EventHandler` hat Zugriff auf das hinzugefügte Event sowie einen `Emitter`, der verwendet werden kann, um null oder mehr States als Reaktion auf das eingehende Event zu emittieren. ::: Wir können dann den `EventHandler` aktualisieren, um das `CounterIncrementPressed` Event zu behandeln: Im obigen Snippet haben wir einen `EventHandler` registriert, um alle `CounterIncrementPressed` Events zu verwalten. Für jedes eingehende `CounterIncrementPressed` Event können wir auf den aktuellen State des Bloc über den `state` Getter zugreifen und `emit(state + 1)` aufrufen. :::note Da die `Bloc` Klasse `BlocBase` erweitert, haben wir Zugriff auf den aktuellen State des Bloc zu jedem Zeitpunkt über den `state` Getter, genau wie in `Cubit`. ::: :::caution Blocs sollten niemals direkt neue States `emit`en. Stattdessen muss jede State-Änderung als Reaktion auf ein eingehendes Event innerhalb eines `EventHandler` ausgegeben werden. ::: :::caution Sowohl Blocs als auch Cubits ignorieren doppelte States. Wenn wir `State nextState` emittieren, wobei `state == nextState`, dann tritt keine State-Änderung auf. ::: ### Verwendung eines Bloc An diesem Punkt können wir eine Instanz unseres `CounterBloc` erstellen und verwenden! #### Grundlegende Verwendung Im obigen Snippet beginnen wir damit, eine Instanz des `CounterBloc` zu erstellen. Wir drucken dann den aktuellen State des `Bloc`, der der initiale State ist (da noch keine neuen States emittiert wurden). Als Nächstes fügen wir das `CounterIncrementPressed` Event hinzu, um eine State-Änderung auszulösen. Schließlich drucken wir den State des `Bloc` erneut, der von `0` auf `1` gegangen ist, und rufen `close` auf dem `Bloc` auf, um den internen State-Stream zu schließen. :::note `await Future.delayed(Duration.zero)` wird hinzugefügt, um sicherzustellen, dass wir auf die nächste Event-Loop-Iteration warten (ermöglicht dem `EventHandler`, das Event zu verarbeiten). ::: #### Stream-Verwendung Genau wie bei `Cubit` ist ein `Bloc` ein spezieller Typ von `Stream`, was bedeutet, dass wir uns auch für einen `Bloc` anmelden können, um Echtzeit-Updates seines States zu erhalten: Im obigen Snippet abonnieren wir den `CounterBloc` und rufen print bei jeder State-Änderung auf. Wir fügen dann das `CounterIncrementPressed` Event hinzu, das den `on` `EventHandler` auslöst und einen neuen State emittiert. Schließlich rufen wir `cancel` auf dem Abonnement auf, wenn wir keine Updates mehr erhalten möchten, und schließen den `Bloc`. :::note `await Future.delayed(Duration.zero)` wird für dieses Beispiel hinzugefügt, um das Abonnement nicht sofort zu kündigen. ::: ### Beobachten eines Bloc Da `Bloc` `BlocBase` erweitert, können wir alle State-Änderungen für einen `Bloc` mit `onChange` beobachten. Wir können dann `main.dart` aktualisieren zu: Wenn wir nun das obige Snippet ausführen, wird die Ausgabe sein: Ein wichtiger Unterscheidungsfaktor zwischen `Bloc` und `Cubit` ist, dass wir, weil `Bloc` event-gesteuert ist, auch Informationen darüber erfassen können, was die State-Änderung ausgelöst hat. Wir können dies tun, indem wir `onTransition` überschreiben. Die Änderung von einem State zu einem anderen wird `Transition` genannt. Eine `Transition` besteht aus dem aktuellen State, dem Event und dem nächsten State. Wenn wir dann das gleiche `main.dart` Snippet von vorher erneut ausführen, sollten wir die folgende Ausgabe sehen: :::note `onTransition` wird vor `onChange` aufgerufen und enthält das Event, das die Änderung von `currentState` zu `nextState` ausgelöst hat. ::: #### BlocObserver Genau wie zuvor können wir `onTransition` in einem benutzerdefinierten `BlocObserver` überschreiben, um alle Transitions zu beobachten, die von einem einzigen Ort aus auftreten. Wir können den `SimpleBlocObserver` genau wie zuvor initialisieren: Wenn wir nun das obige Snippet ausführen, sollte die Ausgabe so aussehen: :::note `onTransition` wird zuerst aufgerufen (lokal vor global) gefolgt von `onChange`. ::: Eine weitere einzigartige Funktion von `Bloc` Instanzen ist, dass sie uns erlauben, `onEvent` zu überschreiben, das aufgerufen wird, wann immer ein neues Event zum `Bloc` hinzugefügt wird. Genau wie bei `onChange` und `onTransition` kann `onEvent` sowohl lokal als auch global überschrieben werden. Wir können das gleiche `main.dart` wie zuvor ausführen und sollten folgende Ausgabe sehen: :::note `onEvent` wird aufgerufen, sobald das Event hinzugefügt wird. Das lokale `onEvent` wird vor dem globalen `onEvent` in `BlocObserver` aufgerufen. ::: ### Bloc Fehlerbehandlung Genau wie bei `Cubit` hat jeder `Bloc` eine `addError` und `onError` Methode. Wir können anzeigen, dass ein Fehler aufgetreten ist, indem wir `addError` von überall innerhalb unseres `Bloc` aufrufen. Wir können dann auf alle Fehler reagieren, indem wir `onError` überschreiben, genau wie bei `Cubit`. Wenn wir das gleiche `main.dart` wie zuvor erneut ausführen, können wir sehen, wie es aussieht, wenn ein Fehler gemeldet wird: :::note Das lokale `onError` wird zuerst aufgerufen, gefolgt vom globalen `onError` in `BlocObserver`. ::: :::note `onError` und `onChange` funktionieren auf genau die gleiche Weise für sowohl `Bloc` als auch `Cubit` Instanzen. ::: :::caution Alle unbehandelten Ausnahmen, die innerhalb eines `EventHandler` auftreten, werden auch an `onError` gemeldet. ::: ## Cubit vs. Bloc Jetzt, da wir die Grundlagen der `Cubit` und `Bloc` Klassen behandelt haben, fragst du dich vielleicht, wann du `Cubit` verwenden solltest und wann du `Bloc` verwenden solltest. ### Cubit Vorteile #### Unkompliziert Einer der größten Vorteile der Verwendung von `Cubit` ist, dass es unkompliziert ist. Beim Erstellen eines `Cubit` müssen wir nur den State sowie die Funktionen definieren, die wir bereitstellen möchten, um den State zu ändern. Im Vergleich dazu müssen wir beim Erstellen eines `Bloc` die States, Events und die `EventHandler` Implementierung definieren. Dies macht `Cubit` einfacher zu verstehen und erfordert weniger Code. Lass uns nun einen Blick auf die beiden Counter-Implementierungen werfen: ##### CounterCubit ##### CounterBloc Die `Cubit` Implementierung ist kompakter und anstatt Events separat zu definieren, verhalten sich die Funktionen wie Events. Zusätzlich können wir, wenn wir einen `Cubit` verwenden, einfach `emit` von überall aufrufen, um eine State-Änderung auszulösen. ### Bloc Vorteile #### Nachverfolgbarkeit Einer der größten Vorteile der Verwendung von `Bloc` ist, die Sequenz von State- Änderungen sowie genau zu wissen, was diese Änderungen ausgelöst hat. Für State, der kritisch für die Funktionalität einer Anwendung ist, könnte es sehr vorteilhaft sein, einen event-gesteuerten Ansatz zu verwenden, um alle Events zusätzlich zu State-Änderungen zu erfassen. Ein häufiger Anwendungsfall könnte die Verwaltung von `AuthenticationState` sein. Der Einfachheit halber nehmen wir an, dass wir `AuthenticationState` über ein `enum` repräsentieren können: Es könnte viele Gründe geben, warum sich der State der Anwendung von `authenticated` zu `unauthenticated` ändern könnte. Zum Beispiel könnte der Benutzer auf einen Logout-Button getippt haben und sich von der Anwendung abmelden wollen. Auf der anderen Seite könnte das Access-Token des Benutzers widerrufen worden sein und sie wurden zwangsweise abgemeldet. Wenn wir `Bloc` verwenden, können wir klar nachverfolgen, wie der Anwendungsstate zu einem bestimmten State gelangt ist. Die obige `Transition` gibt uns alle Informationen, die wir brauchen, um zu verstehen, warum sich der State geändert hat. Wenn wir einen `Cubit` verwendet hätten, um den `AuthenticationState` zu verwalten, würden unsere Logs so aussehen: Dies sagt uns, dass der Benutzer abgemeldet wurde, erklärt aber nicht warum, was für das Debugging und das Verstehen, wie sich der State der Anwendung im Laufe der Zeit ändert, kritisch sein könnte. #### Erweiterte Event-Transformationen Ein weiterer Bereich, in dem `Bloc` gegenüber `Cubit` hervorsticht, ist, wenn wir reaktive Operatoren wie `buffer`, `debounceTime`, `throttle`, etc. nutzen müssen. :::tip Sieh dir [`package:stream_transform`](https://pub.dev/packages/stream_transform) und [`package:rxdart`](https://pub.dev/packages/rxdart) für Stream-Transformatoren an. ::: `Bloc` hat einen Event-Sink, der es uns ermöglicht, den eingehenden Event-Flow zu steuern und zu transformieren. Wenn wir zum Beispiel eine Echtzeit-Suche bauen würden, würden wir wahrscheinlich die Anfragen an das Backend debouncen wollen, um Rate-Limiting zu vermeiden sowie Kosten/Last auf dem Backend zu reduzieren. Mit `Bloc` können wir einen benutzerdefinierten `EventTransformer` bereitstellen, um die Art zu ändern, wie eingehende Events vom `Bloc` verarbeitet werden. Mit dem obigen Code können wir die eingehenden Events mit sehr wenig zusätzlichem Code einfach debouncen. :::tip Sieh dir [`package:bloc_concurrency`](https://pub.dev/packages/bloc_concurrency) für einen vorgefertigten Satz von Event-Transformatoren an. ::: Wenn du unsicher bist, was du verwenden sollst, beginne mit `Cubit` und du kannst später bei Bedarf zu einem `Bloc` refactoren oder hochskalieren. ================================================ FILE: docs/src/content/docs/de/getting-started.mdx ================================================ --- title: Erste Schritte description: Alles, was du brauchst, um mit Bloc zu starten. --- import InstallationTabs from '~/components/getting-started/InstallationTabs.astro'; import ImportTabs from '~/components/getting-started/ImportTabs.astro'; ## Packages Das Bloc-Ökosystem besteht aus mehreren Packages, die unten aufgeführt sind: | Package | Beschreibung | Link | | ------------------------------------------------------------------------------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | AngularDart Komponenten | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | Core Dart APIs | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | Event Transformers | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | Custom Linter | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | Testing APIs | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | Command-line Tools | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | Flutter Widgets | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | Caching/Persistence Support | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | Undo/Redo Support | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | ## Installation :::note Um Bloc verwenden zu können, musst du das [Dart SDK](https://dart.dev/get-dart) auf deinem Computer installiert haben. ::: ## Imports Nachdem wir Bloc erfolgreich installiert haben, können wir unsere `main.dart` erstellen und das entsprechende `bloc` Package importieren. ================================================ FILE: docs/src/content/docs/de/index.mdx ================================================ --- template: splash title: Bloc State Management Library description: Official documentation for the bloc state management library. Support for Dart, Flutter, and AngularDart. Includes examples and tutorials. banner: content: | ✨ Besuche den Bloc Shop ✨ editUrl: false lastUpdated: false hero: title: Bloc v9.2.0 tagline: Eine vorhersehbare State Management Library für Dart. image: alt: Bloc logo file: ~/assets/bloc.svg actions: - text: Erste Schritte link: /de/getting-started/ variant: primary icon: rocket - text: GitHub link: https://github.com/felangel/bloc icon: github variant: secondary --- import { CardGrid } from '@astrojs/starlight/components'; import SponsorsGrid from '~/components/landing/SponsorsGrid.astro'; import Card from '~/components/landing/Card.astro'; import ListCard from '~/components/landing/ListCard.astro'; import SplitCard from '~/components/landing/SplitCard.astro'; import Discord from '~/components/landing/Discord.astro';
```sh # Füge Bloc deinem Projekt hinzu dart pub add bloc ``` In unserem [Getting Started Guide](/de/getting-started) findest du eine Schritt-für-Schritt-Anleitung, wie du Bloc in nur wenigen Minuten nutzen kannst. In den [offiziellen Tutorials](/de/tutorials/flutter-counter) lernst du Best Practices kennen und baust verschiedene Apps mit Bloc. Entdecke hochwertige, vollständig getestete [Beispiel-Apps](https://github.com/felangel/bloc/tree/master/examples) wie z.B. einen Counter, einen Timer, eine unendliche Liste, eine Wetter-App, eine Todo-App und weitere! - [Warum Bloc?](/de/why-bloc) - [Grundlegende Konzepte](/de/bloc-concepts) - [Architektur](/de/architecture) - [Testen](/de/testing) - [Namenskonventionen](/de/naming-conventions) - [FAQs](/de/faqs) - [VSCode Integration](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) - [IntelliJ Integration](https://plugins.jetbrains.com/plugin/12129-bloc) - [Neovim Integration](https://github.com/wa11breaker/flutter-bloc.nvim) - [Mason CLI Integration](https://github.com/felangel/bloc/blob/master/bricks/README.md) - [Custom Templates](https://brickhub.dev/search?q=bloc) - [Entwickler Tools](https://github.com/felangel/bloc/blob/master/packages/bloc_tools/README.md) ================================================ FILE: docs/src/content/docs/de/why-bloc.mdx ================================================ --- title: Warum Bloc? description: Ein Überblick darüber, was Bloc zu einer soliden State Management Lösung macht. sidebar: order: 1 --- Bloc macht es einfach, UI von Geschäftslogik zu trennen, wodurch dein Code _schnell_, _einfach zu testen_ und _wiederverwendbar_ wird. Beim Entwickeln von produktionsreifen Anwendungen wird das State Management kritisch. Als Entwickler möchten wir: - zu jedem Zeitpunkt wissen, in welchem State sich unsere Anwendung befindet. - jeden Fall einfach testen, um sicherzustellen, dass unsere App angemessen reagiert. - jede einzelne Benutzerinteraktion in unserer Anwendung aufzeichnen, damit wir datengetriebene Entscheidungen treffen können. - so effizient wie möglich arbeiten und Komponenten sowohl innerhalb unserer Anwendung als auch in anderen Anwendungen wiederverwenden. - viele Entwickler nahtlos in einer einzigen Codebasis arbeiten lassen, die denselben Mustern und Konventionen folgt. - schnelle und reaktive Apps entwickeln. Bloc wurde entwickelt, um all diese Anforderungen und viele weitere zu erfüllen. Es gibt viele State Management Lösungen und die Entscheidung, welche man verwenden soll, kann eine Entmutigend Aufgabe sein. Es gibt keine perfekte State Management Lösung! Wichtig ist, dass du diejenige wählst, die am besten für dein Team und dein Projekt funktioniert. Bloc wurde nach drei Grundprinzipien entwickelt: - **Einfach:** Leicht zu verstehen & kann von Entwicklern mit unterschiedlichen Fähigkeitsniveaus verwendet werden. - **Mächtig:** Hilft dabei, großartige, komplexe Anwendungen zu erstellen, indem sie aus kleineren Komponenten zusammengesetzt werden. - **Testbar:** Jeden Aspekt einer Anwendung einfach testen, damit wir mit Vertrauen iterieren können. Im Großen und Ganzen versucht Bloc, State-Änderungen vorhersehbar zu machen, indem es regelt, wann eine State-Änderung auftreten kann und eine einzige Art der State-Änderung in einer gesamten Anwendung durchsetzt. ================================================ FILE: docs/src/content/docs/es/architecture.mdx ================================================ --- title: Arquitectura description: Descripción general de los patrones de arquitectura recomendados al usar bloc. --- import DataProviderSnippet from '~/components/architecture/DataProviderSnippet.astro'; import RepositorySnippet from '~/components/architecture/RepositorySnippet.astro'; import BusinessLogicComponentSnippet from '~/components/architecture/BusinessLogicComponentSnippet.astro'; import BlocTightCouplingSnippet from '~/components/architecture/BlocTightCouplingSnippet.astro'; import BlocLooseCouplingPresentationSnippet from '~/components/architecture/BlocLooseCouplingPresentationSnippet.astro'; import AppIdeasRepositorySnippet from '~/components/architecture/AppIdeasRepositorySnippet.astro'; import AppIdeaRankingBlocSnippet from '~/components/architecture/AppIdeaRankingBlocSnippet.astro'; import PresentationComponentSnippet from '~/components/architecture/PresentationComponentSnippet.astro'; ![Arquitectura Bloc](~/assets/concepts/bloc_architecture_full.png) Usar la biblioteca bloc nos permite separar nuestra aplicación en tres capas: - Presentación - Lógica de Negocio - Datos - Repositorio - Proveedor de Datos Vamos a comenzar en la capa de nivel más bajo (más alejada de la interfaz de usuario) y trabajaremos hacia arriba hasta la capa de presentación. ## Capa de Datos La responsabilidad de la capa de datos es recuperar/manipular datos de una o más fuentes. La capa de datos se puede dividir en dos partes: - Repositorio - Proveedor de Datos Esta capa es el nivel más bajo de la aplicación e interactúa con bases de datos, solicitudes de red y otras fuentes de datos asíncronas. ### Proveedor de Datos La responsabilidad del proveedor de datos es proporcionar datos en bruto. El proveedor de datos debe ser genérico y versátil. El proveedor de datos generalmente expondrá APIs simples para realizar operaciones [CRUD](https://es.wikipedia.org/wiki/Crear,_leer,_actualizar_y_borrar). Podríamos tener un método `createData`, `readData`, `updateData` y `deleteData` como parte de nuestra capa de datos. ### Repositorio La capa de repositorio es un envoltorio alrededor de uno o más proveedores de datos con los que se comunica la capa Bloc. Como puedes ver, nuestra capa de repositorio puede interactuar con múltiples proveedores de datos y realizar transformaciones en los datos antes de entregar el resultado a la capa de lógica de negocio. ## Capa de Lógica de Negocio La responsabilidad de la capa de lógica de negocio es responder a la entrada de la capa de presentación con nuevos estados. Esta capa puede depender de uno o más repositorios para recuperar los datos necesarios para construir el estado de la aplicación. Piensa en la capa de lógica de negocio como el puente entre la interfaz de usuario (capa de presentación) y la capa de datos. La capa de lógica de negocio es notificada de eventos/acciones desde la capa de presentación y luego se comunica con el repositorio para construir un nuevo estado para que la capa de presentación lo consuma. ### Comunicación Bloc a Bloc Debido a que los blocs exponen streams, puede ser tentador hacer un bloc que escuche a otro bloc. No deberías **hacer** esto. Hay mejores alternativas que recurrir al siguiente código: Aunque el código anterior no tiene errores (e incluso se limpia después de sí mismo), tiene un problema mayor: crea una dependencia entre dos blocs. Generalmente, las dependencias entre dos entidades en la misma capa arquitectónica deben evitarse a toda costa, ya que crea un acoplamiento fuerte que es difícil de mantener. Dado que los blocs residen en la capa arquitectónica de lógica de negocio, ningún bloc debería conocer a otro bloc. ![Capas de Arquitectura de la Aplicación](~/assets/architecture/architecture.png) Un bloc solo debería recibir información a través de eventos y de repositorios inyectados (es decir, repositorios dados al bloc en su constructor). Si te encuentras en una situación donde un bloc necesita responder a otro bloc, tienes dos opciones. Puedes empujar el problema hacia arriba (a la capa de presentación) o hacia abajo (a la capa de dominio). #### Conectando Blocs a través de la Presentación Puedes usar un `BlocListener` para escuchar a un bloc y agregar un evento a otro bloc cada vez que el primer bloc cambie. El código anterior evita que `SecondBloc` necesite conocer a `FirstBloc`, fomentando el acoplamiento débil. La aplicación [flutter_weather](/es/tutorials/flutter-weather) [usa esta técnica](https://github.com/felangel/bloc/blob/b4c8db938ad71a6b60d4a641ec357905095c3965/examples/flutter_weather/lib/weather/view/weather_page.dart#L38-L42) para cambiar el tema de la aplicación basado en la información del clima que se recibe. En algunas situaciones, puede que no quieras acoplar dos blocs en la capa de presentación. En su lugar, a menudo tiene sentido que dos blocs compartan la misma fuente de datos y se actualicen cada vez que los datos cambien. #### Conectando Blocs a través del Dominio Dos blocs pueden escuchar un stream de un repositorio y actualizar sus estados independientemente cada vez que los datos del repositorio cambien. Usar repositorios reactivos para mantener el estado sincronizado es común en aplicaciones empresariales a gran escala. Primero, crea o usa un repositorio que proporcione un `Stream` de datos. Por ejemplo, el siguiente repositorio expone un stream interminable de las mismas pocas ideas de aplicaciones: El mismo repositorio puede ser inyectado en cada bloc que necesite reaccionar a nuevas ideas de aplicaciones. A continuación se muestra un `AppIdeaRankingBloc` que emite un estado por cada idea de aplicación entrante del repositorio anterior: Para más información sobre cómo usar streams con Bloc, consulta [Cómo usar Bloc con streams y concurrencia](https://verygood.ventures/blog/how-to-use-bloc-with-streams-and-concurrency). ## Capa de Presentación La responsabilidad de la capa de presentación es determinar cómo renderizarse a sí misma en función de uno o más estados del bloc. Además, debe manejar la entrada del usuario y los eventos del ciclo de vida de la aplicación. La mayoría de los flujos de aplicaciones comenzarán con un evento `AppStart` que desencadena la aplicación para obtener algunos datos para presentar al usuario. En este escenario, la capa de presentación agregaría un evento `AppStart`. Además, la capa de presentación tendrá que determinar qué renderizar en la pantalla en función del estado de la capa de bloc. Hasta ahora, aunque hemos tenido algunos fragmentos de código, todo esto ha sido bastante a alto nivel. En la sección de tutoriales vamos a juntar todo esto mientras construimos varias aplicaciones de ejemplo diferentes. ================================================ FILE: docs/src/content/docs/es/bloc-concepts.mdx ================================================ --- title: Conceptos de Bloc description: Una visión general de los conceptos básicos para package:bloc. sidebar: order: 1 --- import CountStreamSnippet from '~/components/concepts/bloc/CountStreamSnippet.astro'; import SumStreamSnippet from '~/components/concepts/bloc/SumStreamSnippet.astro'; import StreamsMainSnippet from '~/components/concepts/bloc/StreamsMainSnippet.astro'; import CounterCubitSnippet from '~/components/concepts/bloc/CounterCubitSnippet.astro'; import CounterCubitInitialStateSnippet from '~/components/concepts/bloc/CounterCubitInitialStateSnippet.astro'; import CounterCubitInstantiationSnippet from '~/components/concepts/bloc/CounterCubitInstantiationSnippet.astro'; import CounterCubitIncrementSnippet from '~/components/concepts/bloc/CounterCubitIncrementSnippet.astro'; import CounterCubitBasicUsageSnippet from '~/components/concepts/bloc/CounterCubitBasicUsageSnippet.astro'; import CounterCubitStreamUsageSnippet from '~/components/concepts/bloc/CounterCubitStreamUsageSnippet.astro'; import CounterCubitOnChangeSnippet from '~/components/concepts/bloc/CounterCubitOnChangeSnippet.astro'; import CounterCubitOnChangeUsageSnippet from '~/components/concepts/bloc/CounterCubitOnChangeUsageSnippet.astro'; import CounterCubitOnChangeOutputSnippet from '~/components/concepts/bloc/CounterCubitOnChangeOutputSnippet.astro'; import SimpleBlocObserverOnChangeSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeSnippet.astro'; import SimpleBlocObserverOnChangeUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeUsageSnippet.astro'; import SimpleBlocObserverOnChangeOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeOutputSnippet.astro'; import CounterCubitOnErrorSnippet from '~/components/concepts/bloc/CounterCubitOnErrorSnippet.astro'; import SimpleBlocObserverOnErrorSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnErrorSnippet.astro'; import CounterCubitOnErrorOutputSnippet from '~/components/concepts/bloc/CounterCubitOnErrorOutputSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/bloc/CounterBlocSnippet.astro'; import CounterBlocEventHandlerSnippet from '~/components/concepts/bloc/CounterBlocEventHandlerSnippet.astro'; import CounterBlocIncrementSnippet from '~/components/concepts/bloc/CounterBlocIncrementSnippet.astro'; import CounterBlocUsageSnippet from '~/components/concepts/bloc/CounterBlocUsageSnippet.astro'; import CounterBlocStreamUsageSnippet from '~/components/concepts/bloc/CounterBlocStreamUsageSnippet.astro'; import CounterBlocOnChangeSnippet from '~/components/concepts/bloc/CounterBlocOnChangeSnippet.astro'; import CounterBlocOnChangeUsageSnippet from '~/components/concepts/bloc/CounterBlocOnChangeUsageSnippet.astro'; import CounterBlocOnChangeOutputSnippet from '~/components/concepts/bloc/CounterBlocOnChangeOutputSnippet.astro'; import CounterBlocOnTransitionSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionSnippet.astro'; import CounterBlocOnTransitionOutputSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionOutputSnippet.astro'; import SimpleBlocObserverOnTransitionSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionSnippet.astro'; import SimpleBlocObserverOnTransitionUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionUsageSnippet.astro'; import SimpleBlocObserverOnTransitionOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionOutputSnippet.astro'; import CounterBlocOnEventSnippet from '~/components/concepts/bloc/CounterBlocOnEventSnippet.astro'; import SimpleBlocObserverOnEventSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventSnippet.astro'; import SimpleBlocObserverOnEventOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventOutputSnippet.astro'; import CounterBlocOnErrorSnippet from '~/components/concepts/bloc/CounterBlocOnErrorSnippet.astro'; import CounterBlocOnErrorOutputSnippet from '~/components/concepts/bloc/CounterBlocOnErrorOutputSnippet.astro'; import CounterCubitFullSnippet from '~/components/concepts/bloc/CounterCubitFullSnippet.astro'; import CounterBlocFullSnippet from '~/components/concepts/bloc/CounterBlocFullSnippet.astro'; import AuthenticationStateSnippet from '~/components/concepts/bloc/AuthenticationStateSnippet.astro'; import AuthenticationTransitionSnippet from '~/components/concepts/bloc/AuthenticationTransitionSnippet.astro'; import AuthenticationChangeSnippet from '~/components/concepts/bloc/AuthenticationChangeSnippet.astro'; import DebounceEventTransformerSnippet from '~/components/concepts/bloc/DebounceEventTransformerSnippet.astro'; :::note Por favor, asegúrate de leer cuidadosamente las siguientes secciones antes de trabajar con [`package:bloc`](https://pub.dev/packages/bloc). ::: Hay varios conceptos clave que son críticos para entender cómo usar el paquete bloc. En las próximas secciones, vamos a discutir cada uno de ellos en detalle y también trabajaremos en cómo se aplicarían a una aplicación de contador. ## Streams :::note Consulta la [Documentación oficial de Dart](https://dart.dev/tutorials/language/streams) para obtener más información sobre `Streams`. ::: Un stream es una secuencia de datos asíncronos. Para usar la biblioteca bloc, es fundamental tener una comprensión básica de los `Streams` y cómo funcionan. Si no estás familiarizado con los `Streams`, piensa en una tubería con agua fluyendo a través de ella. La tubería es el `Stream` y el agua son los datos asíncronos. Podemos crear un `Stream` en Dart escribiendo una función `async*` (generador asíncrono). Al marcar una función como `async*` podemos usar la palabra clave `yield` y devolver un `Stream` de datos. En el ejemplo anterior, estamos devolviendo un `Stream` de enteros hasta el parámetro entero `max`. Cada vez que usamos `yield` en una función `async*` estamos empujando ese dato a través del `Stream`. Podemos consumir el `Stream` anterior de varias maneras. Si quisiéramos escribir una función para devolver la suma de un `Stream` de enteros, podría verse algo así: Al marcar la función anterior como `async` podemos usar la palabra clave `await` y devolver un `Future` de enteros. En este ejemplo, estamos esperando cada valor en el stream y devolviendo la suma de todos los enteros en el stream. Podemos juntar todo de la siguiente manera: Ahora que tenemos una comprensión básica de cómo funcionan los `Streams` en Dart, estamos listos para aprender sobre el componente principal del paquete bloc: un `Cubit`. ## Cubit Un `Cubit` es una clase que extiende `BlocBase` y puede ser extendida para gestionar cualquier tipo de estado. ![Arquitectura de Cubit](~/assets/concepts/cubit_architecture_full.png) Un `Cubit` puede exponer funciones que pueden ser invocadas para desencadenar cambios de estado. Los estados son la salida de un `Cubit` y representan una parte del estado de tu aplicación. Los componentes de la interfaz de usuario pueden ser notificados de los estados y redibujar partes de sí mismos en función del estado actual. :::note Para obtener más información sobre los orígenes de `Cubit`, consulta [el siguiente issue](https://github.com/felangel/cubit/issues/69). ::: ### Creando un Cubit Podemos crear un `CounterCubit` así: Cuando creamos un `Cubit`, necesitamos definir el tipo de estado que el `Cubit` gestionará. En el caso del `CounterCubit` anterior, el estado puede ser representado mediante un `int`, pero en casos más complejos podría ser necesario usar una `class` en lugar de un tipo primitivo. La segunda cosa que necesitamos hacer al crear un `Cubit` es especificar el estado inicial. Podemos hacer esto llamando a `super` con el valor del estado inicial. En el fragmento anterior, estamos configurando el estado inicial a `0` internamente, pero también podemos permitir que el `Cubit` sea más flexible aceptando un valor externo: Esto nos permitiría instanciar `CounterCubit` con diferentes estados iniciales como: ### Cambios de Estado en Cubit Cada `Cubit` tiene la capacidad de emitir un nuevo estado mediante `emit`. En el fragmento anterior, el `CounterCubit` está exponiendo un método público llamado `increment` que puede ser llamado externamente para notificar al `CounterCubit` que incremente su estado. Cuando se llama a `increment`, podemos acceder al estado actual del `Cubit` mediante el getter `state` y emitir un nuevo estado sumando 1 al estado actual. :::caution El método `emit` es protegido, lo que significa que solo debe ser usado dentro de un `Cubit`. ::: ### Usando un Cubit Ahora podemos tomar el `CounterCubit` que hemos implementado y ponerlo en uso. #### Uso Básico En el fragmento anterior, comenzamos creando una instancia del `CounterCubit`. Luego imprimimos el estado actual del cubit, que es el estado inicial (ya que no se han emitido nuevos estados aún). A continuación, llamamos a la función `increment` para desencadenar un cambio de estado. Finalmente, imprimimos el estado del `Cubit` nuevamente, que pasó de `0` a `1` y llamamos a `close` en el `Cubit` para cerrar el stream interno de estado. #### Uso de Stream `Cubit` expone un `Stream` que nos permite recibir actualizaciones de estado en tiempo real: En el fragmento anterior, nos estamos suscribiendo al `CounterCubit` y llamando a imprimir en cada cambio de estado. Luego invocamos la función `increment` que emitirá un nuevo estado. Por último, llamamos a `cancel` en la suscripción cuando ya no queremos recibir actualizaciones y cerramos el `Cubit`. :::note `await Future.delayed(Duration.zero)` se agrega para este ejemplo para evitar cancelar la suscripción inmediatamente. ::: :::caution Solo se recibirán cambios de estado subsecuentes al llamar a `listen` en un `Cubit`. ::: ### Observando un Cubit Cuando un `Cubit` emite un nuevo estado, ocurre un `Change`. Podemos observar todos los cambios para un `Cubit` dado sobrescribiendo `onChange`. Luego podemos interactuar con el `Cubit` y observar todos los cambios impresos en la consola. El ejemplo anterior imprimiría: :::note Un `Change` ocurre justo antes de que el estado del `Cubit` se actualice. Un `Change` consiste en el `currentState` y el `nextState`. ::: #### BlocObserver Una ventaja adicional de usar la biblioteca bloc es que podemos tener acceso a todos los `Changes` en un solo lugar. Aunque en esta aplicación solo tenemos un `Cubit`, es bastante común en aplicaciones más grandes tener muchos `Cubits` gestionando diferentes partes del estado de la aplicación. Si queremos poder hacer algo en respuesta a todos los `Changes`, simplemente podemos crear nuestro propio `BlocObserver`. :::note Todo lo que necesitamos hacer es extender `BlocObserver` y sobrescribir el método `onChange`. ::: Para usar el `SimpleBlocObserver`, solo necesitamos ajustar la función `main`: El fragmento anterior imprimiría: :::note La sobrescritura interna de `onChange` se llama primero, lo que llama a `super.onChange` notificando al `onChange` en el `BlocObserver`. ::: :::tip En `BlocObserver` tenemos acceso a la instancia del `Cubit` además del `Change` en sí. ::: ### Manejo de Errores en Cubit Cada `Cubit` tiene un método `addError` que puede ser usado para indicar que ha ocurrido un error. :::note `onError` puede ser sobrescrito dentro del `Cubit` para manejar todos los errores para un `Cubit` específico. ::: `onError` también puede ser sobrescrito en `BlocObserver` para manejar todos los errores reportados globalmente. Si ejecutamos el mismo programa nuevamente, deberíamos ver la siguiente salida: ## Bloc Un `Bloc` es una clase más avanzada que se basa en `eventos` para desencadenar cambios de `estado` en lugar de funciones. `Bloc` también extiende `BlocBase`, lo que significa que tiene una API pública similar a `Cubit`. Sin embargo, en lugar de llamar a una `función` en un `Bloc` y emitir directamente un nuevo `estado`, los `Blocs` reciben `eventos` y convierten los `eventos` entrantes en `estados` salientes. ![Arquitectura de Bloc](~/assets/concepts/bloc_architecture_full.png) ### Creando un Bloc Crear un `Bloc` es similar a crear un `Cubit`, excepto que además de definir el estado que gestionaremos, también debemos definir el evento que el `Bloc` podrá procesar. Los eventos son la entrada a un Bloc. Comúnmente se agregan en respuesta a interacciones del usuario, como presiones de botones o eventos de ciclo de vida como cargas de página. Al igual que cuando creamos el `CounterCubit`, debemos especificar un estado inicial pasándolo a la superclase a través de `super`. ### Cambios de Estado en Bloc `Bloc` requiere que registremos manejadores de eventos a través de la API `on`, a diferencia de las funciones en `Cubit`. Un manejador de eventos es responsable de convertir cualquier evento entrante en cero o más estados salientes. :::tip Un `EventHandler` tiene acceso al evento agregado así como a un `Emitter` que puede ser usado para emitir cero o más estados en respuesta al evento entrante. ::: Luego podemos actualizar el `EventHandler` para manejar el evento `CounterIncrementPressed`: En el fragmento anterior, hemos registrado un `EventHandler` para gestionar todos los eventos `CounterIncrementPressed`. Para cada evento `CounterIncrementPressed` entrante, podemos acceder al estado actual del bloc a través del getter `state` y `emit(state + 1)`. :::note Dado que la clase `Bloc` extiende `BlocBase`, tenemos acceso al estado actual del bloc en cualquier momento a través del getter `state`, al igual que en `Cubit`. ::: :::caution Los blocs nunca deben emitir directamente nuevos estados. En su lugar, cada cambio de estado debe ser resultado de un evento entrante dentro de un `EventHandler`. ::: :::caution Tanto los blocs como los cubits ignorarán estados duplicados. Si emitimos `State nextState` donde `state == nextState`, entonces no ocurrirá ningún cambio de estado. ::: ### Usando un Bloc En este punto, podemos crear una instancia de nuestro `CounterBloc` y ponerlo en uso. #### Uso Básico En el fragmento anterior, comenzamos creando una instancia del `CounterBloc`. Luego imprimimos el estado actual del `Bloc`, que es el estado inicial (ya que no se han emitido nuevos estados aún). A continuación, agregamos el evento `CounterIncrementPressed` para desencadenar un cambio de estado. Finalmente, imprimimos el estado del `Bloc` nuevamente, que pasó de `0` a `1` y llamamos a `close` en el `Bloc` para cerrar el stream interno de estado. :::note `await Future.delayed(Duration.zero)` se agrega para asegurar que esperemos a la siguiente iteración del ciclo de eventos (permitiendo que el `EventHandler` procese el evento). ::: #### Uso de Stream Al igual que con `Cubit`, un `Bloc` es un tipo especial de `Stream`, lo que significa que también podemos suscribirnos a un `Bloc` para recibir actualizaciones en tiempo real de su estado: En el fragmento anterior, nos estamos suscribiendo al `CounterBloc` y llamando a imprimir en cada cambio de estado. Luego agregamos el evento `CounterIncrementPressed` que desencadena el `EventHandler` `on` y emite un nuevo estado. Por último, llamamos a `cancel` en la suscripción cuando ya no queremos recibir actualizaciones y cerramos el `Bloc`. :::note `await Future.delayed(Duration.zero)` se agrega para este ejemplo para evitar cancelar la suscripción inmediatamente. ::: ### Observando un Bloc Dado que `Bloc` extiende `BlocBase`, podemos observar todos los cambios de estado para un `Bloc` usando `onChange`. Luego podemos actualizar `main.dart` a: Ahora, si ejecutamos el fragmento anterior, la salida será: Un factor diferenciador clave entre `Bloc` y `Cubit` es que, dado que `Bloc` está basado en eventos, también podemos capturar información sobre lo que desencadenó el cambio de estado. Podemos hacer esto sobrescribiendo `onTransition`. El cambio de un estado a otro se llama `Transition`. Una `Transition` consiste en el estado actual, el evento y el siguiente estado. Si luego volvemos a ejecutar el mismo fragmento `main.dart` de antes, deberíamos ver la siguiente salida: :::note `onTransition` se invoca antes que `onChange` y contiene el evento que desencadenó el cambio de `currentState` a `nextState`. ::: #### BlocObserver Al igual que antes, podemos sobrescribir `onTransition` en un `BlocObserver` personalizado para observar todas las transiciones que ocurren desde un solo lugar. Podemos inicializar el `SimpleBlocObserver` de la misma manera que antes: Ahora, si ejecutamos el fragmento anterior, la salida debería verse así: :::note `onTransition` se invoca primero (local antes que global) seguido de `onChange`. ::: Otra característica única de las instancias de `Bloc` es que nos permiten sobrescribir `onEvent`, que se llama cada vez que se agrega un nuevo evento al `Bloc`. Al igual que con `onChange` y `onTransition`, `onEvent` puede ser sobrescrito localmente así como globalmente. Podemos ejecutar el mismo `main.dart` de antes y deberíamos ver la siguiente salida: :::note `onEvent` se llama tan pronto como se agrega el evento. El `onEvent` local se invoca antes que el `onEvent` global en `BlocObserver`. ::: ### Manejo de Errores en Bloc Al igual que con `Cubit`, cada `Bloc` tiene un método `addError` y `onError`. Podemos indicar que ha ocurrido un error llamando a `addError` desde cualquier lugar dentro de nuestro `Bloc`. Luego podemos reaccionar a todos los errores sobrescribiendo `onError` al igual que con `Cubit`. Si volvemos a ejecutar el mismo `main.dart` de antes, podemos ver cómo se ve cuando se informa un error: :::note El `onError` local se invoca primero seguido del `onError` global en `BlocObserver`. ::: :::note `onError` y `onChange` funcionan exactamente de la misma manera tanto para instancias de `Bloc` como de `Cubit`. ::: :::caution Cualquier excepción no manejada que ocurra dentro de un `EventHandler` también se informa a `onError`. ::: ## Cubit vs. Bloc Ahora que hemos cubierto los conceptos básicos de las clases `Cubit` y `Bloc`, podrías preguntarte cuándo deberías usar `Cubit` y cuándo deberías usar `Bloc`. ### Ventajas de Cubit #### Simplicidad Una de las mayores ventajas de usar `Cubit` es la simplicidad. Al crear un `Cubit`, solo tenemos que definir el estado así como las funciones que queremos exponer para cambiar el estado. En comparación, al crear un `Bloc`, tenemos que definir los estados, eventos y la implementación del `EventHandler`. Esto hace que `Cubit` sea más fácil de entender y hay menos código involucrado. Ahora echemos un vistazo a las dos implementaciones del contador: ##### CounterCubit ##### CounterBloc La implementación de `Cubit` es más concisa y en lugar de definir eventos por separado, las funciones actúan como eventos. Además, al usar un `Cubit`, podemos simplemente llamar a `emit` desde cualquier lugar para desencadenar un cambio de estado. ### Ventajas de Bloc #### Rastreabilidad Una de las mayores ventajas de usar `Bloc` es conocer la secuencia de cambios de estado así como exactamente qué desencadenó esos cambios. Para el estado que es crítico para la funcionalidad de una aplicación, podría ser muy beneficioso usar un enfoque más basado en eventos para capturar todos los eventos además de los cambios de estado. Un caso de uso común podría ser gestionar el `AuthenticationState`. Para simplificar, digamos que podemos representar el `AuthenticationState` a través de un `enum`: Podría haber muchas razones por las cuales el estado de la aplicación podría cambiar de `authenticated` a `unauthenticated`. Por ejemplo, el usuario podría haber tocado un botón de cierre de sesión y solicitado ser desconectado de la aplicación. Por otro lado, tal vez el token de acceso del usuario fue revocado y fue desconectado forzosamente. Al usar `Bloc` podemos rastrear claramente cómo el estado de la aplicación llegó a un cierto estado. La `Transition` anterior nos da toda la información que necesitamos para entender por qué cambió el estado. Si hubiéramos usado un `Cubit` para gestionar el `AuthenticationState`, nuestros registros se verían así: Esto nos dice que el usuario fue desconectado pero no explica por qué, lo cual podría ser crítico para depurar y entender cómo está cambiando el estado de la aplicación con el tiempo. #### Transformaciones Avanzadas de Eventos Otra área en la que `Bloc` sobresale sobre `Cubit` es cuando necesitamos aprovechar operadores reactivos como `buffer`, `debounceTime`, `throttle`, etc. :::tip Consulta [`package:stream_transform`](https://pub.dev/packages/stream_transform) y [`package:rxdart`](https://pub.dev/packages/rxdart) para transformadores de streams. ::: `Bloc` tiene un sink de eventos que nos permite controlar y transformar el flujo entrante de eventos. Por ejemplo, si estuviéramos construyendo una búsqueda en tiempo real, probablemente querríamos aplicar debounce a las solicitudes al backend para evitar ser limitados en la tasa de solicitudes, así como para reducir el costo/carga en el backend. Con `Bloc` podemos proporcionar un `EventTransformer` personalizado para cambiar la forma en que los eventos entrantes son procesados por el `Bloc`. Con el código anterior, podemos aplicar fácilmente debounce a los eventos entrantes con muy poco código adicional. :::tip Consulta [`package:bloc_concurrency`](https://pub.dev/packages/bloc_concurrency) para un conjunto de transformadores de eventos con una opinión definida. ::: Si no estás seguro de cuál usar, comienza con `Cubit` y luego puedes refactorizar o escalar a un `Bloc` según sea necesario. ================================================ FILE: docs/src/content/docs/es/faqs.mdx ================================================ --- title: Preguntas Frecuentes description: Respuestas a preguntas frecuentes sobre la biblioteca bloc. --- import StateNotUpdatingGood1Snippet from '~/components/faqs/StateNotUpdatingGood1Snippet.astro'; import StateNotUpdatingGood2Snippet from '~/components/faqs/StateNotUpdatingGood2Snippet.astro'; import StateNotUpdatingGood3Snippet from '~/components/faqs/StateNotUpdatingGood3Snippet.astro'; import StateNotUpdatingBad1Snippet from '~/components/faqs/StateNotUpdatingBad1Snippet.astro'; import StateNotUpdatingBad2Snippet from '~/components/faqs/StateNotUpdatingBad2Snippet.astro'; import StateNotUpdatingBad3Snippet from '~/components/faqs/StateNotUpdatingBad3Snippet.astro'; import EquatableEmitSnippet from '~/components/faqs/EquatableEmitSnippet.astro'; import EquatableBlocTestSnippet from '~/components/faqs/EquatableBlocTestSnippet.astro'; import NoEquatableBlocTestSnippet from '~/components/faqs/NoEquatableBlocTestSnippet.astro'; import SingleStateSnippet from '~/components/faqs/SingleStateSnippet.astro'; import SingleStateUsageSnippet from '~/components/faqs/SingleStateUsageSnippet.astro'; import BlocProviderGood1Snippet from '~/components/faqs/BlocProviderGood1Snippet.astro'; import BlocProviderGood2Snippet from '~/components/faqs/BlocProviderGood2Snippet.astro'; import BlocProviderBad1Snippet from '~/components/faqs/BlocProviderBad1Snippet.astro'; import BlocInternalAddEventSnippet from '~/components/faqs/BlocInternalAddEventSnippet.astro'; import BlocInternalEventSnippet from '~/components/faqs/BlocInternalEventSnippet.astro'; import BlocExternalForEachSnippet from '~/components/faqs/BlocExternalForEachSnippet.astro'; ## Estado No Actualizado ❔ **Pregunta**: Estoy emitiendo un estado en mi bloc pero la interfaz de usuario no se actualiza. ¿Qué estoy haciendo mal? 💡 **Respuesta**: Si estás usando Equatable, asegúrate de pasar todas las propiedades al getter props. ✅ **BUENO** ❌ **MALO** Además, asegúrate de emitir una nueva instancia del estado en tu bloc. ✅ **BUENO** ❌ **MALO** :::caution Las propiedades de `Equatable` siempre deben ser copiadas en lugar de modificadas. Si una clase `Equatable` contiene una `Lista` o `Mapa` como propiedades, asegúrate de usar `List.of` o `Map.of` respectivamente para garantizar que la igualdad se evalúe en función de los valores de las propiedades en lugar de la referencia. ::: ## Cuándo usar Equatable ❔**Pregunta**: ¿Cuándo debo usar Equatable? 💡**Respuesta**: En el escenario anterior, si `StateA` extiende `Equatable`, solo ocurrirá un cambio de estado (el segundo emit será ignorado). En general, debes usar `Equatable` si deseas optimizar tu código para reducir el número de reconstrucciones. No debes usar `Equatable` si deseas que el mismo estado consecutivo desencadene múltiples transiciones. Además, usar `Equatable` facilita mucho las pruebas de blocs, ya que podemos esperar instancias específicas de estados de bloc en lugar de usar `Matchers` o `Predicates`. Sin `Equatable`, la prueba anterior fallaría y necesitaría ser reescrita así: ## Manejo de Errores ❔ **Pregunta**: ¿Cómo puedo manejar un error mientras sigo mostrando datos anteriores? 💡 **Respuesta**: Esto depende en gran medida de cómo se haya modelado el estado del bloc. En casos donde los datos deben mantenerse incluso en presencia de un error, considera usar una sola clase de estado. Esto permitirá que los widgets tengan acceso a las propiedades `data` y `error` simultáneamente y el bloc puede usar `state.copyWith` para mantener los datos antiguos incluso cuando ocurra un error. ## Bloc vs. Redux ❔ **Pregunta**: ¿Cuál es la diferencia entre Bloc y Redux? 💡 **Respuesta**: BLoC es un patrón de diseño que se define por las siguientes reglas: 1. La entrada y salida del BLoC son Streams y Sinks simples. 2. Las dependencias deben ser inyectables y agnósticas de la plataforma. 3. No se permite la bifurcación de la plataforma. 4. La implementación puede ser lo que quieras siempre que sigas las reglas anteriores. Las pautas de la interfaz de usuario son: 1. Cada componente "lo suficientemente complejo" tiene un BLoC correspondiente. 2. Los componentes deben enviar entradas "tal como están". 3. Los componentes deben mostrar salidas lo más cerca posible de "tal como están". 4. Toda la bifurcación debe basarse en salidas booleanas simples del BLoC. La biblioteca Bloc implementa el patrón de diseño BLoC y tiene como objetivo abstraer RxDart para simplificar la experiencia del desarrollador. Los tres principios de Redux son: 1. Fuente única de verdad 2. El estado es de solo lectura 3. Los cambios se realizan con funciones puras La biblioteca bloc viola el primer principio; con bloc, el estado se distribuye a través de múltiples blocs. Además, no hay concepto de middleware en bloc y bloc está diseñado para facilitar los cambios de estado asincrónicos, permitiéndote emitir múltiples estados para un solo evento. ## Bloc vs. Provider ❔ **Pregunta**: ¿Cuál es la diferencia entre Bloc y Provider? 💡 **Respuesta**: `provider` está diseñado para la inyección de dependencias (envuelve `InheritedWidget`). Aún necesitas averiguar cómo gestionar tu estado (a través de `ChangeNotifier`, `Bloc`, `Mobx`, etc...). La biblioteca Bloc usa `provider` internamente para facilitar la provisión y acceso a blocs a lo largo del árbol de widgets. ## BlocProvider.of() No Encuentra el Bloc ❔ **Pregunta**: Cuando uso `BlocProvider.of(context)` no puede encontrar el bloc. ¿Cómo puedo solucionar esto? 💡 **Respuesta**: No puedes acceder a un bloc desde el mismo contexto en el que fue proporcionado, por lo que debes asegurarte de que `BlocProvider.of()` se llame dentro de un `BuildContext` hijo. ✅ **BUENO** ❌ **MALO** ## Estructura del Proyecto ❔ **Pregunta**: ¿Cómo debo estructurar mi proyecto? 💡 **Respuesta**: Aunque realmente no hay una respuesta correcta/incorrecta a esta pregunta, algunas referencias recomendadas son: - [I/O Photobooth](https://github.com/flutter/photobooth) - [I/O Pinball](https://github.com/flutter/pinball) - [Flutter News Toolkit](https://github.com/flutter/news_toolkit) Lo más importante es tener una estructura de proyecto **consistente** e **intencional**. ## Agregar Eventos dentro de un Bloc ❔ **Pregunta**: ¿Está bien agregar eventos dentro de un bloc? 💡 **Respuesta**: En la mayoría de los casos, los eventos deben agregarse externamente, pero en algunos casos selectos puede tener sentido que los eventos se agreguen internamente. La situación más común en la que se utilizan eventos internos es cuando los cambios de estado deben ocurrir en respuesta a actualizaciones en tiempo real desde un repositorio. En estas situaciones, el repositorio es el estímulo para el cambio de estado en lugar de un evento externo como un toque de botón. En el siguiente ejemplo, el estado de `MyBloc` depende del usuario actual que se expone a través del `Stream` del `UserRepository`. `MyBloc` escucha los cambios en el usuario actual y agrega un evento interno `_UserChanged` cada vez que se emite un usuario desde el flujo de usuarios. Al agregar un evento interno, también podemos especificar un `transformer` personalizado para el evento para determinar cómo se procesarán múltiples eventos `_UserChanged` -- por defecto se procesarán concurrentemente. Se recomienda encarecidamente que los eventos internos sean privados. Esta es una forma explícita de señalar que un evento específico se usa solo dentro del bloc y evita que los componentes externos conozcan el evento. Alternativamente, podemos definir un evento externo `Started` y usar la API `emit.forEach` para manejar la reacción a las actualizaciones de usuarios en tiempo real: Los beneficios del enfoque anterior son: - No necesitamos un evento interno `_UserChanged` - No necesitamos gestionar manualmente la `StreamSubscription` - Tenemos control total sobre cuándo el bloc se suscribe al flujo de actualizaciones de usuarios Las desventajas del enfoque anterior son: - No podemos `pausar` o `reanudar` fácilmente la suscripción - Necesitamos exponer un evento público `Started` que debe agregarse externamente - No podemos usar un `transformer` personalizado para ajustar cómo reaccionamos a las actualizaciones de usuarios ## Exponer Métodos Públicos ❔ **Pregunta**: ¿Está bien exponer métodos públicos en mis instancias de bloc y cubit? 💡 **Respuesta** Al crear un cubit, se recomienda exponer solo métodos públicos con el propósito de desencadenar cambios de estado. Como resultado, generalmente todos los métodos públicos en una instancia de cubit deben devolver `void` o `Future`. Al crear un bloc, se recomienda evitar exponer cualquier método público personalizado y en su lugar notificar al bloc de eventos llamando a `add`. ================================================ FILE: docs/src/content/docs/es/flutter-bloc-concepts.mdx ================================================ --- title: Conceptos de Flutter Bloc description: Una visión general de los conceptos básicos para package:flutter_bloc. sidebar: order: 2 --- import BlocBuilderSnippet from '~/components/concepts/flutter-bloc/BlocBuilderSnippet.astro'; import BlocBuilderExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocBuilderExplicitBlocSnippet.astro'; import BlocBuilderConditionSnippet from '~/components/concepts/flutter-bloc/BlocBuilderConditionSnippet.astro'; import BlocSelectorSnippet from '~/components/concepts/flutter-bloc/BlocSelectorSnippet.astro'; import BlocProviderSnippet from '~/components/concepts/flutter-bloc/BlocProviderSnippet.astro'; import BlocProviderEagerSnippet from '~/components/concepts/flutter-bloc/BlocProviderEagerSnippet.astro'; import BlocProviderValueSnippet from '~/components/concepts/flutter-bloc/BlocProviderValueSnippet.astro'; import BlocProviderLookupSnippet from '~/components/concepts/flutter-bloc/BlocProviderLookupSnippet.astro'; import NestedBlocProviderSnippet from '~/components/concepts/flutter-bloc/NestedBlocProviderSnippet.astro'; import MultiBlocProviderSnippet from '~/components/concepts/flutter-bloc/MultiBlocProviderSnippet.astro'; import BlocListenerSnippet from '~/components/concepts/flutter-bloc/BlocListenerSnippet.astro'; import BlocListenerExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocListenerExplicitBlocSnippet.astro'; import BlocListenerConditionSnippet from '~/components/concepts/flutter-bloc/BlocListenerConditionSnippet.astro'; import NestedBlocListenerSnippet from '~/components/concepts/flutter-bloc/NestedBlocListenerSnippet.astro'; import MultiBlocListenerSnippet from '~/components/concepts/flutter-bloc/MultiBlocListenerSnippet.astro'; import BlocConsumerSnippet from '~/components/concepts/flutter-bloc/BlocConsumerSnippet.astro'; import BlocConsumerConditionSnippet from '~/components/concepts/flutter-bloc/BlocConsumerConditionSnippet.astro'; import RepositoryProviderSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderSnippet.astro'; import RepositoryProviderLookupSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderLookupSnippet.astro'; import NestedRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/NestedRepositoryProviderSnippet.astro'; import MultiRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/MultiRepositoryProviderSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/flutter-bloc/CounterBlocSnippet.astro'; import CounterMainSnippet from '~/components/concepts/flutter-bloc/CounterMainSnippet.astro'; import CounterPageSnippet from '~/components/concepts/flutter-bloc/CounterPageSnippet.astro'; import WeatherRepositorySnippet from '~/components/concepts/flutter-bloc/WeatherRepositorySnippet.astro'; import WeatherMainSnippet from '~/components/concepts/flutter-bloc/WeatherMainSnippet.astro'; import WeatherAppSnippet from '~/components/concepts/flutter-bloc/WeatherAppSnippet.astro'; import WeatherPageSnippet from '~/components/concepts/flutter-bloc/WeatherPageSnippet.astro'; :::note Por favor, asegúrate de leer cuidadosamente las siguientes secciones antes de trabajar con [`package:flutter_bloc`](https://pub.dev/packages/flutter_bloc). ::: :::note Todos los widgets exportados por el paquete `flutter_bloc` se integran tanto con instancias de `Cubit` como de `Bloc`. ::: ## Widgets de Bloc ### BlocBuilder **BlocBuilder** es un widget de Flutter que requiere un `Bloc` y una función `builder`. `BlocBuilder` maneja la construcción del widget en respuesta a nuevos estados. `BlocBuilder` es muy similar a `StreamBuilder` pero tiene una API más simple para reducir la cantidad de código boilerplate necesario. La función `builder` potencialmente será llamada muchas veces y debe ser una [función pura](https://es.wikipedia.org/wiki/Funci%C3%B3n_pura) que devuelve un widget en respuesta al estado. Consulta `BlocListener` si deseas "hacer" algo en respuesta a cambios de estado, como navegación, mostrar un diálogo, etc. Si se omite el parámetro `bloc`, `BlocBuilder` realizará automáticamente una búsqueda usando `BlocProvider` y el `BuildContext` actual. Solo especifica el bloc si deseas proporcionar un bloc que estará limitado a un solo widget y no es accesible a través de un `BlocProvider` padre y el `BuildContext` actual. Para un control más detallado sobre cuándo se llama a la función `builder`, se puede proporcionar un `buildWhen` opcional. `buildWhen` toma el estado anterior del bloc y el estado actual del bloc y devuelve un booleano. Si `buildWhen` devuelve verdadero, se llamará a `builder` con `state` y el widget se reconstruirá. Si `buildWhen` devuelve falso, no se llamará a `builder` con `state` y no ocurrirá ninguna reconstrucción. ### BlocSelector **BlocSelector** es un widget de Flutter que es análogo a `BlocBuilder` pero permite a los desarrolladores filtrar actualizaciones seleccionando un nuevo valor basado en el estado actual del bloc. Se previenen construcciones innecesarias si el valor seleccionado no cambia. El valor seleccionado debe ser inmutable para que `BlocSelector` determine con precisión si se debe llamar nuevamente a `builder`. Si se omite el parámetro `bloc`, `BlocSelector` realizará automáticamente una búsqueda usando `BlocProvider` y el `BuildContext` actual. ### BlocProvider **BlocProvider** es un widget de Flutter que proporciona un bloc a sus hijos a través de `BlocProvider.of(context)`. Se utiliza como un widget de inyección de dependencias (DI) para que una sola instancia de un bloc pueda ser proporcionada a múltiples widgets dentro de un subárbol. En la mayoría de los casos, `BlocProvider` debe usarse para crear nuevos blocs que estarán disponibles para el resto del subárbol. En este caso, dado que `BlocProvider` es responsable de crear el bloc, manejará automáticamente el cierre del bloc. Por defecto, `BlocProvider` creará el bloc de manera perezosa, lo que significa que `create` se ejecutará cuando se busque el bloc a través de `BlocProvider.of(context)`. Para anular este comportamiento y forzar que `create` se ejecute inmediatamente, `lazy` se puede establecer en `false`. En algunos casos, `BlocProvider` se puede usar para proporcionar un bloc existente a una nueva porción del árbol de widgets. Esto se usará más comúnmente cuando un bloc existente necesite estar disponible para una nueva ruta. En este caso, `BlocProvider` no cerrará automáticamente el bloc ya que no lo creó. entonces desde `ChildA` o `ScreenA` podemos recuperar `BlocA` con: ### MultiBlocProvider **MultiBlocProvider** es un widget de Flutter que fusiona múltiples widgets `BlocProvider` en uno solo. `MultiBlocProvider` mejora la legibilidad y elimina la necesidad de anidar múltiples `BlocProviders`. Usando `MultiBlocProvider` podemos pasar de: a: ### BlocListener **BlocListener** es un widget de Flutter que toma un `BlocWidgetListener` y un `Bloc` opcional e invoca el `listener` en respuesta a cambios de estado en el bloc. Debe usarse para funcionalidades que necesitan ocurrir una vez por cambio de estado, como navegación, mostrar un `SnackBar`, mostrar un `Dialog`, etc. `listener` solo se llama una vez por cada cambio de estado (**NO** incluyendo el estado inicial) a diferencia de `builder` en `BlocBuilder` y es una función `void`. Si se omite el parámetro `bloc`, `BlocListener` realizará automáticamente una búsqueda usando `BlocProvider` y el `BuildContext` actual. Solo especifica el bloc si deseas proporcionar un bloc que no es accesible a través de `BlocProvider` y el `BuildContext` actual. Para un control más detallado sobre cuándo se llama a la función `listener`, se puede proporcionar un `listenWhen` opcional. `listenWhen` toma el estado anterior del bloc y el estado actual del bloc y devuelve un booleano. Si `listenWhen` devuelve verdadero, se llamará a `listener` con `state`. Si `listenWhen` devuelve falso, no se llamará a `listener` con `state`. ### MultiBlocListener **MultiBlocListener** es un widget de Flutter que fusiona múltiples widgets `BlocListener` en uno solo. `MultiBlocListener` mejora la legibilidad y elimina la necesidad de anidar múltiples `BlocListeners`. Usando `MultiBlocListener` podemos pasar de: a: ### BlocConsumer **BlocConsumer** expone un `builder` y un `listener` para reaccionar a nuevos estados. `BlocConsumer` es análogo a un `BlocListener` y `BlocBuilder` anidados, pero reduce la cantidad de código boilerplate necesario. `BlocConsumer` solo debe usarse cuando es necesario tanto reconstruir la UI como ejecutar otras reacciones a cambios de estado en el `bloc`. `BlocConsumer` toma un `BlocWidgetBuilder` y un `BlocWidgetListener` requeridos y un `bloc`, `BlocBuilderCondition` y `BlocListenerCondition` opcionales. Si se omite el parámetro `bloc`, `BlocConsumer` realizará automáticamente una búsqueda usando `BlocProvider` y el `BuildContext` actual. Se pueden implementar opcionalmente `listenWhen` y `buildWhen` para un control más granular sobre cuándo se llaman `listener` y `builder`. `listenWhen` y `buildWhen` se invocarán en cada cambio de `estado` del `bloc`. Cada uno toma el `estado` anterior y el `estado` actual y debe devolver un `bool` que determina si se invocará la función `builder` y/o `listener`. El `estado` anterior se inicializará al `estado` del `bloc` cuando se inicialice el `BlocConsumer`. `listenWhen` y `buildWhen` son opcionales y si no se implementan, su valor predeterminado será `true`. ### RepositoryProvider **RepositoryProvider** es un widget de Flutter que proporciona un repositorio a sus hijos a través de `RepositoryProvider.of(context)`. Se utiliza como un widget de inyección de dependencias (DI) para que una sola instancia de un repositorio pueda ser proporcionada a múltiples widgets dentro de un subárbol. `BlocProvider` debe usarse para proporcionar blocs, mientras que `RepositoryProvider` solo debe usarse para repositorios. entonces desde `ChildA` podemos recuperar la instancia del `Repository` con: ### MultiRepositoryProvider **MultiRepositoryProvider** es un widget de Flutter que fusiona múltiples widgets `RepositoryProvider` en uno solo. `MultiRepositoryProvider` mejora la legibilidad y elimina la necesidad de anidar múltiples `RepositoryProvider`. Usando `MultiRepositoryProvider` podemos pasar de: a: ## Uso de BlocProvider Veamos cómo usar `BlocProvider` para proporcionar un `CounterBloc` a una `CounterPage` y reaccionar a los cambios de estado con `BlocBuilder`. En este punto, hemos separado con éxito nuestra capa de presentación de nuestra capa de lógica de negocio. Observa que el widget `CounterPage` no sabe nada sobre lo que sucede cuando un usuario toca los botones. El widget simplemente le dice al `CounterBloc` que el usuario ha presionado el botón de incremento o decremento. ## Uso de RepositoryProvider Vamos a ver cómo usar `RepositoryProvider` en el contexto del ejemplo [`flutter_weather`][flutter_weather_link]. Dado que la aplicación tiene una dependencia explícita del `WeatherRepository`, inyectamos una instancia a través del constructor. Esto nos permite inyectar diferentes instancias de `WeatherRepository` según el sabor de compilación o el entorno. Dado que solo tenemos un repositorio en nuestra aplicación, lo inyectaremos en nuestro árbol de widgets a través de `RepositoryProvider.value`. Si tienes más de un repositorio, puedes usar `MultiRepositoryProvider` para proporcionar múltiples instancias de repositorio al subárbol. En la mayoría de los casos, el widget raíz de la aplicación expondrá uno o más repositorios al subárbol a través de `RepositoryProvider`. Ahora, al instanciar un bloc, podemos acceder a la instancia de un repositorio a través de `context.read` e inyectar el repositorio en el bloc a través del constructor. [flutter_weather_link]: https://github.com/felangel/bloc/blob/master/examples/flutter_weather ## Métodos de Extensión [Los métodos de extensión](https://dart.dev/guides/language/extension-methods), introducidos en Dart 2.7, son una forma de agregar funcionalidad a las bibliotecas existentes. En esta sección, veremos los métodos de extensión incluidos en `package:flutter_bloc` y cómo se pueden usar. `flutter_bloc` tiene una dependencia de [package:provider](https://pub.dev/packages/provider) que simplifica el uso de [`InheritedWidget`](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html). Internamente, `package:flutter_bloc` usa `package:provider` para implementar: los widgets `BlocProvider`, `MultiBlocProvider`, `RepositoryProvider` y `MultiRepositoryProvider`. `package:flutter_bloc` exporta las extensiones `ReadContext`, `WatchContext` y `SelectContext` de `package:provider`. :::note Aprende más sobre [`package:provider`](https://pub.dev/packages/provider). ::: ### context.read `context.read()` busca la instancia de ancestro más cercana del tipo `T` y es funcionalmente equivalente a `BlocProvider.of(context)`. `context.read` se usa más comúnmente para recuperar una instancia de bloc con el fin de agregar un evento dentro de las devoluciones de llamada `onPressed`. :::note `context.read()` no escucha a `T` -- si el `Object` proporcionado del tipo `T` cambia, `context.read` no activará una reconstrucción del widget. ::: #### Uso ✅ **USA** `context.read` para agregar eventos en callbacks. ```dart onPressed() { context.read().add(CounterIncrementPressed()), } ``` ❌ **EVITA** usar `context.read` para recuperar el estado dentro de un método `build`. ```dart @override Widget build(BuildContext context) { final state = context.read().state; return Text('$state'); } ``` El uso anterior es propenso a errores porque el widget `Text` no se reconstruirá si el estado del bloc cambia. :::caution Usa `BlocBuilder` o `context.watch` en su lugar para reconstruir en respuesta a cambios de estado. ::: ### context.watch Al igual que `context.read()`, `context.watch()` proporciona la instancia de ancestro más cercana del tipo `T`, sin embargo, también escucha los cambios en la instancia. Es funcionalmente equivalente a `BlocProvider.of(context, listen: true)`. Si el `Object` proporcionado del tipo `T` cambia, `context.watch` activará una reconstrucción. :::caution `context.watch` solo es accesible dentro del método `build` de una clase `StatelessWidget` o `State`. ::: #### Uso ✅ **USA** `BlocBuilder` en lugar de `context.watch` para delimitar explícitamente las reconstrucciones. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (context, state) { // Siempre que el estado cambie, solo se reconstruirá el Text. return Text(state.value); }, ), ), ); } ``` Alternativamente, usa un `Builder` para delimitar las reconstrucciones. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Siempre que el estado cambie, solo se reconstruirá el Text. final state = context.watch().state; return Text(state.value); }, ), ), ); } ``` ✅ **USA** `Builder` y `context.watch` como `MultiBlocBuilder`. ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // devuelve un Widget que depende del estado de BlocA, BlocB y BlocC } ); ``` ❌ **EVITA** usar `context.watch` cuando el widget padre en el método `build` no depende del estado. ```dart @override Widget build(BuildContext context) { // Siempre que el estado cambie, se reconstruirá el MaterialApp // aunque solo se use en el widget Text. final state = context.watch().state; return MaterialApp( home: Scaffold( body: Text(state.value), ), ); } ``` :::caution Usar `context.watch` en la raíz del método `build` resultará en que todo el widget se reconstruya cuando el estado del bloc cambie. ::: ### context.select Al igual que `context.watch()`, `context.select(R function(T value))` proporciona la instancia de ancestro más cercana del tipo `T` y escucha los cambios en `T`. A diferencia de `context.watch`, `context.select` te permite escuchar cambios en una parte más pequeña de un estado. ```dart Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); } ``` Lo anterior solo reconstruirá el widget cuando la propiedad `name` del estado de `ProfileBloc` cambie. #### Uso ✅ **USA** `BlocSelector` en lugar de `context.select` para delimitar explícitamente las reconstrucciones. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector( selector: (state) => state.name, builder: (context, name) { // Siempre que state.name cambie, solo se reconstruirá el Text. return Text(name); }, ), ), ); } ``` Alternativamente, usa un `Builder` para delimitar las reconstrucciones. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Siempre que state.name cambie, solo se reconstruirá el Text. final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), ); } ``` ❌ **EVITA** usar `context.select` cuando el widget padre en el método `build` no depende del estado. ```dart @override Widget build(BuildContext context) { // Siempre que state.value cambie, se reconstruirá el MaterialApp // aunque solo se use en el widget Text. final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), ); } ``` :::caution Usar `context.select` en la raíz del método `build` resultará en que todo el widget se reconstruya cuando la selección cambie. ::: ================================================ FILE: docs/src/content/docs/es/getting-started.mdx ================================================ --- title: Empezando description: Todo lo que necesitas para empezar a construir con Bloc. --- import InstallationTabs from '~/components/getting-started/InstallationTabs.astro'; import ImportTabs from '~/components/getting-started/ImportTabs.astro'; ## Paquetes El ecosistema de bloc consiste en múltiples paquetes listados a continuación: | Paquete | Descripción | Enlace | | ------------------------------------------------------------------------------------------ | ----------------------------- | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | Componentes AngularDart | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | APIs principales de Dart | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | Transformadores de eventos | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | Custom Linter | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | APIs de prueba | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | Command-line Tools | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | Widgets de Flutter | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | Soporte de caché/persistencia | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | Soporte de deshacer/rehacer | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | ## Instalación :::note Para empezar a usar bloc debes tener el [Dart SDK](https://dart.dev/get-dart) instalado en tu máquina. ::: ## Importaciones Ahora que hemos instalado bloc con éxito, podemos crear nuestro `main.dart` e importar el paquete `bloc` correspondiente. ================================================ FILE: docs/src/content/docs/es/index.mdx ================================================ --- template: splash title: Bloc State Management Library description: Official documentation for the bloc state management library. Support for Dart, Flutter, and AngularDart. Includes examples and tutorials. banner: content: | ✨ Visite la tienda de Bloc ✨ editUrl: false lastUpdated: false hero: title: Bloc v9.2.0 tagline: Una biblioteca de administración de estado predecible para Dart. image: alt: Bloc logo file: ~/assets/bloc.svg actions: - text: Empezar link: /es/getting-started/ variant: primary icon: rocket - text: Vista sobre GitHub link: https://github.com/felangel/bloc icon: github variant: secondary --- import { CardGrid } from '@astrojs/starlight/components'; import SponsorsGrid from '~/components/landing/SponsorsGrid.astro'; import Card from '~/components/landing/Card.astro'; import ListCard from '~/components/landing/ListCard.astro'; import SplitCard from '~/components/landing/SplitCard.astro'; import Discord from '~/components/landing/Discord.astro';
```sh # Agregue bloc a su proyecto. dart pub add bloc ``` Nuestra [guía de inicio](/es/getting-started) tiene instrucciones paso a paso sobre cómo comenzar a usar Bloc en solo unos minutos. Complete [los tutoriales oficiales](/es/tutorials/flutter-counter) para aprender mejor Prácticas y construir una variedad de aplicaciones diferentes alimentadas por Bloc. Explore la alta calidad y totalmente probada [muestra aplicaciones](https://github.com/felangel/bloc/tree/master/examples) como el mostrador, ¡Temporizador, lista infinita, clima, tarea y más! - [¿Por qué Bloc?](/es/why-bloc) - [Conceptos Básicos](/es/bloc-concepts) - [Arquitectura](/es/architecture) - [Testeo](/es/testing) - [Convención de Nombres](/es/naming-conventions) - [FAQs](/es/faqs) - [VSCode Integración](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) - [IntelliJ Integración](https://plugins.jetbrains.com/plugin/12129-bloc) - [Neovim Integración](https://github.com/wa11breaker/flutter-bloc.nvim) - [Mason CLI Integración](https://github.com/felangel/bloc/blob/master/bricks/README.md) - [Plantillas Personalizadas](https://brickhub.dev/search?q=bloc) - [Herramientas de Desarrollo](https://github.com/felangel/bloc/blob/master/packages/bloc_tools/README.md) ================================================ FILE: docs/src/content/docs/es/migration.mdx ================================================ --- title: Guía de Migración description: Migra a la última versión estable de Bloc. --- import { Code, Tabs, TabItem } from '@astrojs/starlight/components'; :::tip Por favor, consulta el [registro de versiones](https://github.com/felangel/bloc/releases) para obtener más información sobre los cambios en cada versión. ::: ## v10.0.0 ### `package:bloc_test` #### ❗✨ Desacoplar `blocTest` de `BlocBase` :::note[¿Qué cambió?] En bloc_test v10.0.0, la API `blocTest` ya no está estrechamente acoplada a `BlocBase`. ::: ##### Justificación `blocTest` debería usar las interfaces principales de bloc cuando sea posible para una mayor flexibilidad y reutilización. Anteriormente esto no era posible porque `BlocBase` implementaba `StateStreamableSource`, lo cual no era suficiente para `blocTest` debido a la dependencia interna en la API `emit`. ### `package:hydrated_bloc` #### ❗✨ Soporte para WebAssembly :::note[¿Qué cambió?] En hydrated_bloc v10.0.0, se añadió soporte para compilar a WebAssembly (wasm). ::: ##### Justificación Anteriormente no era posible compilar aplicaciones a wasm cuando se usaba `hydrated_bloc`. En la versión v10.0.0, el paquete fue refactorizado para permitir la compilación a wasm. **v9.x.x** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` **v10.x.x** ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorageDirectory.web : HydratedStorageDirectory((await getTemporaryDirectory()).path), ); runApp(const App()); } ``` ## v9.0.0 ### `package:bloc` #### ❗🧹 Eliminar APIs Obsoletas :::note[¿Qué cambió?] En bloc v9.0.0, todas las APIs previamente obsoletas fueron eliminadas. ::: ##### Resumen - `BlocOverrides` eliminado en favor de `Bloc.observer` y `Bloc.transformer` #### ❗✨ Introducir nueva Interfaz `EmittableStateStreamableSource` :::note[¿Qué cambió?] En bloc v9.0.0, se introdujo una nueva interfaz central `EmittableStateStreamableSource`. ::: ##### Justificación `package:bloc_test` estaba previamente estrechamente acoplado a `BlocBase`. La interfaz `EmittableStateStreamableSource` se introdujo para permitir que `blocTest` se desacople de la implementación concreta de `BlocBase`. ### `package:hydrated_bloc` #### ✨ Reintroducir la API `HydratedBloc.storage` :::note[¿Qué cambió?] En hydrated_bloc v9.0.0, `HydratedBlocOverrides` fue eliminado en favor de la API `HydratedBloc.storage`. ::: ##### Justificación Consulta la [justificación para reintroducir las anulaciones de Bloc.observer y Bloc.transformer](/es/migration#-reintroducir-las-apis-blocobserver-y-bloctransformer). **v8.x.x** ```dart Future main() async { final storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); HydratedBlocOverrides.runZoned( () => runApp(App()), storage: storage, ); } ``` **v9.0.0** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` ## v8.1.0 ### `package:bloc` #### ✨ Reintroducir las APIs `Bloc.observer` y `Bloc.transformer` :::note[¿Qué cambió?] En bloc v8.1.0, `BlocOverrides` fue deprecado en favor de las APIs `Bloc.observer` y `Bloc.transformer`. ::: ##### Justificación La API `BlocOverrides` se introdujo en v8.0.0 en un intento de soportar configuraciones específicas de bloc como `BlocObserver`, `EventTransformer` y `HydratedStorage`. En aplicaciones puras de Dart, los cambios funcionaron bien; sin embargo, en aplicaciones Flutter la nueva API causó más problemas de los que resolvió. La API `BlocOverrides` se inspiró en APIs similares en Flutter/Dart: - [HttpOverrides](https://api.flutter.dev/flutter/dart-io/HttpOverrides-class.html) - [IOOverrides](https://api.flutter.dev/flutter/dart-io/IOOverrides-class.html) **Problemas** Aunque no fue la razón principal para estos cambios, la API `BlocOverrides` introdujo complejidad adicional para los desarrolladores. Además de aumentar la cantidad de anidamiento y líneas de código necesarias para lograr el mismo efecto, la API `BlocOverrides` requería que los desarrolladores tuvieran un sólido entendimiento de [Zones](https://api.dart.dev/stable/2.17.6/dart-async/Zone-class.html) en Dart. Las `Zones` no son un concepto amigable para principiantes y el no entender cómo funcionan podría llevar a la introducción de errores (como observadores, transformadores o instancias de almacenamiento no inicializadas). Por ejemplo, muchos desarrolladores tendrían algo como: ```dart void main() { WidgetsFlutterBinding.ensureInitialized(); BlocOverrides.runZoned(...); } ``` El código anterior, aunque parece inofensivo, puede llevar a muchos errores difíciles de rastrear. La zona desde la cual se llama inicialmente a `WidgetsFlutterBinding.ensureInitialized` será la zona en la que se manejan los eventos de gestos (por ejemplo, callbacks `onTap`, `onPressed`) debido a `GestureBinding.initInstances`. Este es solo uno de los muchos problemas causados por el uso de `zoneValues`. Además, Flutter hace muchas cosas detrás de escena que implican bifurcar/manipular Zonas (especialmente al ejecutar pruebas) lo que puede llevar a comportamientos inesperados (y en muchos casos comportamientos que están fuera del control del desarrollador -- ver problemas a continuación). Debido al uso de [runZoned](https://api.flutter.dev/flutter/dart-async/runZoned.html), la transición a la API `BlocOverrides` llevó al descubrimiento de varios errores/limitaciones en Flutter (específicamente alrededor de las Pruebas de Widgets e Integración): - https://github.com/flutter/flutter/issues/96939 - https://github.com/flutter/flutter/issues/94123 - https://github.com/flutter/flutter/issues/93676 lo cual afectó a muchos desarrolladores que usaban la biblioteca bloc: - https://github.com/felangel/bloc/issues/3394 - https://github.com/felangel/bloc/issues/3350 - https://github.com/felangel/bloc/issues/3319 **v8.0.x** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` **v8.1.0** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` ## v8.0.0 ### `package:bloc` #### ❗✨ Introducir nueva API `BlocOverrides` :::note[¿Qué cambió?] En bloc v8.0.0, `Bloc.observer` y `Bloc.transformer` fueron eliminados en favor de la API `BlocOverrides`. ::: ##### Justificación La API anterior utilizada para sobrescribir el `BlocObserver` y `EventTransformer` predeterminados dependía de un singleton global tanto para el `BlocObserver` como para el `EventTransformer`. Como resultado, no era posible: - Tener múltiples implementaciones de `BlocObserver` o `EventTransformer` limitadas a diferentes partes de la aplicación. - Tener sobrescrituras de `BlocObserver` o `EventTransformer` limitadas a un paquete. - Si un paquete dependía de `package:bloc` y registraba su propio `BlocObserver`, cualquier consumidor del paquete tendría que sobrescribir el `BlocObserver` del paquete o informar al `BlocObserver` del paquete. También era más difícil de probar debido al estado global compartido entre las pruebas. Bloc v8.0.0 introduce una clase `BlocOverrides` que permite a los desarrolladores sobrescribir `BlocObserver` y/o `EventTransformer` para una `Zone` específica en lugar de depender de un singleton global mutable. **v7.x.x** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` **v8.0.0** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` Las instancias de `Bloc` usarán el `BlocObserver` y/o `EventTransformer` para la `Zone` actual a través de `BlocOverrides.current`. Si no hay `BlocOverrides` para la zona, usarán los valores predeterminados internos existentes (sin cambio en comportamiento/funcionalidad). Esto permite que cada `Zone` funcione de manera independiente con sus propios `BlocOverrides`. ```dart BlocOverrides.runZoned( () { // BlocObserverA y eventTransformerA final overrides = BlocOverrides.current; // Los Blocs en esta zona reportan a BlocObserverA // y usan eventTransformerA como el transformador predeterminado. // ... // Más tarde... BlocOverrides.runZoned( () { // BlocObserverB y eventTransformerB final overrides = BlocOverrides.current; // Los Blocs en esta zona reportan a BlocObserverB // y usan eventTransformerB como el transformador predeterminado. // ... }, blocObserver: BlocObserverB(), eventTransformer: eventTransformerB(), ); }, blocObserver: BlocObserverA(), eventTransformer: eventTransformerA(), ); ``` #### ❗✨ Mejorar el Manejo y Reporte de Errores :::note[¿Qué cambió?] En bloc v8.0.0, `BlocUnhandledErrorException` se eliminó. Además, cualquier excepción no capturada siempre se reporta a `onError` y se vuelve a lanzar (independientemente del modo de depuración o lanzamiento). La API `addError` informa errores a `onError`, pero no trata los errores reportados como excepciones no capturadas. ::: ##### Justificación El objetivo de estos cambios es: - hacer que las excepciones internas no manejadas sean extremadamente obvias mientras se preserva la funcionalidad del bloc - soportar `addError` sin interrumpir el flujo de control Anteriormente, el manejo y reporte de errores variaba dependiendo de si la aplicación se ejecutaba en modo de depuración o lanzamiento. Además, los errores reportados a través de `addError` se trataban como excepciones no capturadas en modo de depuración, lo que llevaba a una mala experiencia de desarrollador al usar la API `addError` (específicamente al escribir pruebas unitarias). En v8.0.0, `addError` se puede usar de manera segura para reportar errores y `blocTest` se puede usar para verificar que los errores se reporten. Todos los errores aún se reportan a `onError`, sin embargo, solo las excepciones no capturadas se vuelven a lanzar (independientemente del modo de depuración o lanzamiento). #### ❗🧹 Hacer `BlocObserver` abstracto :::note[¿Qué cambió?] En bloc v8.0.0, `BlocObserver` se convirtió en una clase `abstract`, lo que significa que no se puede instanciar una instancia de `BlocObserver`. ::: ##### Justificación `BlocObserver` estaba destinado a ser una interfaz. Dado que la implementación predeterminada de la API son operaciones nulas, `BlocObserver` es ahora una clase `abstract` para comunicar claramente que la clase está destinada a ser extendida y no instanciada directamente. **v7.x.x** ```dart void main() { // Era posible crear una instancia de la clase base. final observer = BlocObserver(); } ``` **v8.0.0** ```dart class MyBlocObserver extends BlocObserver {...} void main() { // No se puede instanciar la clase base. final observer = BlocObserver(); // ERROR // Extiende `BlocObserver` en su lugar. final observer = MyBlocObserver(); // OK } ``` #### ❗✨ `add` lanza `StateError` si el Bloc está cerrado :::note[¿Qué cambió?] En bloc v8.0.0, llamar a `add` en un bloc cerrado resultará en un `StateError`. ::: ##### Justificación Anteriormente, era posible llamar a `add` en un bloc cerrado y el error interno se tragaba, lo que dificultaba depurar por qué el evento añadido no se estaba procesando. Para hacer este escenario más visible, en v8.0.0, llamar a `add` en un bloc cerrado lanzará un `StateError` que se informará como una excepción no capturada y se propagará a `onError`. #### ❗✨ `emit` lanza `StateError` si el Bloc está cerrado :::note[¿Qué cambió?] En bloc v8.0.0, llamar a `emit` dentro de un bloc cerrado resultará en un `StateError`. ::: ##### Justificación Anteriormente, era posible llamar a `emit` dentro de un bloc cerrado y no ocurría ningún cambio de estado, pero tampoco había una indicación de lo que salió mal, lo que dificultaba la depuración. Para hacer este escenario más visible, en v8.0.0, llamar a `emit` dentro de un bloc cerrado lanzará un `StateError` que se informará como una excepción no capturada y se propagará a `onError`. #### ❗🧹 Eliminar APIs Obsoletas :::note[¿Qué cambió?] En bloc v8.0.0, todas las APIs previamente obsoletas fueron eliminadas. ::: ##### Resumen - `mapEventToState` eliminado en favor de `on` - `transformEvents` eliminado en favor de la API `EventTransformer` - `TransitionFunction` typedef eliminado en favor de la API `EventTransformer` - `listen` eliminado en favor de `stream.listen` ### `package:bloc_test` #### ✨ `MockBloc` y `MockCubit` ya no requieren `registerFallbackValue` :::note[¿Qué cambió?] En bloc_test v9.0.0, los desarrolladores ya no necesitan llamar explícitamente a `registerFallbackValue` al usar `MockBloc` o `MockCubit`. ::: ##### Resumen `registerFallbackValue` solo es necesario cuando se usa el matcher `any()` de `package:mocktail` para un tipo personalizado. Anteriormente, `registerFallbackValue` era necesario para cada `Event` y `State` al usar `MockBloc` o `MockCubit`. **v8.x.x** ```dart class FakeMyEvent extends Fake implements MyEvent {} class FakeMyState extends Fake implements MyState {} class MyMockBloc extends MockBloc implements MyBloc {} void main() { setUpAll(() { registerFallbackValue(FakeMyEvent()); registerFallbackValue(FakeMyState()); }); // Tests... } ``` **v9.0.0** ```dart class MyMockBloc extends MockBloc implements MyBloc {} void main() { // Tests... } ``` ### `package:hydrated_bloc` #### ❗✨ Introducir nueva API `HydratedBlocOverrides` :::note[¿Qué cambió?] En hydrated_bloc v8.0.0, `HydratedBloc.storage` fue eliminado en favor de la API `HydratedBlocOverrides`. ::: ##### Justificación Anteriormente, se utilizaba un singleton global para sobrescribir la implementación de `Storage`. Como resultado, no era posible tener múltiples implementaciones de `Storage` limitadas a diferentes partes de la aplicación. También era más difícil de probar debido al estado global compartido entre las pruebas. `HydratedBloc` v8.0.0 introduce una clase `HydratedBlocOverrides` que permite a los desarrolladores sobrescribir `Storage` para una `Zone` específica en lugar de depender de un singleton global mutable. **v7.x.x** ```dart void main() async { HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); // ... } ``` **v8.0.0** ```dart void main() { final storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); HydratedBlocOverrides.runZoned( () { // ... }, storage: storage, ); } ``` `HydratedBloc` usará el `Storage` para la `Zone` actual a través de `HydratedBlocOverrides.current`. Esto permite que cada `Zone` funcione de manera independiente con sus propios `BlocOverrides`. ## v7.2.0 ### `package:bloc` #### ✨ Introducir nueva API `on` :::note[¿Qué cambió?] En bloc v7.2.0, `mapEventToState` fue deprecado en favor de `on`. `mapEventToState` será eliminado en bloc v8.0.0. ::: ##### Justificación La API `on` se introdujo como parte de [[Propuesta] Reemplazar mapEventToState con on\ en Bloc](https://github.com/felangel/bloc/issues/2526). Debido a [un problema en Dart](https://github.com/dart-lang/sdk/issues/44616) no siempre es obvio cuál será el valor de `state` cuando se trata de generadores asincrónicos anidados (`async*`). Aunque hay formas de solucionar el problema, uno de los principios fundamentales de la biblioteca bloc es ser predecible. La API `on` se creó para hacer que la biblioteca sea lo más segura posible de usar y para eliminar cualquier incertidumbre en lo que respecta a los cambios de estado. :::tip Para más información, [lee la propuesta completa](https://github.com/felangel/bloc/issues/2526). ::: **Resumen** `on` te permite registrar un manejador de eventos para todos los eventos del tipo `E`. Por defecto, los eventos se procesarán concurrentemente cuando se use `on` en lugar de `mapEventToState`, que procesa los eventos `secuencialmente`. **v7.1.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0); @override Stream mapEventToState(CounterEvent event) async* { if (event is Increment) { yield state + 1; } } } ``` **v7.2.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } } ``` :::note Cada `EventHandler` registrado funciona de manera independiente, por lo que es importante registrar manejadores de eventos según el tipo de transformador que desees aplicar. ::: Si deseas mantener el mismo comportamiento exacto que en la versión v7.1.0, puedes registrar un solo manejador de eventos para todos los eventos y aplicar un transformador `sequential`: ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; class MyBloc extends Bloc { MyBloc() : super(MyState()) { on(_onEvent, transformer: sequential()) } FutureOr _onEvent(MyEvent event, Emitter emit) async { // TODO: logic goes here... } } ``` También puedes sobrescribir el `EventTransformer` predeterminado para todos los blocs en tu aplicación: ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; void main() { Bloc.transformer = sequential(); ... } ``` #### ✨ Introducir nueva API `EventTransformer` :::note[¿Qué cambió?] En bloc v7.2.0, `transformEvents` fue deprecado en favor de la API `EventTransformer`. `transformEvents` será eliminado en bloc v8.0.0. ::: ##### Justificación La API `on` abrió la puerta para poder proporcionar un transformador de eventos personalizado por manejador de eventos. Se introdujo un nuevo typedef `EventTransformer` que permite a los desarrolladores transformar el flujo de eventos entrantes para cada manejador de eventos en lugar de tener que especificar un único transformador de eventos para todos los eventos. **Resumen** Un `EventTransformer` es responsable de tomar el flujo entrante de eventos junto con un `EventMapper` (tu manejador de eventos) y devolver un nuevo flujo de eventos. ```dart typedef EventTransformer = Stream Function(Stream events, EventMapper mapper) ``` El `EventTransformer` predeterminado procesa todos los eventos concurrentemente y se ve algo así: ```dart EventTransformer concurrent() { return (events, mapper) => events.flatMap(mapper); } ``` :::tip Consulta [package:bloc_concurrency](https://pub.dev/packages/bloc_concurrency) para un conjunto de transformadores de eventos personalizados y con opiniones. ::: **v7.1.0** ```dart @override Stream> transformEvents(events, transitionFn) { return events .debounceTime(const Duration(milliseconds: 300)) .flatMap(transitionFn); } ``` **v7.2.0** ```dart /// Define un `EventTransformer` personalizado EventTransformer debounce(Duration duration) { return (events, mapper) => events.debounceTime(duration).flatMap(mapper); } MyBloc() : super(MyState()) { /// Aplica el `EventTransformer` personalizado al `EventHandler` on(_onEvent, transformer: debounce(const Duration(milliseconds: 300))) } ``` #### ⚠️ Marcar como obsoleta la API `transformTransitions` :::note[¿Qué cambió?] En bloc v7.2.0, `transformTransitions` fue deprecado en favor de sobrescribir la API `stream`. `transformTransitions` será eliminado en bloc v8.0.0. ::: ##### Justificación El getter `stream` en `Bloc` facilita la sobrescritura del flujo de estados salientes, por lo tanto, ya no es valioso mantener una API `transformTransitions` separada. **Resumen** **v7.1.0** ```dart @override Stream> transformTransitions( Stream> transitions, ) { return transitions.debounceTime(const Duration(milliseconds: 42)); } ``` **v7.2.0** ```dart @override Stream get stream => super.stream.debounceTime(const Duration(milliseconds: 42)); ``` ## v7.0.0 ### `package:bloc` #### ❗ Bloc y Cubit extienden BlocBase ##### Justificación Como desarrollador, la relación entre blocs y cubits era un poco incómoda. Cuando se introdujo cubit por primera vez, comenzó como la clase base para blocs, lo cual tenía sentido porque tenía un subconjunto de la funcionalidad y los blocs simplemente extenderían Cubit y definirían APIs adicionales. Esto tenía algunos inconvenientes: - Todas las APIs tendrían que ser renombradas para aceptar un cubit por precisión o tendrían que mantenerse como bloc por consistencia, aunque jerárquicamente no fuera preciso ([#1708](https://github.com/felangel/bloc/issues/1708), [#1560](https://github.com/felangel/bloc/issues/1560)). - Cubit tendría que extender Stream e implementar EventSink para tener una base común sobre la cual se puedan implementar widgets como BlocBuilder, BlocListener, etc. ([#1429](https://github.com/felangel/bloc/issues/1429)). Más tarde, experimentamos con invertir la relación y hacer que bloc fuera la clase base, lo que resolvió parcialmente el primer punto anterior pero introdujo otros problemas: - La API de cubit está sobrecargada debido a las APIs subyacentes de bloc como mapEventToState, add, etc. ([#2228](https://github.com/felangel/bloc/issues/2228)) - Los desarrolladores técnicamente pueden invocar estas APIs y romper cosas. - Todavía tenemos el mismo problema de cubit exponiendo toda la API de stream como antes ([#1429](https://github.com/felangel/bloc/issues/1429)) Para abordar estos problemas, introdujimos una clase base tanto para `Bloc` como para `Cubit` llamada `BlocBase` para que los componentes upstream puedan seguir interoperando con instancias de bloc y cubit sin exponer toda la API de `Stream` y `EventSink` directamente. **Resumen** **BlocObserver** **v6.1.x** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(Cubit cubit) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(Cubit cubit, Object event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(Cubit cubit, Object error, StackTrace stackTrace) {...} @override void onClose(Cubit cubit) {...} } ``` **v7.0.0** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(BlocBase bloc) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(BlocBase bloc, Object? event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) {...} @override void onClose(BlocBase bloc) {...} } ``` **Bloc/Cubit** **v6.1.x** ```dart final bloc = MyBloc(); bloc.listen((state) {...}); final cubit = MyCubit(); cubit.listen((state) {...}); ``` **v7.0.0** ```dart final bloc = MyBloc(); bloc.stream.listen((state) {...}); final cubit = MyCubit(); cubit.stream.listen((state) {...}); ``` ### `package:bloc_test` #### ❗seed devuelve una función para soportar valores dinámicos ##### Justificación Para soportar tener un valor de semilla mutable que se pueda actualizar dinámicamente en `setUp`, `seed` devuelve una función. **Resumen** **v7.x.x** ```dart blocTest( '...', seed: MyState(), ... ); ``` **v8.0.0** ```dart blocTest( '...', seed: () => MyState(), ... ); ``` #### ❗expect devuelve una función para soportar valores dinámicos y soporte de matchers ##### Justificación Para soportar tener una expectativa mutable que se pueda actualizar dinámicamente en `setUp`, `expect` devuelve una función. `expect` también soporta `Matchers`. **Resumen** **v7.x.x** ```dart blocTest( '...', expect: [MyStateA(), MyStateB()], ... ); ``` **v8.0.0** ```dart blocTest( '...', expect: () => [MyStateA(), MyStateB()], ... ); // It can also be a `Matcher` blocTest( '...', expect: () => contains(MyStateA()), ... ); ``` #### ❗errors devuelve una función para soportar valores dinámicos y soporte de matchers ##### Justificación Para soportar tener un valor de errores mutable que se pueda actualizar dinámicamente en `setUp`, `errors` devuelve una función. `errors` también soporta `Matchers`. **Resumen** **v7.x.x** ```dart blocTest( '...', errors: [MyError()], ... ); ``` **v8.0.0** ```dart blocTest( '...', errors: () => [MyError()], ... ); // It can also be a `Matcher` blocTest( '...', errors: () => contains(MyError()), ... ); ``` #### ❗MockBloc y MockCubit ##### Justificación Para soportar la simulación de varias APIs centrales, `MockBloc` y `MockCubit` se exportan como parte del paquete `bloc_test`. Anteriormente, `MockBloc` tenía que ser utilizado tanto para instancias de `Bloc` como de `Cubit`, lo cual no era intuitivo. **Resumen** **v7.x.x** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockBloc implements MyBloc {} ``` **v8.0.0** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockCubit implements MyCubit {} ``` #### ❗Integración con Mocktail ##### Justificación Debido a varias limitaciones de la versión null-safe del paquete [package:mockito](https://pub.dev/packages/mockito) descritas [aquí](https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#problems-with-typical-mocking-and-stubbing), el paquete [package:mocktail](https://pub.dev/packages/mocktail) es utilizado por `MockBloc` y `MockCubit`. Esto permite a los desarrolladores continuar usando una API de simulación familiar sin la necesidad de escribir stubs manualmente o depender de la generación de código. **Resumen** **v7.x.x** ```dart import 'package:mockito/mockito.dart'; ... when(bloc.state).thenReturn(MyState()); verify(bloc.add(any)).called(1); ``` **v8.0.0** ```dart import 'package:mocktail/mocktail.dart'; ... when(() => bloc.state).thenReturn(MyState()); verify(() => bloc.add(any())).called(1); ``` > Please refer to [#347](https://github.com/dart-lang/mockito/issues/347) as > well as the > [mocktail documentation](https://github.com/felangel/mocktail/tree/main/packages/mocktail) > for more information. ### `package:flutter_bloc` #### ❗ renombrar el parámetro `cubit` a `bloc` ##### Justificación Como resultado de la refactorización en `package:bloc` para introducir `BlocBase`, que extiende `Bloc` y `Cubit`, los parámetros de `BlocBuilder`, `BlocConsumer` y `BlocListener` se renombraron de `cubit` a `bloc` porque los widgets operan sobre el tipo `BlocBase`. Esto también se alinea aún más con el nombre de la biblioteca y, con suerte, mejora la legibilidad. **Resumen** **v6.1.x** ```dart BlocBuilder( cubit: myBloc, ... ) BlocListener( cubit: myBloc, ... ) BlocConsumer( cubit: myBloc, ... ) ``` **v7.0.0** ```dart BlocBuilder( bloc: myBloc, ... ) BlocListener( bloc: myBloc, ... ) BlocConsumer( bloc: myBloc, ... ) ``` ### `package:hydrated_bloc` #### ❗storageDirectory es requerido al llamar a HydratedStorage.build ##### Justificación Para hacer que `package:hydrated_bloc` sea un paquete puro de Dart, se eliminó la dependencia de [package:path_provider](https://pub.dev/packages/path_provider) y el parámetro `storageDirectory` al llamar a `HydratedStorage.build` es requerido y ya no tiene como valor predeterminado `getTemporaryDirectory`. **Resumen** **v6.x.x** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` **v7.0.0** ```dart import 'package:path_provider/path_provider.dart'; ... HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getTemporaryDirectory(), ); ``` ## v6.1.0 ### `package:flutter_bloc` #### ❗context.bloc y context.repository están obsoletos en favor de context.read y context.watch ##### Justificación `context.read`, `context.watch` y `context.select` se añadieron para alinearse con la API existente de [provider](https://pub.dev/packages/provider) con la que muchos desarrolladores están familiarizados y para abordar problemas planteados por la comunidad. Para mejorar la seguridad del código y mantener la consistencia, `context.bloc` se deprecó porque puede ser reemplazado por `context.read` o `context.watch` dependiendo de si se usa directamente dentro de `build`. **context.watch** `context.watch` aborda la solicitud de tener un [MultiBlocBuilder](https://github.com/felangel/bloc/issues/538) porque podemos observar varios blocs dentro de un solo `Builder` para renderizar la UI basada en múltiples estados: ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // return a Widget which depends on the state of BlocA, BlocB, and BlocC } ); ``` **context.select** `context.select` permite a los desarrolladores renderizar/actualizar la UI basada en una parte del estado de un bloc y aborda la solicitud de tener un [buildWhen más simple](https://github.com/felangel/bloc/issues/1521). ```dart final name = context.select((UserBloc bloc) => bloc.state.user.name); ``` El fragmento anterior nos permite acceder y reconstruir el widget solo cuando cambia el nombre del usuario actual. **context.read** Aunque parece que `context.read` es idéntico a `context.bloc`, hay algunas diferencias sutiles pero significativas. Ambos permiten acceder a un bloc con un `BuildContext` y no resultan en reconstrucciones; sin embargo, `context.read` no se puede llamar directamente dentro de un método `build`. Hay dos razones principales para usar `context.bloc` dentro de `build`: 1. **Para acceder al estado del bloc** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` El uso anterior es propenso a errores porque el widget `Text` no se reconstruirá si el estado del bloc cambia. En este escenario, se debe usar un `BlocBuilder` o `context.watch`. ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` or ```dart @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) => Text('$state'), ); } ``` :::note Usar `context.watch` en la raíz del método `build` resultará en que todo el widget se reconstruya cuando cambie el estado del bloc. Si no es necesario reconstruir todo el widget, usa `BlocBuilder` para envolver las partes que deben reconstruirse, usa un `Builder` con `context.watch` para delimitar las reconstrucciones, o descompón el widget en widgets más pequeños. ::: 2. **Para acceder al bloc y poder agregar un evento** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` El uso anterior es ineficiente porque resulta en una búsqueda del bloc en cada reconstrucción cuando el bloc solo es necesario cuando el usuario toca el `ElevatedButton`. En este escenario, es preferible usar `context.read` para acceder al bloc directamente donde se necesita (en este caso, en el callback `onPressed`). ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` **Resumen** **v6.0.x** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` ?> Si accedes a un bloc para agregar un evento, realiza el acceso al bloc usando `context.read` en el callback donde se necesita. **v6.0.x** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` ?> Usa `context.watch` cuando accedas al estado del bloc para asegurar que el widget se reconstruya cuando el estado cambie. ## v6.0.0 ### `package:bloc` #### ❗BlocObserver onError toma Cubit ##### Justificación Debido a la integración de `Cubit`, `onError` ahora se comparte entre las instancias de `Bloc` y `Cubit`. Dado que `Cubit` es la base, `BlocObserver` aceptará un tipo `Cubit` en lugar de un tipo `Bloc` en la sobrescritura de `onError`. **v5.x.x** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Bloc bloc, Object error, StackTrace stackTrace) { super.onError(bloc, error, stackTrace); } } ``` **v6.0.0** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Cubit cubit, Object error, StackTrace stackTrace) { super.onError(cubit, error, stackTrace); } } ``` #### ❗Bloc no emite el último estado en la suscripción ##### Justificación Este cambio se realizó para alinear `Bloc` y `Cubit` con el comportamiento incorporado de `Stream` en `Dart`. Además, conformar este comportamiento antiguo en el contexto de `Cubit` llevó a muchos efectos secundarios no deseados y, en general, complicó innecesariamente las implementaciones internas de otros paquetes como `flutter_bloc` y `bloc_test` (requiriendo `skip(1)`, etc...). **v5.x.x** ```dart final bloc = MyBloc(); bloc.listen(print); ``` Anteriormente, el fragmento anterior mostraría el estado inicial del bloc seguido de los cambios de estado posteriores. **v6.x.x** En v6.0.0, el fragmento anterior no muestra el estado inicial y solo muestra los cambios de estado posteriores. El comportamiento anterior se puede lograr con lo siguiente: ```dart final bloc = MyBloc(); print(bloc.state); bloc.listen(print); ``` ?> **Nota**: Este cambio solo afectará al código que dependa de suscripciones directas a blocs. Al usar `BlocBuilder`, `BlocListener` o `BlocConsumer` no habrá ningún cambio notable en el comportamiento. ### `package:bloc_test` #### ❗MockBloc solo requiere el tipo de Estado ##### Justificación No es necesario y elimina código adicional, además de hacer que `MockBloc` sea compatible con `Cubit`. **v5.x.x** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` **v6.0.0** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` #### ❗whenListen solo requiere el tipo de Estado ##### Justificación No es necesario y elimina código adicional, además de hacer que `whenListen` sea compatible con `Cubit`. **v5.x.x** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` **v6.0.0** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` #### ❗blocTest does not require Event type ##### Justificación No es necesario y elimina código adicional, además de hacer que `blocTest` sea compatible con `Cubit`. **v5.x.x** ```dart blocTest( 'emits [1] when increment is called', build: () async => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` **v6.0.0** ```dart blocTest( 'emits [1] when increment is called', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` #### ❗blocTest skip por defecto es 0 ##### Justificación Dado que las instancias de `bloc` y `cubit` ya no emitirán el último estado para nuevas suscripciones, ya no era necesario que `skip` tuviera un valor predeterminado de `1`. **v5.x.x** ```dart blocTest( 'emits [0] when skip is 0', build: () async => CounterBloc(), skip: 0, expect: const [0], ); ``` **v6.0.0** ```dart blocTest( 'emits [] when skip is 0', build: () => CounterBloc(), skip: 0, expect: const [], ); ``` El estado inicial de un bloc o cubit se puede probar con lo siguiente: ```dart test('initial state is correct', () { expect(MyBloc().state, InitialState()); }); ``` #### ❗blocTest hacer que build sea síncrono ##### Justificación Anteriormente, `build` se hizo `async` para que se pudieran realizar varias preparaciones para poner el bloc bajo prueba en un estado específico. Ya no es necesario y también resuelve varios problemas debido a la latencia añadida entre la construcción y la suscripción internamente. En lugar de hacer una preparación asincrónica para poner un bloc en un estado deseado, ahora podemos establecer el estado del bloc encadenando `emit` con el estado deseado. **v5.x.x** ```dart blocTest( 'emits [2] when increment is added', build: () async { final bloc = CounterBloc(); bloc.add(CounterEvent.increment); await bloc.take(2); return bloc; } act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` **v6.0.0** ```dart blocTest( 'emits [2] when increment is added', build: () => CounterBloc()..emit(1), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` :::note `emit` solo es visible para pruebas y nunca debe usarse fuera de las pruebas. ::: ### `package:flutter_bloc` #### ❗El parámetro `bloc` de BlocBuilder se renombró a `cubit` ##### Justificación Para que `BlocBuilder` pueda interoperar con instancias de `bloc` y `cubit`, el parámetro `bloc` se renombró a `cubit` (ya que `Cubit` es la clase base). **v5.x.x** ```dart BlocBuilder( bloc: myBloc, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocBuilder( cubit: myBloc, builder: (context, state) {...} ) ``` #### ❗BlocListener parámetro bloc renombrado a cubit ##### Justificación Para que `BlocListener` pueda interoperar con instancias de `bloc` y `cubit`, el parámetro `bloc` se renombró a `cubit` (ya que `Cubit` es la clase base). **v5.x.x** ```dart BlocListener( bloc: myBloc, listener: (context, state) {...} ) ``` **v6.0.0** ```dart BlocListener( cubit: myBloc, listener: (context, state) {...} ) ``` #### ❗BlocConsumer parámetro bloc renombrado a cubit ##### Justificación Para que `BlocConsumer` pueda interoperar con instancias de `bloc` y `cubit`, el parámetro `bloc` se renombró a `cubit` (ya que `Cubit` es la clase base). **v5.x.x** ```dart BlocConsumer( bloc: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocConsumer( cubit: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` --- ## v5.0.0 ### `package:bloc` #### ❗initialState ha sido eliminado ##### Justificación Como desarrollador, tener que sobrescribir `initialState` al crear un bloc presenta dos problemas principales: - El `initialState` del bloc puede ser dinámico y también puede ser referenciado en un momento posterior (incluso fuera del propio bloc). De alguna manera, esto puede verse como una filtración de información interna del bloc a la capa de UI. - Es verboso. **v4.x.x** ```dart class CounterBloc extends Bloc { @override int get initialState => 0; ... } ``` **v5.0.0** ```dart class CounterBloc extends Bloc { CounterBloc() : super(0); ... } ``` ?> Para más información, consulta [#1304](https://github.com/felangel/bloc/issues/1304) #### ❗BlocDelegate renombrado a BlocObserver ##### Justificación El nombre `BlocDelegate` no era una descripción precisa del papel que desempeñaba la clase. `BlocDelegate` sugiere que la clase juega un papel activo, mientras que en realidad el papel previsto del `BlocDelegate` era ser un componente pasivo que simplemente observa todos los blocs en una aplicación. :::note Idealmente, no debería haber ninguna funcionalidad o características orientadas al usuario manejadas dentro de `BlocObserver`. ::: **v4.x.x** ```dart class MyBlocDelegate extends BlocDelegate { ... } ``` **v5.0.0** ```dart class MyBlocObserver extends BlocObserver { ... } ``` #### ❗BlocSupervisor ha sido eliminado ##### Justificación `BlocSupervisor` era otro componente que los desarrolladores debían conocer e interactuar con el único propósito de especificar un `BlocDelegate` personalizado. Con el cambio a `BlocObserver`, sentimos que mejoraba la experiencia del desarrollador al establecer el observador directamente en el propio bloc. ?> Este cambio también nos permitió desacoplar otros complementos de bloc como `HydratedStorage` del `BlocObserver`. **v4.x.x** ```dart BlocSupervisor.delegate = MyBlocDelegate(); ``` **v5.0.0** ```dart Bloc.observer = MyBlocObserver(); ``` ### `package:flutter_bloc` #### ❗BlocBuilder condición renombrada a buildWhen ##### Justificación Cuando se usa `BlocBuilder`, anteriormente podíamos especificar una `condición` para determinar si el `builder` debería reconstruirse. ```dart BlocBuilder( buildWhen: (anterior, actual) { // devuelve true/false para determinar si se debe llamar al builder }, builder: (context, state) {...} ) ``` El nombre `condition` no es muy autoexplicativo u obvio y, más importante aún, cuando se interactúa con un `BlocConsumer`, la API se vuelve inconsistente porque los desarrolladores pueden proporcionar dos condiciones (una para `builder` y otra para `listener`). Como resultado, la API de `BlocConsumer` expone un `buildWhen` y `listenWhen`. ```dart BlocConsumer( listenWhen: (anterior, actual) { // devuelve true/false para determinar si se debe llamar al listener }, listener: (context, state) {...}, buildWhen: (anterior, actual) { // devuelve true/false para determinar si se debe llamar al builder }, builder: (context, state) {...}, ) ``` Para alinear la API y proporcionar una experiencia de desarrollador más consistente, `condition` fue renombrado a `buildWhen`. **v4.x.x** ```dart BlocBuilder( buildWhen: (anterior, actual) { // devuelve true/false para determinar si se debe llamar al builder }, builder: (context, state) {...} ) ``` **v5.0.0** ```dart BlocBuilder( buildWhen: (anterior, actual) { // devuelve true/false para determinar si se debe llamar al builder }, builder: (context, state) {...} ) ``` #### ❗BlocListener condición renombrada a listenWhen ##### Justificación Por las mismas razones descritas anteriormente, la condición de `BlocListener` también fue renombrada. **v4.x.x** ```dart BlocListener( listenWhen: (anterior, actual) { // devuelve true/false para determinar si se debe llamar al listener }, listener: (context, state) {...} ) ``` **v5.0.0** ```dart BlocListener( listenWhen: (anterior, actual) { // devuelve true/false para determinar si se debe llamar al listener }, listener: (context, state) {...} ) ``` ### `package:hydrated_bloc` #### ❗HydratedStorage y HydratedBlocStorage renombrados ##### Justificación Para mejorar la reutilización del código entre [hydrated_bloc](https://pub.dev/packages/hydrated_bloc) y [hydrated_cubit](https://pub.dev/packages/hydrated_cubit), la implementación concreta predeterminada de almacenamiento se renombró de `HydratedBlocStorage` a `HydratedStorage`. Además, la interfaz `HydratedStorage` se renombró de `HydratedStorage` a `Storage`. **v4.0.0** ```dart class MyHydratedStorage implements HydratedStorage { ... } ``` **v5.0.0** ```dart class MyHydratedStorage implements Storage { ... } ``` #### ❗HydratedStorage desacoplado de BlocDelegate ##### Justificación Como se mencionó anteriormente, `BlocDelegate` fue renombrado a `BlocObserver` y se estableció directamente como parte del `bloc` a través de: ```dart Bloc.observer = MyBlocObserver(); ``` El siguiente cambio se realizó para: - Mantener la consistencia con la nueva API de observador de bloc - Mantener el almacenamiento limitado solo a `HydratedBloc` - Desacoplar el `BlocObserver` del `Storage` **v4.0.0** ```dart BlocSupervisor.delegate = await HydratedBlocDelegate.build(); ``` **v5.0.0** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` #### ❗Inicialización Simplificada ##### Justificación Anteriormente, los desarrolladores tenían que llamar manualmente a `super.initialState ?? DefaultInitialState()` para configurar sus instancias de `HydratedBloc`. Esto es torpe y verboso y también incompatible con los cambios importantes en `initialState` en `bloc`. Como resultado, en la versión v5.0.0, la inicialización de `HydratedBloc` es idéntica a la inicialización normal de `Bloc`. **v4.0.0** ```dart class CounterBloc extends HydratedBloc { @override int get initialState => super.initialState ?? 0; } ``` **v5.0.0** ```dart class CounterBloc extends HydratedBloc { CounterBloc() : super(0); ... } ``` ================================================ FILE: docs/src/content/docs/es/modeling-state.mdx ================================================ --- title: Modelado de Estado description: Una visión general de varias formas de modelar estados al usar package:bloc. --- import ConcreteClassAndStatusEnumSnippet from '~/components/modeling-state/ConcreteClassAndStatusEnumSnippet.astro'; import SealedClassAndSubclassesSnippet from '~/components/modeling-state/SealedClassAndSubclassesSnippet.astro'; Hay muchos enfoques diferentes cuando se trata de estructurar el estado de la aplicación. Cada uno tiene sus propias ventajas y desventajas. En esta sección, veremos varios enfoques, sus pros y contras, y cuándo usar cada uno. Los siguientes enfoques son simplemente recomendaciones y son completamente opcionales. Siéntase libre de usar el enfoque que prefiera. Puede encontrar que algunos de los ejemplos/documentación no siguen los enfoques principalmente por simplicidad/concisión. :::tip Los siguientes fragmentos de código están enfocados en la estructura del estado. En la práctica, también puede querer: - Extender `Equatable` de [`package:equatable`](https://pub.dev/packages/equatable) - Anotar la clase con `@Data()` de [`package:data_class`](https://pub.dev/packages/data_class) - Anotar la clase con **@immutable** de [`package:meta`](https://pub.dev/packages/meta) - Implementar un método `copyWith` - Usar la palabra clave `const` para los constructores ::: ## Clase Concreta y Enum de Estado Este enfoque consiste en una **clase concreta única** para todos los estados junto con un `enum` que representa diferentes estados. Las propiedades son anulables y se manejan según el estado actual. Este enfoque funciona mejor para estados que no son estrictamente exclusivos y/o contienen muchas propiedades compartidas. #### Pros - **Simple**: Fácil de gestionar una sola clase y un enum de estado y todas las propiedades son fácilmente accesibles. - **Conciso**: Generalmente requiere menos líneas de código en comparación con otros enfoques. #### Contras - **No es Seguro en Tipos**: Requiere verificar el `estado` antes de acceder a las propiedades. Es posible emitir un estado malformado que puede llevar a errores. Las propiedades para estados específicos son anulables, lo que puede ser engorroso de manejar y requiere ya sea desenvolvimiento forzado o realizar verificaciones de nulidad. Algunos de estos contras pueden mitigarse escribiendo pruebas unitarias y constructores especializados y nombrados. - **Inflado**: Resulta en un solo estado que puede volverse inflado con muchas propiedades con el tiempo. #### Veredicto Este enfoque funciona mejor para estados simples o cuando los requisitos llaman a estados que no son exclusivos (por ejemplo, mostrar un snackbar cuando ocurre un error mientras se sigue mostrando datos antiguos del último estado exitoso). Este enfoque proporciona flexibilidad y concisión a costa de la seguridad en tipos. ## Clase Sellada y Subclases Este enfoque consiste en una **clase sellada** que contiene cualquier propiedad compartida y múltiples subclases para los estados separados. Este enfoque es ideal para estados separados y exclusivos. #### Pros - **Seguro en Tipos**: El código es seguro en tiempo de compilación y no es posible acceder accidentalmente a una propiedad no válida. Cada subclase contiene sus propias propiedades, lo que hace claro qué propiedades pertenecen a qué estado. - **Explícito**: Separa las propiedades compartidas de las propiedades específicas del estado. - **Exhaustivo**: Usar una declaración `switch` para verificaciones exhaustivas asegura que cada estado sea manejado explícitamente. - Si no desea [verificaciones exhaustivas](https://dart.dev/language/branches#exhaustiveness-checking) o desea poder agregar subtipos más tarde sin romper la API, use el modificador [final](https://dart.dev/language/class-modifiers#final). - Consulte la [documentación de clases selladas](https://dart.dev/language/class-modifiers#sealed) para más detalles. #### Contras - **Verborrea**: Requiere más código (una clase base y una subclase por estado). También puede requerir código duplicado para propiedades compartidas entre subclases. - **Complejidad**: Agregar nuevas propiedades requiere actualizar cada subclase y la clase base, lo que puede ser engorroso y llevar a un aumento en la complejidad del estado. Además, puede requerir verificaciones de tipo innecesarias/excesivas para acceder a propiedades. #### Veredicto Este enfoque funciona mejor para estados bien definidos y exclusivos con propiedades únicas. Este enfoque proporciona seguridad en tipos y verificaciones exhaustivas y enfatiza la seguridad sobre la concisión y simplicidad. ================================================ FILE: docs/src/content/docs/es/naming-conventions.mdx ================================================ --- title: Convenciones de Nombres description: Descripción general de las convenciones de nombres recomendadas al usar bloc. --- import EventExamplesGood1 from '~/components/naming-conventions/EventExamplesGood1Snippet.astro'; import EventExamplesBad1 from '~/components/naming-conventions/EventExamplesBad1Snippet.astro'; import StateExamplesGood1Snippet from '~/components/naming-conventions/StateExamplesGood1Snippet.astro'; import SingleStateExamplesGood1Snippet from '~/components/naming-conventions/SingleStateExamplesGood1Snippet.astro'; import StateExamplesBad1Snippet from '~/components/naming-conventions/StateExamplesBad1Snippet.astro'; Las siguientes convenciones de nombres son simplemente recomendaciones y son completamente opcionales. Siéntase libre de usar las convenciones de nombres que prefiera. Puede encontrar algunos de los ejemplos/documentación que no siguen las convenciones de nombres principalmente por simplicidad/concisión. Estas convenciones son fuertemente recomendadas para proyectos grandes con múltiples desarrolladores. ## Convenciones de Eventos Los eventos deben ser nombrados en **pasado** porque los eventos son cosas que ya han ocurrido desde la perspectiva del bloc. ### Anatomía `BlocSubject` + `Sustantivo (opcional)` + `Verbo (evento)` Los eventos de carga inicial deben seguir la convención: `BlocSubject` + `Started` :::note La clase base del evento debe llamarse: `BlocSubject` + `Event`. ::: ### Ejemplos ✅ **Bueno** ❌ **Malo** ## Convenciones de Estado Los estados deben ser sustantivos porque un estado es simplemente una instantánea en un momento particular en el tiempo. Hay dos formas comunes de representar el estado: usando subclases o usando una sola clase. ### Anatomía #### Subclases `BlocSubject` + `Verbo (acción)` + `State` Cuando se representa el estado como múltiples subclases, `State` debe ser uno de los siguientes: `Initial` | `Success` | `Failure` | `InProgress` :::note Los estados iniciales deben seguir la convención: `BlocSubject` + `Initial`. ::: #### Clase Única `BlocSubject` + `State` Cuando se representa el estado como una sola clase base, se debe usar un enum llamado `BlocSubject` + `Status` para representar el estado: `initial` | `success` | `failure` | `loading`. :::note La clase base del estado siempre debe llamarse: `BlocSubject` + `State`. ::: ### Ejemplos ✅ **Bueno** ##### Subclases ##### Clase Única ❌ **Malo** ================================================ FILE: docs/src/content/docs/es/testing.mdx ================================================ --- title: Pruebas description: Los conceptos básicos de cómo escribir pruebas para tus blocs. --- import CounterBlocSnippet from '~/components/testing/CounterBlocSnippet.astro'; import AddDevDependenciesSnippet from '~/components/testing/AddDevDependenciesSnippet.astro'; import CounterBlocTestImportsSnippet from '~/components/testing/CounterBlocTestImportsSnippet.astro'; import CounterBlocTestMainSnippet from '~/components/testing/CounterBlocTestMainSnippet.astro'; import CounterBlocTestSetupSnippet from '~/components/testing/CounterBlocTestSetupSnippet.astro'; import CounterBlocTestInitialStateSnippet from '~/components/testing/CounterBlocTestInitialStateSnippet.astro'; import CounterBlocTestBlocTestSnippet from '~/components/testing/CounterBlocTestBlocTestSnippet.astro'; Bloc fue diseñado para ser extremadamente fácil de probar. En esta sección, repasaremos cómo hacer pruebas unitarias a un bloc. Para simplificar, escribamos pruebas para el `CounterBloc` que creamos en [Conceptos Básicos](/es/bloc-concepts). Para recapitular, la implementación de `CounterBloc` se ve así: ## Configuración Antes de comenzar a escribir nuestras pruebas, vamos a necesitar agregar un marco de pruebas a nuestras dependencias. Necesitamos agregar [test](https://pub.dev/packages/test) y [bloc_test](https://pub.dev/packages/bloc_test) a nuestro proyecto. ## Pruebas Comencemos creando el archivo para nuestras pruebas de `CounterBloc`, `counter_bloc_test.dart` e importando el paquete de pruebas. A continuación, necesitamos crear nuestro `main` así como nuestro grupo de pruebas. :::note Los grupos son para organizar pruebas individuales así como para crear un contexto en el que se puede compartir un `setUp` y `tearDown` común en todas las pruebas individuales. ::: Comencemos creando una instancia de nuestro `CounterBloc` que se utilizará en todas nuestras pruebas. Ahora podemos comenzar a escribir nuestras pruebas individuales. :::note Podemos ejecutar todas nuestras pruebas con el comando `dart test`. ::: ¡En este punto deberíamos tener nuestra primera prueba aprobada! Ahora escribamos una prueba más compleja usando el paquete [bloc_test](https://pub.dev/packages/bloc_test). Deberíamos poder ejecutar las pruebas y ver que todas están aprobadas. Eso es todo, las pruebas deberían ser fáciles y deberíamos sentirnos seguros al hacer cambios y refactorizar nuestro código. Puedes consultar la [Aplicación del Clima](https://github.com/felangel/bloc/tree/master/examples/flutter_weather) para un ejemplo de una aplicación completamente probada. ================================================ FILE: docs/src/content/docs/es/why-bloc.mdx ================================================ --- title: ¿Por qué Bloc? description: Una visión general de lo que hace de Bloc una solución sólida para la gestión de estado. sidebar: order: 1 --- Bloc facilita la separación de la presentación de la lógica de negocio, haciendo que tu código sea _rápido_, _fácil de probar_ y _reutilizable_. Al construir aplicaciones de calidad para producción, la gestión del estado se vuelve crítica. Como desarrolladores queremos: - saber en qué estado se encuentra nuestra aplicación en cualquier momento. - probar fácilmente cada caso para asegurarnos de que nuestra aplicación responde adecuadamente. - registrar cada interacción del usuario en nuestra aplicación para poder tomar decisiones basadas en datos. - trabajar de la manera más eficiente posible y reutilizar componentes tanto dentro de nuestra aplicación como en otras aplicaciones. - tener muchos desarrolladores trabajando sin problemas dentro de una sola base de código siguiendo los mismos patrones y convenciones. - desarrollar aplicaciones rápidas y reactivas. Bloc fue diseñado para satisfacer todas estas necesidades y muchas más. Existen muchas soluciones para la gestión del estado y decidir cuál usar puede ser una tarea desalentadora. ¡No hay una solución perfecta para la gestión del estado! Lo importante es que elijas la que mejor funcione para tu equipo y tu proyecto. Bloc fue diseñado con tres valores fundamentales en mente: - **Simple:** Fácil de entender y puede ser utilizado por desarrolladores con diferentes niveles de habilidad. - **Poderoso:** Ayuda a crear aplicaciones increíbles y complejas componiéndolas de componentes más pequeños. - **Probable:** Probar fácilmente cada aspecto de una aplicación para que podamos iterar con confianza. En general, Bloc intenta hacer que los cambios de estado sean predecibles regulando cuándo puede ocurrir un cambio de estado y haciendo cumplir una única forma de cambiar el estado en toda la aplicación. ================================================ FILE: docs/src/content/docs/fa/architecture.mdx ================================================ --- title: معماری description: نمای کلی از الگوهای معماری پیشنهادی هنگام استفاده از Bloc. --- import DataProviderSnippet from '~/components/architecture/DataProviderSnippet.astro'; import RepositorySnippet from '~/components/architecture/RepositorySnippet.astro'; import BusinessLogicComponentSnippet from '~/components/architecture/BusinessLogicComponentSnippet.astro'; import BlocTightCouplingSnippet from '~/components/architecture/BlocTightCouplingSnippet.astro'; import BlocLooseCouplingPresentationSnippet from '~/components/architecture/BlocLooseCouplingPresentationSnippet.astro'; import AppIdeasRepositorySnippet from '~/components/architecture/AppIdeasRepositorySnippet.astro'; import AppIdeaRankingBlocSnippet from '~/components/architecture/AppIdeaRankingBlocSnippet.astro'; import PresentationComponentSnippet from '~/components/architecture/PresentationComponentSnippet.astro'; ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) استفاده از کتابخانه Bloc به ما اجازه می‌دهد برنامه خود را به سه لایه جدا کنیم: - رابط کاربری Presentation - منطق کسب و کار Business Logic - داده Data - Repository - Data Provider ما از پایین‌ترین لایه (دورترین از رابط کاربری) شروع می‌کنیم و به سمت لایه رابط کاربری پیش می‌رویم. ## لایه داده مسئولیت لایه داده، بازیابی/دستکاری داده‌ها از یک یا چند منبع است. لایه داده می‌تواند به دو قسمت تقسیم شود: - Repository - Data Provider این لایه پایین‌ترین سطح برنامه است و با پایگاه داده‌ها، درخواست‌های شبکه و سایر منابع داده ناهمگام تعامل دارد. ### Data Provider مسئولیت data provider، ارائه داده‌های خام است. Data provider باید عمومی و همه منظوره باشد. Data provider معمولاً APIهای ساده‌ای برای انجام عملیات [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) ارائه می‌دهد. ممکن است متدهایی مانند `createData`, `readData`, `updateData` و `deleteData` به عنوان بخشی از لایه داده داشته باشیم. ### Repository لایه repository یک پوشش در اطراف یک یا چند data provider است که لایه Bloc با آن ارتباط برقرار می‌کند. همانطور که می‌بینید، لایه repository می‌تواند با چندین data provider تعامل کند و تبدیلاتی روی داده‌ها انجام دهد قبل از اینکه نتیجه را به لایه منطق کسب‌وکار تحویل دهد. ## لایه منطق کسب‌وکار مسئولیت لایه منطق کسب‌وکار، پاسخ به ورودی از لایه رابط کاربری با حالت‌های جدید است. این لایه می‌تواند به یک یا چند repository وابسته باشد تا داده‌های مورد نیاز برای ساخت حالت برنامه را بازیابی کند. لایه منطق کسب‌وکار را به عنوان پل بین رابط کاربری (لایه رابط کاربری) و لایه داده در نظر بگیرید. لایه منطق کسب‌وکار از رویدادها/اقدامات از لایه رابط کاربری مطلع می‌شود و سپس با repository ارتباط برقرار می‌کند تا یک حالت جدید برای لایه رابط کاربری بسازد. ### ارتباط Bloc به Bloc از آنجایی که blocها streamها را نمایش می‌دهند، ممکن است وسوسه شوید که یک bloc بسازید که به bloc دیگری گوش دهد. شما **نباید** این کار را بکنید. گزینه‌های بهتری نسبت به استفاده از کد زیر وجود دارد: در حالی که کد بالا بدون خطا است (و حتی خودش را پاکسازی می‌کند)، مشکل بزرگ‌تری دارد: وابستگی بین دو bloc ایجاد می‌کند. به طور کلی، وابستگی‌های خواهر و برادر بین دو موجودیت در یک لایه معماری باید به هر قیمتی اجتناب شود، زیرا اتصال محکم ایجاد می‌کند که نگهداری آن سخت است. از آنجایی که blocها در لایه معماری منطق کسب‌وکار قرار دارند، هیچ bloc نباید از هیچ bloc دیگری اطلاع داشته باشد. ![Application Architecture Layers](~/assets/architecture/architecture.png) یک bloc باید فقط از طریق رویدادها و از repositoryهای تزریق شده (یعنی repositoryهایی که در سازنده bloc به آن داده می‌شود) اطلاعات دریافت کند. اگر در موقعیتی هستید که یک bloc نیاز به پاسخ به bloc دیگری دارد، دو گزینه دیگر دارید. می‌توانید مشکل را به لایه بالاتر (به لایه رابط کاربری) ببرید، یا به لایه پایین‌تر (به لایه domain). #### اتصال Blocها از طریق رابط کاربری می‌توانید از `BlocListener` استفاده کنید تا به یک bloc گوش دهید و هر زمان که bloc اول تغییر کند، یک رویداد به bloc دیگری اضافه کنید. کد بالا از نیاز `SecondBloc` به دانستن درباره `FirstBloc` جلوگیری می‌کند و اتصال شل را تشویق می‌کند. برنامه [flutter_weather](/fa/tutorials/flutter-weather) از [این تکنیک](https://github.com/felangel/bloc/blob/b4c8db938ad71a6b60d4a641ec357905095c3965/examples/flutter_weather/lib/weather/view/weather_page.dart#L38-L42) استفاده می‌کند تا تم برنامه را بر اساس اطلاعات آب و هوایی که دریافت می‌شود تغییر دهد. در برخی موقعیت‌ها، ممکن است نخواهید دو bloc را در لایه رابط کاربری جفت کنید. در عوض، اغلب منطقی است که دو bloc منبع داده یکسانی داشته باشند و هر زمان که داده‌ها تغییر کنند، به‌روزرسانی شوند. #### اتصال Blocها از طریق Domain دو bloc می‌توانند به یک stream از repository گوش دهند و حالت‌های خود را مستقل از یکدیگر هر زمان که داده‌های repository تغییر کند، به‌روزرسانی کنند. استفاده از repositoryهای واکنشی برای نگه داشتن حالت همگام‌سازی شده در برنامه‌های بزرگ سازمانی رایج است. ابتدا، یک repository ایجاد کنید یا از repository موجودی استفاده کنید که یک `Stream` داده ارائه می‌دهد. برای مثال، repository زیر یک stream بی‌پایان از چند ایده برنامه یکسانی را نمایش می‌دهد: همان repository می‌تواند به هر bloc که نیاز به واکنش به ایده‌های برنامه جدید دارد، تزریق شود. در مثال زیر یک `AppIdeaRankingBloc` وجود دارد که برای هر ایده برنامه ورودی از repository بالا، یک حالت تولید می‌کند: برای اطلاعات بیشتر درباره استفاده از streamها با Bloc، این مقاله را مطالعه کنید [How to use Bloc with streams and concurrency](https://verygood.ventures/blog/how-to-use-bloc-with-streams-and-concurrency). ## لایه رابط کاربری مسئولیت لایه رابط کاربری، تصمیم‌گیری درباره نحوه رندر خود بر اساس یک یا چند حالت bloc است. علاوه بر این، باید ورودی کاربر و رویدادهای چرخه حیات برنامه را مدیریت کند. اکثر streamهای برنامه با یک رویداد `AppStart` شروع می‌شود که برنامه را برای واکشی برخی داده‌ها برای نمایش به کاربر راه‌اندازی می‌کند. در این سناریو، لایه رابط کاربری یک رویداد `AppStart` اضافه می‌کند. علاوه بر این، لایه رابط کاربری باید تصمیم بگیرد که چه چیزی را روی صفحه بر اساس حالت از لایه bloc رندر کند. تا اینجا، حتی با وجود داشتن چند قطعه کد، همه چیز نسبتاً سطح بالا بوده است. در بخش آموزش، همه این‌ها را کنار هم می‌گذاریم زیرا چندین برنامه نمونه مختلف می‌سازیم. ================================================ FILE: docs/src/content/docs/fa/bloc-concepts.mdx ================================================ --- title: مفاهیم Bloc description: An overview of the core concepts for package:bloc. sidebar: order: 1 --- import CountStreamSnippet from '~/components/concepts/bloc/CountStreamSnippet.astro'; import SumStreamSnippet from '~/components/concepts/bloc/SumStreamSnippet.astro'; import StreamsMainSnippet from '~/components/concepts/bloc/StreamsMainSnippet.astro'; import CounterCubitSnippet from '~/components/concepts/bloc/CounterCubitSnippet.astro'; import CounterCubitInitialStateSnippet from '~/components/concepts/bloc/CounterCubitInitialStateSnippet.astro'; import CounterCubitInstantiationSnippet from '~/components/concepts/bloc/CounterCubitInstantiationSnippet.astro'; import CounterCubitIncrementSnippet from '~/components/concepts/bloc/CounterCubitIncrementSnippet.astro'; import CounterCubitBasicUsageSnippet from '~/components/concepts/bloc/CounterCubitBasicUsageSnippet.astro'; import CounterCubitStreamUsageSnippet from '~/components/concepts/bloc/CounterCubitStreamUsageSnippet.astro'; import CounterCubitOnChangeSnippet from '~/components/concepts/bloc/CounterCubitOnChangeSnippet.astro'; import CounterCubitOnChangeUsageSnippet from '~/components/concepts/bloc/CounterCubitOnChangeUsageSnippet.astro'; import CounterCubitOnChangeOutputSnippet from '~/components/concepts/bloc/CounterCubitOnChangeOutputSnippet.astro'; import SimpleBlocObserverOnChangeSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeSnippet.astro'; import SimpleBlocObserverOnChangeUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeUsageSnippet.astro'; import SimpleBlocObserverOnChangeOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeOutputSnippet.astro'; import CounterCubitOnErrorSnippet from '~/components/concepts/bloc/CounterCubitOnErrorSnippet.astro'; import SimpleBlocObserverOnErrorSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnErrorSnippet.astro'; import CounterCubitOnErrorOutputSnippet from '~/components/concepts/bloc/CounterCubitOnErrorOutputSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/bloc/CounterBlocSnippet.astro'; import CounterBlocEventHandlerSnippet from '~/components/concepts/bloc/CounterBlocEventHandlerSnippet.astro'; import CounterBlocIncrementSnippet from '~/components/concepts/bloc/CounterBlocIncrementSnippet.astro'; import CounterBlocUsageSnippet from '~/components/concepts/bloc/CounterBlocUsageSnippet.astro'; import CounterBlocStreamUsageSnippet from '~/components/concepts/bloc/CounterBlocStreamUsageSnippet.astro'; import CounterBlocOnChangeSnippet from '~/components/concepts/bloc/CounterBlocOnChangeSnippet.astro'; import CounterBlocOnChangeUsageSnippet from '~/components/concepts/bloc/CounterBlocOnChangeUsageSnippet.astro'; import CounterBlocOnChangeOutputSnippet from '~/components/concepts/bloc/CounterBlocOnChangeOutputSnippet.astro'; import CounterBlocOnTransitionSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionSnippet.astro'; import CounterBlocOnTransitionOutputSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionOutputSnippet.astro'; import SimpleBlocObserverOnTransitionSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionSnippet.astro'; import SimpleBlocObserverOnTransitionUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionUsageSnippet.astro'; import SimpleBlocObserverOnTransitionOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionOutputSnippet.astro'; import CounterBlocOnEventSnippet from '~/components/concepts/bloc/CounterBlocOnEventSnippet.astro'; import SimpleBlocObserverOnEventSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventSnippet.astro'; import SimpleBlocObserverOnEventOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventOutputSnippet.astro'; import CounterBlocOnErrorSnippet from '~/components/concepts/bloc/CounterBlocOnErrorSnippet.astro'; import CounterBlocOnErrorOutputSnippet from '~/components/concepts/bloc/CounterBlocOnErrorOutputSnippet.astro'; import CounterCubitFullSnippet from '~/components/concepts/bloc/CounterCubitFullSnippet.astro'; import CounterBlocFullSnippet from '~/components/concepts/bloc/CounterBlocFullSnippet.astro'; import AuthenticationStateSnippet from '~/components/concepts/bloc/AuthenticationStateSnippet.astro'; import AuthenticationTransitionSnippet from '~/components/concepts/bloc/AuthenticationTransitionSnippet.astro'; import AuthenticationChangeSnippet from '~/components/concepts/bloc/AuthenticationChangeSnippet.astro'; import DebounceEventTransformerSnippet from '~/components/concepts/bloc/DebounceEventTransformerSnippet.astro'; :::note لطفاً قبل از کار با[`package:bloc`](https://pub.dev/packages/bloc) بخش‌های زیر را به‌دقت مطالعه کنید. ::: چندین مفهوم اصلی وجود دارد که درک صحیح آن‌ها برای استفاده از پکیج bloc ضروری است. در بخش‌های بعدی، هر کدام از این مفاهیم را به‌طور مفصل بررسی خواهیم کرد و همچنین نحوه اعمال آن‌ها را در یک اپلیکیشن شمارنده (counter app) مثال می‌زنیم. ## استریم‌ها (Streams) :::note برای اطلاعات بیشتر در مورد `Streams` به مستندات رسمی زبان دارت [Dart Documentation](https://dart.dev/tutorials/language/streams) مراجعه کنید. ::: یک استریم، دنباله‌ای از داده‌های ناهمزمان (asynchronous) است. برای استفاده از کتابخانه bloc، داشتن درک پایه‌ای از `Streams` و نحوه عملکرد آن‌ها بسیار مهم است. اگر با `Streams` آشنا نیستید، آن را مانند یک لوله آب تصور کنید که آب از داخل آن جریان دارد. لوله همان `Stream` است و آب، داده‌های ناهمزمان هستند. می‌توانیم یک `Stream` در زبان دارت ایجاد کنیم با نوشتن یک تابع `async*` (تولید کننده ناهمزمان). با علامت‌گذاری یک تابع به صورت `async*`، می‌توانیم از کلمه کلیدی `yield` استفاده کنیم و یک `Stream` از داده‌ها برگردانیم. در مثال بالا، ما یک `Stream` از اعداد صحیح (integers) تا مقدار پارامتر `max` برمی‌گردانیم. هر بار که در یک تابع `async*` از `yield` استفاده می‌کنیم، آن قطعه داده را از طریق `Stream` به جلو می‌فرستیم (push می‌کنیم). می‌توانیم `Stream` بالا را به چندین روش مصرف کنیم. اگر بخواهیم تابعی بنویسیم که مجموع یک `Stream` از اعداد صحیح را برگرداند، می‌تواند چیزی شبیه به این باشد: با علامت‌گذاری تابع بالا به صورت `async`، می‌توانیم از کلمه کلیدی `await` استفاده کنیم و یک `Future` از اعداد صحیح برگردانیم. در این مثال، ما روی هر مقدار در استریم `await` می‌کنیم و مجموع تمام اعداد صحیح موجود در استریم را برمی‌گردانیم. می‌توانیم همه چیز را به این شکل کنار هم بگذاریم: حالا که درک پایه‌ای از نحوه کار `Streams` در زبان دارت داریم، آماده‌ایم تا درباره مؤلفه اصلی پکیج bloc یاد بگیریم: یک `Cubit`. ## Cubit یک Cubit کلاسی است که از `BlocBase` ارث‌بری می‌کند و می‌توان آن را گسترش داد تا هر نوعی از `حالت (state)` را مدیریت کند. ![Cubit Architecture](~/assets/concepts/cubit_architecture_full.png) یک `Cubit` می‌تواند توابعی را در معرض دید قرار دهد که با فراخوانی آن‌ها می‌توان تغییرات حالت را تحریک کرد. حالت‌ها (States) خروجی یک `Cubit` هستند و بخشی از حالت برنامه شما را نشان می‌دهند. کامپوننت‌های رابط کاربری می‌توانند از حالت‌ها مطلع شوند و بخش‌هایی از خود را بر اساس حالت فعلی دوباره رسم کنند. :::note برای کسب اطلاعات بیشتر در مورد ریشه‌های `Cubit`این [مسئله](https://github.com/felangel/cubit/issues/69) را بررسی کنید. ::: ### ساخت یک Cubit ما می‌توانیم یک `CounterCubit` را به شکل زیر بسازیم: هنگام ساخت یک `Cubit` باید نوع حالتی (State) را تعریف کنیم که `Cubit` مدیریت خواهد کرد. در مورد `CounterCubit` بالا، وضعیت می‌تواند با یک `int` نمایش داده شود، اما در موارد پیچیده‌تر، ممکن است لازم باشد به جای یک نوع اولیه ، از یک `class` استفاده شود. دومین کاری که هنگام ساخت یک `Cubit` باید انجام دهیم، مشخص کردن حالت اولیه (initial state) است. می‌توانیم این کار را با فراخوانی `super` به همراه مقدار حالت اولیه انجام دهیم. در قطعه کد بالا، ما به صورت داخلی، حالت اولیه را `0` قرار می‌دهیم، اما همچنین می‌توانیم با پذیرش یک مقدار بیرونی، `Cubit` را انعطاف‌پذیرتر کنیم: این امر به ما اجازه می‌دهد تا نمونه‌های `CounterCubit` را با حالت‌های اولیه مختلفی ایجاد کنیم، مانند: ### تغییرات حالت Cubit هر `Cubit` این قابلیت را دارد که با استفاده از `emit،` یک حالت جدید را خروجی دهد. در قطعه کد بالا، `CounterCubit` یک متد عمومی به نام `increment` را در معرض دید قرار می‌دهد که می‌توان آن را از بیرون فراخوانی کرد تا به `CounterCubit` اطلاع دهد که حالت خود را افزایش دهد. هنگامی که `increment` فراخوانی می‌شود، می‌توانیم از طریق ویژگی (getter) `state` به حالت فعلی `Cubit` دسترسی پیدا کرده و با اضافه کردن ۱ به حالت فعلی، یک حالت جدید را `emit` کنیم. :::caution متد `emit` محافظت‌شده (protected) است، به این معنی که تنها باید در داخل یک `Cubit` استفاده شود ::: ### استفاده از یک Cubit حالا می‌توانیم `CounterCubit` که پیاده‌سازی کرده‌ایم را برداشته و از آن استفاده کنیم! #### استفاده پایه در قطعه کد بالا، ابتدا یک نمونه از `CounterCubit` ایجاد می‌کنیم. سپس، حالت فعلی `Cubit` را که همان حالت اولیه است (زیرا هنوز هیچ حالت جدیدی انتشار/emit نشده است)، چاپ می‌کنیم. در مرحله بعد، تابع `increment` را برای راه‌اندازی یک تغییر حالت فراخوانی می‌کنیم. در نهایت، مجدداً حالت `Cubit` را که از `0` به `1` تغییر کرده است، چاپ کرده و تابع `close` را روی `Cubit` فراخوانی می‌کنیم تا استریم داخلی حالت بسته شود. #### استفاده از استریم `Cubit` یک `استریم` را در معرض دید قرار می‌دهد که به ما امکان می‌دهد به‌روزرسانی‌های حالت را به صورت همزمان دریافت کنیم: در قطعه کد بالا، ما در حال مشترک شدن در `CounterCubit` هستیم و هنگام هر تغییر حالت، پرینت را فراخوانی می‌کنیم. سپس، تابع `increment` را فراخوانی می‌کنیم که یک حالت جدید را `emit` خواهد کرد. در پایان، زمانی که دیگر نمی‌خواهیم به‌روزرسانی‌ها را دریافت کنیم، `cancel` را روی `subscription` فراخوانی کرده و `Cubit` را می‌بندیم. :::note `await Future.delayed(Duration.zero)` برای این مثال اضافه شده است تا از لغو فوری اشتراک جلوگیری شود. ::: :::caution فقط تغییرات حالت متعاقب هنگام فراخوانی `listen` روی یک `Cubit` دریافت خواهند شد. ::: ### مشاهده Cubit هنگامی که یک `Cubit` حالت جدیدی را `emit` می‌کند، یک `Change` (تغییر) رخ می‌دهد. ما می‌توانیم تمام تغییرات یک `Cubit` مشخص را با بازنویسی متد `onChange` مشاهده کنیم. سپس می‌توانیم با `Cubit` تعامل داشته باشیم و تمام تغییرات خروجی داده شده در کنسول را مشاهده کنیم. مثال بالا خروجی زیر را خواهد داشت: :::note یک `Change` درست قبل از به‌روزرسانی حالت `Cubit` رخ می‌دهد. یک `Change` شامل `currentState` (حالت فعلی) و `nextState` (حالت بعدی) است. ::: #### BlocObserver یکی از مزایای اضافی استفاده از کتابخانه `bloc` این است که می‌توانیم به تمام `تغییرات` در یک مکان دسترسی داشته باشیم. حتی اگر در این برنامه فقط یک `Cubit` داشته باشیم، در برنامه‌های بزرگ‌تر معمول است که بسیاری از `Cubits` بخش‌های مختلف حالت برنامه را مدیریت کنند. اگر بخواهیم در پاسخ به تمام `تغییرات` کاری انجام دهیم، می‌توانیم به سادگی `BlocObserver` خودمان را ایجاد کنیم :::note تنها کاری که باید انجام دهیم این است که `BlocObserver` را گسترش دهیم و متد `onChange` را بازنویسی کنیم. ::: برای استفاده از `SimpleBlocObserver`، فقط نیاز داریم تابع `main` را کمی تغییر دهیم: قطعه کد بالا خروجی زیر را خواهد داشت: :::note بازنویسی داخلی `onChange` ابتدا فراخوانی می‌شود، که `super.onChange` را فراخوانی می‌کند و `onChange` در `BlocObserver` را مطلع می‌کند. ::: :::tip در `BlocObserver` ما به نمونه `Cubit` علاوه بر خود `Change` دسترسی داریم. ::: ### مدیریت خطا Cubit هر `Cubit` دارای متد `addError` است که می‌توان از آن برای نشان دادن اینکه خطایی رخ داده است استفاده کرد. :::note `onError` می‌تواند در داخل `Cubit` بازنویسی شود تا تمام خطاهای یک `Cubit` خاص را مدیریت کند. ::: `onError` همچنین می‌تواند در `BlocObserver` بازنویسی شود تا تمام خطاهای گزارش‌شده را به صورت جهانی مدیریت کند. اگر همان برنامه را دوباره اجرا کنیم، باید خروجی زیر را ببینیم: ## Bloc یک `Bloc` کلاسی پیشرفته‌تر است که به جای توابع، از `events` برای تحریک تغییرات `state` استفاده می‌کند. `Bloc` همچنین از `BlocBase` ارث‌بری می‌کند، به این معنی که API عمومی مشابهی با `Cubit` دارد. با این حال، به جای فراخوانی یک `function` روی یک `Bloc` و انتشار مستقیم یک `state` جدید، `Blocs` `events` را دریافت می‌کنند و `events` ورودی را به `states` خروجی تبدیل می‌کنند. ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) ### ایجاد یک Bloc ساخت یک `Bloc` مشابه ساخت یک `Cubit` است، به جز اینکه علاوه بر تعریف `state` که مدیریت خواهیم کرد، باید `event` که `Bloc` قادر به پردازش آن خواهد بود را نیز تعریف کنیم. Event ها ورودی های به یک Bloc هستند. آن‌ها معمولاً در پاسخ به تعاملات کاربر مانند فشار دکمه یا رویدادهای چرخه حیات مانند بارگذاری صفحه اضافه می‌شوند. دقیقاً مانند هنگام ساخت `CounterCubit`، باید یک حالت اولیه را با ارسال آن به superclass از طریق `super` مشخص کنیم. ### تغییرات حالت Bloc `Bloc` از ما می‌خواهد که مدیریت کنندگان رویداد(event) را از طریق API `on` ثبت کنیم، بر خلاف توابع در `Cubit`. یک مدیریت کننده رویداد مسئول تبدیل هر event ورودی به صفر یا بیشتر states خروجی است. :::tip یک `EventHandler` به رویداد اضافه‌شده و همچنین یک `Emitter` دسترسی دارد که می‌توان از آن برای انتشار صفر یا بیشتر states در پاسخ به رویداد ورودی استفاده کرد. ::: سپس می‌توانیم `EventHandler` را به‌روزرسانی کنیم تا رویداد `CounterIncrementPressed` را مدیریت کند: در قطعه کد بالا، ما یک `EventHandler` را برای مدیریت تمام رویدادها `CounterIncrementPressed` ثبت کرده‌ایم. برای هر رویداد ورودی `CounterIncrementPressed`، می‌توانیم از طریق getter `state` به حالت فعلی bloc دسترسی پیدا کنیم و `emit(state + 1)` کنیم. :::note از آنجایی که کلاس `Bloc` از `BlocBase` ارث‌بری می‌کند، ما به حالت فعلی bloc در هر زمان از طریق getter `state` دسترسی داریم، دقیقاً مانند `Cubit`. ::: :::caution Blocs هرگز نباید مستقیماً states جدید را `emit` کنند. در عوض، هر تغییر حالت باید در پاسخ به یک رویداد ورودی در داخل یک `EventHandler` خروجی داده شود. ::: :::caution هر دو blocs و cubits حالت های تکراری را نادیده می‌گیرند. اگر `State nextState` را emit کنیم که در آن `state == nextState` باشد، هیچ تغییر حالتی رخ نخواهد داد. ::: ### استفاده از Bloc در این مرحله، می‌توانیم یک نمونه از `CounterBloc` خود ایجاد کنیم و از آن استفاده کنیم! #### استفاده پایه در قطعه کد بالا، ابتدا یک نمونه از `CounterBloc` ایجاد می‌کنیم. سپس حالت فعلی `Bloc` را که حالت اولیه است (زیرا هنوز هیچ حالت جدیدی emit نشده است) چاپ می‌کنیم. در مرحله بعد، رویداد `CounterIncrementPressed` را برای تحریک یک تغییر حالت اضافه می‌کنیم. در نهایت، حالت `Bloc` را دوباره چاپ می‌کنیم که از `0` به `1` تغییر کرده است و `close` را روی `Bloc` فراخوانی می‌کنیم تا استریم داخلی حالت بسته شود. :::note `await Future.delayed(Duration.zero)` اضافه شده است تا اطمینان حاصل شود که منتظر تکرار بعدی event-loop هستیم (اجازه می‌دهد `EventHandler` رویداد را پردازش کند). ::: #### استفاده Stream دقیقاً مانند `Cubit`، یک `Bloc` یک نوع خاصی از `Stream` است، به این معنی که می‌توانیم برای به‌روزرسانی‌های real-time به حالت آن مشترک شویم: در قطعه کد بالا، ما در حال مشترک شدن در `CounterBloc` هستیم و پرینت را روی هر تغییر حالت فراخوانی می‌کنیم. سپس رویداد `CounterIncrementPressed` را اضافه می‌کنیم که `EventHandler` `on` را تحریک می‌کند و یک حالت جدید emit می‌کند. در نهایت، `cancel` را روی subscription فراخوانی می‌کنیم وقتی که دیگر نمی‌خواهیم به‌روزرسانی‌ها را دریافت کنیم و `Bloc` را می‌بندیم. :::note `await Future.delayed(Duration.zero)` برای این مثال اضافه شده است تا از لغو فوری subscription جلوگیری شود. ::: ### مشاهده یک Bloc از آنجایی که `Bloc` از `BlocBase` ارث‌بری می‌کند، می‌توانیم تمام تغییرات حالت برای یک `Bloc` را با استفاده از `onChange` مشاهده کنیم. سپس می‌توانیم `main.dart` را به‌روزرسانی کنیم به: حالا اگر قطعه کد بالا را اجرا کنیم، خروجی به شکل زیر خواهد بود: یکی از عوامل کلیدی تمایز بین `Bloc` و `Cubit` این است که چون `Bloc` event-driven است، ما همچنین می‌توانیم اطلاعاتی درباره آنچه که تغییر حالت را تحریک کرده است، ثبت کنیم. می‌توانیم این کار را با بازنویسی `onTransition` انجام دهیم. تغییر از یک حالت به حالت دیگر یک `Transition` نامیده می‌شود. یک `Transition` شامل حالت فعلی، event، و حالت بعدی است. اگر سپس همان قطعه `main.dart` را از قبل دوباره اجرا کنیم، باید خروجی زیر را ببینیم: :::note `onTransition` قبل از `onChange` فراخوانی می‌شود و شامل event است که تغییر از `currentState` به `nextState` را تحریک کرده است. ::: #### BlocObserver دقیقاً مانند قبل، می‌توانیم `onTransition` را در یک `BlocObserver` سفارشی بازنویسی کنیم تا تمام transitions که از یک مکان رخ می‌دهند را مشاهده کنیم. :::note `onTransition` ابتدا فراخوانی می‌شود (local قبل از global) و سپس `onChange`. ::: ویژگی منحصر به فرد دیگری از نمونه‌های `Bloc` این است که به ما اجازه می‌دهند `onEvent` را بازنویسی کنیم که هر زمان یک رویداد جدید به `Bloc` اضافه شود فراخوانی می‌شود. دقیقاً مانند `onChange` و `onTransition`، `onEvent` می‌تواند local و همچنین global بازنویسی شود. می‌توانیم همان `main.dart` را از قبل اجرا کنیم و باید خروجی زیر را ببینیم: :::note `onEvent` به محض اضافه شدن event فراخوانی می‌شود. `onEvent` local قبل از `onEvent` global در `BlocObserver` فراخوانی می‌شود. ::: ### مدیریت خطا Bloc دقیقاً مانند `Cubit`، هر `Bloc` دارای متدهای `addError` و `onError` است. می‌توانیم نشان دهیم که خطایی رخ داده است با فراخوانی `addError` از هر جایی داخل `Bloc` خود. سپس می‌توانیم به تمام خطاها با بازنویسی `onError` واکنش نشان دهیم، دقیقاً مانند `Cubit`. اگر همان `main.dart` را از قبل دوباره اجرا کنیم، می‌توانیم ببینیم وقتی خطایی گزارش می‌شود چگونه به نظر می‌رسد: :::note `onError` local ابتدا فراخوانی می‌شود و سپس `onError` global در `BlocObserver`. ::: :::note `onError` و `onChange` دقیقاً به همان شکل برای نمونه‌های `Bloc` و `Cubit` کار می‌کنند. ::: :::caution هر استثنای مدیریت نشده‌ای که در داخل یک `EventHandler` رخ دهد نیز به `onError` گزارش می‌شود. ::: ## Cubit در مقابل Bloc حالا که مبانی کلاس‌های `Cubit` و `Bloc` را پوشش داده‌ایم، ممکن است در حال فکر کردن باشید که چه زمانی باید از `Cubit` استفاده کنید و چه زمانی از `Bloc`. ### Cubit مزایای #### سادگی یکی از بزرگترین مزایای استفاده از `Cubit` سادگی است. هنگام ساخت یک `Cubit`، فقط باید `state` و همچنین توابعی که می‌خواهیم برای تغییر حالت در معرض دید قرار دهیم را تعریف کنیم. در مقایسه، هنگام ساخت یک `Bloc`، باید states، events، و پیاده‌سازی `EventHandler` را تعریف کنیم. این باعث می‌شود `Cubit` آسان‌تر درک شود و کد کمتری درگیر باشد. حالا بیایید به دو پیاده‌سازی counter نگاه کنیم: ##### CounterCubit ##### CounterBloc پیاده‌سازی `Cubit` مختصرتر است و به جای تعریف رویدادهای جداگانه، توابع مانند رویدادها عمل می‌کنند. علاوه بر این، هنگام استفاده از `Cubit`، می‌توانیم به سادگی `emit` را از هر جایی فراخوانی کنیم تا یک تغییر حالت تحریک کنیم. ### مزایای Bloc #### قابلیت ردیابی یکی از بزرگترین مزایای استفاده از `Bloc` دانستن توالی تغییرات حالت و همچنین دقیقاً آنچه که آن تغییرات را تحریک کرده است. برای حالتی که برای عملکرد یک برنامه حیاتی است، ممکن است بسیار مفید باشد که از رویکرد event-driven بیشتری استفاده کنیم تا تمام رویدادها علاوه بر تغییرات حالت را ثبت کنیم. یک مورد استفاده رایج ممکن است مدیریت `AuthenticationState` باشد. برای سادگی، بیایید بگوییم می‌توانیم `AuthenticationState` را از طریق یک `enum` نمایش دهیم: می‌تواند دلایل زیادی وجود داشته باشد که چرا حالت برنامه از `authenticated` به `unauthenticated` تغییر کند. برای مثال، کاربر ممکن است دکمه logout را زده باشد و درخواست کرده باشد از برنامه خارج شود. از طرف دیگر، شاید token دسترسی کاربر لغو شده باشد و او به زور logout شده باشد. هنگام استفاده از `Bloc` می‌توانیم به وضوح ردگیری کنیم که چگونه حالت برنامه به یک حالت خاص رسیده است. `Transition` بالا تمام اطلاعاتی را که نیاز داریم برای درک اینکه چرا حالت تغییر کرده است به ما می‌دهد. اگر از `Cubit` برای مدیریت `AuthenticationState` استفاده کرده بودیم، logs ما به شکل زیر می‌بود: این به ما می‌گوید که کاربر logout شده است اما توضیح نمی‌دهد چرا که ممکن است برای دیباگ و درک اینکه چگونه حالت برنامه در طول زمان تغییر می‌کند حیاتی باشد. #### تبدیل رویدادهای پیشرفته بخش دیگری که `Bloc` نسبت به `Cubit` برتری دارد وقتی است که نیاز داریم از عملگرهای reactive مانند `buffer`، `debounceTime`، `throttle` و غیره استفاده کنیم. :::tip برای stream transformers به [`package:stream_transform`](https://pub.dev/packages/stream_transform) و [`package:rxdart`](https://pub.dev/packages/rxdart) نگاه کنید. ::: `Bloc` دارای یک event sink است که به ما اجازه می‌دهد جریان ورودی رویدادها را کنترل و تبدیل کنیم. برای مثال، اگر در حال ساخت یک جستجوی real-time هستیم، احتمالاً می‌خواهیم درخواست‌ها به backend را debounce کنیم تا از rate-limited شدن جلوگیری کنیم و همچنین هزینه/بار روی backend را کاهش دهیم. با `Bloc` می‌توانیم یک `EventTransformer` سفارشی ارائه دهیم تا نحوه پردازش رویدادها ورودی توسط `Bloc` را تغییر دهیم. با کد بالا، می‌توانیم به راحتی رویدادها ورودی را با کد اضافی بسیار کمی debounce کنیم. :::tip برای مجموعه‌ای opinionated از event transformers [`package:bloc_concurrency`](https://pub.dev/packages/bloc_concurrency) را بررسی کنید. ::: اگر مطمئن نیستید از کدام استفاده کنید، با `Cubit` شروع کنید و می‌توانید بعداً در صورت نیاز به `Bloc` refactor یا scale-up کنید. ================================================ FILE: docs/src/content/docs/fa/faqs.mdx ================================================ --- title: پرسش‌های متداول description: پاسخ به پرسش‌های متداول در مورد کتابخانه bloc. --- import StateNotUpdatingGood1Snippet from '~/components/faqs/StateNotUpdatingGood1Snippet.astro'; import StateNotUpdatingGood2Snippet from '~/components/faqs/StateNotUpdatingGood2Snippet.astro'; import StateNotUpdatingGood3Snippet from '~/components/faqs/StateNotUpdatingGood3Snippet.astro'; import StateNotUpdatingBad1Snippet from '~/components/faqs/StateNotUpdatingBad1Snippet.astro'; import StateNotUpdatingBad2Snippet from '~/components/faqs/StateNotUpdatingBad2Snippet.astro'; import StateNotUpdatingBad3Snippet from '~/components/faqs/StateNotUpdatingBad3Snippet.astro'; import EquatableEmitSnippet from '~/components/faqs/EquatableEmitSnippet.astro'; import EquatableBlocTestSnippet from '~/components/faqs/EquatableBlocTestSnippet.astro'; import NoEquatableBlocTestSnippet from '~/components/faqs/NoEquatableBlocTestSnippet.astro'; import SingleStateSnippet from '~/components/faqs/SingleStateSnippet.astro'; import SingleStateUsageSnippet from '~/components/faqs/SingleStateUsageSnippet.astro'; import BlocProviderGood1Snippet from '~/components/faqs/BlocProviderGood1Snippet.astro'; import BlocProviderGood2Snippet from '~/components/faqs/BlocProviderGood2Snippet.astro'; import BlocProviderBad1Snippet from '~/components/faqs/BlocProviderBad1Snippet.astro'; import BlocInternalAddEventSnippet from '~/components/faqs/BlocInternalAddEventSnippet.astro'; import BlocInternalEventSnippet from '~/components/faqs/BlocInternalEventSnippet.astro'; import BlocExternalForEachSnippet from '~/components/faqs/BlocExternalForEachSnippet.astro'; ## حالت به‌روزرسانی نمی‌شود ❔ **پرسش**: من یک حالت را در bloc خود emit می‌کنم اما UI به‌روزرسانی نمی‌شود. چه اشتباهی کرده‌ام؟ 💡 **پاسخ**: اگر از Equatable استفاده می‌کنید، مطمئن شوید که همه ویژگی‌ها را به props getter ارسال کنید. ✅ **خوب** ❌ **بد** علاوه بر این، مطمئن شوید که یک نمونه جدید از حالت را در bloc خود emit می‌کنید. ✅ **خوب** ❌ **بد** :::caution ویژگی‌های `Equatable` همیشه باید کپی شوند نه اینکه تغییر داده شوند. اگر یک کلاس `Equatable` شامل `List` یا `Map` به عنوان ویژگی‌ها باشد، مطمئن شوید از `List.of` یا `Map.of` به ترتیب استفاده کنید تا اطمینان حاصل شود که برابری بر اساس مقادیر ویژگی‌ها ارزیابی می‌شود نه مرجع. ::: ## زمان استفاده از Equatable ❔**پرسش**: چه زمانی باید از Equatable استفاده کنم؟ 💡**پاسخ**: در سناریوی بالا اگر `StateA` از `Equatable` ارث‌بری کند، فقط یک تغییر حالت رخ خواهد داد (emit دوم نادیده گرفته خواهد شد). به طور کلی، باید از `Equatable` استفاده کنید اگر بخواهید کد خود را برای کاهش تعداد rebuildها بهینه کنید. نباید از `Equatable` استفاده کنید اگر بخواهید همان حالت پشت سر هم چندین انتقال را راه‌اندازی کند. علاوه بر این، استفاده از `Equatable` تست blocها را بسیار آسان‌تر می‌کند زیرا می‌توانیم نمونه‌های خاصی از حالت‌های bloc را انتظار داشته باشیم نه اینکه از `Matchers` یا `Predicates` استفاده کنیم. بدون `Equatable` تست بالا شکست خواهد خورد و نیاز به بازنویسی خواهد داشت مانند: ## مدیریت خطاها ❔ **پرسش**: چگونه می‌توانم یک خطا را مدیریت کنم در حالی که هنوز داده‌های قبلی را نشان می‌دهم؟ 💡 **پاسخ**: این سوال کاملا به نحوه مدل‌سازی حالت bloc بستگی دارد. در مواردی که داده‌ها باید حتی در حضور خطا حفظ شوند، از یک کلاس حالت واحد استفاده کنید. این کار اجازه می‌دهد تا ویجت‌ها همزمان به ویژگی‌های `data` و `error` دسترسی داشته باشند و bloc می‌تواند از `state.copyWith` برای حفظ داده‌های قدیمی حتی زمانی که خطایی رخ داده استفاده کند. ## Bloc در برابر Redux ❔ **پرسش**: تفاوت بین Bloc و Redux چیست؟ 💡 **پاسخ**: BLoC یک الگوی طراحی است که توسط قوانین زیر تعریف می‌شود: 1. ورودی و خروجی BLoC جریان‌ها و sinkهای ساده هستند. 2. وابستگی‌ها باید قابل تزریق و مستقل از پلتفرم باشند. 3. هیچ شاخه‌بندی پلتفرم مجاز نیست. 4. پیاده‌سازی می‌تواند هر چیزی باشد تا زمانی که قوانین بالا را دنبال کنید. راهنمایی‌های UI عبارتند از: 1. هر کامپوننت "به اندازه کافی پیچیده" یک BLoC مربوطه دارد. 2. کامپوننت‌ها باید ورودی‌ها را "همانطور که هست" ارسال کنند. 3. کامپوننت‌ها باید خروجی‌ها را تا حد امکان نزدیک به "همانطور که هست" نشان دهند. 4. همه شاخه‌بندی باید بر اساس خروجی‌های boolean ساده BLoC باشد. کتابخانه Bloc الگوی طراحی BLoC را پیاده‌سازی می‌کند و هدف آن انتزاع RxDart برای ساده‌سازی تجربه توسعه‌دهنده است. سه اصل Redux عبارتند از: 1. منبع حقیقت واحد 2. حالت فقط خواندنی است 3. تغییرات با توابع خالص انجام می‌شوند کتابخانه bloc اصل اول را نقض می‌کند؛ با bloc حالت در چندین bloc توزیع می‌شود. علاوه بر این، مفهومی از middleware در bloc وجود ندارد و bloc طراحی شده تا تغییرات حالت async را بسیار آسان کند، اجازه می‌دهد چندین حالت برای یک رویداد emit شود. ## Bloc در برابر Provider ❔ **پرسش**: تفاوت بین Bloc و Provider چیست؟ 💡 **پاسخ**: `provider` برای تزریق وابستگی طراحی شده است (پوششی برای InheritedWidget). هنوز نیاز دارید که نحوه مدیریت حالت خود را مشخص کنید (از طریق `ChangeNotifier`, `Bloc`, `Mobx`, و غیره...). کتابخانه Bloc از `provider` به طور داخلی استفاده می‌کند تا ارائه و دسترسی به blocها در درخت ویجت آسان شود. ## BlocProvider.of() نمی‌تواند Bloc را پیدا کند ❔ **پرسش**: هنگام استفاده از `BlocProvider.of(context)` نمی‌تواند bloc را پیدا کند. چگونه می‌توانم این را برطرف کنم؟ 💡 **پاسخ**: نمی‌توانید به bloc از همان context که در آن ارائه شده دسترسی پیدا کنید بنابراین باید اطمینان حاصل کنید که `BlocProvider.of()` در یک `BuildContext` فرزند فراخوانی شود. ✅ **خوب** ❌ **بد** ## ساختار پروژه ❔ **پرسش**: چگونه باید پروژه خود را ساختاردهی کنم؟ 💡 **پاسخ**: در حالی که واقعاً پاسخ درست/غلطی برای این پرسش وجود ندارد، برخی مراجع توصیه‌شده عبارتند از - [I/O Photobooth](https://github.com/flutter/photobooth) - [I/O Pinball](https://github.com/flutter/pinball) - [Flutter News Toolkit](https://github.com/flutter/news_toolkit) مهم‌ترین چیز داشتن یک ساختار پروژه **ثابت** و **عمدی** است. ## افزودن رویدادها در داخل یک Bloc ❔ **پرسش**: آیا افزودن رویدادها در داخل یک bloc خوب است؟ 💡 **پاسخ**: در اکثر موارد، رویدادها باید از خارج اضافه شوند اما در برخی موارد انتخابی، ممکن است افزودن رویدادها از داخل منطقی باشد. شایع‌ترین موقعیت که رویدادهای داخلی استفاده می‌شوند، زمانی است که تغییرات حالت باید در پاسخ به به‌روزرسانی‌های real-time از یک repository رخ دهند. در این موقعیت‌ها، repository محرک تغییر حالت است نه یک رویداد خارجی مانند ضربه زدن به دکمه. در مثال زیر، حالت `MyBloc` به کاربر فعلی بستگی دارد که از طریق `Stream` از `UserRepository` نمایش داده می‌شود. `MyBloc` برای تغییرات در کاربر فعلی گوش می‌دهد و هر زمان که یک کاربر از جریان کاربر emit شود، یک رویداد داخلی `_UserChanged` اضافه می‌کند. با افزودن یک رویداد داخلی، همچنین می‌توانیم یک `transformer` سفارشی برای رویداد مشخص کنیم تا تعیین کنیم چگونه چندین رویداد `_UserChanged` پردازش خواهند شد -- به طور پیش‌فرض آنها همزمان پردازش خواهند شد. بسیار توصیه می‌شود که رویدادهای داخلی خصوصی باشند. این یک راه صریح برای نشان دادن است که یک رویداد خاص فقط در داخل bloc استفاده می‌شود و از دانستن کامپوننت‌های خارجی درباره رویداد جلوگیری می‌کند. ما می‌توانیم به طور جایگزین یک رویداد خارجی `Started` تعریف کنیم و از API `emit.forEach` برای مدیریت واکنش به به‌روزرسانی‌های real-time کاربر استفاده کنیم: مزایای رویکرد بالا عبارتند از: - نیازی به رویداد داخلی `_UserChanged` نداریم - نیازی به مدیریت دستی `StreamSubscription` نداریم - کنترل کامل بر زمانی که bloc به جریان به‌روزرسانی‌های کاربر subscribe می‌کند داریم معایب رویکرد بالا عبارتند از: - نمی‌توانیم به راحتی subscription را `pause` یا `resume` کنیم - نیاز داریم که یک رویداد عمومی `Started` را نمایش دهیم که باید از خارج اضافه شود - نمی‌توانیم از یک `transformer` سفارشی برای تنظیم نحوه واکنش به به‌روزرسانی‌های کاربر استفاده کنیم ## نمایش متدهای عمومی ❔ **پرسش**: آیا نمایش متدهای عمومی در نمونه‌های bloc و cubit من خوب است؟ 💡 **پاسخ** وقتی یک Cubit ایجاد می‌کنید، توصیه می‌شود فقط متدهای عمومی را برای ایجاد تغییر در حالت در دسترس قرار دهید. در نتیجه، به طور کلی همه متدهای عمومی در یک نمونه cubit باید `void` یا `Future` را برگردانند. هنگام ایجاد یک bloc، توصیه می‌شود که از نمایش هر متد عمومی سفارشی اجتناب کنید و در عوض با فراخوانی `add` bloc را از رویدادها مطلع کنید. ================================================ FILE: docs/src/content/docs/fa/flutter-bloc-concepts.mdx ================================================ --- title: مفاهیم بلوک فلاتر description: An overview of the core concepts for package:flutter_bloc. sidebar: order: 2 --- import BlocBuilderSnippet from '~/components/concepts/flutter-bloc/BlocBuilderSnippet.astro'; import BlocBuilderExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocBuilderExplicitBlocSnippet.astro'; import BlocBuilderConditionSnippet from '~/components/concepts/flutter-bloc/BlocBuilderConditionSnippet.astro'; import BlocSelectorSnippet from '~/components/concepts/flutter-bloc/BlocSelectorSnippet.astro'; import BlocProviderSnippet from '~/components/concepts/flutter-bloc/BlocProviderSnippet.astro'; import BlocProviderEagerSnippet from '~/components/concepts/flutter-bloc/BlocProviderEagerSnippet.astro'; import BlocProviderValueSnippet from '~/components/concepts/flutter-bloc/BlocProviderValueSnippet.astro'; import BlocProviderLookupSnippet from '~/components/concepts/flutter-bloc/BlocProviderLookupSnippet.astro'; import NestedBlocProviderSnippet from '~/components/concepts/flutter-bloc/NestedBlocProviderSnippet.astro'; import MultiBlocProviderSnippet from '~/components/concepts/flutter-bloc/MultiBlocProviderSnippet.astro'; import BlocListenerSnippet from '~/components/concepts/flutter-bloc/BlocListenerSnippet.astro'; import BlocListenerExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocListenerExplicitBlocSnippet.astro'; import BlocListenerConditionSnippet from '~/components/concepts/flutter-bloc/BlocListenerConditionSnippet.astro'; import NestedBlocListenerSnippet from '~/components/concepts/flutter-bloc/NestedBlocListenerSnippet.astro'; import MultiBlocListenerSnippet from '~/components/concepts/flutter-bloc/MultiBlocListenerSnippet.astro'; import BlocConsumerSnippet from '~/components/concepts/flutter-bloc/BlocConsumerSnippet.astro'; import BlocConsumerConditionSnippet from '~/components/concepts/flutter-bloc/BlocConsumerConditionSnippet.astro'; import RepositoryProviderSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderSnippet.astro'; import RepositoryProviderLookupSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderLookupSnippet.astro'; import RepositoryProviderDisposeSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderDisposeSnippet.astro'; import NestedRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/NestedRepositoryProviderSnippet.astro'; import MultiRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/MultiRepositoryProviderSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/flutter-bloc/CounterBlocSnippet.astro'; import CounterMainSnippet from '~/components/concepts/flutter-bloc/CounterMainSnippet.astro'; import CounterPageSnippet from '~/components/concepts/flutter-bloc/CounterPageSnippet.astro'; import WeatherRepositorySnippet from '~/components/concepts/flutter-bloc/WeatherRepositorySnippet.astro'; import WeatherMainSnippet from '~/components/concepts/flutter-bloc/WeatherMainSnippet.astro'; import WeatherAppSnippet from '~/components/concepts/flutter-bloc/WeatherAppSnippet.astro'; import WeatherPageSnippet from '~/components/concepts/flutter-bloc/WeatherPageSnippet.astro'; :::note لطفاً اطمینان حاصل کنید که بخش‌های زیر را با دقت مطالعه کنید قبل از کار با [`package:flutter_bloc`](https://pub.dev/packages/flutter_bloc). ::: :::note تمام ویجت‌های منتشر شده توسط پکیج `flutter_bloc` با نمونه‌های `Cubit` و `Bloc` یکپارچه می‌شوند. ::: ## ویجت‌های Bloc ### BlocBuilder **BlocBuilder** یک ویجت فلاتر است که نیاز به یک `Bloc` و یک تابع `builder` دارد. `BlocBuilder` ساخت ویجت را در پاسخ به حالت‌های جدید مدیریت می‌کند. `BlocBuilder` بسیار شبیه به `StreamBuilder` است اما API ساده‌تری دارد تا مقدار کد اضافی را کاهش دهد. تابع `builder` ممکن است چندین بار فراخوانی شود و باید یک [تابع خالص](https://en.wikipedia.org/wiki/Pure_function) باشد که یک ویجت را در پاسخ به حالت برمی‌گرداند. اگر می‌خواهید در پاسخ به تغییرات حالت کاری انجام دهید مانند ناوبری، نمایش دیالوگ و غیره، به `BlocListener` نگاه کنید... اگر پارامتر `bloc` حذف شود، `BlocBuilder` به طور خودکار جستجویی با استفاده از `BlocProvider` و `BuildContext` فعلی انجام می‌دهد. فقط در صورتی bloc را مشخص کنید که بخواهید یک bloc ارائه دهید که به یک ویجت واحد محدود شود و از طریق `BlocProvider` والد و `BuildContext` فعلی قابل دسترسی نباشد. برای کنترل دقیق‌تر بر زمانی که تابع `builder` فراخوانی می‌شود، می‌توان یک پارامتر اختیاری `buildWhen` ارائه داد. `buildWhen` حالت قبلی bloc و حالت فعلی bloc را دریافت می‌کند و یک مقدار بولی برمی‌گرداند. اگر `buildWhen` مقدار true برگرداند، `builder` با `state` فراخوانی می‌شود و ویجت دوباره ساخته می‌شود. اگر `buildWhen` مقدار false برگرداند، `builder` با `state` فراخوانی نمی‌شود و دوباره‌سازی رخ نخواهد داد. ### BlocSelector **BlocSelector** یک ویجت فلاتر است که مشابه `BlocBuilder` است اما به توسعه‌دهندگان اجازه می‌دهد تا به‌روزرسانی‌ها را با انتخاب یک مقدار جدید بر اساس حالت فعلی bloc فیلتر کنند. اگر مقدار یا نمونه انتخاب شده تغییر نکند, از بروزرسانی ویجت یا ساخت های جدید جلوگیری میشود. مقدار انتخاب‌شده باید تغییرناپذیر باشد تا `BlocSelector` بتواند به طور دقیق تعیین کند که آیا `builder` دوباره فراخوانی شود یا نه. اگر پارامتر `bloc` حذف شود، `BlocSelector` به طور خودکار جستجویی با استفاده از `BlocProvider` و `BuildContext` فعلی انجام می‌دهد. ### BlocProvider **BlocProvider** یک ویجت فلاتر است که یک bloc را از طریق `BlocProvider.of(context)` به فرزندان خود ارائه می‌دهد. این ویجت به عنوان یک ویجت تزریق وابستگی (DI) استفاده می‌شود تا یک نمونه واحد از یک bloc بتواند به چندین ویجت در یک زیردرخت ارائه شود. در بیشتر موارد، `BlocProvider` باید برای ایجاد bloc‌های جدید استفاده شود که برای بقیه زیردرخت در دسترس خواهند بود. در این حالت، از آنجایی که `BlocProvider` مسئول ایجاد bloc است، به طور خودکار بسته شدن bloc را مدیریت خواهد کرد. به طور پیش‌فرض، `BlocProvider` bloc را به صورت تنبل ایجاد می‌کند، به این معنی که `create` زمانی اجرا می‌شود که bloc از طریق `BlocProvider.of(context)` جستجو شود. برای لغو این رفتار و اجبار به اجرای فوری `create`، می‌توان `lazy` را روی `false` تنظیم کرد. در برخی موارد، `BlocProvider` می‌تواند برای ارائه یک bloc موجود به بخشی جدید از درخت ویجت استفاده شود. این معمولاً زمانی استفاده می‌شود که یک bloc موجود نیاز به در دسترس بودن برای یک مسیر جدید داشته باشد. در این حالت، `BlocProvider` به طور خودکار bloc را نمیبندد زیرا آن را ایجاد نکرده است. سپس از ChildA یا ScreenA می‌توانیم BlocA را بازیابی کنیم, با استفاده از: ### MultiBlocProvider **MultiBlocProvider** یک ویجت فلاتر است که چندین ویجت `BlocProvider` را به یک ویجت ادغام می‌کند. `MultiBlocProvider` خوانایی را بهبود می‌بخشد و نیاز به تودرتو کردن چندین `BlocProvider` را از بین می‌برد. با استفاده از `MultiBlocProvider` می‌توانیم از: برویم به: :::caution زمانی که یک `BlocProvider` در زمینه یک `MultiBlocProvider` تعریف شود، هر `child` نادیده گرفته می‌شود. ::: ### BlocListener **BlocListener** یک ویجت فلاتر است که یک `BlocWidgetListener` و یک `Bloc` اختیاری دریافت می‌کند و `listener` را در پاسخ به تغییرات حالت در bloc فراخوانی می‌کند. باید برای عملکردهایی استفاده شود که نیاز به رخ دادن یک بار در هر تغییر حالت دارند مانند ناوبری، نمایش `SnackBar`، نمایش `Dialog` و غیره... `listener` فقط یک بار برای هر تغییر حالت فراخوانی می‌شود (**نه** شامل حالت اولیه) بر خلاف `builder` در `BlocBuilder` و یک تابع `void` است. اگر پارامتر `bloc` حذف شود، `BlocListener` به طور خودکار جستجویی با استفاده از `BlocProvider` و `BuildContext` فعلی انجام می‌دهد. فقط در صورتی bloc را مشخص کنید که بخواهید یک bloc ارائه دهید که از طریق `BlocProvider` والد و `BuildContext` فعلی قابل دسترسی نباشد. برای کنترل دقیق‌تر بر زمانی که تابع `listener` فراخوانی می‌شود، می‌توان یک پارامتر اختیاری `listenWhen` ارائه داد. `listenWhen` حالت قبلی bloc و حالت فعلی bloc را دریافت می‌کند و یک مقدار بولی برمی‌گرداند. اگر `listenWhen` مقدار true برگرداند، `listener` با `state` فراخوانی می‌شود. اگر `listenWhen` مقدار false برگرداند، `listener` با `state` فراخوانی نخواهد شد. ### MultiBlocListener **MultiBlocListener** یک ویجت فلاتر است که چندین ویجت `BlocListener` را به یک ویجت ادغام می‌کند. `MultiBlocListener` خوانایی را بهبود می‌بخشد و نیاز به تودرتو کردن چندین `BlocListener` را از بین می‌برد. با استفاده از `MultiBlocListener` می‌توانیم از: برسیم, به: :::caution زمانی که یک `BlocListener` در زمینه یک `MultiBlocListener` تعریف شود، هر `child` نادیده گرفته می‌شود. ::: ### BlocConsumer **BlocConsumer** یک `builder` و `listener` را برای واکنش به حالت‌های جدید نمایش می‌دهد. `BlocConsumer` مشابه یک `BlocListener` و `BlocBuilder` تودرتو است اما مقدار کد اضافی مورد نیاز را کاهش می‌دهد. `BlocConsumer` باید فقط زمانی استفاده شود که لازم باشد هم UI دوباره ساخته شود و هم واکنش‌های دیگری به تغییرات حالت در `bloc` اجرا شوند. `BlocConsumer` یک `BlocWidgetBuilder` و `BlocWidgetListener` مورد نیاز و یک `bloc`، `BlocBuilderCondition` و `BlocListenerCondition` اختیاری دریافت می‌کند. اگر پارامتر `bloc` حذف شود، `BlocConsumer` به طور خودکار جستجویی با استفاده از `BlocProvider` و `BuildContext` فعلی انجام می‌دهد. یک `listenWhen` و `buildWhen` اختیاری می‌توانند برای کنترل دانه‌ای‌تر بر زمانی که `listener` و `builder` فراخوانی شوند پیاده‌سازی شوند. `listenWhen` و `buildWhen` در هر تغییر `state` `bloc` فراخوانی خواهند شد. هر کدام حالت قبلی و حالت فعلی را دریافت می‌کنند و باید یک `bool` برگردانند که تعیین کند آیا تابع `builder` و/یا `listener` فراخوانی شود یا نه. حالت قبلی به حالت `bloc` هنگام مقداردهی اولیه `BlocConsumer` مقداردهی خواهد شد. `listenWhen` و `buildWhen` اختیاری هستند و اگر پیاده‌سازی نشوند، به طور پیش‌فرض `true` خواهند بود. ### RepositoryProvider **RepositoryProvider** یک ویجت فلاتر است که یک repository را از طریق `RepositoryProvider.of(context)` به فرزندان خود ارائه می‌دهد. این ویجت به عنوان یک ویجت تزریق وابستگی (DI) استفاده می‌شود تا یک نمونه واحد از یک repository بتواند به چندین ویجت در یک زیردرخت ارائه شود. `BlocProvider` باید برای ارائه bloc‌ها استفاده شود در حالی که `RepositoryProvider` فقط باید برای repository‌ها استفاده شود. سپس از `ChildA` می‌توانیم نمونه `Repository` را با استفاده از: بازیابی کنیم. Repository‌هایی که منابع را مدیریت می‌کنند که باید dispose شوند می‌توانند این کار را از طریق callback `dispose` انجام دهند: ### MultiRepositoryProvider **MultiRepositoryProvider** یک ویجت فلاتر است که چندین ویجت `RepositoryProvider` را به یک ویجت ادغام می‌کند. `MultiRepositoryProvider` خوانایی را بهبود می‌بخشد و نیاز به تودرتو کردن چندین `RepositoryProvider` را از بین می‌برد. با استفاده از `MultiRepositoryProvider` می‌توانیم از: برسیم, به: :::caution زمانی که یک `RepositoryProvider` در زمینه یک `MultiRepositoryProvider` تعریف شود، هر `child` نادیده گرفته می‌شود. ::: ## کاربرد BlocProvider بیایید نگاهی بیندازیم به اینکه چگونه از `BlocProvider` برای ارائه یک `CounterBloc` به یک `CounterPage` استفاده کنیم و با `BlocBuilder` به تغییرات حالت واکنش نشان دهیم. در این نقطه، ما با موفقیت لایه نمایشی خود را از لایه منطق کسب‌وکار جدا کرده‌ایم. توجه کنید که ویجت `CounterPage` هیچ اطلاعی از اینکه هنگام فشار دادن دکمه‌ها توسط کاربر چه اتفاقی می‌افتد ندارد. ویجت به سادگی به `CounterBloc` می‌گوید که کاربر دکمه افزایش یا کاهش را فشار داده است. ## کاربرد RepositoryProvider ما قصد داریم نگاهی به نحوه استفاده از `RepositoryProvider` در زمینه مثال [`flutter_weather`][flutter_weather_link] بیندازیم. در `main.dart` ما، `runApp` را با ویجت `WeatherApp` خود فراخوانی می‌کنیم. ما نمونه `WeatherRepository` خود را از طریق `RepositoryProvider` به درخت ویجت تزریق خواهیم کرد. هنگام نمونه‌سازی یک bloc، می‌توانیم به نمونه یک repository از طریق `context.read` دسترسی پیدا کنیم و repository را از طریق سازنده به bloc تزریق کنیم. :::tip اگر بیش از یک repository دارید، می‌توانید از `MultiRepositoryProvider` برای ارائه چندین نمونه repository به زیردرخت استفاده کنید. ::: :::note از callback `dispose` برای مدیریت آزادسازی هر منبع هنگام unmount شدن `RepositoryProvider` استفاده کنید. ::: [flutter_weather_link]: https://github.com/felangel/bloc/blob/master/examples/flutter_weather ## Extension Methods [متدهای توسعه](https://dart.dev/guides/language/extension-methods)، که در Dart 2.7 معرفی شدند، راهی برای افزودن عملکرد به کتابخانه‌های موجود هستند. در این بخش، نگاهی به متدهای توسعه موجود در `package:flutter_bloc` و نحوه استفاده از آنها خواهیم داشت. `flutter_bloc` وابستگی به [package:provider](https://pub.dev/packages/provider) دارد که استفاده از [`InheritedWidget`](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html) را ساده می‌کند. داخل پکیج `package:flutter_bloc` از `package:provider` برای پیاده‌سازی ویجت‌های `BlocProvider`، `MultiBlocProvider`، `RepositoryProvider` و `MultiRepositoryProvider` استفاده می‌کند. `package:flutter_bloc` توسعه‌های `ReadContext`، `WatchContext` و `SelectContext` را از `package:provider` صادر می‌کند. :::note درباره [`package:provider`](https://pub.dev/packages/provider) بیشتر بیاموزید. ::: ### context.read `context.read()` نزدیک‌ترین نمونه اجداد از نوع `T` را جستجو می‌کند و عملکردی معادل `BlocProvider.of(context)` دارد. `context.read` معمولاً برای بازیابی نمونه یک bloc به منظور افزودن یک رویداد در callback‌های `onPressed` استفاده می‌شود. :::note `context.read()` به `T` گوش نمی‌دهد -- اگر شیء ارائه‌شده از نوع `T` تغییر کند، `context.read` باعث دوباره‌سازی ویجت نخواهد شد. ::: #### کاربرد ✅ **انجام دهید:** از `context.read` برای افزودن رویدادها در callback‌ها استفاده کنید. ```dart onPressed() { context.read().add(CounterIncrementPressed()), } ``` ❌ **پرهیز کنید:** استفاده از `context.read` برای بازیابی حالت در یک متد `build`. ```dart @override Widget build(BuildContext context) { final state = context.read().state; return Text('$state'); } ``` مثال بالا مستعد خطا است زیرا ویجت Text دوباره ساخته نخواهد شد اگر حالت bloc تغییر کند. :::caution به جای آن از `BlocBuilder` یا `context.watch` استفاده کنید تا در پاسخ به تغییرات حالت دوباره ساخته شوند. ::: ### context.watch مانند `context.read()`، `context.watch`() نزدیک‌ترین نمونه اجداد از نوع `T` را ارائه می‌دهد، اما همچنین به تغییرات در نمونه گوش می‌دهد. عملکردی معادل `BlocProvider.of(context, listen: true)` دارد. اگر شیء ارائه‌شده از نوع `T` تغییر کند، `context.watch` باعث دوباره‌سازی خواهد شد :::caution `context.watch` فقط در متد `build` یک `StatelessWidget` یا کلاس `State` قابل دسترسی است. ::: #### کاربرد ✅ **انجام دهید:** از `BlocBuilder` به جای `context.watch` برای تعیین محدوده دوباره‌سازی‌ها استفاده کنید. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (context, state) { // Whenever the state changes, only the Text is rebuilt. return Text(state.value); }, ), ), ); } ``` به طور جایگزین، از `Builder` برای تعیین محدوده دوباره‌سازی‌ها استفاده کنید. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Whenever the state changes, only the Text is rebuilt. final state = context.watch().state; return Text(state.value); }, ), ), ); } ``` ✅ **انجام دهید:** از `Builder` و `context.watch` به عنوان `MultiBlocBuilder` استفاده کنید. ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // return a Widget which depends on the state of BlocA, BlocB, and BlocC } ); ``` ❌ **پرهیز کنید:** استفاده از `context.watch` وقتی ویجت والد در متد `build` به حالت وابسته نیست. ```dart @override Widget build(BuildContext context) { // Whenever the state changes, the MaterialApp is rebuilt // even though it is only used in the Text widget. final state = context.watch().state; return MaterialApp( home: Scaffold( body: Text(state.value), ), ); } ``` :::caution استفاده از `context.watch` در ریشه متد `build` باعث می‌شود کل ویجت دوباره ساخته شود وقتی حالت bloc تغییر کند. ::: ### context.select مانند `context.watch()`، `context.select(R function(T value))` نزدیک‌ترین نمونه اجداد از نوع `T` را ارائه می‌دهد و به تغییرات در `T` گوش می‌دهد. بر خلاف `context.watch`، `context.select` به شما اجازه می‌دهد تا به تغییرات در بخشی کوچک‌تر از حالت گوش دهید. ```dart Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); } ``` بالا فقط ویجت را دوباره می‌سازد وقتی ویژگی `name` از حالت `ProfileBloc` تغییر کند. #### کاربرد ✅ **انجام دهید:** از `BlocSelector` به جای `context.select` برای تعیین محدوده دوباره‌سازی‌ها استفاده کنید. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector( selector: (state) => state.name, builder: (context, name) { // Whenever the state.name changes, only the Text is rebuilt. return Text(name); }, ), ), ); } ``` به طور جایگزین، از `Builder` برای تعیین محدوده دوباره‌سازی‌ها استفاده کنید. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Whenever state.name changes, only the Text is rebuilt. final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), ); } ``` ❌ **پرهیز کنید:** استفاده از `context.select` وقتی ویجت والد در متد build به حالت وابسته نیست. ```dart @override Widget build(BuildContext context) { // Whenever the state.value changes, the MaterialApp is rebuilt // even though it is only used in the Text widget. final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), ); } ``` :::caution استفاده از `context.select` در ریشه متد `build` باعث می‌شود کل ویجت دوباره ساخته شود وقتی انتخاب تغییر کند. ::: ================================================ FILE: docs/src/content/docs/fa/getting-started.mdx ================================================ --- title: شروع شدن description: همه چیزی که برای شروع ساختن با استفاده از Bloc نیاز دارید. --- import InstallationTabs from '~/components/getting-started/InstallationTabs.astro'; import ImportTabs from '~/components/getting-started/ImportTabs.astro'; ## بسته‌ها اکوسیستم Bloc از بسته های متعددی تشکیل شده است که در زیر فهرست شده اند: | بسته | توصیف | لینک | | ------------------------------------------------------------------------------------------ | ------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | اجزای AngularDart | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | API‌های اصلی Dart | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | تبدیل‌کننده‌های رویداد | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | Custom Linter | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | API های تست | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | Command-line Tools | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | ویجت ها فلاتر | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | پشتیبانی از حافظه پنهان/ماندگاری | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | پشتیبانی از واگَرد/اَزنو (Undo/Redo) | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | ## نصب :::note برای شروع استفاده از Bloc، باید [SDK دارت](https://dart.dev/get-dart) را در دستگاه خود نصب کنید. ::: ## وارد کردن (Imports) حالا که با موفقیت bloc را نصب کردیم، می‌توانیم `main.dart` خود را ایجاد کنیم و بسته `bloc` مربوطه را وارد کنیم. ================================================ FILE: docs/src/content/docs/fa/index.mdx ================================================ --- template: splash title: Bloc State Management Library description: Official documentation for the bloc state management library. Support for Dart, Flutter, and AngularDart. Includes examples and tutorials. banner: content: | ✨ از فروشگاه Bloc دیدن کنید✨ editUrl: false lastUpdated: false hero: title: Bloc v9.2.0 tagline: یک کتابخانه مدیریت وضعیت قابل پیش بینی برای دارت. image: alt: Bloc logo file: ~/assets/bloc.svg actions: - text: شروع کنید link: /fa/getting-started/ variant: primary icon: rocket - text: نمایش در گیت‌هاب link: https://github.com/felangel/bloc icon: github variant: secondary --- import { CardGrid } from '@astrojs/starlight/components'; import SponsorsGrid from '~/components/landing/SponsorsGrid.astro'; import Card from '~/components/landing/Card.astro'; import ListCard from '~/components/landing/ListCard.astro'; import SplitCard from '~/components/landing/SplitCard.astro'; import Discord from '~/components/landing/Discord.astro';
```sh # Bloc را به پروژه خود اضافه کنید. dart pub add bloc ``` [راهنمای شروع کار](/fa/getting-started) ما, دستورالعمل‌های گام به گامی را برای شروع استفاده از Bloc در عرض چند دقیقه ارائه می‌دهد. [آموزش های رسمی](/fa/tutorials/flutter-counter) را تکمیل کنید تا بهترین روش ها (Best practices) را بیاموزید و انواع برنامه های مختلف را با پشتیبانی Bloc بسازید. [برنامه های نمونه](https://github.com/felangel/bloc/tree/master/examples) با کیفیت بالا و کاملاً آزمایش شده مانند شمارنده، تایمر، لیست بی نهایت، آب و هوا، انجام کار و موارد دیگر را بررسی کنید! - [چرا Bloc؟](/fa/why-bloc) - [مفاهیم اصلی](/fa/bloc-concepts) - [معماری](/fa/architecture) - [تست کردن](/fa/testing) - [قراردادهای نامگذاری](/fa/naming-conventions) - [FAQs](/fa/faqs) - [یکپارچگی با VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) - [یکپارچگی با IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) - [Neovim Integration](https://github.com/wa11breaker/flutter-bloc.nvim) - [یکپارچگی با ابزار خط فرمان Mason CLI](https://github.com/felangel/bloc/blob/master/bricks/README.md) - [قالب‌های سفارشی](https://brickhub.dev/search?q=bloc) - [ابزارهای توسعه‌دهندگان](https://github.com/felangel/bloc/blob/master/packages/bloc_tools/README.md) ================================================ FILE: docs/src/content/docs/fa/lint/configuration.mdx ================================================ --- title: پیکربندی لینتر description: پیکربندی لینتر bloc. sidebar: order: 3 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import BlocLintBasicAnalysisOptionsSnippet from '~/components/lint/BlocLintBasicAnalysisOptionsSnippet.astro'; import RunBlocLintInCurrentDirectorySnippet from '~/components/lint/RunBlocLintInCurrentDirectorySnippet.astro'; import RunBlocLintInSrcTestSnippet from '~/components/lint/RunBlocLintInSrcTestSnippet.astro'; import AvoidFlutterImportsWarningSnippet from '~/components/lint/ImportFlutterWarningSnippet.mdx'; import RunBlocLintCounterCubitSnippet from '~/components/lint/RunBlocLintCounterCubitSnippet.astro'; import AvoidFlutterImportsWarningOutputSnippet from '~/components/lint/ImportFlutterWarningOutputSnippet.astro'; به‌طور پیش‌فرض، لینتر bloc هیچ تشخیصی گزارش نمی‌دهد مگر اینکه گزینه‌های تحلیل (analysis options) پروژه را به‌طور صریح پیکربندی کرده باشید. برای شروع، یک فایل `analysis_options.yaml` در ریشهٔ پروژه ایجاد کنید یا فایل موجود را ویرایش نمایید و فهرستی از قوانین را زیر کلید سطح‌بالای `bloc` قرار دهید: لینتر را با دستور زیر در ترمینال اجرا کنید: دستور بالا تمام فایل‌ها در پوشهٔ جاری و زیرپوشه‌های آن را تحلیل خواهد کرد، اما می‌توانید با ارسال آرگومان‌های خط فرمان، تنها فایل‌ها یا پوشه‌های خاصی را هم نیز لینت کنید: دستور بالا تمام کدهای موجود در پوشه‌های `src` و `test` را تحلیل خواهد کرد. اگر قانون `avoid_flutter_imports` فعال باشد، هر فایل bloc یا cubit که شامل یک import مربوط به فلاتر باشد به‌صورت هشدار گزارش خواهد شد: می‌توانید این هشدار را با اجرای دستور `bloc lint` مشاهده کنید: خروجی باید چیزی مانند زیر باشد: :::note در اینجا فهرست تمام قوانین لینت پشتیبانی‌شده قرار دارد: ::: ================================================ FILE: docs/src/content/docs/fa/lint/customizing-rules.mdx ================================================ --- title: سفارشی‌سازی قواعد لینت description: سفارشی‌سازی قواعد لینتر bloc sidebar: order: 4 --- import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import BlocLintEnablingRulesSnippet from '~/components/lint/BlocLintEnablingRulesSnippet.astro'; import BlocLintDisablingRulesSnippet from '~/components/lint/BlocLintDisablingRulesSnippet.astro'; import BlocLintChangingSeveritySnippet from '~/components/lint/BlocLintChangingSeveritySnippet.astro'; import ImportFlutterInfoSnippet from '~/components/lint/ImportFlutterInfoSnippet.mdx'; import ImportFlutterInfoOutputSnippet from '~/components/lint/ImportFlutterInfoOutputSnippet.astro'; import BlocLintExcludingFilesSnippet from '~/components/lint/BlocLintExcludingFilesSnippet.astro'; import BlocLintIgnoreForLineSnippet from '~/components/lint/BlocLintIgnoreForLineSnippet.astro'; import BlocLintIgnoreForFileSnippet from '~/components/lint/BlocLintIgnoreForFileSnippet.astro'; می‌توانید رفتار لینتر bloc را با تغییر سطح شدت (severity) قواعد، فعال یا غیرفعال‌سازی قواعد به‌صورت تکی، و یا استثنا کردن فایل‌ها از تحلیل ایستا، شخصی‌سازی کنید. ## فعال و غیرفعال کردن قواعد لینتر bloc مجموعه‌ای در حال رشد از قواعد لینت را پشتیبانی می‌کند. توجه کنید که قواعد لینت لزوماً با هم سازگار نیستند. برای مثال بعضی از توسعه‌دهندگان ممکن است ترجیح دهند از بلوک‌ها (blocs) استفاده کنند (`prefer_bloc`) در حالی که دیگران ممکن است cubit را ترجیح دهند (`prefer_cubit`). :::note برخلاف تحلیل ایستا، قواعد لینت ممکن است مثبت کاذب (false positive) تولید کنند. در صورت مشاهدهٔ چنین مواردی یا هر مشکل دیگر، لطفاً با باز کردن یک [issue](https://github.com/felangel/bloc/issues/new/choose) ما را مطلع کنید. ::: ### فعال‌سازی قواعد پیشنهادی کتابخانهٔ bloc مجموعه‌ای از قواعد پیشنهادی لینت را در بستهٔ [`bloc_lint`](https://pub.dev/packages/bloc_lint) ارائه می‌دهد. برای فعال‌سازی مجموعهٔ پیشنهادی، پکیج `bloc_lint` را به‌عنوان یک وابستگی dev اضافه کنید: سپس `analysis_options.yaml` خود را ویرایش کنید تا مجموعهٔ قواعد را وارد کنید: :::note وقتی نسخهٔ جدیدی از `bloc_lint` منتشر شود، ممکن است کدی که قبلاً از تحلیل ایستا عبور می‌کرد اکنون با قواعد جدید مشکل پیدا کند. توصیه می‌کنیم کد خود را برای سازگاری با قواعد جدید به‌روزرسانی کنید؛ یا در صورت نیاز می‌توانید به صورت اختیاری برخی قواعد را فعال یا غیرفعال کنید. ::: ### فعال‌سازی قواعد به‌صورت تکی برای فعال‌سازی قواعد به‌صورت تکی، کلید `bloc:` را به‌عنوان کلید سطح بالا در فایل `analysis_options.yaml` اضافه کنید و `rules:` را به‌عنوان کلید سطح دوم قرار دهید. در خطوط بعدی، قواعد موردنظر را به‌صورت یک لیست YAML (با پیشوند خط تیره) مشخص کنید. برای مثال: ### غیرفعال کردن قواعد به‌صورت تکی اگر مجموعهٔ قواعد موجودی مانند مجموعهٔ `recommended` را وارد کرده‌اید، ممکن است بخواهید یک یا چند قاعدهٔ واردشده را غیرفعال کنید. غیرفعال‌سازی قواعد مشابه فعال‌سازی است، اما به‌جای لیست از یک نقشهٔ YAML استفاده می‌شود. برای مثال، نمونهٔ زیر مجموعهٔ قواعد پیشنهادی را (recommended) وارد می‌کند اما قاعدهٔ `avoid_public_bloc_methods` را حذف می‌کند و در عوض قاعدهٔ `prefer_bloc` را فعال می‌نماید: ## سفارشی‌سازی شدت قواعد می‌توانید شدت هر قاعده را به این صورت تغییر دهید: در این حالت همان قاعدهٔ لینت با شدت `info` به‌جای `warning` گزارش خواهد شد: خروجی دستور `bloc lint` باید چیزی شبیه زیر باشد: گزینه‌های سطح شدتِ پشتیبانی‌شده عبارت‌اند از: | شدت | توضیحات | | --------- | --------------------------------------------------------- | | `error` | نشان می‌دهد که این الگو اجازه‌پذیر نیست. | | `warning` | نشان می‌دهد که الگو مشکوک است اما قابل‌قبول است. | | `info` | اطلاعاتی به کاربر می‌دهد اما مشکل محسوب نمی‌شود. | | `hint` | راهی بهتر یا بهینه‌تر برای رسیدن به نتیجه پیشنهاد می‌کند. | ## استثنا کردن فایل‌ها گاهی اوقات پذیرفتنی است که تحلیل ایستا شکست بخورد. برای مثال ممکن است بخواهید هشدارها یا خطاهایی که در کد تولیدشده (generated code) گزارش شده و توسط شما نوشته نشده را نادیده بگیرید. همانند قواعد رسمی Dart lint، می‌توانید از گزینهٔ `exclude:` در analyzer برای استثنا کردن فایل‌ها از تحلیل ایستا استفاده کنید. می‌توانید نام فایل‌های جداگانه را لیست کنید یا از الگوهای [`glob`](https://pub.dev/packages/glob) استفاده نمایید. :::note تمام استفاده‌ها از الگوهای glob باید نسبت به پوشه‌ای باشد که فایل `analysis_options.yaml` مربوطه در آن قرار دارد. ::: برای مثال، می‌توانیم تمام کد Dart تولیدشده را با استفاده از گزینه‌های زیر از تحلیل مستثنی کنیم: ## نادیده گرفتن قواعد مشابه قواعد رسمی Dart lint، می‌توانید قواعد bloc lint را برای یک فایل یا یک خط خاص با استفاده از `// ignore_for_file` و `// ignore` نادیده بگیرید. :::note برای نادیده گرفتن چند قاعده برای یک خط یا فایل، فهرستی از قواعد را با کاما جدا کنید. ::: ### نادیده گرفتن خطوط می‌توانیم وقوع‌های خاص نقض قواعد را با افزودن یک کامنت `ignore` درست بالای خط مشکل‌ساز یا الحاق آن به انتهای همان خط نادیده بگیریم. برای مثال، می‌توانیم موارد خاص `prefer_file_naming_conventions` را در یک فایل مشخص نادیده بگیریم: ### نادیده گرفتن فایل‌ها می‌توانیم تمام وقوع‌های نقض قواعد داخل یک فایل را با افزودن کامنت `ignore_for_file` در هر نقطه از فایل نادیده بگیریم. برای مثال، می‌توانیم تمام وقوع‌های `prefer_file_naming_conventions` را در یک فایل مشخص نادیده بگیریم: ================================================ FILE: docs/src/content/docs/fa/lint/index.mdx ================================================ --- title: مروری بر لینتر description: معرفی لینتر bloc. sidebar: order: 1 --- import AvoidFlutterImportsWarningSnippet from '~/components/lint/ImportFlutterWarningSnippet.mdx'; import AvoidFlutterImportsWarningOutputSnippet from '~/components/lint/ImportFlutterWarningOutputSnippet.astro'; import InstallBlocToolsSnippet from '~/components/lint/InstallBlocToolsSnippet.astro'; import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import RunBlocLintInCurrentDirectorySnippet from '~/components/lint/RunBlocLintInCurrentDirectorySnippet.astro'; لینتینگ فرایند تحلیل ایستأ کد برای شناسایی باگ‌ها و خطاهای منطقی و نگارشی است. Bloc دارای یک لینتر داخلی است که می‌توان از طریق IDE یا ابزار خط فرمان [`bloc command-line tools`](https://pub.dev/packages/bloc_tools) با دستور `bloc lint` از آن استفاده کرد. با کمک لینتر bloc می‌توانید کیفیت کد و یک‌دستی (consistency) در کدبیس خود را بدون اجرای هیچ‌کدام از خطوط کد بهبود دهید. برای مثال، ممکن است به‌صورت سهوی یک وابستگی فلاتر را در cubit خود وارد کرده باشید: در صورت پیکربندی صحیح، لینتر bloc به آن import اشاره کرده و هشدار زیر را تولید خواهد کرد: در بخش‌های بعدی، نحوه نصب، پیکربندی و سفارشی‌سازی لینتر bloc را بررسی خواهیم کرد تا بتوانید از تحلیل ایستا در کد خود بهره ببرید. ## شروع سریع با چند گام ساده می‌توانید استفاده از لینتر bloc را آغاز کنید. :::note برای شروع کار با bloc باید [Dart SDK](https://dart.dev/get-dart) را روی ماشین خود نصب داشته باشید. ::: 1. ابزارهای خط فرمان [bloc](https://pub.dev/packages/bloc_tools) را نصب کنید 1. پکیج [bloc_lint](https://pub.dev/packages/bloc_lint) را نصب کنید 1. یک فایل `analysis_options.yaml` به ریشه پروژه اضافه کنید و قوانین پیشنهادی را قرار دهید 1. لینتر را اجرا کنید تمام شد 🎉 برای بررسی عمیق‌تر در مورد پیکربندی و سفارشی‌سازی لینتر bloc، ادامه مطلب را مطالعه کنید. ================================================ FILE: docs/src/content/docs/fa/lint/installation.mdx ================================================ --- title: نصب لینتر description: نصب لینتر bloc. sidebar: order: 2 --- import { CardGrid } from '@astrojs/starlight/components'; import Card from '~/components/landing/Card.astro'; import InstallBlocToolsSnippet from '~/components/lint/InstallBlocToolsSnippet.astro'; import BlocToolsLintHelpOutputSnippet from '~/components/lint/BlocToolsLintHelpOutputSnippet.astro'; import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import BlocLintMultipleRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintMultipleRecommendedAnalysisOptionsSnippet.astro'; ## ابزارهای خط فرمان برای استفاده از لینتر از خط فرمان، پکیج [`package:bloc_tools`](https://pub.dev/packages/bloc_tools) را با دستور زیر نصب کنید: پس از نصب ابزارهای خط فرمان bloc، می‌توانید لینتر bloc را با دستور `bloc lint` اجرا کنید: ## مجموعه قوانین پیشنهادی برای نصب مجموعه قوانین لینت پیشنهادی، پکیج [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) را به‌عنوان یک وابستگی dev با دستور زیر نصب کنید: سپس یک فایل `analysis_options.yaml` در ریشهٔ پروژه اضافه کنید و مجموعه قوانین پیشنهادی را در آن قرار دهید: در صورت نیاز می‌توانید چند مجموعه قانون را با تعریف آن‌ها به‌صورت یک لیست اضافه کنید: ## ادغام با IDE IDEهای زیر به‌صورت رسمی از لینتر bloc و language server آن پشتیبانی می‌کنند تا تشخیص‌های آنی (diagnostics) را مستقیماً درون محیط توسعه‌تان نمایش دهند. پشتیبانی از [Bloc VSCode Extension](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) از نسخهٔ v6.8.0 در دسترس است. پشتیبانی از [Bloc IntelliJ Plugin](https://plugins.jetbrains.com/plugin/12129-bloc) از نسخهٔ v4.1.0 در دسترس است. ================================================ FILE: docs/src/content/docs/fa/lint-rules/avoid_build_context_extensions.mdx ================================================ --- title: از اکستنشن‌های BuildContext اجتناب کنید description: قانون avoid_build_context_extensions. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_build_context_extensions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_build_context_extensions/GoodSnippet.astro';
از استفاده از اکستنشن‌های `BuildContext` برای دسترسی به نمونه‌های `Bloc` یا `Cubit` خودداری کنید. :::note این قانون lint در نسخهٔ `0.3.0` از [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) معرفی شده است. ::: ## دلیل برای یکپارچگی و برای صراحت بیشتر، به‌جای استفاده از اکستنشن‌های `BuildContext` مستقیم از روش‌های زیرین استفاده کنید. این رویکرد برای تست نیز مفید است، زیرا امکان موک کردن یک متد اکستنشن وجود ندارد. | اکستنشن | روش صریح | | ---------------- | ------------------------------------------------------------------- | | `context.read` | `BlocProvider.of(context, listen: false)` | | `context.watch` | `BlocBuilder(...)` یا `BlocProvider.of(context)` | | `context.select` | `BlocSelector(...)` | ## مثال‌ها از استفاده از اکستنشن‌های `BuildContext` برای تعامل با نمونه‌های `Bloc` یا `Cubit` خودداری کنید. **بد**: **خوب**: ## فعال‌سازی برای فعال کردن قانون `avoid_build_context_extensions`، آن را به فایل `analysis_options.yaml` خود تحت `bloc` > `rules` اضافه کنید: ================================================ FILE: docs/src/content/docs/fa/lint-rules/avoid_flutter_imports.mdx ================================================ --- title: از وارد کردن Flutter خودداری کنید description: قانون lint avoid_flutter_imports. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_flutter_imports/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_flutter_imports/GoodSnippet.astro';
از وارد کردن وابستگی به فلاتر داخل کامپوننت‌های لایهٔ منطق کسب‌وکار (نمونه‌های `Bloc` یا `Cubit`) خودداری کنید. ## دلیل لایه‌بندی یک برنامه بخش مهمی از ساخت یک کدبیس قابل نگهداری است و به توسعه‌دهندگان کمک می‌کند تا سریع و با اطمینان تغییرات را اعمال کنند. هر لایه باید مسئولیت مجزایی داشته باشد و قادر باشد به‌طور جداگانه کار کند و تست شود. این امکان را فراهم می‌کند تا تغییرات را در لایه‌های مشخص محدود کنید و تأثیر آن‌ها بر کل برنامه را کاهش دهید. بنابراین، کامپوننت‌های منطق کسب‌وکار معمولاً باید مدیریت وضعیت ویژگی‌ها را به‌عهده داشته باشند و از لایهٔ رابط کاربری جدا نگه داشته شوند. رویدادها باید از لایهٔ رابط کاربری وارد کامپوننت‌های منطق کسب‌وکار شوند و وضعیت از لایهٔ منطق کسب‌وکار به لایهٔ رابط کاربری جریان یابد. جدا نگه داشتن منطق کسب‌وکار از فلاتر این امکان را می‌دهد که منطق کسب‌وکار را در پلتفرم‌ها یا فریم‌ورک‌های مختلف دوباره استفاده کنید (مثلاً Flutter، AngularDart، Jaspr و غیره). ## مثال‌ها **از وارد کردن Flutter در کامپوننت‌های منطق کسب‌وکار خود خودداری کنید.** **بد**: **خوب**: ## فعال‌سازی برای فعال کردن قانون `avoid_flutter_imports`، آن را به فایل `analysis_options.yaml` خود تحت `bloc` > `rules` اضافه کنید: ================================================ FILE: docs/src/content/docs/fa/lint-rules/avoid_public_bloc_methods.mdx ================================================ --- title: از متدهای عمومی Bloc خودداری کنید description: قانون avoid_public_bloc_methods. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_public_bloc_methods/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_public_bloc_methods/GoodSnippet.astro';
از در معرض قرار دادن متدهای عمومی در نمونه‌های `Bloc` خودداری کنید. ## دلیل Blocها به رویدادهای ورودی واکنش نشان می‌دهند و وضعیت‌های خروجی را منتشر می‌کنند. بنابراین روش پیشنهادی برای ارتباط با یک نمونهٔ bloc استفاده از متد `add` است. در اکثر موارد نیازی به ایجاد انتزاع‌های اضافی روی API `add` نیست. ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) ## مثال‌ها **اجتناب کنید** از در معرض قرار دادن متدهای عمومی روی نمونه‌های bloc **بد**: **خوب**: ## فعال‌سازی برای فعال کردن قانون `avoid_public_bloc_methods`، آن را به فایل `analysis_options.yaml` خود تحت `bloc` > `rules` اضافه کنید: ================================================ FILE: docs/src/content/docs/fa/lint-rules/avoid_public_fields.mdx ================================================ --- title: از فیلدهای عمومی خودداری کنید description: قانون avoid_public_fields. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_public_fields/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_public_fields/GoodSnippet.astro';
از در معرض قرار دادن فیلدهای عمومی در نمونه‌های `Bloc` و `Cubit` خودداری کنید. ## دلیل کامپوننت‌های منطق کسب‌وکار وضعیت `state` خود را مدیریت می‌کنند و تغییرات حالت را از طریق API `emit` منتشر می‌کنند. بنابراین تمام وضعیت‌هایی که قصد دارید در دسترس عموم قرار گیرند باید از طریق شیء `state` در دسترس باشند. ## مثال‌ها **اجتناب کنید:** از در معرض قرار دادن فیلدهای عمومی بر روی نمونه‌های bloc و cubit **بد**: **خوب**: ## فعال‌سازی برای فعال کردن قانون `avoid_public_fields`، آن را به فایل `analysis_options.yaml` خود تحت `bloc` > `rules` اضافه کنید: ================================================ FILE: docs/src/content/docs/fa/lint-rules/prefer_bloc.mdx ================================================ --- title: Bloc را ترجیح دهید description: قانون prefer_bloc. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_bloc/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_bloc/GoodSnippet.astro';
استفاده از نمونه‌های `Bloc` را به‌جای نمونه‌های `Cubit` ترجیح دهید. ## دلیل این قانون صرفاً یک قاعدهٔ سبک است. در برخی موارد تیم‌ها ممکن است ترجیح دهند تا برای حفظ یکپارچگی و سازگاری، در سراسر برنامه فقط از نمونه‌های `Bloc` استفاده کنند. :::tip برای آشنایی بیشتر با مزایای `Bloc` به [Core Concepts](/fa/bloc-concepts/#مزایای-bloc) مراجعه کنید. ::: ## مثال‌ها **اجتناب کنید** از به‌کارگیری نمونه‌های `Cubit` **بد**: **خوب**: ## فعال‌سازی برای فعال کردن قانون `prefer_bloc`، آن را به فایل `analysis_options.yaml` خود تحت `bloc` > `rules` اضافه کنید: ================================================ FILE: docs/src/content/docs/fa/lint-rules/prefer_build_context_extensions.mdx ================================================ --- title: اکستنشن‌های BuildContext را ترجیح دهید description: قانون prefer_build_context_extensions. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_build_context_extensions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_build_context_extensions/GoodSnippet.astro';
ترجیح دهید از اکستنشن‌های `BuildContext` برای دسترسی به یک نمونهٔ `Bloc` یا `Repository` استفاده کنید. :::note این قانون lint در نسخهٔ `0.3.2` از [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) معرفی شده است. ::: ## دلیل برای حفظ یکپارچگی، ترجیح دهید از اکستنشن‌های `BuildContext` مانند `context.read`، `context.watch` و `context.select` به‌جای `BlocProvider.of`، `RepositoryProvider.of`، `BlocBuilder` یا `BlocSelector` استفاده کنید. | روش صریح | اکستنشن | | ------------------------------------------------------------------- | --------------------- | | `BlocProvider.of(context, listen: false)` | `context.read` | | `BlocBuilder(...)` یا `BlocProvider.of(context)` | `context.watch` | | `BlocSelector(...)` | `context.select` | ## مثال‌ها **اجتناب کنید** از استفاده از `BlocProvider.of(context)` برای دسترسی به یک نمونهٔ `Bloc` **بد**: **خوب**: ## فعال‌سازی برای فعال کردن قانون `prefer_build_context_extensions`، آن را به فایل `analysis_options.yaml` خود تحت `bloc` > `rules` اضافه کنید: ================================================ FILE: docs/src/content/docs/fa/lint-rules/prefer_cubit.mdx ================================================ --- title: Cubit را ترجیح دهید description: قانون prefer_cubit. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_cubit/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_cubit/GoodSnippet.astro';
استفاده از نمونه‌های `Cubit` را به‌جای نمونه‌های `Bloc` ترجیح دهید. ## دلیل این قانون صرفاً یک قاعدهٔ سبک است. در برخی موارد، تیم‌ها ممکن است ترجیح دهند برای حفظ یکپارچگی و سازگاری، در سراسر برنامه فقط از نمونه‌های `Cubit` استفاده کنند. :::tip برای آشنایی بیشتر با مزایای `Cubit` به [Core Concepts](/fa/bloc-concepts/#cubit-مزایای) مراجعه کنید. ::: ## مثال‌ها **اجتناب کنید** از به‌کارگیری نمونه‌های `Bloc` **بد**: **خوب**: ## فعال‌سازی برای فعال کردن قانون `prefer_cubit`، آن را به فایل `analysis_options.yaml` خود تحت `bloc` > `rules` اضافه کنید: ================================================ FILE: docs/src/content/docs/fa/lint-rules/prefer_file_naming_conventions.mdx ================================================ --- title: از قراردادهای نام‌گذاری فایل پیروی کنید description: قانون prefer_file_naming_conventions. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_file_naming_conventions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_file_naming_conventions/GoodSnippet.astro';
ترجیح داده می‌شود از قراردادهای نام‌گذاری فایل پیروی کنید. :::note این قانون lint در نسخهٔ `0.3.0` از [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) معرفی شده است ::: ## دلیل برای حفظ یکپارچگی، سهولت نگهداری و جداسازی مسئولیت‌ها، ترجیح دهید نمونه‌های `bloc` و `cubit` را در فایل‌های Dart مربوط به خودشان تعریف کنید به‌جای آنکه آن‌ها را به‌صورت درون‌خطی قرار دهید. :::tip در نظر داشته باشید از دستور `bloc new ` از [package:bloc_tools](https://pub.dev/packages/bloc_tools) برای تولید سریع و یکنواخت نمونه‌های جدید bloc/cubit استفاده کنید. ::: ## مثال‌ها **ترجیح دهید** نمونه‌های bloc/cubit را در فایل‌های مربوطهٔ خودشان اعلام کنید. **خوب**: **بد**: ## فعال‌سازی برای فعال کردن قانون `prefer_file_naming_conventions`، آن را به فایل `analysis_options.yaml` خود تحت `bloc` > `rules` اضافه کنید: ================================================ FILE: docs/src/content/docs/fa/lint-rules/prefer_void_public_cubit_methods.mdx ================================================ --- title: ترجیح دهید متدهای عمومی Cubit از نوع void باشند description: قانون prefer_void_public_cubit_methods. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_void_public_cubit_methods/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_void_public_cubit_methods/GoodSnippet.astro';
ترجیح داده می‌شود متدهای عمومی در نمونه‌های `Cubit` از نوع `void` باشند. :::note این قانون lint در نسخهٔ `0.2.0-dev.2` از [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) معرفی شده است ::: ## دلیل متدهای عمومی روی نمونه‌های `Cubit` باید برای اطلاع‌رسانی به `Cubit` و آغاز تغییرات وضعیت از طریق متد `emit` استفاده شوند. اگر فراخواننده نیاز به دسترسی به اطلاعات وضعیت داشته باشد، باید آن را از طریق شیء `state` دریافت کند. :::note قوانین زیر مرتبط هستند و معمولاً همراه با `prefer_void_public_cubit_methods` فعال می‌شوند: - [`avoid_public_bloc_methods`](/fa/lint-rules/avoid_public_bloc_methods) - [`avoid_public_fields`](/fa/lint-rules/avoid_public_fields) ::: ## مثال‌ها **اجتناب کنید** از تعریف متدهای عمومی غیر-void روی نمونه‌های `Cubit` **بد**: **خوب**: ## فعال‌سازی برای فعال کردن قانون `prefer_void_public_cubit_methods`، آن را به فایل `analysis_options.yaml` خود تحت `bloc` > `rules` اضافه کنید: ================================================ FILE: docs/src/content/docs/fa/migration.mdx ================================================ --- title: راهنمای مهاجرت description: مهاجرت به آخرین نسخه پایدار Bloc. --- import { Code, Tabs, TabItem } from '@astrojs/starlight/components'; :::tip لطفاً برای اطلاعات بیشتر در مورد آنچه در هر انتشار تغییر کرده، به [release log](https://github.com/felangel/bloc/releases) مراجعه کنید. ::: ## v10.0.0 ### `package:bloc_test` #### ❗✨ جدا کردن `blocTest` از `BlocBase` :::note[چه چیزی تغییر کرد؟] در bloc_test v10.0.0، API `blocTest` دیگر به شدت به `BlocBase` متصل نیست. ::: ##### دلیل `blocTest` باید برای افزایش انعطاف پذیری و قابلیت استفاده مجدد تا حد امکان از رابط های اصلی bloc استفاده کند. قبلاً این امکان‌پذیر نبود زیرا `BlocBase` `StateStreamableSource` را پیاده‌سازی می‌کرد که برای `blocTest` کافی نبود به دلیل وابستگی داخلی به API `emit`. ### `package:hydrated_bloc` #### ❗✨ پشتیبانی از WebAssembly :::note[چه چیزی تغییر کرد؟] در hydrated_bloc v10.0.0، پشتیبانی از کامپایل به WebAssembly (wasm) اضافه شد. ::: ##### دلیل قبلاً امکان کامپایل برنامه‌ها به wasm هنگام استفاده از `hydrated_bloc` وجود نداشت. در v10.0.0، بسته بازسازی شد تا اجازه کامپایل به wasm را بدهد. **v9.x.x** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` **v10.x.x** ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorageDirectory.web : HydratedStorageDirectory((await getTemporaryDirectory()).path), ); runApp(const App()); } ``` ## v9.0.0 ### `package:bloc` #### ❗🧹 حذف APIهای منسوخ شده :::note[چه چیزی تغییر کرد؟] در bloc v9.0.0، همه APIهای منسوخ شده قبلی حذف شدند. ::: ##### خلاصه - `BlocOverrides` حذف شد به نفع `Bloc.observer` و `Bloc.transformer` #### ❗✨ معرفی رابط جدید `EmittableStateStreamableSource` :::note[چه چیزی تغییر کرد؟] در bloc v9.0.0، یک رابط اصلی جدید `EmittableStateStreamableSource` معرفی شد. ::: ##### دلیل `package:bloc_test` قبلاً به شدت به `BlocBase` متصل بود. رابط `EmittableStateStreamableSource` معرفی شد تا اجازه دهد `blocTest` از پیاده‌سازی پایه `BlocBase` جدا شود. ### `package:hydrated_bloc` #### ✨ باز تعریف API `HydratedBloc.storage` :::note[چه چیزی تغییر کرد؟] در hydrated_bloc v9.0.0، `HydratedBlocOverrides` حذف شد به نفع API `HydratedBloc.storage`. ::: ##### دلیل به [دلیل باز تعریف overrides Bloc.observer و Bloc.transformer](/fa/migration#-باز-تعریف-apiهای-blocobserver-و-bloctransformer) مراجعه کنید. **v8.x.x** ```dart Future main() async { final storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); HydratedBlocOverrides.runZoned( () => runApp(App()), storage: storage, ); } ``` **v9.0.0** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` ## v8.1.0 ### `package:bloc` #### ✨ باز تعریف APIهای `Bloc.observer` و `Bloc.transformer` :::note[چه چیزی تغییر کرد؟] در bloc v8.1.0، `BlocOverrides` منسوخ شد به نفع APIهای `Bloc.observer` و `Bloc.transformer`. ::: ##### دلیل API `BlocOverrides` در v8.0.0 معرفی شد در تلاش برای پشتیبانی از scoping تنظیمات خاص bloc مانند `BlocObserver`, `EventTransformer`, و `HydratedStorage`. در برنامه‌های دارت خالص، تغییرات خوب کار کردند؛ با این حال، در برنامه‌های فلاتر، API جدید مشکلات بیشتری نسبت به آنچه حل کرد ایجاد کرد. API `BlocOverrides` الهام گرفته از APIهای مشابه در Flutter/Dart بود: - [HttpOverrides](https://api.flutter.dev/flutter/dart-io/HttpOverrides-class.html) - [IOOverrides](https://api.flutter.dev/flutter/dart-io/IOOverrides-class.html) **مشکلات** در حالی که دلیل اصلی این تغییرات نبود، API `BlocOverrides` پیچیدگی اضافی برای توسعه‌دهندگان معرفی کرد. علاوه بر افزایش مقدار nesting و خطوط کد مورد نیاز برای دستیابی به همان اثر، API `BlocOverrides` نیاز داشت توسعه‌دهندگان درک جامعی از [Zones](https://api.dart.dev/stable/2.17.6/dart-async/Zone-class.html) در زبان دارت داشته باشند. `Zones` مفهومی دوستانه برای مبتدیان نیست و شکست در درک نحوه کار Zones می‌تواند منجر به معرفی باگ شود (مانند observers، transformers، نمونه‌های storage غیر اولیه). برای مثال، بسیاری از توسعه‌دهندگان چیزی مانند این داشتند: ```dart void main() { WidgetsFlutterBinding.ensureInitialized(); BlocOverrides.runZoned(...); } ``` کد بالا، در حالی که بی‌ضرر به نظر می‌رسد، می‌تواند منجر به بسیاری از باگ‌های دشوار برای ردیابی شود. هر zone که `WidgetsFlutterBinding.ensureInitialized` ابتدا از آن فراخوانی شود، zone خواهد بود که رویدادهای gesture در آن مدیریت می‌شوند (مثلاً callbacks `onTap`, `onPressed`) به دلیل `GestureBinding.initInstances`. این فقط یکی از بسیاری از مسائل ناشی از استفاده از `zoneValues` است. علاوه بر این، فلاتر بسیاری از کارها را پشت صحنه انجام می‌دهد که شامل forking/manipulating Zones است (به ویژه هنگام اجرای تست‌ها) که می‌تواند منجر به رفتارهای غیرمنتظره شود (و در بسیاری از موارد رفتارهایی که خارج از کنترل توسعه‌دهنده است -- مسائل زیر را ببینید). به دلیل استفاده از [runZoned](https://api.flutter.dev/flutter/dart-async/runZoned.html), انتقال به API `BlocOverrides` منجر به کشف چندین باگ/محدودیت در فلاتر شد (به طور خاص در مورد Widget و Integration Tests): - https://github.com/flutter/flutter/issues/96939 - https://github.com/flutter/flutter/issues/94123 - https://github.com/flutter/flutter/issues/93676 که بسیاری از توسعه‌دهندگان استفاده‌کننده از کتابخانه bloc را تحت تأثیر قرار داد: - https://github.com/felangel/bloc/issues/3394 - https://github.com/felangel/bloc/issues/3350 - https://github.com/felangel/bloc/issues/3319 **v8.0.x** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` **v8.1.0** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` ## v8.0.0 ### `package:bloc` #### ❗✨ معرفی API جدید `BlocOverrides` :::note[چه چیزی تغییر کرد؟] در bloc v8.0.0، `Bloc.observer` و `Bloc.transformer` حذف شدند به نفع API `BlocOverrides`. ::: ##### دلیل API قبلی برای override کردن پیش‌فرض `BlocObserver` و `EventTransformer` به یک singleton جهانی برای هر دو `BlocObserver` و `EventTransformer` متکی بود. در نتیجه، امکان‌پذیر نبود: - داشتن چندین پیاده‌سازی `BlocObserver` یا `EventTransformer` scoped به بخش‌های مختلف برنامه - داشتن overrides `BlocObserver` یا `EventTransformer` scoped به یک بسته - اگر بسته‌ای به `package:bloc` وابسته بود و `BlocObserver` خود را ثبت می‌کرد، هر مصرف‌کننده بسته یا باید `BlocObserver` بسته را overwrite می‌کرد یا به `BlocObserver` بسته گزارش می‌کرد. همچنین تست کردن دشوارتر بود به دلیل حالت جهانی مشترک در تست‌ها. Bloc v8.0.0 کلاس `BlocOverrides` را معرفی می‌کند که به توسعه‌دهندگان اجازه می‌دهد `BlocObserver` و/یا `EventTransformer` را برای یک `Zone` خاص override کنند به جای متکی بودن به یک singleton mutable جهانی. **v7.x.x** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` **v8.0.0** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` نمونه‌های `Bloc` از `BlocObserver` و/یا `EventTransformer` برای zone فعلی از طریق `BlocOverrides.current` استفاده خواهند کرد. اگر هیچ `BlocOverrides` برای zone وجود نداشته باشد، از پیش‌فرض‌های داخلی موجود استفاده خواهند کرد (هیچ تغییری در رفتار/عملکرد). این اجازه می‌دهد هر `Zone` مستقل با `BlocOverrides` خود عمل کند. ```dart BlocOverrides.runZoned( () { // BlocObserverA and eventTransformerA final overrides = BlocOverrides.current; // Blocs in this zone report to BlocObserverA // and use eventTransformerA as the default transformer. // ... // Later... BlocOverrides.runZoned( () { // BlocObserverB and eventTransformerB final overrides = BlocOverrides.current; // Blocs in this zone report to BlocObserverB // and use eventTransformerB as the default transformer. // ... }, blocObserver: BlocObserverB(), eventTransformer: eventTransformerB(), ); }, blocObserver: BlocObserverA(), eventTransformer: eventTransformerA(), ); ``` #### ❗✨ بهبود مدیریت خطا و گزارش :::note[چه چیزی تغییر کرد؟] در bloc v8.0.0، `BlocUnhandledErrorException` حذف شد. در عوض، هر استثنای catch نشده همیشه به `onError` گزارش می‌شود و rethrown می‌شود (بدون توجه به حالت debug یا release). API `addError` خطاها را به `onError` گزارش می‌دهد، اما خطاهای گزارش شده را به عنوان استثناهای catch نشده تلقی نمی‌کند. ::: ##### دلیل هدف این تغییرات: - استثناهای داخلی unhandled را بسیار واضح کند در حالی که عملکرد bloc را حفظ کند - پشتیبانی از `addError` بدون اختلال در جریان کنترل قبلاً، مدیریت خطا و گزارش بسته به اینکه برنامه در حالت debug یا release اجرا می‌شد متفاوت بود. علاوه بر این، خطاهای گزارش شده از طریق `addError` در حالت debug به عنوان استثناهای catch نشده تلقی می‌شدند که منجر به تجربه توسعه ضعیف هنگام استفاده از API `addError` می‌شد (به طور خاص هنگام نوشتن تست‌های واحد). در v8.0.0، `addError` می‌تواند به شکل ایمن برای گزارش خطاها استفاده شود و `blocTest` می‌تواند برای تأیید گزارش خطاها استفاده شود. همه خطاها هنوز به `onError` گزارش می‌شوند، با این حال، فقط استثناهای catch نشده rethrown می‌شوند (بدون توجه به حالت debug یا release). #### ❗🧹 تبدیل `BlocObserver` به abstract :::note[چه چیزی تغییر کرد؟] در bloc v8.0.0، `BlocObserver` به یک کلاس `abstract` تبدیل شد که به معنای امکان نمونه‌سازی از `BlocObserver` نیست. ::: ##### دلیل `BlocObserver` قرار بود یک interface باشد. از آنجایی که پیاده‌سازی‌های API پیش‌فرض no-ops هستند، `BlocObserver` اکنون یک کلاس `abstract` است تا به وضوح ارتباط دهد که کلاس قرار است extend شود نه مستقیماً نمونه‌سازی شود. **v7.x.x** ```dart void main() { // امکان ایجاد نمونه از کلاس پایه وجود داشت. final observer = BlocObserver(); } ``` **v8.0.0** ```dart class MyBlocObserver extends BlocObserver {...} void main() { // نمی‌توان نمونه از کلاس پایه ایجاد کرد. final observer = BlocObserver(); // ERROR // در عوض `BlocObserver` را extend کنید. final observer = MyBlocObserver(); // OK } ``` #### ❗✨متد `add` باعث ایجاد `StateError` میشود اگر Bloc بسته باشد :::note[چه چیزی تغییر کرد؟] در bloc v8.0.0، فراخوانی `add` روی یک bloc بسته منجر به `StateError` می‌شود. ::: ##### دلیل قبلاً امکان این وجود داشت که متد `add` را روی یک bloc بسته‌شده فراخوانی کنید و خطای داخلی نادیده گرفته می‌شد، که باعث می‌شد پیدا کردن دلیل پردازش نشدن رویداد اضافه‌شده سخت باشد. برای قابل‌مشاهده‌تر کردن این وضعیت، از نسخهٔ v8.0.0 به بعد، فراخوانی متد `add` روی یک bloc بسته‌شده باعث ایجاد شدن یک `StateError` می‌شود که به‌عنوان یک استثنای گرفته‌نشده گزارش شده و به `onError` منتقل می‌گردد. #### ❗✨ `emit` باعث ایجاد `StateError` میشود اگر Bloc بسته باشد :::note[چه چیزی تغییر کرد؟] در bloc v8.0.0، فراخوانی `emit` در یک bloc بسته منجر به `StateError` می‌شود. ::: ##### دلیل قبلاً، امکان فراخوانی `emit` در یک bloc بسته وجود داشت و هیچ تغییر حالت رخ نمی‌داد اما هیچ نشانگری از آنچه اشتباه رفت وجود نداشت، که اشکال‌زدایی را دشوار می‌کرد. برای اینکه این سناریو بیشتر قابل مشاهده باشد، در v8.0.0، فراخوانی `emit` در یک bloc بسته `StateError` می‌اندازد که به عنوان یک استثنا catch نشده گزارش می‌شود و به `onError` propagate می‌شود. #### ❗🧹 حذف APIهای منسوخ شده :::note[چه چیزی تغییر کرد؟] در bloc v8.0.0، همه APIهای منسوخ شده قبلی حذف شدند. ::: ##### خلاصه - `mapEventToState` حذف شد به نفع `on` - `transformEvents` حذف شد به نفع API `EventTransformer` - typedef `TransitionFunction` حذف شد به نفع API `EventTransformer` - `listen` حذف شد به نفع `stream.listen` ### `package:bloc_test` #### ✨ `MockBloc` و `MockCubit` دیگر نیاز به `registerFallbackValue` ندارند :::note[چه چیزی تغییر کرد؟] در bloc_test v9.0.0، توسعه‌دهندگان دیگر نیازی به فراخوانی صریح `registerFallbackValue` هنگام استفاده از `MockBloc` یا `MockCubit` ندارند. ::: ##### خلاصه `registerFallbackValue` فقط زمانی نیاز است که از تطبیق دهنده `any()` از `package:mocktail` برای یک نوع سفارشی استفاده شود. قبلاً، `registerFallbackValue` برای هر `رویداد` و `حالت` هنگام استفاده از `MockBloc` یا `MockCubit` نیاز بود. **v8.x.x** ```dart class FakeMyEvent extends Fake implements MyEvent {} class FakeMyState extends Fake implements MyState {} class MyMockBloc extends MockBloc implements MyBloc {} void main() { setUpAll(() { registerFallbackValue(FakeMyEvent()); registerFallbackValue(FakeMyState()); }); // Tests... } ``` **v9.0.0** ```dart class MyMockBloc extends MockBloc implements MyBloc {} void main() { // Tests... } ``` ### `package:hydrated_bloc` #### ❗✨ معرفی API جدید `HydratedBlocOverrides` :::note[چه چیزی تغییر کرد؟] در hydrated_bloc v8.0.0، `HydratedBloc.storage` حذف شد به نفع API `HydratedBlocOverrides`. ::: ##### دلیل قبلاً، یک singleton جهانی برای override پیاده‌سازی `Storage` استفاده می‌شد. در نتیجه، امکان‌پذیر نبود داشتن چندین پیاده‌سازی `Storage` scoped به بخش‌های مختلف برنامه. همچنین تست کردن دشوارتر بود به دلیل حالت جهانی مشترک در تست‌ها. `HydratedBloc` v8.0.0 کلاس `HydratedBlocOverrides` را معرفی می‌کند که به توسعه‌دهندگان اجازه می‌دهد `Storage` را برای یک `Zone` خاص override کنند به جای متکی بودن به یک singleton قابل تغییر جهانی. **v7.x.x** ```dart void main() async { HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); // ... } ``` **v8.0.0** ```dart void main() { final storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); HydratedBlocOverrides.runZoned( () { // ... }, storage: storage, ); } ``` نمونه‌های `HydratedBloc` از `Storage` برای zone فعلی از طریق `HydratedBlocOverrides.current` استفاده خواهند کرد. این اجازه می‌دهد هر `Zone` مستقل با `BlocOverrides` خود عمل کند. ## v7.2.0 ### `package:bloc` #### ✨ معرفی API جدید `on` :::note[چه چیزی تغییر کرد؟] در bloc v7.2.0، `mapEventToState` منسوخ شد به نفع `on`. `mapEventToState` در bloc v8.0.0 حذف خواهد شد. ::: ##### دلیل API `on` به عنوان بخشی از [[Proposal] Replace mapEventToState with on\ in Bloc](https://github.com/felangel/bloc/issues/2526) معرفی شد. به دلیل [یک مشکل در زبان دارت](https://github.com/dart-lang/sdk/issues/44616) همیشه واضح نیست که مقدار `state` چه خواهد بود هنگام برخورد با async generators nested (`async*`). حتی اگر راه‌هایی برای کار کردن با مسئله وجود دارد، یکی از اصول اصلی کتابخانه bloc قابل پیش‌بینی بودن است. API `on` ایجاد شد تا کتابخانه را تا حد امکان ایمن برای استفاده کند و هر عدم قطعیتی در مورد تغییرات حالت را از بین ببرد. :::tip برای اطلاعات بیشتر، [پیشنهاد کامل را بخوانید](https://github.com/felangel/bloc/issues/2526). ::: **خلاصه** `on` به شما اجازه می‌دهد تا یک هندلر رویداد برای تمام رویدادهای نوع `E` ثبت کنید. به طور پیش‌فرض، رویدادها هنگام استفاده از `on` به طور همزمان پردازش می‌شوند در حالی که `mapEventToState` که رویدادها را به صورت `سریالی` پردازش می‌کند. **v7.1.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0); @override Stream mapEventToState(CounterEvent event) async* { if (event is Increment) { yield state + 1; } } } ``` **v7.2.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } } ``` :::note هر تابع ثبت شده `EventHandler` به طور مستقل عمل می‌کند بنابراین مهم است که هندلرهای رویداد را بر اساس نوع ترنسفورمری که می‌خواهید اعمال شود ثبت کنید. ::: اگر می‌خواهید همان رفتار دقیق را مانند v7.1.0 حفظ کنید می‌توانید یک هندلر رویداد واحد برای تمام رویدادها ثبت کنید و یک ترنسفورمر `سریالی` اعمال کنید: ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; class MyBloc extends Bloc { MyBloc() : super(MyState()) { on(_onEvent, transformer: sequential()) } FutureOr _onEvent(MyEvent event, Emitter emit) async { // TODO: logic goes here... } } ``` شما همچنین می‌توانید ترنسفورمر پیش‌فرض `EventTransformer` را برای تمام blocs در برنامه خود override کنید: ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; void main() { Bloc.transformer = sequential(); ... } ``` #### ✨ معرفی API جدید `EventTransformer` :::note[چه چیزی تغییر کرد؟] در bloc v7.2.0، `transformEvents` منسوخ شد به نفع API `EventTransformer`. `transformEvents` در bloc v8.0.0 حذف خواهد شد. ::: ##### دلیل API `on` این امکان را فراهم کرد که بتوانید یک ترنسفورمر رویداد سفارشی برای هر هندلر رویداد فراهم کنید. یک typedef جدید به نام `EventTransformer` معرفی شد که به توسعه‌دهندگان این امکان را می‌دهد که جریان ورودی رویدادها را برای هر هندلر رویداد تغییر دهند به جای اینکه مجبور باشند یک ترنسفورمر رویداد واحد برای تمام رویدادها مشخص کنند. **خلاصه** یک `EventTransformer` مسئول دریافت جریان ورودی رویدادها به همراه یک `EventMapper` (هندلر رویداد شما) و بازگرداندن یک جریان جدید رویدادها است. ```dart typedef EventTransformer = Stream Function(Stream events, EventMapper mapper) ``` پیش‌فرض `EventTransformer` تمام رویدادها را به طور همزمان پردازش می‌کند و چیزی شبیه به این دارد: ```dart EventTransformer concurrent() { return (events, mapper) => events.flatMap(mapper); } ``` :::tip برای مجموعه‌ای از ترنسفورمرهای رویداد سفارشی، به [package:bloc_concurrency](https://pub.dev/packages/bloc_concurrency) مراجعه کنید ::: **v7.1.0** ```dart @override Stream> transformEvents(events, transitionFn) { return events .debounceTime(const Duration(milliseconds: 300)) .flatMap(transitionFn); } ``` **v7.2.0** ```dart /// تعریف یک `EventTransformer` سفارشی EventTransformer debounce(Duration duration) { return (events, mapper) => events.debounceTime(duration).flatMap(mapper); } MyBloc() : super(MyState()) { /// اعمال `EventTransformer` سفارشی بر روی `EventHandler` on(_onEvent, transformer: debounce(const Duration(milliseconds: 300))) } ``` #### ⚠️ منسوخ کردن API `transformTransitions` :::note[چه چیزی تغییر کرد؟] در bloc v7.2.0، `transformTransitions` منسوخ شد به نفع override کردن API `stream`. `transformTransitions` در bloc v8.0.0 حذف خواهد شد. ::: ##### دلیل getter `stream` در `Bloc` این امکان را می‌دهد که جریان خروجی حالات را به راحتی override کنید، بنابراین دیگر ارزشمند نیست که یک API جداگانه `transformTransitions` نگه‌داری شود. **خلاصه** **v7.1.0** ```dart @override Stream> transformTransitions( Stream> transitions, ) { return transitions.debounceTime(const Duration(milliseconds: 42)); } ``` **v7.2.0** ```dart @override Stream get stream => super.stream.debounceTime(const Duration(milliseconds: 42)); ``` ## v7.0.0 ### `package:bloc` #### ❗ Bloc و Cubit از BlocBase ارث‌بری می‌کنند ##### دلیل به‌عنوان یک توسعه‌دهنده، رابطهٔ بین bloc ها و cubit ها کمی نامتعارف و عجیب بود. وقتی Cubit برای اولین بار معرفی شد، به‌عنوان کلاس پایه برای Bloc ها در نظر گرفته شد که منطقی به نظر می‌رسید، چون Cubit فقط زیرمجموعه‌ای از قابلیت‌ها را داشت و Bloc ها صرفاً Cubit را گسترش می‌دادند و API های اضافی را تعریف می‌کردند. اما این رویکرد چند ایراد به همراه داشت: - یا باید تمام API ها تغییر نام داده می‌شدند تا از نظر مفهومی با Cubit سازگار باشند، یا برای حفظ یکپارچگی، نام Bloc روی آن‌ها باقی می‌ماند، با اینکه از نظر ساختار سلسله‌مراتبی نادرست بود ([#1708](https://github.com/felangel/bloc/issues/1708), [#1560](https://github.com/felangel/bloc/issues/1560)). - Cubit مجبور بود از Stream ارث‌بری کند و EventSink را پیاده‌سازی کند تا یک پایهٔ مشترک داشته باشیم که ویجت‌هایی مثل BlocBuilder، BlocListener و غیره بتوانند بر اساس آن پیاده‌سازی شوند ([#1429](https://github.com/felangel/bloc/issues/1429)). بعدتر، ما رابطه را معکوس کردیم و Bloc را به‌عنوان کلاس پایه در نظر گرفتیم که تا حدی مشکل مورد اول را حل کرد، اما مسائل جدیدی را به وجود آورد: - API های Cubit به دلیل وجود API های داخلی Bloc مثل mapEventToState، add و غیره بیش‌ازحد حجیم و شلوغ شد ([#2228](https://github.com/felangel/bloc/issues/2228)) - از نظر فنی، توسعه‌دهندگان می‌توانستند این API ها را فراخوانی کنند و باعث بروز مشکلات ناخواسته شوند - همچنان همان مشکل قبلی وجود داشت و Cubit کل API مربوط به Stream را در معرض دسترس قرار می‌داد ([#1429](https://github.com/felangel/bloc/issues/1429)). برای حل این مشکلات، ما یک کلاس پایهٔ مشترک برای هر دو `Bloc` و `Cubit` با نام `BlocBase` معرفی کردیم تا اجزای بالادستی همچنان بتوانند با هر دو نمونهٔ bloc و cubit کار کنند، اما بدون اینکه کل API های `Stream` و `EventSink` به‌صورت مستقیم در معرض دسترس قرار بگیرند. **خلاصه** **BlocObserver** **v6.1.x** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(Cubit cubit) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(Cubit cubit, Object event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(Cubit cubit, Object error, StackTrace stackTrace) {...} @override void onClose(Cubit cubit) {...} } ``` **v7.0.0** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(BlocBase bloc) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(BlocBase bloc, Object? event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) {...} @override void onClose(BlocBase bloc) {...} } ``` **Bloc/Cubit** **v6.1.x** ```dart final bloc = MyBloc(); bloc.listen((state) {...}); final cubit = MyCubit(); cubit.listen((state) {...}); ``` **v7.0.0** ```dart final bloc = MyBloc(); bloc.stream.listen((state) {...}); final cubit = MyCubit(); cubit.stream.listen((state) {...}); ``` ### `package:bloc_test` #### ❗seed یک تابع برای پشتیبانی از مقادیر پویا برمی‌گرداند ##### دلیل برای پشتیبانی از داشتن یک مقدار seed قابل تغییر که می‌تواند به طور پویا در `setUp` به‌روزرسانی شود، `seed` یک تابع برمی‌گرداند. **خلاصه** **v7.x.x** ```dart blocTest( '...', seed: MyState(), ... ); ``` **v8.0.0** ```dart blocTest( '...', seed: () => MyState(), ... ); ``` #### ❗expect یک تابع برای پشتیبانی از مقادیر پویا و شامل پشتیبانی از تطبیق دهنده برمی‌گرداند ##### دلیل برای پشتیبانی از داشتن یک انتظار قابل تغییر که می‌تواند به طور پویا در `setUp` به‌روزرسانی شود، `expect` یک تابع برمی‌گرداند. `expect` همچنین از `تطبیق دهنده` پشتیبانی می‌کند. **خلاصه** **v7.x.x** ```dart blocTest( '...', expect: [MyStateA(), MyStateB()], ... ); ``` **v8.0.0** ```dart blocTest( '...', expect: () => [MyStateA(), MyStateB()], ... ); // همچنین می‌تواند یک `تطبیق دهنده` باشد blocTest( '...', expect: () => contains(MyStateA()), ... ); ``` #### ❗errors یک تابع برای پشتیبانی از مقادیر پویا و شامل پشتیبانی از تطبیق دهنده برمی‌گرداند ##### دلیل برای پشتیبانی از داشتن خطاهای قابل تغییر که می‌توانند به طور پویا در `setUp` به‌روزرسانی شوند، `errors` یک تابع برمی‌گرداند. `errors` همچنین از `تطبیق دهنده` پشتیبانی می‌کند. **خلاصه** **v7.x.x** ```dart blocTest( '...', errors: [MyError()], ... ); ``` **v8.0.0** ```dart blocTest( '...', errors: () => [MyError()], ... ); // همچنین می‌تواند یک `تطبیق دهنده` باشد blocTest( '...', errors: () => contains(MyError()), ... ); ``` #### ❗MockBloc و MockCubit ##### دلیل برای پشتیبانی از شبیه سازی کردن APIهای مختلف هسته، `MockBloc` و `MockCubit` به عنوان بخشی از بسته `bloc_test` ارائه شده اند. قبلاً، `MockBloc` باید برای هر دو نمونه `Bloc` و `Cubit` استفاده می‌شد که شهودی نبود. **خلاصه** **v7.x.x** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockBloc implements MyBloc {} ``` **v8.0.0** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockCubit implements MyCubit {} ``` #### ❗ادغام با Mocktail ##### دلیل به دلیل محدودیت‌های مختلف null-safe [package:mockito](https://pub.dev/packages/mockito) توصیف شده [اینجا](https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#problems-with-typical-mocking-and-stubbing), [package:mocktail](https://pub.dev/packages/mocktail) توسط `MockBloc` و `MockCubit` استفاده می‌شود. این به توسعه‌دهندگان اجازه می‌دهد تا بدون نیاز به نوشتن دستی stubs یا وابستگی به تولید کد، از یک API mocking آشنا استفاده کنند. **خلاصه** **v7.x.x** ```dart import 'package:mockito/mockito.dart'; ... when(bloc.state).thenReturn(MyState()); verify(bloc.add(any)).called(1); ``` **v8.0.0** ```dart import 'package:mocktail/mocktail.dart'; ... when(() => bloc.state).thenReturn(MyState()); verify(() => bloc.add(any())).called(1); ``` > لطفاً برای اطلاعات بیشتر به > [#347](https://github.com/dart-lang/mockito/issues/347) و همچنین > [مستندات mocktail](https://github.com/felangel/mocktail/tree/main/packages/mocktail) > مراجعه کنید. ### `package:flutter_bloc` #### ❗ تغییر نام پارامتر `cubit` به `bloc` ##### دلیل به عنوان نتیجه بازسازی در `package:bloc` برای معرفی `BlocBase` که `Bloc` و `Cubit` از آن ارث‌بری می‌کنند، پارامترهای `BlocBuilder`، `BlocConsumer` و `BlocListener` از `cubit` به `bloc` تغییر نام یافتند زیرا ویجت‌ها بر روی نوع `BlocBase` عمل می‌کنند. این همچنین بیشتر با نام کتابخانه هماهنگ می‌شود و امیدواریم خوانایی را بهبود بخشد. **خلاصه** **v6.1.x** ```dart BlocBuilder( cubit: myBloc, ... ) BlocListener( cubit: myBloc, ... ) BlocConsumer( cubit: myBloc, ... ) ``` **v7.0.0** ```dart BlocBuilder( bloc: myBloc, ... ) BlocListener( bloc: myBloc, ... ) BlocConsumer( bloc: myBloc, ... ) ``` ### `package:hydrated_bloc` #### ❗storageDirectory هنگام فراخوانی HydratedStorage.build الزامی است ##### دلیل به منظور تبدیل `package:hydrated_bloc` به یک بسته خالص زبان دارت، وابستگی به [package:path_provider](https://pub.dev/packages/path_provider) حذف شد و پارامتر `storageDirectory` هنگام فراخوانی `HydratedStorage.build` الزامی است و دیگر به `getTemporaryDirectory` پیش‌فرض نیست. **خلاصه** **v6.x.x** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` **v7.0.0** ```dart import 'package:path_provider/path_provider.dart'; ... HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getTemporaryDirectory(), ); ``` ## v6.1.0 ### `package:flutter_bloc` #### ❗context.bloc و context.repository به نفع context.read و context.watch منسوخ شده‌اند ##### دلیل `context.read`، `context.watch` و `context.select` برای هماهنگی با API موجود [provider](https://pub.dev/packages/provider) که بسیاری از توسعه‌دهندگان با آن آشنا هستند و برای رفع مسائلی که توسط جامعه مطرح شده بود، اضافه شدند. به منظور بهبود ایمنی کد و حفظ سازگاری، `context.bloc` به دلیل اینکه می‌تواند با یکی از `context.read` یا `context.watch` بسته به اینکه آیا مستقیماً درون `build` استفاده می‌شود، جایگزین شد. **context.watch** `context.watch` به درخواست داشتن یک [MultiBlocBuilder](https://github.com/felangel/bloc/issues/538) پاسخ می‌دهد زیرا می‌توانیم چندین bloc را درون یک `Builder` واحد مشاهده کنیم تا UI را بر اساس چندین حالت رندر کنیم: ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // return a Widget which depends on the state of BlocA, BlocB, and BlocC } ); ``` **context.select** `context.select` به توسعه‌دهندگان این امکان را می‌دهد که UI را بر اساس یک بخش از حالت bloc رندر/به‌روزرسانی کنند و به درخواست داشتن یک [simpler buildWhen](https://github.com/felangel/bloc/issues/1521) پاسخ می‌دهد. ```dart final name = context.select((UserBloc bloc) => bloc.state.user.name); ``` قطعه کد بالا به ما این امکان را می‌دهد که به کاربر فعلی دسترسی پیدا کنیم و فقط زمانی که نام کاربر تغییر می‌کند، ویجت را دوباره بسازیم. **context.read** اگرچه به نظر می‌رسد `context.read` با `context.bloc` یکسان است، اما تفاوت‌های ظریف اما قابل توجهی وجود دارد. هر دو به شما اجازه می‌دهند تا با یک `BuildContext` به یک bloc دسترسی پیدا کنید و منجر به rebuild نشوند؛ با این حال، `context.read` نمی‌تواند مستقیماً درون یک متد `build` فراخوانی شود. دو دلیل اصلی برای استفاده از `context.bloc` درون `build` وجود دارد: 1. **برای دسترسی به حالت bloc** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` استفاده از کد بالا خطازا است زیرا ویجت `Text` دوباره ساخته نخواهد شد اگر حالت bloc تغییر کند. در این سناریو، باید از `BlocBuilder` یا `context.watch` استفاده کنید. ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` یا ```dart @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) => Text('$state'), ); } ``` :::note استفاده از `context.watch` در ریشه متد `build` منجر به این می‌شود که کل ویجت هنگام تغییر حالت bloc دوباره ساخته شود. اگر کل ویجت نیاز به بازسازی ندارد، از `BlocBuilder` برای احاطه کردن بخش‌هایی که باید دوباره بسازند، استفاده کنید، از `Builder` با `context.watch` برای محدود کردن بازسازی‌ها استفاده کنید، یا ویجت را به ویجت‌های کوچکتر تقسیم کنید. ::: 2. **برای دسترسی به bloc به طوری که یک رویداد بتواند اضافه شود** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` استفاده از کد بالا ناکارآمد است زیرا منجر به جستجوی bloc در هر بار بازسازی می‌شود در حالی که bloc فقط زمانی که کاربر دکمه `ElevatedButton` را فشار می‌دهد، مورد نیاز است. در این سناریو، بهتر است از `context.read` برای دسترسی به bloc به طور مستقیم در جایی که مورد نیاز است استفاده کنید (در این مورد، در callback `onPressed`). ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` **خلاصه** **v6.0.x** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` اگر به یک bloc برای اضافه کردن یک رویداد دسترسی پیدا می‌کنید، دسترسی به bloc را با استفاده از `context.read` در callback جایی که مورد نیاز است، انجام دهید. **v6.0.x** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` هنگام دسترسی به حالت bloc از `context.watch` استفاده کنید تا اطمینان حاصل شود که ویجت هنگام تغییر حالت دوباره ساخته می‌شود. ## v6.0.0 ### `package:bloc` #### ❗متد BlocObserver.onError، Cubit را دریافت می‌کند ##### دلیل به دلیل ادغام `Cubit`، `onError` اکنون بین `Bloc` و `Cubit` مشترک است. از آنجایی که `Cubit` پایه است، `BlocObserver` نوع `Cubit` را به جای نوع `Bloc` در override `onError` خواهد پذیرفت. **v5.x.x** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Bloc bloc, Object error, StackTrace stackTrace) { super.onError(bloc, error, stackTrace); } } ``` **v6.0.0** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Cubit cubit, Object error, StackTrace stackTrace) { super.onError(cubit, error, stackTrace); } } ``` #### ❗Bloc هنگام اشتراک (subscription) آخرین حالت را منتشر نمی‌کند ##### دلیل این تغییر به منظور همسان‌سازی `Bloc` و `Cubit` با رفتار داخلی `Stream` در `زبان دارت` انجام شد. علاوه بر این، تطبیق این تغییر در زمینه `Cubit` منجر به بسیاری از عوارض جانبی ناخواسته و به طور کلی پیاده‌سازی داخلی سایر بسته‌ها مانند `flutter_bloc` و `bloc_test` را پیچیده‌تر کرد (نیاز به `skip(1)` و غیره...). **v5.x.x** ```dart final bloc = MyBloc(); bloc.listen(print); ``` قبلاً، قطعه کد بالا وضعیت اولیه bloc را به همراه تغییرات بعدی حالت‌ها چاپ می‌کرد. **v6.x.x** در v6.0.0، قطعه کد بالا وضعیت اولیه را چاپ نمی‌کند و فقط تغییرات بعدی حالت‌ها را چاپ می‌کند. رفتار قبلی را می‌توان با کد زیر به دست آورد: ```dart final bloc = MyBloc(); print(bloc.state); bloc.listen(print); ``` **توجه**: این تغییر فقط بر روی کدهایی که به اشتراک‌گذاری مستقیم bloc وابسته‌اند تأثیر خواهد گذاشت. هنگام استفاده از `BlocBuilder`، `BlocListener` یا `BlocConsumer` تغییر قابل توجهی در رفتار وجود نخواهد داشت. ### `package:bloc_test` #### ❗MockBloc فقط نیاز به نوع State دارد ##### دلیل این غیرضروری است و کد اضافی را حذف می‌کند در حالی که همچنین `MockBloc` را با `Cubit` سازگار می‌کند. **v5.x.x** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` **v6.0.0** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` #### ❗whenListen فقط نیاز به نوع State دارد ##### دلیل این غیرضروری است و کد اضافی را حذف می‌کند در حالی که همچنین `whenListen` را با `Cubit` سازگار می‌کند. **v5.x.x** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` **v6.0.0** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` #### ❗blocTest به نوع Event نیاز ندارد ##### دلیل این غیرضروری است و کد اضافی را حذف می‌کند در حالی که همچنین `blocTest` را با `Cubit` سازگار می‌کند. **v5.x.x** ```dart blocTest( 'emits [1] when increment is called', build: () async => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` **v6.0.0** ```dart blocTest( 'emits [1] when increment is called', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` #### ❗blocTest skip به طور پیش‌فرض به 0 تنظیم شده است ##### دلیل از آنجایی که نمونه‌های `bloc` و `cubit` دیگر آخرین حالت را برای اشتراک‌گذاری‌های جدید منتشر نمی‌کنند، دیگر لازم نبود که `skip` به طور پیش‌فرض به `1` تنظیم شود. **v5.x.x** ```dart blocTest( 'emits [0] when skip is 0', build: () async => CounterBloc(), skip: 0, expect: const [0], ); ``` **v6.0.0** ```dart blocTest( 'emits [] when skip is 0', build: () => CounterBloc(), skip: 0, expect: const [], ); ``` وضعیت اولیه یک bloc یا cubit را می‌توان با کد زیر تست کرد: ```dart test('initial state is correct', () { expect(MyBloc().state, InitialState()); }); ``` #### ❗blocTest ساخت را همزمان می‌کند ##### دلیل قبلاً، `build` به صورت `async` تنظیم شده بود تا آماده‌سازی‌های مختلفی انجام شود تا bloc تحت تست در یک حالت خاص قرار گیرد. دیگر لازم نیست و همچنین چندین مشکل را به دلیل تأخیر اضافی بین ساخت و اشتراک‌گذاری داخلی حل می‌کند. به جای انجام آماده‌سازی async برای قرار دادن یک bloc در حالت دلخواه، اکنون می‌توانیم وضعیت bloc را با زنجیره‌کردن `emit` با وضعیت دلخواه تنظیم کنیم. **v5.x.x** ```dart blocTest( 'emits [2] when increment is added', build: () async { final bloc = CounterBloc(); bloc.add(CounterEvent.increment); await bloc.take(2); return bloc; } act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` **v6.0.0** ```dart blocTest( 'emits [2] when increment is added', build: () => CounterBloc()..emit(1), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` :::note `emit` فقط برای تست قابل مشاهده است و هرگز نباید در خارج از تست‌ها استفاده شود. ::: ### `package:flutter_bloc` #### ❗BlocBuilder پارامتر bloc به cubit تغییر نام داد ##### دلیل به منظور هماهنگ‌سازی `BlocBuilder` با نمونه‌های `bloc` و `cubit`، پارامتر `bloc` به `cubit` تغییر نام یافت (زیرا `Cubit` کلاس پایه است). **v5.x.x** ```dart BlocBuilder( bloc: myBloc, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocBuilder( cubit: myBloc, builder: (context, state) {...} ) ``` #### ❗BlocListener پارامتر bloc به cubit تغییر نام داد ##### دلیل به منظور هماهنگ‌سازی `BlocListener` با نمونه‌های `bloc` و `cubit`، پارامتر `bloc` به `cubit` تغییر نام یافت (زیرا `Cubit` کلاس پایه است). **v5.x.x** ```dart BlocListener( bloc: myBloc, listener: (context, state) {...} ) ``` **v6.0.0** ```dart BlocListener( cubit: myBloc, listener: (context, state) {...} ) ``` #### ❗BlocConsumer پارامتر bloc به cubit تغییر نام داد ##### دلیل به منظور هماهنگ‌سازی `BlocConsumer` با نمونه‌های `bloc` و `cubit`، پارامتر `bloc` به `cubit` تغییر نام یافت (زیرا `Cubit` کلاس پایه است). **v5.x.x** ```dart BlocConsumer( bloc: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocConsumer( cubit: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` --- ## v5.0.0 ### `package:bloc` #### ❗initialState حذف شده است ##### دلیل به عنوان یک توسعه‌دهنده، مجبور بودن به override کردن `initialState` هنگام ایجاد یک bloc دو مشکل اصلی دارد: - `initialState` bloc می‌تواند پویا باشد و می‌تواند در زمان بعدی (حتی خارج از خود bloc) به آن ارجاع داده شود. از برخی جهات، این می‌تواند به عنوان نشت اطلاعات داخلی bloc به لایه UI مشاهده شود. - این بسیار verbose است. **v4.x.x** ```dart class CounterBloc extends Bloc { @override int get initialState => 0; ... } ``` **v5.0.0** ```dart class CounterBloc extends Bloc { CounterBloc() : super(0); ... } ``` برای اطلاعات بیشتر به [#1304](https://github.com/felangel/bloc/issues/1304) مراجعه کنید. #### ❗BlocDelegate به BlocObserver تغییر نام یافت ##### دلیل نام `BlocDelegate` توصیف دقیقی از نقشی که کلاس ایفا می‌کند، نبود. `BlocDelegate` نشان می‌دهد که کلاس نقش فعالی ایفا می‌کند در حالی که در واقع هدف از `BlocDelegate` این بود که یک مؤلفه غیرفعال باشد که به سادگی تمام blocs در یک برنامه را مشاهده می‌کند. :::note به طور ایده‌آل، باید هیچ عملکرد یا ویژگی کاربرپسند در `BlocObserver` مدیریت نشود. ::: **v4.x.x** ```dart class MyBlocDelegate extends BlocDelegate { ... } ``` **v5.0.0** ```dart class MyBlocObserver extends BlocObserver { ... } ``` #### ❗BlocSupervisor حذف شده است ##### دلیل `BlocSupervisor` هنوز یک مؤلفه دیگر بود که توسعه‌دهندگان باید درباره آن می‌دانستند و با آن تعامل می‌کردند فقط به منظور مشخص کردن یک `BlocDelegate` سفارشی. با تغییر به `BlocObserver`، احساس کردیم که تجربه توسعه‌دهنده را بهبود می‌بخشد تا ناظر را مستقیماً بر روی bloc خود تنظیم کنید. این تغییر همچنین به ما این امکان را داد که سایر افزودنی‌های bloc مانند `HydratedStorage` را از `BlocObserver` جدا کنیم. **v4.x.x** ```dart BlocSupervisor.delegate = MyBlocDelegate(); ``` **v5.0.0** ```dart Bloc.observer = MyBlocObserver(); ``` ### `package:flutter_bloc` #### ❗BlocBuilder شرط به buildWhen تغییر نام یافت ##### دلیل هنگام استفاده از `BlocBuilder`، قبلاً می‌توانستیم یک `شرط` مشخص کنیم تا تعیین کنیم آیا `سازنده` باید دوباره بسازد یا خیر. ```dart BlocBuilder( condition: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...} ) ``` نام `شرط` توصیف چندانی ندارد و به طور خاص، زمانی که با `BlocConsumer` تعامل دارد، API را ناسازگار می‌کند زیرا توسعه‌دهندگان می‌توانند دو شرط (یکی برای `سازنده` و یکی برای `شنونده`) فراهم کنند. به عنوان نتیجه، API `BlocConsumer` یک `buildWhen` و `listenWhen` را در معرض دید قرار داد ```dart BlocConsumer( listenWhen: (previous, current) { // return true/false to determine whether to call listener }, listener: (context, state) {...}, buildWhen: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...}, ) ``` به منظور همسان‌سازی API و ارائه یک تجربه توسعه‌دهنده سازگارتر، `شرط` به `buildWhen` تغییر نام یافت. **v4.x.x** ```dart BlocBuilder( condition: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...} ) ``` **v5.0.0** ```dart BlocBuilder( buildWhen: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...} ) ``` #### ❗BlocListener شرط به listenWhen تغییر نام یافت ##### دلیل به همان دلایلی که در بالا توضیح داده شد، شرط `BlocListener` نیز تغییر نام یافت. **v4.x.x** ```dart BlocListener( condition: (previous, current) { // return true/false to determine whether to call listener }, listener: (context, state) {...} ) ``` **v5.0.0** ```dart BlocListener( listenWhen: (previous, current) { // return true/false to determine whether to call listener }, listener: (context, state) {...} ) ``` ### `package:hydrated_bloc` #### ❗HydratedStorage و HydratedBlocStorage تغییر نام یافتند ##### دلیل به منظور بهبود استفاده مجدد از کد بین [hydrated_bloc](https://pub.dev/packages/hydrated_bloc) و [hydrated_cubit](https://pub.dev/packages/hydrated_cubit)، پیاده‌سازی پیش‌فرض ذخیره‌سازی تغییر نام یافت از `HydratedBlocStorage` به `HydratedStorage`. علاوه بر این، رابط `HydratedStorage` از `HydratedStorage` به `Storage` تغییر نام یافت. **v4.0.0** ```dart class MyHydratedStorage implements HydratedStorage { ... } ``` **v5.0.0** ```dart class MyHydratedStorage implements Storage { ... } ``` #### ❗HydratedStorage از BlocDelegate جدا شد ##### دلیل همانطور که قبلاً ذکر شد، `BlocDelegate` به `BlocObserver` تغییر نام یافت و مستقیماً بر روی `bloc` تنظیم شد: ```dart Bloc.observer = MyBlocObserver(); ``` تغییر زیر انجام شد تا: - با API ناظر جدید bloc سازگار باشد - ذخیره‌سازی را فقط به `HydratedBloc` محدود کند - `BlocObserver` را از `Storage` جدا کند **v4.0.0** ```dart BlocSupervisor.delegate = await HydratedBlocDelegate.build(); ``` **v5.0.0** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` #### ❗ساده‌سازی راه‌اندازی ##### دلیل قبلاً، توسعه‌دهندگان مجبور بودند به صورت دستی فراخوانی کنند `super.initialState ?? DefaultInitialState()` به منظور راه‌اندازی نمونه‌های `HydratedBloc` خود. این کار دست و پاگیر و verbose بود و همچنین با تغییرات شکستن در `initialState` در `bloc` ناسازگار بود. به عنوان نتیجه، در v5.0.0 راه‌اندازی `HydratedBloc` کاملاً مشابه با راه‌اندازی عادی `Bloc` است. **v4.0.0** ```dart class CounterBloc extends HydratedBloc { @override int get initialState => super.initialState ?? 0; } ``` **v5.0.0** ```dart class CounterBloc extends HydratedBloc { CounterBloc() : super(0); ... } ``` ================================================ FILE: docs/src/content/docs/fa/modeling-state.mdx ================================================ --- title: مدل‌سازی حالت description: نمای کلی از چندین روش برای مدل‌سازی حالت‌ها هنگام استفاده از package:bloc. --- import ConcreteClassAndStatusEnumSnippet from '~/components/modeling-state/ConcreteClassAndStatusEnumSnippet.astro'; import SealedClassAndSubclassesSnippet from '~/components/modeling-state/SealedClassAndSubclassesSnippet.astro'; روش‌های مختلفی برای ساختاردهی به حالت برنامه وجود دارد. هر کدام مزایا و معایب خود را دارد. در این بخش، به چندین رویکرد نگاه خواهیم کرد، مزایا و معایب آنها، و زمان استفاده از هر کدام. رویکردهای زیر صرفاً توصیه هستند و کاملاً اختیاری. آزادانه از هر رویکردی که ترجیح می‌دهید استفاده کنید. ممکن است برخی از مثال‌ها/مستندات از رویکردها پیروی نکنند، عمدتاً برای سادگی/کوتاهی. :::tip قطعه کدهای زیر بر ساختار حالت تمرکز دارند. در عمل، ممکن است بخواهید: - از `Equatable` ارث‌بری کنید از [`package:equatable`](https://pub.dev/packages/equatable) - کلاس را با `@Data()` از [`package:data_class`](https://pub.dev/packages/data_class) حاشیه‌نویسی کنید - کلاس را با **@immutable** از [`package:meta`](https://pub.dev/packages/meta) حاشیه‌نویسی کنید - یک متد `copyWith` پیاده‌سازی کنید - از کلمه کلیدی `const` برای سازنده‌ها استفاده کنید ::: ## کلاس پایه و Enum وضعیت این رویکرد از یک **کلاس پایه واحد** برای همه حالت‌ها به همراه یک `enum` که وضعیت‌های مختلف را نشان می‌دهد تشکیل شده است. ویژگی‌ها nullable هستند و بر اساس وضعیت فعلی مدیریت می‌شوند. این رویکرد برای حالت‌هایی که کاملاً انحصاری نیستند و/یا دارای ویژگی‌های مشترک زیادی هستند بهترین کار را می‌کند. #### مزایا - **ساده**: مدیریت یک کلاس و یک enum وضعیت آسان است و همه ویژگی‌ها به راحتی قابل دسترسی هستند. - **کوتاه**: به طور کلی نیاز به خطوط کد کمتری نسبت به رویکردهای دیگر دارد. #### معایب - **ایمن از نظر نوع نیست**: نیاز به بررسی `status` قبل از دسترسی به ویژگی‌ها دارد. امکان `emit` کردن یک حالت نادرست وجود دارد که می‌تواند منجر به باگ شود. ویژگی‌ها برای حالت‌های خاص nullable هستند، که مدیریت آنها می‌تواند دشوار باشد و نیاز به باز کردن اجباری یا انجام بررسی‌های null دارد. برخی از این معایب می‌توانند با نوشتن تست‌های واحد و نوشتن سازنده‌های تخصصی، نام‌گذاری شده کاهش یابند. - **متورم**: منجر به یک حالت واحد می‌شود که می‌تواند با گذشت زمان با ویژگی‌های زیادی متورم شود. #### حکم این رویکرد برای حالت‌های ساده یا زمانی که نیازها حالت‌هایی را که انحصاری نیستند (مثلاً نمایش یک snackbar هنگام وقوع خطا در حالی که هنوز داده‌های قدیمی از آخرین حالت موفقیت را نشان می‌دهد) فراخوانی می‌کنند بهترین کار را می‌کند. این رویکرد انعطاف‌پذیری و کوتاهی را با هزینه ایمنی نوع فراهم می‌کند. ## کلاس Sealed و زیرکلاس‌ها این رویکرد از یک **کلاس sealed** که هر ویژگی مشترکی را نگه می‌دارد و چندین زیرکلاس برای حالت‌های جداگانه تشکیل شده است. این رویکرد عالی برای حالت‌های جداگانه، انحصاری است. #### مزایا - **ایمن از نظر نوع**: کد ایمن از کامپایل است و امکان دسترسی تصادفی به یک ویژگی نامعتبر وجود ندارد. هر زیرکلاس ویژگی‌های خود را نگه می‌دارد، که مشخص می‌کند کدام ویژگی‌ها به کدام حالت تعلق دارند. - **صریح**: ویژگی‌های مشترک را از ویژگی‌های خاص حالت جدا می‌کند. - **جامع**: استفاده از یک عبارت `switch` برای بررسی جامع بودن برای اطمینان از اینکه هر حالت به طور صریح مدیریت می‌شود. - اگر نمی‌خواهید [switching جامع](https://dart.dev/language/branches#exhaustiveness-checking) یا می‌خواهید بتوانید زیرنوع‌ها را بعداً بدون شکستن API اضافه کنید، از اصلاح‌کننده [final](https://dart.dev/language/class-modifiers#final) استفاده کنید. - برای جزئیات بیشتر، [مستندات کلاس sealed](https://dart.dev/language/class-modifiers#sealed) را ببینید. #### معایب - **طولانی**: نیاز به کد بیشتری دارد (یک کلاس پایه و یک زیرکلاس برای هر حالت). همچنین ممکن است نیاز به کد تکراری برای ویژگی‌های مشترک در زیرکلاس‌ها داشته باشد. - **پیچیده**: افزودن ویژگی‌های جدید نیاز به به‌روزرسانی هر زیرکلاس و کلاس پایه دارد، که می‌تواند دشوار باشد و منجر به افزایش پیچیدگی حالت شود. علاوه بر این، ممکن است نیاز به بررسی نوع غیرضروری/زیادی برای دسترسی به ویژگی‌ها داشته باشد. #### حکم این رویکرد برای حالت‌های انحصاری، خوب تعریف‌شده با ویژگی‌های منحصر به فرد بهترین کار را می‌کند. این رویکرد ایمنی نوع و بررسی‌های جامع بودن را فراهم می‌کند و بر ایمنی نسبت به کوتاهی و سادگی تأکید دارد. ================================================ FILE: docs/src/content/docs/fa/naming-conventions.mdx ================================================ --- title: قراردادهای نامگذاری description: مروری بر قوانین نامگذاری توصیه شده هنگام استفاده از بلوک. --- import EventExamplesGood1 from '~/components/naming-conventions/EventExamplesGood1Snippet.astro'; import EventExamplesBad1 from '~/components/naming-conventions/EventExamplesBad1Snippet.astro'; import StateExamplesGood1Snippet from '~/components/naming-conventions/StateExamplesGood1Snippet.astro'; import SingleStateExamplesGood1Snippet from '~/components/naming-conventions/SingleStateExamplesGood1Snippet.astro'; import StateExamplesBad1Snippet from '~/components/naming-conventions/StateExamplesBad1Snippet.astro'; قراردادهای نامگذاری زیر صرفاً توصیه شده و کاملاً اختیاری هستند. با خیال راحت از هر گونه قرارداد نامگذاری که ترجیح می دهید استفاده کنید. ممکن است دریابید که برخی از مثال‌ها/اسناد اصولاً به دلیل سادگی/مختصر بودن از قراردادهای نام‌گذاری پیروی نمی‌کنند. این قرارداد ها به شدت برای پروژه های بزرگ با توسعه دهندگان متعدد توصیه می شود. ## قراردادهای کلاس رویداد رویدادها باید با **گذشته ساده** نامگذاری شوند، زیرا رویدادها چیزهایی هستند که از دیدگاه Bloc قبلاً اتفاق افتاده اند. ### ساختار `BlocSubject` + `Noun (optional)` + `Verb (event)` رویدادهای لود اولیه باید این قرارداد را دنبال کنند: `BlocSubject` + `Started` :::note کلاس های رویداد پایه باید این نامگذاری را داشته باشند: `BlocSubject` + `Event`. ::: ### مثال ها ✅ **خوب** ❌ **بد** ## قراردادهای کلاس وضعیت وضعیت‌ها باید اسم باشند، چرا که یک وضعیت فقط یک لحظه‌ی خاص در زمان را نمایان می‌کند. دو روش رایج برای نمایش وضعیت وجود دارد: استفاده از زیرکلاس‌ها (Subclasses) یا استفاده از یک کلاس تکی (Single class). ### ساختار #### زیرکلاس‌ها `BlocSubject` + `Verb (action)` + `State` هنگام نمایش وضعیت به عنوان چندین زیرکلاس، `State` باید یکی از موارد زیر را دارا باشد: `Initial` | `Success` | `Failure` | `InProgress` :::note وضعیت‌های اولیه باید طبق این قرار داد عمل کنند: `BlocSubject` + `Initial`. ::: #### کلاس تکی `BlocSubject` + `State` هنگام نمایش وضعیت به عنوان یک کلاس پایه تکی، باید از یک enum با نام `BlocSubject` + `Status` برای نمایش وضعیت‌های مختلف استفاده شود: `initial` | `success` | `failure` | `loading`. :::note کلاس وضعیت پایه همیشه باید به این روش نام گذاری شوند: `BlocSubject` + `State`. ::: ### مثال ها ✅ **خوب** ##### زیرکلاس‌ها ##### کلاس تکی ❌ **بد** ================================================ FILE: docs/src/content/docs/fa/testing.mdx ================================================ --- title: آزمایش کردن (Testing) description: اصول اولیه نحوه نوشتن تست برای Bloc های خود. --- import CounterBlocSnippet from '~/components/testing/CounterBlocSnippet.astro'; import AddDevDependenciesSnippet from '~/components/testing/AddDevDependenciesSnippet.astro'; import CounterBlocTestImportsSnippet from '~/components/testing/CounterBlocTestImportsSnippet.astro'; import CounterBlocTestMainSnippet from '~/components/testing/CounterBlocTestMainSnippet.astro'; import CounterBlocTestSetupSnippet from '~/components/testing/CounterBlocTestSetupSnippet.astro'; import CounterBlocTestInitialStateSnippet from '~/components/testing/CounterBlocTestInitialStateSnippet.astro'; import CounterBlocTestBlocTestSnippet from '~/components/testing/CounterBlocTestBlocTestSnippet.astro'; Bloc به گونه ای طراحی شده است که آزمایش آن بسیار آسان باشد.در این بخش، نحوه تست واحد (Unit Test) یک Bloc را توضیح خواهیم داد. به خاطر سادگی، بیایید تست ها را برای `CounterBloc` که در [مفاهیم اصلی](/fa/bloc-concepts) ایجاد کردیم بنویسیم. برای خلاصه‌ی مطلب، پیاده‌سازی `CounterBloc` به شکل زیر است: ## راه اندازی (Setup) قبل از شروع نوشتن تست های خود، باید یک چارچوب آزمایشی (Testing Framework) را به وابستگی های خود اضافه کنیم. ما باید [test](https://pub.dev/packages/test) و [bloc_test](https://pub.dev/packages/bloc_test) را به پروژه خود اضافه کنیم. ## آزمایش کردن بیایید با ایجاد فایل تست برای `CounterBloc`، به نام `counter_bloc_test.dart`، شروع کنیم و بسته تست را وارد کنیم. بعداز آن، باید `main` و گروه تست خود را ایجاد کنیم. :::note گروه‌ها برای سازماندهی تست‌های فردی (Individual) و همچنین برای ایجاد یک محیط (Context) که در آن می‌توانید یک `setUp` و `tearDown` مشترک را در تمام تست‌های فردی به اشتراک بگذارید، استفاده می‌شوند. ::: بیایید با ایجاد نمونه‌ای از `CounterBloc` خود که در تمامی تست‌هایمان استفاده خواهد شد، شروع کنیم. حالا می‌توانیم شروع به نوشتن تست‌های فردی خود کنیم. :::note می‌توانیم تمامی تست‌های خود را با استفاده از دستور `pub run test` اجرا کنیم. ::: در این نقطه باید تست اولیه ما را پاس کرده باشیم! حالا بیایید یک تست پیچیده‌تر را با استفاده از بسته [bloc_test](https://pub.dev/packages/bloc_test) بنویسیم. باید بتوانیم تست‌ها را اجرا کنیم و ببینیم که همه آنها پاس می‌شوند. این تمام چیزی است که در آن وجود دارد، آزمایش باید سریع باشد و ما باید هنگام ایجاد تغییرات و بازسازی کد خود احساس اطمینان کنیم. شما می‌توانید به برنامه [Weather App](https://github.com/felangel/bloc/tree/master/examples/flutter_weather) مراجعه کنید تا یک مثال از یک برنامه کاملاً تست شده را ببینید. ================================================ FILE: docs/src/content/docs/fa/why-bloc.mdx ================================================ --- title: چرا Bloc؟ description: یک مروری بر ویژگی‌هایی که باعث می‌شود بلاک یک راه حل مدیریت وضعیت قوی باشد. sidebar: order: 1 --- Bloc به شما امکان می‌دهد به راحتی لایه نمایش (Presentation) را از منطق کسب و کار (Business logic) جدا کنید، که این امر باعث می‌شود, کد شما قابلیت هایی مانند _سرعت بالا_، _آزمون آسان_ و _قابل استفاده مجدد_ را داشته باشد. وقتی اپلیکیشن‌های با کیفیت تولید می‌شوند، مدیریت وضعیت (State) به مسئله‌ای حیاتی تبدیل می‌شود. ما به عنوان توسعه دهندگان می خواهیم: - بدانیم درخواست ما در هر مقطع زمانی در چه وضعیتی است. - به راحتی هر مورد را آزمایش کنیم تا مطمئن شویم برنامه ما به درستی پاسخ می دهد. - هر تعامل کاربر را در برنامه خود ثبت کنیم تا بتوانیم تصمیمات مبتنی بر داده را اتخاذ کنیم. - به صورت بهینه و کارآمد کار کنیم و اجزای مختلف را, هم در داخل برنامه‌ی خود و هم در برنامه‌های دیگر استفاده مجدد کنیم. - امکان کار همزمان برای چندین توسعه‌دهنده و بدون هیچ مشکلی در یک کد پایه با رعایت الگوها و قواعد مشترک، فراهم باشد. - برنامه های سریع و پاسخگو ایجاد کنیم. بلاک برای برآورده کردن همه این نیازها و بسیاری دیگر طراحی شده است. همچنین، راه‌حل‌های مدیریت وضعیت (State Management) مختلفی وجود دارد و تصمیم گیری برای استفاده از یکی از آن‌ها ممکن است یک وظیفه سخت باشد. هیچ راه‌حل مدیریت وضعیتی کامل و بی‌نقص وجود ندارد! مهم این است که شما یکی را انتخاب کنید که برای تیم و پروژه شما بهترین عمل کند. Bloc با در نظر گرفتن سه ارزش اصلی طراحی شده است: - **ساده:** ساده درک شود و می‌تواند و توسط توسعه‌دهندگان با سطوح مهارتی متفاوت استفاده شود. - **قدرتمند:** با ترکیب کردن اجزای کوچکتر، به شما کمک می‌کند برنامه‌های شگفت‌انگیز و پیچیده‌ای را ایجاد کنید. - **قابل آزمایش:** با امکان تست آسان و سریع هر جنبه‌ای از برنامه، میتوان با اطمینان بیشتری به بهبود و تغییرات نرم‌افزاری پرداخت. بطور کلی، Bloc سعی می‌کند با تنظیم زمانی که یک تغییر وضعیت می‌تواند رخ دهد و اجرای یک روش یکتا برای تغییر وضعیت در سراسر برنامه، تغییرات وضعیت را قابل پیش‌بینی کند. ================================================ FILE: docs/src/content/docs/faqs.mdx ================================================ --- title: FAQs description: Answers to frequently asked questions regarding the bloc library. --- import StateNotUpdatingGood1Snippet from '~/components/faqs/StateNotUpdatingGood1Snippet.astro'; import StateNotUpdatingGood2Snippet from '~/components/faqs/StateNotUpdatingGood2Snippet.astro'; import StateNotUpdatingGood3Snippet from '~/components/faqs/StateNotUpdatingGood3Snippet.astro'; import StateNotUpdatingBad1Snippet from '~/components/faqs/StateNotUpdatingBad1Snippet.astro'; import StateNotUpdatingBad2Snippet from '~/components/faqs/StateNotUpdatingBad2Snippet.astro'; import StateNotUpdatingBad3Snippet from '~/components/faqs/StateNotUpdatingBad3Snippet.astro'; import EquatableEmitSnippet from '~/components/faqs/EquatableEmitSnippet.astro'; import EquatableBlocTestSnippet from '~/components/faqs/EquatableBlocTestSnippet.astro'; import NoEquatableBlocTestSnippet from '~/components/faqs/NoEquatableBlocTestSnippet.astro'; import SingleStateSnippet from '~/components/faqs/SingleStateSnippet.astro'; import SingleStateUsageSnippet from '~/components/faqs/SingleStateUsageSnippet.astro'; import BlocProviderGood1Snippet from '~/components/faqs/BlocProviderGood1Snippet.astro'; import BlocProviderGood2Snippet from '~/components/faqs/BlocProviderGood2Snippet.astro'; import BlocProviderBad1Snippet from '~/components/faqs/BlocProviderBad1Snippet.astro'; import BlocInternalAddEventSnippet from '~/components/faqs/BlocInternalAddEventSnippet.astro'; import BlocInternalEventSnippet from '~/components/faqs/BlocInternalEventSnippet.astro'; import BlocExternalForEachSnippet from '~/components/faqs/BlocExternalForEachSnippet.astro'; ## State Not Updating ❔ **Question**: I'm emitting a state in my bloc but the UI is not updating. What am I doing wrong? 💡 **Answer**: If you're using Equatable make sure to pass all properties to the props getter. ✅ **GOOD** ❌ **BAD** In addition, make sure you are emitting a new instance of the state in your bloc. ✅ **GOOD** ❌ **BAD** :::caution `Equatable` properties should always be copied rather than modified. If an `Equatable` class contains a `List` or `Map` as properties, be sure to use `List.of` or `Map.of` respectively to ensure that equality is evaluated based on the values of the properties rather than the reference. ::: ## When to use Equatable ❔**Question**: When should I use Equatable? 💡**Answer**: In the above scenario if `StateA` extends `Equatable` only one state change will occur (the second emit will be ignored). In general, you should use `Equatable` if you want to optimize your code to reduce the number of rebuilds. You should not use `Equatable` if you want the same state back-to-back to trigger multiple transitions. In addition, using `Equatable` makes it much easier to test blocs since we can expect specific instances of bloc states rather than using `Matchers` or `Predicates`. Without `Equatable` the above test would fail and would need to be rewritten like: ## Handling Errors ❔ **Question**: How can I handle an error while still showing previous data? 💡 **Answer**: This highly depends on how the state of the bloc has been modeled. In cases where data should still be retained even in the presence of an error, consider using a single state class. This will allow widgets to have access to the `data` and `error` properties simultaneously and the bloc can use `state.copyWith` to retain old data even when an error has occurred. ## Bloc vs. Redux ❔ **Question**: What's the difference between Bloc and Redux? 💡 **Answer**: BLoC is a design pattern that is defined by the following rules: 1. Input and Output of the BLoC are simple Streams and Sinks. 2. Dependencies must be injectable and Platform agnostic. 3. No platform branching is allowed. 4. Implementation can be whatever you want as long as you follow the above rules. The UI guidelines are: 1. Each "complex enough" component has a corresponding BLoC. 2. Components should send inputs "as is". 3. Components should show outputs as close as possible to "as is". 4. All branching should be based on simple BLoC boolean outputs. The Bloc Library implements the BLoC Design Pattern and aims to abstract RxDart in order to simplify the developer experience. The three principles of Redux are: 1. Single source of truth 2. State is read-only 3. Changes are made with pure functions The bloc library violates the first principle; with bloc state is distributed across multiple blocs. Furthermore, there is no concept of middleware in bloc and bloc is designed to make async state changes very easy, allowing you to emit multiple states for a single event. ## Bloc vs. Provider ❔ **Question**: What's the difference between Bloc and Provider? 💡 **Answer**: `provider` is designed for dependency injection (it wraps `InheritedWidget`). You still need to figure out how to manage your state (via `ChangeNotifier`, `Bloc`, `Mobx`, etc...). The Bloc Library uses `provider` internally to make it easy to provide and access blocs throughout the widget tree. ## BlocProvider.of() Fails to Find Bloc ❔ **Question**: When using `BlocProvider.of(context)` it cannot find the bloc. How can I fix this? 💡 **Answer**: You cannot access a bloc from the same context in which it was provided so you must ensure `BlocProvider.of()` is called within a child `BuildContext`. ✅ **GOOD** ❌ **BAD** ## Project Structure ❔ **Question**: How should I structure my project? 💡 **Answer**: While there is really no right/wrong answer to this question, some recommended references are - [I/O Photobooth](https://github.com/flutter/photobooth) - [I/O Pinball](https://github.com/flutter/pinball) - [Flutter News Toolkit](https://github.com/flutter/news_toolkit) The most important thing is having a **consistent** and **intentional** project structure. ## Adding Events within a Bloc ❔ **Question**: Is it okay to add events within a bloc? 💡 **Answer**: In most cases, events should be added externally but in some select cases it may make sense for events to be added internally. The most common situation in which internal events are used is when state changes must occur in response to real-time updates from a repository. In these situations, the repository is the stimulus for the state change instead of an external event such as a button tap. In the following example, the state of `MyBloc` is dependent on the current user which is exposed via the `Stream` from the `UserRepository`. `MyBloc` listens for changes in the current user and adds an internal `_UserChanged` event whenever a user is emitted from the user stream. By adding an internal event, we are also able to specify a custom `transformer` for the event to determine how multiple `_UserChanged` events will be processed -- by default they will be processed concurrently. It's highly recommended that internal events are private. This is an explicit way of signaling that a specific event is used only within the bloc itself and prevents external components from knowing about the event. We can alternatively define an external `Started` event and use the `emit.forEach` API to handle reacting to real-time user updates: The benefits of the above approach are: - We do not need an internal `_UserChanged` event - We do not need to manage the `StreamSubscription` manually - We have full control over when the bloc subscribes to the stream of user updates The drawbacks of the above approach are: - We cannot easily `pause` or `resume` the subscription - We need to expose a public `Started` event which must be added externally - We cannot use a custom `transformer` to adjust how we react to user updates ## Exposing Public Methods ❔ **Question**: Is it okay to expose public methods on my bloc and cubit instances? 💡 **Answer** When creating a cubit, it's recommended to only expose public methods for the purposes of triggering state changes. As a result, generally all public methods on a cubit instance should return `void` or `Future`. When creating a bloc, it's recommended to avoid exposing any custom public methods and instead notify the bloc of events by calling `add`. ================================================ FILE: docs/src/content/docs/fil/getting-started.mdx ================================================ --- title: Panimulang Hakbang description: Lahat ng mga kailangan mo upang magamit ang Bloc. --- import InstallationTabs from '~/components/getting-started/InstallationTabs.astro'; import ImportTabs from '~/components/getting-started/ImportTabs.astro'; ## Packages Ang ecosystem ng bloc ay binubuo ng maraming mga package na nakalista sa ibaba: | Package | Description | Link | | ------------------------------------------------------------------------------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | AngularDart Components | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | Core Dart APIs | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | Event Transformers | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | Custom Linter | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | Testing APIs | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | Command-line Tools | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | Flutter Widgets | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | Caching/Persistence Support | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | Undo/Redo Support | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | ## Paggamit :::note Upang magsimula sa paggamit ng bloc, kinakailangan mo ng naka-install na [Dart SDK](https://dart.dev/get-dart) sa iyong machine. ::: ## Imports Ngayong na-install na natin ang bloc, maaari tayong gumawa ng `main.dart` at i-import ang gagamiting bloc na package. ================================================ FILE: docs/src/content/docs/fil/index.mdx ================================================ --- template: splash title: Bloc State Management Library description: Official documentation for the bloc state management library. Support for Dart, Flutter, and AngularDart. Includes examples and tutorials. banner: content: | ✨ Bisitahin ang Bloc Shop ✨ editUrl: false lastUpdated: false hero: title: Bloc v9.2.0 tagline: Isang library para sa maaasahang pamamahala ng state sa Dart image: alt: Bloc logo file: ~/assets/bloc.svg actions: - text: Magsimula link: /fil/getting-started/ variant: primary icon: rocket - text: Bisitahin sa GitHub link: https://github.com/felangel/bloc icon: github variant: secondary --- import { CardGrid } from '@astrojs/starlight/components'; import SponsorsGrid from '~/components/landing/SponsorsGrid.astro'; import Card from '~/components/landing/Card.astro'; import ListCard from '~/components/landing/ListCard.astro'; import SplitCard from '~/components/landing/SplitCard.astro'; import Discord from '~/components/landing/Discord.astro';
```sh # Idagdag ang bloc sa iyong proyekto. dart pub add bloc ``` Ang aming [gabay sa pagsisimula](/fil/getting-started) ay naglalaman ng mga sunod-sunod na instruction kung paano magsimula sa paggamit ng Bloc sa loob lamang ng ilang minuto. Kumpletuhin [ang opisyal na pagtuturo](/fil/tutorials/flutter-counter) para matutuhan ang tamang practices at gumawa ng iba't-ibang apps gamit si Bloc. Siyasatin ang mga [halimbawang apps](https://github.com/felangel/bloc/tree/master/examples) na may mataas na kalidad at puno ng test tulad ng counter, timer, infinite list, weather, todo at iba pa! - [Bakit Bloc?](/fil/why-bloc) - [Mahahalagang Konsepto](/fil/bloc-concepts) - [Arkitektura](/fil/architecture) - [Pag-test](/fil/testing) - [Kumbensyon sa Pagpangalan](/fil/naming-conventions) - [FAQs](/fil/faqs) - [Integrasyon sa VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) - [Integrasyon sa IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) - [Integrasyon sa Neovim](https://github.com/wa11breaker/flutter-bloc.nvim) - [Integrasyon sa Mason CLI](https://github.com/felangel/bloc/blob/master/bricks/README.md) - [Personal na Template](https://brickhub.dev/search?q=bloc) - [Integrasyon sa Developer Tools](https://github.com/felangel/bloc/blob/master/packages/bloc_tools/README.md) ================================================ FILE: docs/src/content/docs/fil/why-bloc.mdx ================================================ --- title: Bakit Bloc? description: Kabuuang idea kung bakit ang Bloc ay isang solid na solusyon sa pamamahala ng state. sidebar: order: 1 --- Pinadadali ng Bloc na ihiwalay ang presentation sa business logic, ginawawang _mabilis_, _madali i-test_, at _reusable_ ang iyong code. Sa pagbuo ng mga application na may kalidad para sa produksiyon, naging kritikal ang pamamahala ng state. Bilang developers gusto nating: - malaman kung anong state ng ating application anumang oras. - madaling i-test ang bawat kalagayan sa application upang tiyakin na ito ay tumutugon nang naaayon. - i-record ang bawat interaksyon ng user sa ating application upang magkaruon ng mga desisyon na data-driven. - makapagtrabaho nang maayos at epektibo at gamitin muli ang ilang mga bahagi ng application sa iba pang application. - magkaruon ng maraming developers na maayos na nagtatrabaho sa loob ng isang code base na sinusunod ang parehong mga pattern at kumbensiyon. - gumawa ng mabilis at responsibong mga application. Ang Bloc ay idinisenyo upang matugunan ang lahat ng pangangailangan na ito at marami pang iba. Marami ang mga solusyon sa pamamahala ng state at ang pagpili kung alin ang gagamitin ay maaaring maging isang masalimuot na gawain. Wala namang perpektong solusyon sa pamamahala ng state! Ang mahalaga ay pumili ka ng isang solusyon na pinakamabuti para sa iyong mga kasama at proyekto. Ang Bloc ay idinisenyo na may tatlong pangunahing halaga sa isipan: - **Simple:** Madaling maunawaan at maaaring gamitin ng mga developer na may iba't ibang antas ng kasanayan. - **Mabisa:** Makatulong na gumawa ng kamangha-mangha at kumplikadong mga application sa pamamagitan ng pag-compose ng mga ito mula sa mas maliit na mga bahagi. - **Pwedeng I-test:** Madaling mai-test ang bawat bahagi ng application upang magkaruon tayo ng kumpiyansa sa bawat pagbabago. Sa kabuuan, ang Bloc ay sumusubok na gawing maaasahan ang mga pagbabago sa state sa pamamagitan ng pagre-regulate kung kailan maaaring mangyari ang pagbabago sa state at ipinapatupad ang iisang paraan ng pagbabago ng state sa buong application. ================================================ FILE: docs/src/content/docs/flutter-bloc-concepts.mdx ================================================ --- title: Flutter Bloc Concepts description: An overview of the core concepts for package:flutter_bloc. sidebar: order: 2 --- import BlocBuilderSnippet from '~/components/concepts/flutter-bloc/BlocBuilderSnippet.astro'; import BlocBuilderExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocBuilderExplicitBlocSnippet.astro'; import BlocBuilderConditionSnippet from '~/components/concepts/flutter-bloc/BlocBuilderConditionSnippet.astro'; import BlocSelectorSnippet from '~/components/concepts/flutter-bloc/BlocSelectorSnippet.astro'; import BlocProviderSnippet from '~/components/concepts/flutter-bloc/BlocProviderSnippet.astro'; import BlocProviderEagerSnippet from '~/components/concepts/flutter-bloc/BlocProviderEagerSnippet.astro'; import BlocProviderValueSnippet from '~/components/concepts/flutter-bloc/BlocProviderValueSnippet.astro'; import BlocProviderLookupSnippet from '~/components/concepts/flutter-bloc/BlocProviderLookupSnippet.astro'; import NestedBlocProviderSnippet from '~/components/concepts/flutter-bloc/NestedBlocProviderSnippet.astro'; import MultiBlocProviderSnippet from '~/components/concepts/flutter-bloc/MultiBlocProviderSnippet.astro'; import BlocListenerSnippet from '~/components/concepts/flutter-bloc/BlocListenerSnippet.astro'; import BlocListenerExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocListenerExplicitBlocSnippet.astro'; import BlocListenerConditionSnippet from '~/components/concepts/flutter-bloc/BlocListenerConditionSnippet.astro'; import NestedBlocListenerSnippet from '~/components/concepts/flutter-bloc/NestedBlocListenerSnippet.astro'; import MultiBlocListenerSnippet from '~/components/concepts/flutter-bloc/MultiBlocListenerSnippet.astro'; import BlocConsumerSnippet from '~/components/concepts/flutter-bloc/BlocConsumerSnippet.astro'; import BlocConsumerConditionSnippet from '~/components/concepts/flutter-bloc/BlocConsumerConditionSnippet.astro'; import RepositoryProviderSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderSnippet.astro'; import RepositoryProviderLookupSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderLookupSnippet.astro'; import RepositoryProviderDisposeSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderDisposeSnippet.astro'; import NestedRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/NestedRepositoryProviderSnippet.astro'; import MultiRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/MultiRepositoryProviderSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/flutter-bloc/CounterBlocSnippet.astro'; import CounterMainSnippet from '~/components/concepts/flutter-bloc/CounterMainSnippet.astro'; import CounterPageSnippet from '~/components/concepts/flutter-bloc/CounterPageSnippet.astro'; import WeatherRepositorySnippet from '~/components/concepts/flutter-bloc/WeatherRepositorySnippet.astro'; import WeatherMainSnippet from '~/components/concepts/flutter-bloc/WeatherMainSnippet.astro'; import WeatherAppSnippet from '~/components/concepts/flutter-bloc/WeatherAppSnippet.astro'; import WeatherPageSnippet from '~/components/concepts/flutter-bloc/WeatherPageSnippet.astro'; :::note Please make sure to carefully read the following sections before working with [`package:flutter_bloc`](https://pub.dev/packages/flutter_bloc). ::: :::note All widgets exported by the `flutter_bloc` package integrate with both `Cubit` and `Bloc` instances. ::: ## Bloc Widgets ### BlocBuilder **BlocBuilder** is a Flutter widget which requires a `Bloc` and a `builder` function. `BlocBuilder` handles building the widget in response to new states. `BlocBuilder` is very similar to `StreamBuilder` but has a more simple API to reduce the amount of boilerplate code needed. The `builder` function will potentially be called many times and should be a [pure function](https://en.wikipedia.org/wiki/Pure_function) that returns a widget in response to the state. See `BlocListener` if you want to "do" anything in response to state changes such as navigation, showing a dialog, etc... If the `bloc` parameter is omitted, `BlocBuilder` will automatically perform a lookup using `BlocProvider` and the current `BuildContext`. Only specify the bloc if you wish to provide a bloc that will be scoped to a single widget and isn't accessible via a parent `BlocProvider` and the current `BuildContext`. For fine-grained control over when the `builder` function is called an optional `buildWhen` can be provided. `buildWhen` takes the previous bloc state and current bloc state and returns a boolean. If `buildWhen` returns true, `builder` will be called with `state` and the widget will rebuild. If `buildWhen` returns false, `builder` will not be called with `state` and no rebuild will occur. ### BlocSelector **BlocSelector** is a Flutter widget which is analogous to `BlocBuilder` but allows developers to filter updates by selecting a new value based on the current bloc state. Unnecessary builds are prevented if the selected value does not change. The selected value must be immutable in order for `BlocSelector` to accurately determine whether `builder` should be called again. If the `bloc` parameter is omitted, `BlocSelector` will automatically perform a lookup using `BlocProvider` and the current `BuildContext`. ### BlocProvider **BlocProvider** is a Flutter widget which provides a bloc to its children via `BlocProvider.of(context)`. It is used as a dependency injection (DI) widget so that a single instance of a bloc can be provided to multiple widgets within a subtree. In most cases, `BlocProvider` should be used to create new blocs which will be made available to the rest of the subtree. In this case, since `BlocProvider` is responsible for creating the bloc, it will automatically handle closing the bloc. By default, `BlocProvider` will create the bloc lazily, meaning `create` will get executed when the bloc is looked up via `BlocProvider.of(context)`. To override this behavior and force `create` to be run immediately, `lazy` can be set to `false`. In some cases, `BlocProvider` can be used to provide an existing bloc to a new portion of the widget tree. This will be most commonly used when an existing bloc needs to be made available to a new route. In this case, `BlocProvider` will not automatically close the bloc since it did not create it. then from either `ChildA`, or `ScreenA` we can retrieve `BlocA` with: ### MultiBlocProvider **MultiBlocProvider** is a Flutter widget that merges multiple `BlocProvider` widgets into one. `MultiBlocProvider` improves the readability and eliminates the need to nest multiple `BlocProviders`. By using `MultiBlocProvider` we can go from: to: :::caution When a `BlocProvider` is defined within the context of a `MultiBlocProvider`, any `child` will be ignored. ::: ### BlocListener **BlocListener** is a Flutter widget which takes a `BlocWidgetListener` and an optional `Bloc` and invokes the `listener` in response to state changes in the bloc. It should be used for functionality that needs to occur once per state change such as navigation, showing a `SnackBar`, showing a `Dialog`, etc... `listener` is only called once for each state change (**NOT** including the initial state) unlike `builder` in `BlocBuilder` and is a `void` function. If the `bloc` parameter is omitted, `BlocListener` will automatically perform a lookup using `BlocProvider` and the current `BuildContext`. Only specify the bloc if you wish to provide a bloc that is otherwise not accessible via `BlocProvider` and the current `BuildContext`. For fine-grained control over when the `listener` function is called an optional `listenWhen` can be provided. `listenWhen` takes the previous bloc state and current bloc state and returns a boolean. If `listenWhen` returns true, `listener` will be called with `state`. If `listenWhen` returns false, `listener` will not be called with `state`. ### MultiBlocListener **MultiBlocListener** is a Flutter widget that merges multiple `BlocListener` widgets into one. `MultiBlocListener` improves the readability and eliminates the need to nest multiple `BlocListeners`. By using `MultiBlocListener` we can go from: to: :::caution When a `BlocListener` is defined within the context of a `MultiBlocListener`, any `child` will be ignored. ::: ### BlocConsumer **BlocConsumer** exposes a `builder` and `listener` in order to react to new states. `BlocConsumer` is analogous to a nested `BlocListener` and `BlocBuilder` but reduces the amount of boilerplate needed. `BlocConsumer` should only be used when it is necessary to both rebuild UI and execute other reactions to state changes in the `bloc`. `BlocConsumer` takes a required `BlocWidgetBuilder` and `BlocWidgetListener` and an optional `bloc`, `BlocBuilderCondition`, and `BlocListenerCondition`. If the `bloc` parameter is omitted, `BlocConsumer` will automatically perform a lookup using `BlocProvider` and the current `BuildContext`. An optional `listenWhen` and `buildWhen` can be implemented for more granular control over when `listener` and `builder` are called. The `listenWhen` and `buildWhen` will be invoked on each `bloc` `state` change. They each take the previous `state` and current `state` and must return a `bool` which determines whether or not the `builder` and/or `listener` function will be invoked. The previous `state` will be initialized to the `state` of the `bloc` when the `BlocConsumer` is initialized. `listenWhen` and `buildWhen` are optional and if they aren't implemented, they will default to `true`. ### RepositoryProvider **RepositoryProvider** is a Flutter widget which provides a repository to its children via `RepositoryProvider.of(context)`. It is used as a dependency injection (DI) widget so that a single instance of a repository can be provided to multiple widgets within a subtree. `BlocProvider` should be used to provide blocs whereas `RepositoryProvider` should only be used for repositories. then from `ChildA` we can retrieve the `Repository` instance with: Repositories that manage resources which must be disposed can do so via the `dispose` callback: ### MultiRepositoryProvider **MultiRepositoryProvider** is a Flutter widget that merges multiple `RepositoryProvider` widgets into one. `MultiRepositoryProvider` improves the readability and eliminates the need to nest multiple `RepositoryProvider`. By using `MultiRepositoryProvider` we can go from: to: :::caution When a `RepositoryProvider` is defined within the context of a `MultiRepositoryProvider`, any `child` will be ignored. ::: ## BlocProvider Usage Lets take a look at how to use `BlocProvider` to provide a `CounterBloc` to a `CounterPage` and react to state changes with `BlocBuilder`. At this point we have successfully separated our presentational layer from our business logic layer. Notice that the `CounterPage` widget knows nothing about what happens when a user taps the buttons. The widget simply tells the `CounterBloc` that the user has pressed either the increment or decrement button. ## RepositoryProvider Usage We are going to take a look at how to use `RepositoryProvider` within the context of the [`flutter_weather`][flutter_weather_link] example. In our `main.dart`, we call `runApp` with our `WeatherApp` widget. We will inject our `WeatherRepository` instance into our widget tree via `RepositoryProvider`. When instantiating a bloc, we can access the instance of a repository via `context.read` and inject the repository into the bloc via constructor. :::tip If you have more than one repository, you can use `MultiRepositoryProvider` to provide multiple repository instances to the subtree. ::: :::note Use the `dispose` callback to handle freeing any resources when the `RepositoryProvider` is unmounted. ::: [flutter_weather_link]: https://github.com/felangel/bloc/blob/master/examples/flutter_weather ## Extension Methods [Extension methods](https://dart.dev/guides/language/extension-methods), introduced in Dart 2.7, are a way to add functionality to existing libraries. In this section, we'll take a look at extension methods included in `package:flutter_bloc` and how they can be used. `flutter_bloc` has a dependency on [package:provider](https://pub.dev/packages/provider) which simplifies the use of [`InheritedWidget`](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html). Internally, `package:flutter_bloc` uses `package:provider` to implement: `BlocProvider`, `MultiBlocProvider`, `RepositoryProvider` and `MultiRepositoryProvider` widgets. `package:flutter_bloc` exports the `ReadContext`, `WatchContext` and `SelectContext`, extensions from `package:provider`. :::note Learn more about [`package:provider`](https://pub.dev/packages/provider). ::: ### context.read `context.read()` looks up the closest ancestor instance of type `T` and is functionally equivalent to `BlocProvider.of(context)`. `context.read` is most commonly used for retrieving a bloc instance in order to add an event within `onPressed` callbacks. :::note `context.read()` does not listen to `T` -- if the provided `Object` of type `T` changes, `context.read` will not trigger a widget rebuild. ::: #### Usage ✅ **DO** use `context.read` to add events in callbacks. ```dart onPressed() { context.read().add(CounterIncrementPressed()), } ``` ❌ **AVOID** using `context.read` to retrieve state within a `build` method. ```dart @override Widget build(BuildContext context) { final state = context.read().state; return Text('$state'); } ``` The above usage is error prone because the `Text` widget will not be rebuilt if the state of the bloc changes. :::caution Use `BlocBuilder` or `context.watch` instead in order to rebuild in response to state changes. ::: ### context.watch Like `context.read()`, `context.watch()` provides the closest ancestor instance of type `T`, however it also listens to changes on the instance. It is functionally equivalent to `BlocProvider.of(context, listen: true)`. If the provided `Object` of type `T` changes, `context.watch` will trigger a rebuild. :::caution `context.watch` is only accessible within the `build` method of a `StatelessWidget` or `State` class. ::: #### Usage ✅ **DO** use `BlocBuilder` instead of `context.watch` to explicitly scope rebuilds. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (context, state) { // Whenever the state changes, only the Text is rebuilt. return Text(state.value); }, ), ), ); } ``` Alternatively, use a `Builder` to scope rebuilds. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Whenever the state changes, only the Text is rebuilt. final state = context.watch().state; return Text(state.value); }, ), ), ); } ``` ✅ **DO** use `Builder` and `context.watch` as `MultiBlocBuilder`. ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // return a Widget which depends on the state of BlocA, BlocB, and BlocC } ); ``` ❌ **AVOID** using `context.watch` when the parent widget in the `build` method doesn't depend on the state. ```dart @override Widget build(BuildContext context) { // Whenever the state changes, the MaterialApp is rebuilt // even though it is only used in the Text widget. final state = context.watch().state; return MaterialApp( home: Scaffold( body: Text(state.value), ), ); } ``` :::caution Using `context.watch` at the root of the `build` method will result in the entire widget being rebuilt when the bloc state changes. ::: ### context.select Just like `context.watch()`, `context.select(R function(T value))` provides the closest ancestor instance of type `T` and listens to changes on `T`. Unlike `context.watch`, `context.select` allows you listen for changes in a smaller part of a state. ```dart Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); } ``` The above will only rebuild the widget when the property `name` of the `ProfileBloc`'s state changes. #### Usage ✅ **DO** use `BlocSelector` instead of `context.select` to explicitly scope rebuilds. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector( selector: (state) => state.name, builder: (context, name) { // Whenever the state.name changes, only the Text is rebuilt. return Text(name); }, ), ), ); } ``` Alternatively, use a `Builder` to scope rebuilds. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Whenever state.name changes, only the Text is rebuilt. final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), ); } ``` ❌ **AVOID** using `context.select` when the parent widget in a build method doesn't depend on the state. ```dart @override Widget build(BuildContext context) { // Whenever the state.value changes, the MaterialApp is rebuilt // even though it is only used in the Text widget. final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), ); } ``` :::caution Using `context.select` at the root of the `build` method will result in the entire widget being rebuilt when the selection changes. ::: ================================================ FILE: docs/src/content/docs/fr/architecture.mdx ================================================ --- title: Architecture description: Vue d'ensemble des modèles d'architecture recommandés pour l'utilisation de Bloc. --- import DataProviderSnippet from '~/components/architecture/DataProviderSnippet.astro'; import RepositorySnippet from '~/components/architecture/RepositorySnippet.astro'; import BusinessLogicComponentSnippet from '~/components/architecture/BusinessLogicComponentSnippet.astro'; import BlocTightCouplingSnippet from '~/components/architecture/BlocTightCouplingSnippet.astro'; import BlocLooseCouplingPresentationSnippet from '~/components/architecture/BlocLooseCouplingPresentationSnippet.astro'; import AppIdeasRepositorySnippet from '~/components/architecture/AppIdeasRepositorySnippet.astro'; import AppIdeaRankingBlocSnippet from '~/components/architecture/AppIdeaRankingBlocSnippet.astro'; import PresentationComponentSnippet from '~/components/architecture/PresentationComponentSnippet.astro'; ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) L'utilisation de la librairie bloc nous permet de séparer notre application en trois couches : - Presentation - Logique Métier (Business Logic) - Data - Repository - Data Provider Nous allons commencer avec la couche la plus basse (la plus éloignée de l'interfacer utilisateur) et remonter jusqu'à la couche de Présentation. ## Couche Data Le rôle de la couche Data est de récupérer et de manipuler des données provenant d'une ou plusieurs sources. La couche Data peut être divisée en deux parties : - Repository - Data Provider Cette couche est le niveau le plus bas de l'application, elle interagit avec les bases de données, les requêtes réseau et d'autres sources de données asynchrones. ### Data Provider Le rôle du Data Provider est de fournir des données brutes. Il doit être générique et polyvalent. Le Data Provider permet généralement d’interagir avec des API simple afin d'effectuer des opérations [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete). Nous pourrions avoir des méthodes `createData`, `readData`, `updateData`, et `deleteData` qui feront partie de notre couche Data. ### Repository La couche Repository est un wrapper d'un ou plusieurs Data Provider avec lesquels la couche Bloc communique. Comme vous pouvez le constater, notre couche Repository peut interagir avec plusieurs Data Provider et effectuer des transformations sur les données avant de transmettre le résultat à la couche Logic Métier. ## Couche Logique Métier (Business Logic layer) La fonction de la couche de Logique Métier est de répondre aux entrées provenant de la couche Présentation avec de nouveaux états. Cette couche peut dépendre d'un ou plusieurs Repository afin de récupérer les données nécessaires à la construction de l'état de l'application. Considérez la couche Logique Métier comme le pont entre l'interface utilisateur (couche Présentation) et la couche Data. La couche Logique Métier est informée des événements/actions de la couche Présentation et communique alors avec le Repository afin de construire un nouvel état que la couche Présentation pourra appliquer. ### Communication de Bloc-à-Bloc Because blocs expose streams, it may be tempting to make a bloc which listens to another bloc. You should **not** do this. There are better alternatives than resorting to the code below: Comme les blocs mettent à disposition des flux, il peut être tentant de créer un bloc qui écoute un autre bloc. Vous ne devriez **pas** faire ça. Il existe de meilleures alternatives que le code ci-dessous : Bien que le code ci-dessus ne comporte pas d'erreurs, il présente un problème plus important : il crée une dépendance entre deux blocs. En règle générale, il faut éviter à tout prix les dépendances entre deux entités de la même couche architecturale, car elles créent un couplage fort difficile à maintenir. Étant donné que ces deux blocs résident tous les deux dans la couche architecturale de la Logique Métier, aucun des deux blocs ne devraient connaître l'existence de l'un autre bloc. ![Application Architecture Layers](~/assets/architecture/architecture.png) Un bloc ne doit recevoir des informations que par le biais d'événements et de Repository injectés (c'est-à-dire des Repository qui sont passés au bloc via son constructeur). Si vous êtes dans une situation où un bloc doit répondre à un autre bloc, vous avez deux autres options. Vous pouvez soit faire remonter le problème d'une couche (couche de Présentation), soit le faire descendre d'une couche (couche Domain). #### Relier des Blocs à travers la couche Présentation Vous pouvez utiliser un `BlocListener` pour écouter un bloc et ajouter un événement à un autre bloc lorsque le premier change. Le code ci-dessus empêche `SecondBloc` d'avoir besoin de connaître `FirstBloc`, créant ainsi un couplage faible. L'application [flutter_weather](/fr/tutorials/flutter-weather) [utilise cette technique](https://github.com/felangel/bloc/blob/b4c8db938ad71a6b60d4a641ec357905095c3965/examples/flutter_weather/lib/weather/view/weather_page.dart#L38-L42) pour changer le thème de l'application en fonction des informations météorologiques reçues. Dans certains cas, il n'est pas préférable de coupler deux blocs dans la couche Présentation. En revanche, il est souvent logique que deux blocs partagent la même source de données et se mettent à jour lorsque ces données changent. #### Relier des Blocs à travers la couche Domain Deux blocs peuvent écouter un flux provenant d'un Repository et mettre à jour leur état indépendamment l'un de l'autre chaque fois que les données du Repository changent. L'utilisation de Repository réactifs pour maintenir la synchronisation des états est courante dans les applications d'entreprise à grande échelle. En premier lieu, créez ou utilisez un Repository qui met à disposition un flux de données (`Stream`). Par exemple, le Repository suivant met à disposition un `Stream` infini comportant cinq idées d'applications. Le même Repository peut être injecté dans chaque bloc qui doit réagir aux nouvelles idées d'applications. Voici un `AppIdeaRankingBloc` qui crée un état pour chaque idée d'application provenant du Repository ci-dessus : Pour en savoir plus sur l'utilisation des flux avec Bloc, voir [How to use Bloc with streams and concurrency](https://verygood.ventures/blog/how-to-use-bloc-with-streams-and-concurrency). ## Couche Presentation Le rôle de la couche Presentation est de déterminer comment elle doit s'afficher, en fonction d'un ou plusieurs états de bloc. Elle doit également gérer les entrées de l'utilisateur et les événements du cycle de vie de l'application. La plupart des flux d'applications commencent par un événement `AppStart` qui déclenche la récupération des données qui vont être présentées à l'utilisateur. Dans ce scénario, la couche Presentation provoquerait un événement `AppStart`. En plus de cela, elle devra déterminer ce qu'il faut afficher à l'écran en fonction de l'état de la couche Bloc. Jusqu'à présent, bien que nous ayons vu quelques extraits de code, tout cela est resté assez théorique. Dans la section tutoriel, nous allons mettre en pratique ces concepts en réalisant plusieurs exemples d'applications. ================================================ FILE: docs/src/content/docs/fr/getting-started.mdx ================================================ --- title: Démarrer description: Tout le nécessaire pour débuter avec Bloc. --- import InstallationTabs from '~/components/getting-started/InstallationTabs.astro'; import ImportTabs from '~/components/getting-started/ImportTabs.astro'; ## Packages L'écosystème bloc contient plusieurs packages listés ci-dessous : | Package | Description | Lien | | ------------------------------------------------------------------------------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | AngularDart Components | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | Core Dart APIs | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | Event Transformers | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | Custom Linter | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | Testing APIs | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | Command-line Tools | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | Flutter Widgets | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | Caching/Persistence Support | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | Undo/Redo Support | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | ## Installation :::note Pour commencer à utiliser bloc, le [Dart SDK](https://dart.dev/get-dart) doit préalablement être installé sur votre machine. ::: ## Imports Maintenant que nous avons installé Bloc avec succès, nous pouvons créer notre fichier `main.dart` et importer le package `bloc`. ================================================ FILE: docs/src/content/docs/fr/index.mdx ================================================ --- template: splash title: Bloc State Management Library description: Documentation officielle de la bibliothèque de gestion d'état Bloc. Supportée pour Dart, Flutter et AngularDart. Inclut des exemples et des tutoriels. banner: content: | ✨ Visite le Bloc Shop ✨ editUrl: false lastUpdated: false hero: title: Bloc v9.2.0 tagline: Une bibliothèque de gestion d'états prévisibles pour Dart. image: alt: Bloc logo file: ~/assets/bloc.svg actions: - text: Démarrer link: /fr/getting-started/ variant: primary icon: rocket - text: Voir sur GitHub link: https://github.com/felangel/bloc icon: github variant: secondary --- import { CardGrid } from '@astrojs/starlight/components'; import SponsorsGrid from '~/components/landing/SponsorsGrid.astro'; import Card from '~/components/landing/Card.astro'; import ListCard from '~/components/landing/ListCard.astro'; import SplitCard from '~/components/landing/SplitCard.astro'; import Discord from '~/components/landing/Discord.astro';
```sh # Ajoute un bloc ton projet dart pub add bloc ``` Notre [guide de démarrage](/fr/getting-started) possède des instructions étape par étape pour commencer à utiliser Bloc en quelques minutes seulement. Complète les [tutoriels officiels](/fr/tutorials/flutter-counter) pour apprendre les meilleures pratiques et créer une variété d'applications avec Bloc. Explore des [applications demo](https://github.com/felangel/bloc/tree/master/examples) de qualité et entièrement testées telles qu'un compteur, un minuteur, une liste infinie, une application de météo, une todo-list et plus encore! - [Pourquoi utiliser Bloc?](/fr/why-bloc) - [Concepts fondamentaux](/fr/bloc-concepts) - [Architecture](/fr/architecture) - [Tester](/fr/testing) - [Conventions de nommage](/fr/naming-conventions) - [FAQs](/fr/faqs) - [Intégration pour VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) - [Intégration pour IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) - [Intégration pour Neovim](https://github.com/wa11breaker/flutter-bloc.nvim) - [Intégration de Mason CLI ](https://github.com/felangel/bloc/blob/master/bricks/README.md) - [Templates customisées](https://brickhub.dev/search?q=bloc) - [Outils de développeur](https://github.com/felangel/bloc/blob/master/packages/bloc_tools/README.md) ================================================ FILE: docs/src/content/docs/fr/lint/index.mdx ================================================ --- title: Vue d'ensemble du Linter description: Introduction à bloc_lint. sidebar: order: 1 --- import AvoidFlutterImportsWarningSnippet from '~/components/lint/ImportFlutterWarningSnippet.mdx'; import AvoidFlutterImportsWarningOutputSnippet from '~/components/lint/ImportFlutterWarningOutputSnippet.astro'; import InstallBlocToolsSnippet from '~/components/lint/InstallBlocToolsSnippet.astro'; import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import RunBlocLintInCurrentDirectorySnippet from '~/components/lint/RunBlocLintInCurrentDirectorySnippet.astro'; La vérification de code (linting) est un processus d'analyse statique du code qui permet d'identifier les bugs potentiels ainsi que les erreurs programmatiques et stylistiques. Bloc dispose d'un linter intégré, qui peut être utilisé via votre IDE ou la [CLI `bloc`](https://pub.dev/packages/bloc_tools) avec la commande `bloc lint`. Grâce au linter bloc, vous pouvez améliorer la qualité de votre codebase et assurer la cohérence sans avoir à écrire une seule ligne de code. Par exemple, si vous importez accidentellement une dépendance Flutter dans votre cubit : S'il est correctement configuré, le linter bloc pointera du doigt l'`import` et produira l'avertissement suivant. Dans les sections suivantes, nous verrons comment installer, configurer et personnaliser le linter bloc pour que vous puissiez profiter des avantages de l'analyse statique. ## Démarrage rapide Commencez à utiliser le linter bloc en quelques étapes simples et rapides. :::note Pour commencer à utiliser bloc, vous devez avoir le [SDK Dart](https://dart.dev/get-dart) installé sur votre machine. ::: 1. Installez la [CLI bloc](https://pub.dev/packages/bloc_tools) 1. Installez le [package bloc_lint](https://pub.dev/packages/bloc_lint) 1. Ajoutez un fichier `analysis_options.yaml` à la racine de votre projet avec les règles recommandées 1. Exécutez le linter Félicitations, c'est tout ce qu'il y avait à faire 🎉 Continuez la lecture de cette documentation pour un aperçu plus approfondi de la configuration et de la personnalisation du linter bloc. ================================================ FILE: docs/src/content/docs/fr/lint/installation.mdx ================================================ --- title: Installation du Linter description: Installation de bloc_lint. sidebar: order: 2 --- import { CardGrid } from '@astrojs/starlight/components'; import Card from '~/components/landing/Card.astro'; import InstallBlocToolsSnippet from '~/components/lint/InstallBlocToolsSnippet.astro'; import BlocToolsLintHelpOutputSnippet from '~/components/lint/BlocToolsLintHelpOutputSnippet.astro'; import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import BlocLintMultipleRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintMultipleRecommendedAnalysisOptionsSnippet.astro'; ## Outils en Ligne de Commande (CLI) Pour utiliser le linter depuis la ligne de commande, installez [`package:bloc_tools`](https://pub.dev/packages/bloc_tools) via la commande suivante : Une fois la CLI bloc installée, vous pouvez exécuter le linter bloc via la commande `bloc lint` : ## Set de Règles Recommandées Pour installer le set de règles de lint recommandées, installez [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) comme dépendance de développement via la commande suivante : `package:bloc_lint` Ensuite, ajoutez un fichier à la racine de votre projet avec le set de règles recommandées : `analysis_options.yaml` Si nécessaire, vous pouvez inclure plusieurs sets de règles en les définissant comme une liste : ## Intégrations dans les IDE Les IDE suivants prennent officiellement en charge le linter bloc et le language server (LSP) pour fournir des diagnostics instantanés directement dans votre IDE. La prise en charge du [plugin Bloc pour VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) est disponible à partir de la v6.8.0. La prise en charge du [plugin Bloc pour IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) est disponible à partir de la v4.1.0. ================================================ FILE: docs/src/content/docs/fr/testing.mdx ================================================ --- title: Tester description: Les bases de l'écriture de tests pour vos blocs. --- import CounterBlocSnippet from '~/components/testing/CounterBlocSnippet.astro'; import AddDevDependenciesSnippet from '~/components/testing/AddDevDependenciesSnippet.astro'; import CounterBlocTestImportsSnippet from '~/components/testing/CounterBlocTestImportsSnippet.astro'; import CounterBlocTestMainSnippet from '~/components/testing/CounterBlocTestMainSnippet.astro'; import CounterBlocTestSetupSnippet from '~/components/testing/CounterBlocTestSetupSnippet.astro'; import CounterBlocTestInitialStateSnippet from '~/components/testing/CounterBlocTestInitialStateSnippet.astro'; import CounterBlocTestBlocTestSnippet from '~/components/testing/CounterBlocTestBlocTestSnippet.astro'; Bloc a été conçu pour être extrêmement facile à tester. Dans cette section, nous allons voir comment tester un bloc de manière unitaire. Par souci de simplicité, nous allons écrire des tests pour le `CounterBloc` que nous avons créé dans [Core Concepts](/fr/bloc-concepts). Pour rappel, l'implémentation du `CounterBloc` ressemble à ceci: ## Configuration Avant de commencer à écrire nos tests, nous avons besoin d'ajouter des framework de test dans nos dépendances, Nous allons ajouter les packages [test](https://pub.dev/packages/test) et [bloc_test](https://pub.dev/packages/bloc_test) à notre projet. ## Tester Commençons par créer le fichier `counter_bloc_test.dart` afin de tester notre `CounterBloc` et importons le package de test. Ensuite, nous devons créer notre `main` ainsi que notre groupe de tests. :::note Les groupes servent à organiser des tests unitaires et à créer un contexte dans lequel vous pouvez partager un `setUp` et un `tearDown` communs à tous les tests du groupe. ::: Commençons par créer une instance de notre `CounterBloc` qui sera utilisée dans tous nos tests. Maintenant, nous pouvons commencer à écrire des tests unitaires. :::note Nous pouvons exécuter tous nos tests avec la commande `dart test`. ::: À ce stade, nous devrions avoir notre premier test réussi! Écrivons maintenant des tests plus complexes en utilisant le package [bloc_test](https://pub.dev/packages/bloc_test). Nous devrions être en mesure d'exécuter les tests et de constater qu'ils passent tous. C'est aussi simple que ça, tester devrait être un jeu d'enfant et nous devrions nous sentir en confiance lorsque nous apportons des modifications et que nous refactorisons notre code. Vous pouvez vous référer à l'application [Weather App](https://github.com/felangel/bloc/tree/master/examples/flutter_weather) pour un exemple d'application entièrement testée. ================================================ FILE: docs/src/content/docs/fr/why-bloc.mdx ================================================ --- title: Pourquoi utiliser Bloc? description: Un aperçu de ce qui fait de Bloc une solution solide pour la gestion d’états. sidebar: order: 1 --- Bloc permet de facilement séparer les composants visuels de la logique métier, rendant votre code `rapide`, `facile à tester` et `réutilisable`. Lorsqu'on développe des application de qualité destinées à être utilisé en production, la gestion des états devient critique. En tant que développeurs, nous souhaitons : - connaître l'état de notre application à tout moment. - tester facilement chaque cas pour nous assurer que notre application réagit correctement. - enregistrer chaque interaction utilisateur dans notre application - travailler de manière aussi efficace que possible et réutiliser des composants au sein de notre application ainsi que dans des applications différentes. - permettre à de nombreux développeurs de travailler en harmonie en suivant des modèles et des conventions communes. - développer des applications performantes et réactives. Bloc a été conçu pour répondre à tous ces besoins et bien plus encore. Il existe beaucoup de solutions pour gérer les états et décider laquelle utiliser peut être une tâche laborieuse. Il n'y a pas de solution parfaite! Ce qui est important est de choisir celle qui correspond le mieux à ton équipe et à ton projet. Bloc a été conçu avec trois principes fondamentaux en tête : - **Simple:** Facile à comprendre et utilisable par des développeurs de niveaux variés. - **Puissant:** Aide à faire des applications complexes en les assemblant à partir de composants plus simples. - **Testable:** Permet de tester facilement tous les aspects d'une application afin de pouvoir itérer en toute confiance. Globalement, Bloc tente de rendre les changements d'état prévisibles en réglementant le moment où un changement d'état peut se produire et en imposant une unique façon de changer d'état dans l'ensemble d'une application. ================================================ FILE: docs/src/content/docs/getting-started.mdx ================================================ --- title: Getting Started description: Everything you need to start building with Bloc. --- import InstallationTabs from '~/components/getting-started/InstallationTabs.astro'; import ImportTabs from '~/components/getting-started/ImportTabs.astro'; ## Packages The bloc ecosystem consists of multiple packages listed below: | Package | Description | Link | | ------------------------------------------------------------------------------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | AngularDart Components | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | Core Dart APIs | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | Event Transformers | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | Custom Linter | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | Testing APIs | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | Command-line Tools | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | Flutter Widgets | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | Caching/Persistence Support | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | Undo/Redo Support | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | ## Installation :::note In order to start using bloc you must have the [Dart SDK](https://dart.dev/get-dart) installed on your machine. ::: ## Imports Now that we have successfully installed bloc, we can create our `main.dart` and import the respective `bloc` package. ================================================ FILE: docs/src/content/docs/index.mdx ================================================ --- template: splash title: Bloc State Management Library description: Official documentation for the bloc state management library. Support for Dart, Flutter, and AngularDart. Includes examples and tutorials. banner: content: | ✨ Visit the Bloc Shop ✨ editUrl: false lastUpdated: false hero: title: Bloc v9.2.0 tagline: A predictable state management library for Dart. image: alt: Bloc logo file: ~/assets/bloc.svg actions: - text: Get Started link: /getting-started/ variant: primary icon: rocket - text: View on GitHub link: https://github.com/felangel/bloc icon: github variant: secondary --- import { CardGrid } from '@astrojs/starlight/components'; import SponsorsGrid from '~/components/landing/SponsorsGrid.astro'; import Card from '~/components/landing/Card.astro'; import ListCard from '~/components/landing/ListCard.astro'; import SplitCard from '~/components/landing/SplitCard.astro'; import Discord from '~/components/landing/Discord.astro';
```sh # Add bloc to your project. dart pub add bloc ``` Our [getting started guide](/getting-started) has step-by-step instructions on how to start using Bloc in just a few minutes. Complete [the official tutorials](/tutorials/flutter-counter) to learn best practices and build a variety of different apps powered by Bloc. Explore high quality, fully tested [sample apps](https://github.com/felangel/bloc/tree/master/examples) like the counter, timer, infinite list, weather, todo and more! - [Why Bloc?](/why-bloc) - [Core Concepts](/bloc-concepts) - [Architecture](/architecture) - [Testing](/testing) - [Naming Conventions](/naming-conventions) - [FAQs](/faqs) - [VSCode Integration](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) - [IntelliJ Integration](https://plugins.jetbrains.com/plugin/12129-bloc) - [Neovim Integration](https://github.com/wa11breaker/flutter-bloc.nvim) - [Mason CLI Integration](https://github.com/felangel/bloc/blob/master/bricks/README.md) - [Custom Templates](https://brickhub.dev/search?q=bloc) - [Developer Tools](https://github.com/felangel/bloc/blob/master/packages/bloc_tools/README.md) ================================================ FILE: docs/src/content/docs/it/architecture.mdx ================================================ --- title: Architettura description: Panoramica dei modelli architetturali consigliati quando si utilizza bloc. --- import DataProviderSnippet from '~/components/architecture/DataProviderSnippet.astro'; import RepositorySnippet from '~/components/architecture/RepositorySnippet.astro'; import BusinessLogicComponentSnippet from '~/components/architecture/BusinessLogicComponentSnippet.astro'; import BlocTightCouplingSnippet from '~/components/architecture/BlocTightCouplingSnippet.astro'; import BlocLooseCouplingPresentationSnippet from '~/components/architecture/BlocLooseCouplingPresentationSnippet.astro'; import AppIdeasRepositorySnippet from '~/components/architecture/AppIdeasRepositorySnippet.astro'; import AppIdeaRankingBlocSnippet from '~/components/architecture/AppIdeaRankingBlocSnippet.astro'; import PresentationComponentSnippet from '~/components/architecture/PresentationComponentSnippet.astro'; ![Architettura Bloc](~/assets/concepts/bloc_architecture_full.png) L'utilizzo della libreria bloc ci consente di separare la nostra applicazione in tre livelli: - Presentazione - Logica Applicativa - Dati - "Repository" - "Data Provider" Inizieremo dal livello più basso (più lontano dall'interfaccia utente) e risaliremo fino al livello di presentazione. ## Livello Dati Ha il compito di recuperare e manipolare i dati da uno o più sorgenti. Può essere suddiviso in due parti: - "Repository" - "Data Provider" Questo livello è il più basso dell'applicazione e interagisce con i database, le richieste di rete e altre sorgenti di dati asincrone. ### Data Provider Il compito del "Data Provider" è fornire dati grezzi. Dovrebbe essere generico e versatile. Di solito espone API semplici per eseguire operazioni [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete). Potremmo avere metodi come `createData`, `readData`, `updateData` e `deleteData` come parte del nostro livello dati. ### Repository Il livello "Repository" funge da "wrapper" attorno a uno o più "Data Provider" e comunica con il livello di logica applicativa. Come puoi vedere, il nostro livello repository può interagire con più "Data Provider" ed eseguire trasformazioni sui dati prima di passare il risultato al livello della logica applicativa. ## Livello Logica Applicativa Il livello di logica applicativa risponde agli input provenienti dal livello di presentazione emettendo nuovi stati. Può dipendere da uno o più repository per recuperare i dati necessari a costruire lo stato dell'applicazione. Pensa a questo livello come al ponte tra l'interfaccia utente (livello di presentazione) e il livello dati. La logica applicativa riceve notifiche di eventi e azioni dalla presentazione e poi comunica con il repository per costruire un nuovo stato che il livello di presentazione potrà poi utilizzare. ### Comunicazione tra Bloc Poiché i bloc espongono stream, potrebbe essere allettante creare un bloc che ascolta un altro bloc. **Non dovresti** farlo. Esistono alternative migliori rispetto al codice riportato di seguito: Anche se il codice sopra è privo di errori (e gestisce automaticamente la cancellazione delle dipendenze), presenta un problema più grave: crea una dipendenza tra due bloc. In generale, le dipendenze tra entità dello stesso livello architetturale dovrebbero essere evitate a tutti i costi, poiché creano un accoppiamento stretto difficile da mantenere. Poiché i bloc risiedono nel livello di logica applicativa, nessun bloc dovrebbe conoscere altri bloc. ![Layer applicativo](~/assets/architecture/architecture.png) Un bloc dovrebbe ricevere informazioni solo attraverso eventi e da repository iniettati (ovvero, repository forniti al bloc tramite il suo costruttore). Se ti trovi in una situazione in cui un bloc deve rispondere a un altro bloc, hai due alternative. Puoi spostare il problema un livello più in alto (nel livello di presentazione) oppure un livello più in basso (nel livello di dominio). #### Collegare i Bloc attraverso la Presentazione Puoi usare un `BlocListener` per ascoltare un bloc e aggiungere un evento a un altro bloc ogni volta che il primo bloc cambia. Il codice sopra impedisce a `SecondBloc` di dover conoscere `FirstBloc`, favorendo un accoppiamento lasco. L'applicazione [flutter_weather](/it/tutorials/flutter-weather) [usa questa tecnica](https://github.com/felangel/bloc/blob/b4c8db938ad71a6b60d4a641ec357905095c3965/examples/flutter_weather/lib/weather/view/weather_page.dart#L38-L42) per cambiare il tema dell'applicazione in base alle informazioni meteorologiche ricevute. In alcune situazioni, potresti non voler accoppiare due bloc nel livello di presentazione. In questi casi può avere senso che due bloc condividano la stessa sorgente di dati e si aggiornino ogni volta che i dati cambiano. #### Collegare i Bloc attraverso il Dominio Due bloc possono ascoltare uno stream da un repository e aggiornare i loro stati indipendentemente l'uno dall'altro ogni volta che i dati del repository cambiano. L'uso di repository reattivi per mantenere lo stato sincronizzato è comune nelle applicazioni enterprise su larga scala. Prima, crea o usa un repository che fornisce uno `Stream` di dati. Ad esempio, il seguente repository espone uno stream infinito di "idee": Lo stesso repository può essere iniettato in ogni bloc che deve reagire a nuove "idee". Di seguito è riportato un `AppIdeaRankingBloc` che emette uno stato per ogni "idea" in arrivo dal repository sopra: Per maggiori informazioni sull'uso degli stream con Bloc, consulta [Come usare Bloc con stream e concorrenza](https://verygood.ventures/blog/how-to-use-bloc-with-streams-and-concurrency). ## Livello di Presentazione Il livello di presentazione ha il compito di capire come renderizzare se stesso in base a uno o più stati del bloc. Inoltre, dovrebbe gestire l'input dell'utente e gli eventi del ciclo di vita dell'applicazione. La maggior parte dei flussi delle applicazioni inizierà con un evento `AppStart` che attiva l'applicazione per recuperare alcuni dati da presentare all'utente. In questo scenario, il livello di presentazione aggiungerebbe un evento `AppStart`. Inoltre, il livello di presentazione dovrà capire cosa renderizzare sullo schermo in base allo stato proveniente dal bloc. Finora, anche se abbiamo visto alcuni frammenti di codice, siamo rimasti abbastanza ad alto livello. Nella sezione tutorial metteremo tutto insieme mentre costruiremo diversi esempi di app. ================================================ FILE: docs/src/content/docs/it/bloc-concepts.mdx ================================================ --- title: Concetti Bloc description: Una panoramica dei concetti fondamentali per package:bloc. sidebar: order: 1 --- import CountStreamSnippet from '~/components/concepts/bloc/CountStreamSnippet.astro'; import SumStreamSnippet from '~/components/concepts/bloc/SumStreamSnippet.astro'; import StreamsMainSnippet from '~/components/concepts/bloc/StreamsMainSnippet.astro'; import CounterCubitSnippet from '~/components/concepts/bloc/CounterCubitSnippet.astro'; import CounterCubitInitialStateSnippet from '~/components/concepts/bloc/CounterCubitInitialStateSnippet.astro'; import CounterCubitInstantiationSnippet from '~/components/concepts/bloc/CounterCubitInstantiationSnippet.astro'; import CounterCubitIncrementSnippet from '~/components/concepts/bloc/CounterCubitIncrementSnippet.astro'; import CounterCubitBasicUsageSnippet from '~/components/concepts/bloc/CounterCubitBasicUsageSnippet.astro'; import CounterCubitStreamUsageSnippet from '~/components/concepts/bloc/CounterCubitStreamUsageSnippet.astro'; import CounterCubitOnChangeSnippet from '~/components/concepts/bloc/CounterCubitOnChangeSnippet.astro'; import CounterCubitOnChangeUsageSnippet from '~/components/concepts/bloc/CounterCubitOnChangeUsageSnippet.astro'; import CounterCubitOnChangeOutputSnippet from '~/components/concepts/bloc/CounterCubitOnChangeOutputSnippet.astro'; import SimpleBlocObserverOnChangeSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeSnippet.astro'; import SimpleBlocObserverOnChangeUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeUsageSnippet.astro'; import SimpleBlocObserverOnChangeOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeOutputSnippet.astro'; import CounterCubitOnErrorSnippet from '~/components/concepts/bloc/CounterCubitOnErrorSnippet.astro'; import SimpleBlocObserverOnErrorSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnErrorSnippet.astro'; import CounterCubitOnErrorOutputSnippet from '~/components/concepts/bloc/CounterCubitOnErrorOutputSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/bloc/CounterBlocSnippet.astro'; import CounterBlocEventHandlerSnippet from '~/components/concepts/bloc/CounterBlocEventHandlerSnippet.astro'; import CounterBlocIncrementSnippet from '~/components/concepts/bloc/CounterBlocIncrementSnippet.astro'; import CounterBlocUsageSnippet from '~/components/concepts/bloc/CounterBlocUsageSnippet.astro'; import CounterBlocStreamUsageSnippet from '~/components/concepts/bloc/CounterBlocStreamUsageSnippet.astro'; import CounterBlocOnChangeSnippet from '~/components/concepts/bloc/CounterBlocOnChangeSnippet.astro'; import CounterBlocOnChangeUsageSnippet from '~/components/concepts/bloc/CounterBlocOnChangeUsageSnippet.astro'; import CounterBlocOnChangeOutputSnippet from '~/components/concepts/bloc/CounterBlocOnChangeOutputSnippet.astro'; import CounterBlocOnTransitionSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionSnippet.astro'; import CounterBlocOnTransitionOutputSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionOutputSnippet.astro'; import SimpleBlocObserverOnTransitionSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionSnippet.astro'; import SimpleBlocObserverOnTransitionUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionUsageSnippet.astro'; import SimpleBlocObserverOnTransitionOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionOutputSnippet.astro'; import CounterBlocOnEventSnippet from '~/components/concepts/bloc/CounterBlocOnEventSnippet.astro'; import SimpleBlocObserverOnEventSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventSnippet.astro'; import SimpleBlocObserverOnEventOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventOutputSnippet.astro'; import CounterBlocOnErrorSnippet from '~/components/concepts/bloc/CounterBlocOnErrorSnippet.astro'; import CounterBlocOnErrorOutputSnippet from '~/components/concepts/bloc/CounterBlocOnErrorOutputSnippet.astro'; import CounterCubitFullSnippet from '~/components/concepts/bloc/CounterCubitFullSnippet.astro'; import CounterBlocFullSnippet from '~/components/concepts/bloc/CounterBlocFullSnippet.astro'; import AuthenticationStateSnippet from '~/components/concepts/bloc/AuthenticationStateSnippet.astro'; import AuthenticationTransitionSnippet from '~/components/concepts/bloc/AuthenticationTransitionSnippet.astro'; import AuthenticationChangeSnippet from '~/components/concepts/bloc/AuthenticationChangeSnippet.astro'; import DebounceEventTransformerSnippet from '~/components/concepts/bloc/DebounceEventTransformerSnippet.astro'; :::note Assicurati di leggere attentamente le seguenti sezioni prima di lavorare con [`package:bloc`](https://pub.dev/packages/bloc). ::: Ci sono diversi concetti fondamentali che sono essenziali per comprendere come usare la libreria bloc. Nelle sezioni seguenti, discuteremo ciascuno di essi in dettaglio e lavoreremo su come si applicherebbero ad un'app contatore. ## Stream :::note Consulta la [Documentazione ufficiale di Dart](https://dart.dev/tutorials/language/streams) per maggiori informazioni riguardo gli `Stream`. ::: Uno stream è una sequenza di dati asincroni. Per usare la libreria bloc, è fondamentale avere una comprensione di base degli `Stream` e di come funzionano. Se non hai familiarità con `Stream`, pensa semplicemente ad un tubo con dell'acqua che scorre attraverso di esso. Il tubo è lo `Stream` e l'acqua sono i dati asincroni. Possiamo creare uno `Stream` in Dart scrivendo una funzione `async*` (generatore asincrono). Contrassegnando una funzione come `async*` siamo in grado di usare la parola chiave `yield` e restituire uno `Stream` di dati. Nell'esempio sopra, stiamo restituendo uno `Stream` di interi che arriva fino al valore del parametro `max`. Ogni volta che facciamo `yield` in una funzione `async*` stiamo spingendo quel pezzo di dati attraverso lo `Stream`. Possiamo consumare lo `Stream` sopra in diversi modi. Se volessimo scrivere una funzione per restituire la somma di uno `Stream` di interi potremmo scrivere qualcosa del tipo: Contrassegnando la funzione sopra come `async` siamo in grado di usare la parola chiave `await` e restituire un `Future` di interi. In questo esempio, stiamo aspettando ogni valore dello stream per poi restituirne la somma. Infine possiamo mettere tutto insieme così: Ora che abbiamo compreso le basi del funzionamento degli `Stream` in Dart, siamo pronti a scoprire il componente core del pacchetto bloc: il `Cubit`. ## Cubit Un `Cubit` è una classe che estende `BlocBase` e può essere estesa per gestire qualsiasi tipo di stato. ![Architettura Cubit](~/assets/concepts/cubit_architecture_full.png) Può esporre funzioni che possono essere invocate per attivare cambiamenti di stato. Gli stati sono l'output di un `Cubit` e rappresentano una parte dello stato della tua applicazione. I componenti UI possono essere notificati degli stati e ridisegnare porzioni di se stessi in base allo stato corrente. :::note Per maggiori informazioni sulle origini di `Cubit` consulta [la seguente "issue"](https://github.com/felangel/cubit/issues/69). ::: ### Creare un Cubit Possiamo creare un `CounterCubit` così: Quando creiamo un `Cubit`, dobbiamo definire il tipo di stato che il `Cubit` gestirà. Nel caso del `CounterCubit` sopra, lo stato può essere rappresentato tramite un `int` ma in casi più complessi potrebbe essere necessario definire una classe invece di usare un tipo primitivo. La seconda cosa che dobbiamo fare quando creiamo un `Cubit` è specificare lo stato iniziale. Possiamo farlo chiamando `super` con il valore dello stato iniziale. Nel frammento sopra, viene inizializzato lo stato internamente a `0` ma possiamo anche permettere al `Cubit` di essere più flessibile accettando un valore esterno: Questo ci permetterebbe di istanziare istanze di `CounterCubit` con diversi stati iniziali come: ### Cambiamenti di Stato del Cubit Ogni `Cubit` ha la capacità di emettere un nuovo stato tramite `emit`. Nel frammento sopra, il `CounterCubit` espone un metodo pubblico chiamato `increment` che può essere chiamato esternamente per notificare il `CounterCubit` di incrementare il suo stato. Quando `increment` viene chiamato, possiamo accedere allo stato corrente del `Cubit` tramite il getter `state` ed emettere un nuovo stato aggiungendo 1 a quello corrente. :::caution Il metodo `emit` è protetto, il che significa che dovrebbe essere usato solo all'interno di un `Cubit`. ::: ### Usare un Cubit Ora che abbiamo implementato `CounterCubit` lo possiamo mettere in uso! #### Uso semplice Nel frammento sopra, iniziamo creando un'istanza del `CounterCubit`. Poi stampiamo lo stato corrente del cubit che è lo stato iniziale (dato che nessuno stato nuovo è stato ancora emesso). Successivamente, chiamiamo la funzione `increment` per attivare un cambiamento di stato. Infine, stampiamo di nuovo lo stato del `Cubit` che è passato da `0` a `1` e chiamiamo `close` sul `Cubit` per chiudere lo stream di stato interno. #### Uso con Stream `Cubit` espone uno `Stream` che ci permette di ricevere aggiornamenti di stato in tempo reale: Nello snippet sopra, effettuiamo la sottoscrizione al `CounterCubit` stampando a video ogni variazione di stato. Invochiamo quindi la funzione `increment`, che emetterà un nuovo stato. Infine, chiamiamo `cancel` sulla `subscription` quando non desideriamo più ricevere aggiornamenti e chiudiamo il `Cubit`. :::note `await Future.delayed(Duration.zero)` è aggiunto per questo esempio per evitare di cancellare la sottoscrizione immediatamente. ::: :::caution Invocando `listen` su un `Cubit`, si riceveranno esclusivamente i cambiamenti di stato che avvengono dopo il momento della sottoscrizione. ::: ### Osservare un Cubit Quando un `Cubit` emette un nuovo stato, si verifica un `Change`. Possiamo osservare tutti i cambiamenti per un dato `Cubit` sovrascrivendo `onChange`. Possiamo poi interagire con il `Cubit` e osservare tutti i cambiamenti emessi nella console. L'esempio sopra produrrebbe: :::note Un `Change` si verifica appena prima che lo stato del `Cubit` sia aggiornato. Un `Change` consiste di `currentState` e `nextState`. ::: #### BlocObserver Un vantaggio aggiuntivo dell'usare la libreria bloc è che possiamo avere accesso a tutti i `Changes` in un unico posto. Anche se in questa applicazione abbiamo solo un `Cubit`, è abbastanza comune in applicazioni più grandi avere molti `Cubit` che gestiscono diverse parti dello stato dell'applicazione. Se vogliamo essere in grado di fare qualcosa in risposta a tutti i `Changes` possiamo semplicemente creare il nostro `BlocObserver`. :::note Tutto quello che dobbiamo fare è estendere `BlocObserver` e sovrascrivere il metodo `onChange`. ::: Per usare il `SimpleBlocObserver`, dobbiamo solo modificare la funzione `main`: Il frammento sopra produrrebbe quindi: :::note L'override interno di `onChange` viene chiamato per primo, che chiama `super.onChange` notificando l'`onChange` nel `BlocObserver`. ::: :::tip In `BlocObserver` abbiamo accesso all'istanza del `Cubit` in aggiunta al `Change` stesso. ::: ### Gestione degli Errori del Cubit Ogni `Cubit` ha un metodo `addError` che può essere usato per indicare che si è verificato un errore. :::note `onError` può essere sovrascritto all'interno del `Cubit` per gestire tutti gli errori per un `Cubit` specifico. ::: `onError` può anche essere sovrascritto in `BlocObserver` per gestire tutti gli errori segnalati globalmente. Se eseguiamo di nuovo lo stesso programma dovremmo vedere il seguente output: ## Bloc Un `Bloc` è una classe più avanzata che si basa su `eventi` per attivare cambiamenti di `stato` piuttosto che funzioni. `Bloc` estende anche `BlocBase` il che significa che ha un'API pubblica simile a `Cubit`. Tuttavia, piuttosto che chiamare una `funzione` su un `Bloc` ed emettere direttamente un nuovo `stato`, i `Bloc` ricevono `eventi` e convertono gli `eventi` in arrivo in `stati` in uscita. ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) ### Creare un Bloc Creare un `Bloc` è simile a creare un `Cubit` tranne che, oltre allo stato, dobbiamo anche definire l'evento che il `Bloc` sarà in grado di processare. Gli eventi sono l'input di un Bloc. Sono comunemente aggiunti in risposta a interazioni dell'utente come la pressione di pulsanti o eventi del ciclo di vita come il caricamenti di una pagina. Proprio come quando creiamo il `CounterCubit`, dobbiamo specificare uno stato iniziale attraverso `super`. ### Cambiamenti di Stato del Bloc `Bloc`, a differenza delle funzioni in `Cubit`, richiede la registrazione dei gestori di eventi tramite l'API `on`. Un gestore di eventi è responsabile della conversione di qualsiasi evento in entrata in zero o più stati in uscita. :::tip Un `EventHandler` ha accesso all'evento aggiunto così come a un `Emitter` che può essere usato per emettere zero o più stati in risposta all'evento in entrata. ::: Possiamo poi aggiornare l'`EventHandler` per gestire l'evento `CounterIncrementPressed`: Nel frammento sopra, abbiamo registrato un `EventHandler` per gestire tutti gli eventi `CounterIncrementPressed`. Per ogni evento `CounterIncrementPressed` in arrivo possiamo accedere allo stato corrente del bloc tramite il getter `state` ed emettere `emit(state + 1)`. :::note Poiché la classe `Bloc` estende `BlocBase`, abbiamo accesso allo stato corrente del bloc in qualsiasi momento tramite il getter `state`, proprio come in `Cubit`. ::: :::caution I Bloc non dovrebbero mai emettere direttamente nuovi stati (`emit`). Invece, ogni cambiamento di stato deve essere emesso in risposta a un evento in arrivo all'interno di un `EventHandler`. ::: :::caution Sia i bloc che i cubit ignoreranno stati duplicati. Se emettiamo `State nextState` dove `state == nextState`, allora non si verificherà alcun cambiamento di stato. ::: ### Usare un Bloc A questo punto, possiamo creare un'istanza del nostro `CounterBloc` e metterlo in uso! #### Uso Base Nel frammento sopra, iniziamo creando un'istanza del `CounterBloc`. Poi stampiamo lo stato corrente del `Bloc` che è lo stato iniziale (dato che nessuno stato nuovo è stato ancora emesso). Successivamente, aggiungiamo l'evento `CounterIncrementPressed` per attivare un cambiamento di stato. Infine, stampiamo di nuovo lo stato del `Bloc` che è passato da `0` a `1` e chiamiamo `close` sul `Bloc` per chiudere lo stream di stato interno. :::note `await Future.delayed(Duration.zero)` è aggiunto per assicurarci di aspettare la prossima iterazione dell'event-loop (permettendo all'`EventHandler` di processare l'evento). ::: #### Uso con Stream Proprio come con `Cubit`, un `Bloc` è un tipo speciale di `Stream`, il che significa che possiamo anche sottoscriverci a un `Bloc` per aggiornamenti in tempo reale del suo stato: Nel frammento sopra, ci stiamo sottoscrivendo al `CounterBloc` e chiamando `print` su ogni cambiamento di stato. Poi stiamo aggiungendo l'evento `CounterIncrementPressed` che attiva l'`EventHandler` `on` ed emette un nuovo stato. Infine, stiamo chiamando `cancel` sulla sottoscrizione quando non vogliamo più ricevere aggiornamenti e chiudendo il `Bloc`. :::note `await Future.delayed(Duration.zero)` è aggiunto per questo esempio per evitare di cancellare la sottoscrizione immediatamente. ::: ### Osservare un Bloc Poiché `Bloc` estende `BlocBase`, possiamo osservare tutti i cambiamenti di stato per un `Bloc` usando `onChange`. Possiamo poi aggiornare `main.dart` a: Ora se eseguiamo il frammento sopra, l'output sarà: Un differenza chiave tra `Bloc` e `Cubit` è che poiché `Bloc` è orientato agli eventi, siamo anche in grado di catturare informazioni su cosa ha attivato il cambiamento di stato. Possiamo farlo sovrascrivendo `onTransition`. Il cambiamento da uno stato a un altro è chiamato `Transition`. Una `Transition` consiste dello stato corrente, dell'evento e dello stato successivo. Se poi rieseguiamo lo stesso `main.dart` di prima, dovremmo vedere il seguente output: :::note `onTransition` viene invocato prima di `onChange` e contiene l'evento che ha attivato il cambiamento da `currentState` a `nextState`. ::: #### BlocObserver Proprio come prima, possiamo sovrascrivere `onTransition` in un `BlocObserver` personalizzato per osservare tutte le transizioni che si verificano da un unico posto. Possiamo inizializzare il `SimpleBlocObserver` proprio come prima: Ora se eseguiamo il frammento sopra, l'output dovrebbe essere: :::note `onTransition` viene invocato per primo (locale prima di globale) seguito da `onChange`. ::: Un'altra caratteristica unica delle istanze di `Bloc` è che ci permettono di sovrascrivere `onEvent` che viene chiamato ogni volta che un nuovo evento viene aggiunto al `Bloc`. Proprio come con `onChange` e `onTransition`, `onEvent` può essere sovrascritto localmente così come globalmente. Possiamo eseguire lo stesso `main.dart` di prima e dovremmo vedere il seguente output: :::note `onEvent` viene chiamato non appena l'evento viene aggiunto. L'`onEvent` locale viene invocato prima dell'`onEvent` globale in `BlocObserver`. ::: ### Gestione degli Errori del Bloc Proprio come con `Cubit`, ogni `Bloc` ha un metodo `addError` e `onError`. Possiamo indicare che si è verificato un errore chiamando `addError` da qualsiasi punto all'interno del nostro `Bloc`. Possiamo poi reagire a tutti gli errori sovrascrivendo `onError` proprio come con `Cubit`. Se rieseguiamo lo stesso `main.dart` di prima, possiamo vedere come appare quando viene segnalato un errore: :::note L'`onError` locale viene invocato per primo seguito da quello globale presente in `BlocObserver`. ::: :::note `onError` e `onChange` funzionano esattamente allo stesso modo per entrambe le istanze `Bloc` e `Cubit`. ::: :::caution Qualsiasi eccezione non gestita che si verifica all'interno di un `EventHandler` viene anche segnalata a `onError`. ::: ## Cubit vs. Bloc Ora che abbiamo coperto le basi delle classi `Cubit` e `Bloc`, potresti chiederti quando dovresti usare `Cubit` e quando dovresti usare `Bloc`. ### Vantaggi del Cubit #### Semplicità Uno dei maggiori vantaggi dell'usare `Cubit` è la semplicità. Quando creiamo un `Cubit`, dobbiamo solo definire lo stato così come le funzioni che vogliamo esporre per cambiare lo stato. In confronto, quando creiamo un `Bloc`, dobbiamo definire gli stati, gli eventi e l'implementazione dell'`EventHandler`. Questo rende `Cubit` più facile da comprendere e richiede la scrittura di meno codice. Ora diamo un'occhiata alle due implementazioni del `Counter`: ##### CounterCubit ##### CounterBloc L'implementazione del `Cubit` è più concisa e invece di definire eventi separatamente, le funzioni agiscono come eventi. Inoltre, quando usiamo un `Cubit`, possiamo semplicemente chiamare `emit` da qualsiasi punto per attivare un cambiamento di stato. ### Vantaggi del Bloc #### Tracciabilità Uno dei maggiori vantaggi dell'usare `Bloc` è conoscere la sequenza dei cambiamenti di stato così come esattamente cosa ha attivato quei cambiamenti. Per gli stati cruciali per il funzionamento dell'applicazione, può essere molto vantaggioso adottare un approccio orientato agli eventi, così da poter tracciare ogni evento oltre alle semplici variazioni di stato. Un caso d'uso comune potrebbe essere gestire `AuthenticationState`. Per semplicità, diciamo che possiamo rappresentare `AuthenticationState` tramite un `enum`: Potrebbero esserci molte ragioni per cui lo stato dell'applicazione potrebbe cambiare da `authenticated` a `unauthenticated`. Ad esempio, l'utente potrebbe aver toccato un pulsante di logout e richiesto di essere disconnesso dall'applicazione. D'altra parte, forse il token di accesso dell'utente è stato revocato e sono stati forzatamente disconnessi. Quando usiamo `Bloc` possiamo tracciare chiaramente come lo stato dell'applicazione è arrivato a un certo stato. La `Transition` sopra ci dà tutte le informazioni di cui abbiamo bisogno per capire perché lo stato è cambiato. Se avessimo usato un `Cubit` per gestire l'`AuthenticationState`, i nostri log sarebbero: Questo ci dice che l'utente è stato disconnesso ma non spiega il perché, il che potrebbe essere critico per il debug e la comprensione di come lo stato dell' applicazione sta cambiando nel tempo. #### Trasformazioni Avanzate degli Eventi Un altro ambito in cui `Bloc` eccelle rispetto a `Cubit` è quando dobbiamo sfruttare operatori reattivi come `buffer`, `debounceTime`, `throttle`, ecc. :::tip Vedi [`package:stream_transform`](https://pub.dev/packages/stream_transform) e [`package:rxdart`](https://pub.dev/packages/rxdart) per i trasformatori di stream. ::: `Bloc` ha un `sink` di eventi che ci permette di controllare e trasformare il flusso in arrivo di eventi. Per esempio, nella creazione di una funzionalità di ricerca real-time, vorremmo probabilmente limitare la frequenza delle chiamate al backend (tramite `debounce`). Questo serve a non superare le soglie di traffico consentite e a ridurre il carico computazionale e i costi dell'infrastruttura. Con `Bloc` possiamo fornire un `EventTransformer` personalizzato per cambiare il modo in cui gli eventi in arrivo sono processati dal `Bloc`. Con il codice sopra, possiamo facilmente applicare un `debounce` degli eventi in arrivo con pochissimo codice aggiuntivo. :::tip Dai un'occhiata a [`package:bloc_concurrency`](https://pub.dev/packages/bloc_concurrency) per una raccolta di trasformatori di eventi che implementano strategie standard già ottimizzate. ::: Nel dubbio, parti con un `Cubit`; potrai sempre evolverlo in un `Bloc` in futuro, man mano che le esigenze del progetto aumentano. ================================================ FILE: docs/src/content/docs/it/faqs.mdx ================================================ --- title: FAQ description: Risposte alle domande frequenti riguardo la libreria bloc. --- import StateNotUpdatingGood1Snippet from '~/components/faqs/StateNotUpdatingGood1Snippet.astro'; import StateNotUpdatingGood2Snippet from '~/components/faqs/StateNotUpdatingGood2Snippet.astro'; import StateNotUpdatingGood3Snippet from '~/components/faqs/StateNotUpdatingGood3Snippet.astro'; import StateNotUpdatingBad1Snippet from '~/components/faqs/StateNotUpdatingBad1Snippet.astro'; import StateNotUpdatingBad2Snippet from '~/components/faqs/StateNotUpdatingBad2Snippet.astro'; import StateNotUpdatingBad3Snippet from '~/components/faqs/StateNotUpdatingBad3Snippet.astro'; import EquatableEmitSnippet from '~/components/faqs/EquatableEmitSnippet.astro'; import EquatableBlocTestSnippet from '~/components/faqs/EquatableBlocTestSnippet.astro'; import NoEquatableBlocTestSnippet from '~/components/faqs/NoEquatableBlocTestSnippet.astro'; import SingleStateSnippet from '~/components/faqs/SingleStateSnippet.astro'; import SingleStateUsageSnippet from '~/components/faqs/SingleStateUsageSnippet.astro'; import BlocProviderGood1Snippet from '~/components/faqs/BlocProviderGood1Snippet.astro'; import BlocProviderGood2Snippet from '~/components/faqs/BlocProviderGood2Snippet.astro'; import BlocProviderBad1Snippet from '~/components/faqs/BlocProviderBad1Snippet.astro'; import BlocInternalAddEventSnippet from '~/components/faqs/BlocInternalAddEventSnippet.astro'; import BlocInternalEventSnippet from '~/components/faqs/BlocInternalEventSnippet.astro'; import BlocExternalForEachSnippet from '~/components/faqs/BlocExternalForEachSnippet.astro'; ## Lo stato non si aggiorna ❔ **Domanda**: Sto emettendo uno stato nel mio bloc ma l'interfaccia utente non si aggiorna. Cosa sto sbagliando? 💡 **Risposta**: Se stai usando Equatable assicurati di passare tutte le proprietà al getter `props`. ✅ **Buono** ❌ **Errato** Inoltre, assicurati di emettere una nuova istanza dello stato nel tuo bloc. ✅ **Buono** ❌ **Errato** :::caution Le proprietà `Equatable` dovrebbero sempre essere copiate piuttosto che modificate. Se una classe `Equatable` contiene una `List` o `Map` come proprietà, assicurati di usare `List.of` o `Map.of` rispettivamente per assicurarti che l'uguaglianza sia valutata in base ai valori delle proprietà piuttosto che al riferimento. ::: ## Quando Usare Equatable ❔**Domanda**: Quando dovrei usare Equatable? 💡**Risposta**: Nello scenario sopra, se `StateA` estende `Equatable` si verificherà solo un cambiamento di stato (la seconda emissione sarà ignorata). In generale, dovresti usare `Equatable` se vuoi ottimizzare il tuo codice per ridurre il numero di aggiornamenti. Non dovresti usare `Equatable` se vuoi che lo stesso stato consecutivo attivi più transizioni. Inoltre, usare `Equatable` rende molto più facile testare i bloc poiché possiamo aspettarci istanze specifiche degli stati del bloc piuttosto che usare `Matchers` o `Predicates`. Senza `Equatable` il test sopra fallirebbe e dovrebbe essere riscritto così: ## Gestione degli Errori ❔ **Domanda**: Come posso gestire un errore continuando a mostrare i dati precedenti? 💡 **Risposta**: Questo dipende molto da come lo stato del bloc è stato modellato. Nei casi in cui i dati dovrebbero essere conservati anche in presenza di un errore, considera l'uso di una singola classe per lo stato. Questo permetterà ai widget di avere accesso alle proprietà `data` e `error` contemporaneamente e il bloc può usare `state.copyWith` per conservare i vecchi dati anche quando si è verificato un errore. ## Bloc vs. Redux ❔ **Domanda**: Qual è la differenza tra Bloc e Redux? 💡 **Risposta**: BLoC è un modello di progettazione basato sulle seguenti regole: 1. Input e Output del BLoC sono semplici Stream e Sink; 2. Le dipendenze devono essere iniettabili e indipendenti dalla piattaforma; 3. Non è consentita alcuna ramificazione sulla base della piattaforma; 4. L'implementazione può essere qualsiasi cosa tu voglia purché segua le regole sopra indicate. Le linee guida UI sono: 1. Ogni componente "abbastanza complesso" ha un BLoC corrispondente; 2. I componenti dovrebbero inviare input "così come sono" (non deve elaborare, filtrare o trasformare il dato); 3. I componenti dovrebbero mostrare output il più vicino possibile a "così come sono"; 4. Tutto il branching dovrebbe essere basato su semplici output booleani del BLoC. La libreria Bloc implementa il modello BLoC e mira ad astrarre RxDart per semplificare l'esperienza dello sviluppatore. I tre principi di Redux sono: 1. Singola fonte di verità; 2. Lo stato è read-only; 3. I cambiamenti sono fatti con funzioni pure. La libreria bloc viola il primo principio; con bloc lo stato è distribuito attraverso più bloc. Inoltre, non c'è il concetto di middleware in bloc e bloc è progettato per facilitare i cambiamenti di stato in modo asincrono, permettendoti di emettere più stati per un singolo evento. ## Bloc vs. Provider ❔ **Domanda**: Qual è la differenza tra Bloc e Provider? 💡 **Risposta**: `provider` è progettato per la dependency injection ("wrapper" attorno a `InheritedWidget`). Devi ancora capire come gestire il tuo stato (tramite `ChangeNotifier`, `Bloc`, `Mobx`, ecc...). La libreria Bloc usa `provider` internamente per rendere facile fornire e accedere ai bloc in tutto l'albero dei widget. ## BlocProvider.of() Non Trova il Bloc ❔ **Domanda**: Quando uso `BlocProvider.of(context)` non riesce a trovare il bloc, come posso risolverlo? 💡 **Risposta**: Non puoi accedere a un bloc dallo stesso contesto in cui è stato fornito (provide) quindi devi assicurarti che `BlocProvider.of()` sia chiamato all'interno di un `BuildContext` figlio. ✅ **Buono** ❌ **Errato** ## Struttura del Progetto ❔ **Domanda**: Come dovrei strutturare il mio progetto? 💡 **Risposta**: Anche se non c'è davvero una risposta giusta/sbagliata a questa domanda, questi sono dei riferimenti che possono risponderti: - [I/O Photobooth](https://github.com/flutter/photobooth); - [I/O Pinball](https://github.com/flutter/pinball); - [Flutter News Toolkit](https://github.com/flutter/news_toolkit). La cosa più importante è avere una struttura del progetto **consistente** e **ben definita**. ## Aggiungere Eventi all'Interno di un Bloc ❔ **Domanda**: Va bene aggiungere eventi all'interno di un bloc? 💡 **Risposta**: Nella maggior parte dei casi, gli eventi dovrebbero essere aggiunti esternamente ma in alcuni casi particolari può avere senso. La situazione più comune in cui vengono usati eventi interni è quando i cambiamenti di stato devono verificarsi in risposta ad aggiornamenti in tempo reale da un repository. In queste situazioni, il repository è lo stimolo per il cambiamento di stato invece di un evento esterno come il tocco di un pulsante. Nell'esempio seguente, lo stato di `MyBloc` dipende dall'utente corrente che è esposto tramite lo `Stream` dal `UserRepository`. `MyBloc` ascolta i cambiamenti nell'utente corrente e aggiunge un evento interno `_UserChanged` ogni volta che un utente viene emesso dallo stream degli utenti. Aggiungendo un evento interno, siamo anche in grado di specificare un `transformer` personalizzato per l'evento per determinare come più eventi `_UserChanged` saranno processati -- di default saranno processati concorrentemente. È altamente raccomandato che gli eventi interni siano privati. Questo è un modo esplicito di segnalare che un evento specifico è usato solo all'interno del bloc stesso e previene che i componenti esterni conoscano l'evento. Possiamo alternativamente definire un evento esterno `Started` e usare l'API `emit.forEach` per gestire la reazione agli aggiornamenti degli utenti in tempo reale: I vantaggi dell'approccio sopra sono: - Non abbiamo bisogno di un evento interno `_UserChanged`; - Non abbiamo bisogno di gestire manualmente la `StreamSubscription`; - Abbiamo pieno controllo su quando il bloc si sottoscrive allo stream degli aggiornamenti degli utenti. Gli svantaggi dell'approccio sopra sono: - Non possiamo facilmente mettere in pausa (`pause`) o riprendere (`resume`) la sottoscrizione; - Dobbiamo esporre un evento pubblico `Started` che poi sarà da emittare esternamente; - Non possiamo usare un `transformer` personalizzato per regolare come reagiamo agli aggiornamenti degli utenti. ## Esporre Metodi Pubblici ❔ **Domanda**: Va bene esporre metodi pubblici sulle mie istanze di bloc e cubit? 💡 **Risposta** Quando crei un cubit, è raccomandato esporre metodi pubblici solo per attivare dei cambiamenti di stato. Di conseguenza, generalmente tutti i metodi pubblici su un'istanza di cubit dovrebbero restituire `void` o `Future`. Quando crei un bloc, è raccomandato evitare di esporre qualsiasi metodo pubblico personalizzato mentre è corretto notificare il bloc degli eventi chiamando `add`. ================================================ FILE: docs/src/content/docs/it/flutter-bloc-concepts.mdx ================================================ --- title: Concetti Flutter Bloc description: Una panoramica dei concetti fondamentali per package:flutter_bloc. sidebar: order: 2 --- import BlocBuilderSnippet from '~/components/concepts/flutter-bloc/BlocBuilderSnippet.astro'; import BlocBuilderExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocBuilderExplicitBlocSnippet.astro'; import BlocBuilderConditionSnippet from '~/components/concepts/flutter-bloc/BlocBuilderConditionSnippet.astro'; import BlocSelectorSnippet from '~/components/concepts/flutter-bloc/BlocSelectorSnippet.astro'; import BlocProviderSnippet from '~/components/concepts/flutter-bloc/BlocProviderSnippet.astro'; import BlocProviderEagerSnippet from '~/components/concepts/flutter-bloc/BlocProviderEagerSnippet.astro'; import BlocProviderValueSnippet from '~/components/concepts/flutter-bloc/BlocProviderValueSnippet.astro'; import BlocProviderLookupSnippet from '~/components/concepts/flutter-bloc/BlocProviderLookupSnippet.astro'; import NestedBlocProviderSnippet from '~/components/concepts/flutter-bloc/NestedBlocProviderSnippet.astro'; import MultiBlocProviderSnippet from '~/components/concepts/flutter-bloc/MultiBlocProviderSnippet.astro'; import BlocListenerSnippet from '~/components/concepts/flutter-bloc/BlocListenerSnippet.astro'; import BlocListenerExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocListenerExplicitBlocSnippet.astro'; import BlocListenerConditionSnippet from '~/components/concepts/flutter-bloc/BlocListenerConditionSnippet.astro'; import NestedBlocListenerSnippet from '~/components/concepts/flutter-bloc/NestedBlocListenerSnippet.astro'; import MultiBlocListenerSnippet from '~/components/concepts/flutter-bloc/MultiBlocListenerSnippet.astro'; import BlocConsumerSnippet from '~/components/concepts/flutter-bloc/BlocConsumerSnippet.astro'; import BlocConsumerConditionSnippet from '~/components/concepts/flutter-bloc/BlocConsumerConditionSnippet.astro'; import RepositoryProviderSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderSnippet.astro'; import RepositoryProviderLookupSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderLookupSnippet.astro'; import RepositoryProviderDisposeSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderDisposeSnippet.astro'; import NestedRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/NestedRepositoryProviderSnippet.astro'; import MultiRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/MultiRepositoryProviderSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/flutter-bloc/CounterBlocSnippet.astro'; import CounterMainSnippet from '~/components/concepts/flutter-bloc/CounterMainSnippet.astro'; import CounterPageSnippet from '~/components/concepts/flutter-bloc/CounterPageSnippet.astro'; import WeatherRepositorySnippet from '~/components/concepts/flutter-bloc/WeatherRepositorySnippet.astro'; import WeatherMainSnippet from '~/components/concepts/flutter-bloc/WeatherMainSnippet.astro'; import WeatherAppSnippet from '~/components/concepts/flutter-bloc/WeatherAppSnippet.astro'; import WeatherPageSnippet from '~/components/concepts/flutter-bloc/WeatherPageSnippet.astro'; :::note Assicurati di leggere attentamente le seguenti sezioni prima di lavorare con [`package:flutter_bloc`](https://pub.dev/packages/flutter_bloc). ::: :::note Tutti i widget esportati dal pacchetto `flutter_bloc` si integrano con entrambe le istanze `Cubit` e `Bloc`. ::: ## Widget Bloc ### BlocBuilder **BlocBuilder** è un widget Flutter che richiede un `Bloc` e una funzione `builder`. `BlocBuilder` gestisce la costruzione del widget in risposta a nuovi stati. `BlocBuilder` è molto simile a `StreamBuilder` ma ha un'API più semplice per ridurre la quantità di codice boilerplate necessario. La funzione `builder` sarà potenzialmente chiamata molte volte e dovrebbe essere una [funzione pura](https://en.wikipedia.org/wiki/Pure_function) che restituisce un widget in risposta allo stato. Vedi `BlocListener` se vuoi "fare" qualcosa in risposta ai cambiamenti di stato come navigazione, mostrare un dialog, ecc... Se il parametro `bloc` è omesso, `BlocBuilder` eseguirà automaticamente una ricerca usando `BlocProvider` e il `BuildContext` corrente. Specifica il bloc solo se desideri fornire un bloc che sarà limitato a un singolo widget e non è accessibile tramite un `BlocProvider` padre e il `BuildContext` corrente. Per un controllo granulare su quando la funzione `builder` viene chiamata può essere fornit opzionalmente un `buildWhen`. `buildWhen` prende lo stato precedente e quello corrente del bloc e restituisce un booleano. Se `buildWhen` restituisce `true`, `builder` sarà chiamato con `state` e il widget si ricostruirà. Se `buildWhen` restituisce `false`, `builder` non sarà chiamato con `state` e non si verificherà alcun aggiornamento. ### BlocSelector **BlocSelector** è un widget Flutter che è analogo a `BlocBuilder` ma permette agli sviluppatori di filtrare gli aggiornamenti selezionando un nuovo valore in base allo stato corrente del bloc. Gli aggiornamenti non necessari sono prevenuti se il valore selezionato non cambia. Il valore selezionato deve essere immutabile affinché `BlocSelector` possa determinare accuratamente se `builder` dovrebbe essere chiamato di nuovo. Se il parametro `bloc` è omesso, `BlocSelector` eseguirà automaticamente una ricerca usando `BlocProvider` e il `BuildContext` corrente. ### BlocProvider **BlocProvider** è un widget Flutter che fornisce un bloc ai suoi figli tramite `BlocProvider.of(context)`. È usato come widget di dependency injection (DI) così che una singola istanza di un bloc possa essere fornita a più widget all'interno di un sottoalbero. Nella maggior parte dei casi, `BlocProvider` dovrebbe essere usato per creare nuovi bloc che saranno resi disponibili al resto del sottoalbero. In questo caso, poiché `BlocProvider` è responsabile della creazione del bloc, gestirà automaticamente la chiusura del bloc. Di default, `BlocProvider` creerà il bloc in modo pigro (`lazy`), il che significa che `create` sarà eseguito quando il bloc viene cercato tramite `BlocProvider.of(context)`. Per sovrascrivere questo comportamento e forzare `create` ad essere eseguito immediatamente, `lazy` può essere impostato a `false`. In alcuni casi, `BlocProvider` può essere usato per fornire un bloc esistente a una nuova porzione dell'albero dei widget. Questo sarà più comunemente usato quando un bloc esistente deve essere reso disponibile a una nuova route. In questo caso, `BlocProvider` non chiuderà automaticamente il bloc poiché non l'ha creato. poi da `ChildA`, o `ScreenA` possiamo recuperare `BlocA` con: ### MultiBlocProvider **MultiBlocProvider** è un widget Flutter che unisce più widget `BlocProvider` in uno unico. `MultiBlocProvider` migliora la leggibilità ed elimina la necessità di annidare più `BlocProvider`. Usando `MultiBlocProvider` possiamo passare da: a: :::caution Quando un `BlocProvider` è definito nel contesto di un `MultiBlocProvider`, qualsiasi `child` sarà ignorato. ::: ### BlocListener **BlocListener** è un widget Flutter che prende un `BlocWidgetListener` e opzionalmente un `Bloc` e invoca il `listener` in risposta ai cambiamenti di stato nel bloc. Dovrebbe essere usato per funzionalità che devono verificarsi una volta per cambiamento di stato come navigazione, mostrare uno `SnackBar`, mostrare un `Dialog`, ecc... `listener` è chiamato solo una volta per ogni cambiamento di stato (**NON** è chiamato con lo stato iniziale) ed è una funzione `void` a differenza di `builder` in `BlocBuilder`. Se il parametro `bloc` è omesso, `BlocListener` eseguirà automaticamente una ricerca usando `BlocProvider` e il `BuildContext` corrente. Specifica il bloc solo se desideri fornire un bloc che altrimenti non è accessibile tramite `BlocProvider` e il `BuildContext` corrente. Per un controllo granulare su quando la funzione `listener` viene chiamata può essere fornito opzionalmente un `listenWhen`. `listenWhen` prende lo stato precedente e quello corrente del bloc e restituisce un booleano. Se `listenWhen` restituisce `true`, `listener` sarà chiamato con `state`. Se `listenWhen` restituisce `false`, `listener` non sarà chiamato con `state`. ### MultiBlocListener **MultiBlocListener** è un widget Flutter che unisce più widget `BlocListener` in uno. `MultiBlocListener` migliora la leggibilità ed elimina la necessità di annidare più `BlocListener`. Usando `MultiBlocListener` possiamo passare da: a: :::caution Quando un `BlocListener` è definito nel contesto di un `MultiBlocListener`, qualsiasi `child` sarà ignorato. ::: ### BlocConsumer **BlocConsumer** espone un `builder` e un `listener` per reagire a nuovi stati. `BlocConsumer` è analogo a un `BlocListener` e `BlocBuilder` annidati ma riduce la quantità di boilerplate necessario. `BlocConsumer` dovrebbe essere usato solo quando è necessario sia ricostruire la UI che eseguire altre reazioni ai cambiamenti di stato nel `bloc`. `BlocConsumer` richiede un `BlocWidgetBuilder` e un `BlocWidgetListener`, opzionalmente prende un `bloc`, un `BlocBuilderCondition` e un `BlocListenerCondition`. Se il parametro `bloc` è omesso, `BlocConsumer` eseguirà automaticamente una ricerca usando `BlocProvider` e il `BuildContext` corrente. Un `listenWhen` e `buildWhen` opzionali possono essere implementati per un controllo più granulare su quando `listener` e `builder` sono chiamati. `listenWhen` e `buildWhen` saranno invocati su ogni cambiamento di `state` del `bloc`. Prendono ciascuno lo `state` precedente e lo `state` corrente e devono restituire un `bool` che determina se la funzione `builder` e/o `listener` sarà invocata. Lo `state` precedente sarà inizializzato allo `state` del `bloc` quando il `BlocConsumer` è inizializzato. `listenWhen` e `buildWhen` sono opzionali e se non sono implementati, avranno default a `true`. ### RepositoryProvider **RepositoryProvider** è un widget Flutter che fornisce un repository ai suoi figli tramite `RepositoryProvider.of(context)`. È usato come widget di dependency injection (DI) così che una singola istanza di un repository possa essere fornita a più widget all'interno di un sottoalbero. `BlocProvider` dovrebbe essere usato per fornire bloc mentre `RepositoryProvider` dovrebbe essere usato solo per repository. poi da `ChildA` possiamo recuperare l'istanza del `Repository` con: I repository che gestiscono risorse che devono essere eliminate possono farlo tramite la callback `dispose`: ### MultiRepositoryProvider **MultiRepositoryProvider** è un widget Flutter che unisce più widget `RepositoryProvider` in uno. `MultiRepositoryProvider` migliora la leggibilità ed elimina la necessità di annidare più `RepositoryProvider`. Usando `MultiRepositoryProvider` possiamo passare da: a: :::caution Quando un `RepositoryProvider` è definito nel contesto di un `MultiRepositoryProvider`, qualsiasi `child` sarà ignorato. ::: ## Uso di BlocProvider Diamo un'occhiata a come usare `BlocProvider` per fornire un `CounterBloc` a una `CounterPage` e reagire ai cambiamenti di stato con `BlocBuilder`. A questo punto abbiamo separato con successo il nostro livello di presentazione dal nostro livello di logica applicativa. Nota che il widget `CounterPage` non sa nulla di cosa succede quando un utente tocca i pulsanti. Il widget semplicemente dice al `CounterBloc` che l'utente ha premuto il pulsante di incremento o decremento. ## Uso di RepositoryProvider Daremo un'occhiata a come usare `RepositoryProvider` nel contesto dell'esempio [`flutter_weather`][flutter_weather_link]. Nel nostro `main.dart`, chiamiamo `runApp` con il nostro widget `WeatherApp`. Inietteremo la nostra istanza di `WeatherRepository` nel nostro albero dei widget tramite `RepositoryProvider`. Quando istanziamo un bloc, possiamo accedere all'istanza di un repository tramite `context.read` e iniettare il repository nel bloc tramite costruttore. :::tip Se hai più di un repository, puoi usare `MultiRepositoryProvider` per fornire più istanze di repository al sottoalbero. ::: :::note Usa la callback `dispose` per gestire l'eliminazione di qualsiasi risorsa quando il `RepositoryProvider` viene smontato. ::: [flutter_weather_link]: https://github.com/felangel/bloc/blob/master/examples/flutter_weather ## Extension methods Gli ["extension methods"](https://dart.dev/guides/language/extension-methods), introdotti in Dart 2.7, sono un modo per aggiungere funzionalità a librerie esistenti. In questa sezione, daremo un'occhiata alle estensione incluse in `package:flutter_bloc` e a come possono essere usate. `flutter_bloc` ha una dipendenza versp [package:provider](https://pub.dev/packages/provider) che semplifica l'uso di [`InheritedWidget`](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html). Internamente, `package:flutter_bloc` usa `package:provider` per implementare: `BlocProvider`, `MultiBlocProvider`, `RepositoryProvider` e widget `MultiRepositoryProvider`. `package:flutter_bloc` esporta le estensioni `ReadContext`, `WatchContext` e `SelectContext` da `package:provider`. :::note Scopri di più su [`package:provider`](https://pub.dev/packages/provider). ::: ### context.read `context.read()` cerca l'istanza dell'antenato più vicina di tipo `T` ed è funzionalmente equivalente a `BlocProvider.of(context)`. `context.read` è più comunemente usato per recuperare un'istanza di bloc per aggiungere un evento all'interno di una callback `onPressed`. :::note `context.read()` non ascolta `T` -- se l'oggetto fornito di tipo `T` cambia, `context.read` non attiverà un aggiornamento del widget. ::: #### Uso ✅ **FARE** usa `context.read` per aggiungere eventi nelle callback. ```dart onPressed() { context.read().add(CounterIncrementPressed()), } ``` ❌ **EVITARE** di usare `context.read` per recuperare lo stato all'interno di un metodo `build`. ```dart @override Widget build(BuildContext context) { final state = context.read().state; return Text('$state'); } ``` L'uso sopra è soggetto a errori perché il widget `Text` non sarà ricostruito se lo stato del bloc cambia. :::caution Usa invece `BlocBuilder` o `context.watch` per ricostruire la UI in risposta ai cambiamenti di stato. ::: ### context.watch Come `context.read()`, `context.watch()` fornisce l'istanza dell'antenato più vicina di tipo `T`, tuttavia ascolta anche i cambiamenti sull'istanza. È funzionalmente equivalente a `BlocProvider.of(context, listen: true)`. Se l'oggetto fornito di tipo `T` cambia, `context.watch` attiverà un aggiornamento. :::caution `context.watch` è accessibile solo all'interno del metodo `build` di un `StatelessWidget` o classe `State`. ::: #### Uso ✅ **FARE** usa `BlocBuilder` invece di `context.watch` per limitare esplicitamente gli aggiornamenti. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (context, state) { // Ogni volta che lo stato cambia, solo il Text viene ricostruito. return Text(state.value); }, ), ), ); } ``` In alternativa, usa un `Builder` per limitare gli aggiornamenti. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Ogni volta che lo stato cambia, solo il Text viene ricostruito. final state = context.watch().state; return Text(state.value); }, ), ), ); } ``` ✅ **FARE** usa `Builder` e `context.watch` come `MultiBlocBuilder`. ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // restituisci un Widget che dipende dallo stato di BlocA, BlocB e BlocC } ); ``` ❌ **EVITARE** di usare `context.watch` quando il widget padre nel metodo `build` non dipende dallo stato. ```dart @override Widget build(BuildContext context) { // Ogni volta che lo stato cambia, la MaterialApp viene ricostruit a // anche se è usato solo nel widget Text. final state = context.watch().state; return MaterialApp( home: Scaffold( body: Text(state.value), ), ); } ``` :::caution Usare `context.watch` alla radice del metodo `build` risulterà nel aggiornamento dell'intero widget quando lo stato del bloc cambia. ::: ### context.select Proprio come `context.watch()`, `context.select(R function(T value))` fornisce l'istanza dell'antenato più vicina di tipo `T` e ascolta i cambiamenti su `T`. A differenza di `context.watch`, `context.select` ti permette di ascoltare i cambiamenti in una parte più piccola di uno stato. ```dart Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); } ``` Il codice sopra ricostruirà il widget solo quando la proprietà `name` dello stato del `ProfileBloc` cambia. #### Uso ✅ **FARE** usa `BlocSelector` invece di `context.select` per limitare esplicitamente gli aggiornamenti. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector( selector: (state) => state.name, builder: (context, name) { // Ogni volta che state.name cambia, solo il Text viene ricostruito. return Text(name); }, ), ), ); } ``` In alternativa, usa un `Builder` per limitare gli aggiornamenti. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Ogni volta che state.name cambia, solo il Text viene ricostruito. final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), ); } ``` ❌ **EVITARE** di usare `context.select` quando il widget padre in un metodo build non dipende dallo stato. ```dart @override Widget build(BuildContext context) { // Ogni volta che state.value cambia, il MaterialApp viene ricostruito // anche se è usato solo nel widget Text. final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), ); } ``` :::caution Usare `context.select` alla radice del metodo `build` risulterà nel aggiornamento dell'intero widget quando la selezione cambia. ::: ================================================ FILE: docs/src/content/docs/it/getting-started.mdx ================================================ --- title: Inizia description: Tutto ciò di cui hai bisogno per iniziare a costruire con Bloc. --- import InstallationTabs from '~/components/getting-started/InstallationTabs.astro'; import ImportTabs from '~/components/getting-started/ImportTabs.astro'; ## Librerie L'ecosistema bloc si compone di molteplici librerie elencate qui di seguito: | Pacchetto | Descrizione | Link | | ------------------------------------------------------------------------------------------ | ----------------------------- | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | Componenti AngularDart | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | API Core di Dart | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | Trasformatori di Eventi | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | Linter Personalizzato | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | API di Testing | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | Strumenti da Linea di Comando | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | Widget Flutter | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | Supporto Cache/Persistenza | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | Supporto Undo/Redo | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | ## Installazione :::note Per iniziare a usare bloc devi avere [Dart SDK](https://dart.dev/get-dart) installato sulla tua macchina. ::: ## Import Ora che abbiamo installato con successo bloc, possiamo creare il nostro `main.dart` e importare la corrispettiva libreria `bloc`. ================================================ FILE: docs/src/content/docs/it/index.mdx ================================================ --- template: splash title: Libreria di "State Management" Bloc description: Documentazione ufficiale per la libreria di "state management" bloc. Supporto per Dart, Flutter e AngularDart. Include esempi e tutorial. banner: content: | ✨ Visita il Bloc Shop ✨ editUrl: false lastUpdated: false hero: title: Bloc v9.2.0 tagline: Una libreria di "state management" prevedibile per Dart. image: alt: Bloc logo file: ~/assets/bloc.svg actions: - text: Inizia link: /it/getting-started/ variant: primary icon: rocket - text: Vedi su GitHub link: https://github.com/felangel/bloc icon: github variant: secondary --- import { CardGrid } from '@astrojs/starlight/components'; import SponsorsGrid from '~/components/landing/SponsorsGrid.astro'; import Card from '~/components/landing/Card.astro'; import ListCard from '~/components/landing/ListCard.astro'; import SplitCard from '~/components/landing/SplitCard.astro'; import Discord from '~/components/landing/Discord.astro';
```sh # Aggiungi bloc al tuo progetto. dart pub add bloc ``` La nostra [guida introduttiva](/it/getting-started) contiene istruzioni passo-passo su come iniziare a usare Bloc in pochi minuti. Completa [i tutorial ufficiali](/it/tutorials/flutter-counter) per imparare le best practice e costruire una varietà di app diverse alimentate da Bloc. Esplora app di esempio di alta qualità e completamente testate [come il counter, timer, lista infinita, meteo, todo e altro ancora!](https://github.com/felangel/bloc/tree/master/examples) - [Perché Bloc?](/it/why-bloc) - [Concetti Bloc](/it/bloc-concepts) - [Architettura](/it/architecture) - [Testing](/it/testing) - [Convenzioni di Nomenclatura](/it/naming-conventions) - [FAQ](/it/faqs) - [Integrazione VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) - [Integrazione IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) - [Integrazione Neovim](https://github.com/wa11breaker/flutter-bloc.nvim) - [Integrazione Mason CLI](https://github.com/felangel/bloc/blob/master/bricks/README.md) - [Template Personalizzati](https://brickhub.dev/search?q=bloc) - [Strumenti per Sviluppatori](https://github.com/felangel/bloc/blob/master/packages/bloc_tools/README.md) ================================================ FILE: docs/src/content/docs/it/lint/configuration.mdx ================================================ --- title: Configurazione del Linter description: Configurare il linter bloc. sidebar: order: 3 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import BlocLintBasicAnalysisOptionsSnippet from '~/components/lint/BlocLintBasicAnalysisOptionsSnippet.astro'; import RunBlocLintInCurrentDirectorySnippet from '~/components/lint/RunBlocLintInCurrentDirectorySnippet.astro'; import RunBlocLintInSrcTestSnippet from '~/components/lint/RunBlocLintInSrcTestSnippet.astro'; import AvoidFlutterImportsWarningSnippet from '~/components/lint/ImportFlutterWarningSnippet.mdx'; import RunBlocLintCounterCubitSnippet from '~/components/lint/RunBlocLintCounterCubitSnippet.astro'; import AvoidFlutterImportsWarningOutputSnippet from '~/components/lint/ImportFlutterWarningOutputSnippet.astro'; Di default, il linter di bloc non segnalerà alcun avviso finché non avrai configurato esplicitamente le opzioni di analisi del progetto. Per iniziare, crea o modifica l'`analysis_options.yaml` esistente alla radice del tuo progetto per includere una lista di regole sotto la chiave bloc di primo livello: Esegui il linter usando il seguente comando nel tuo terminale: Il comando sopra analizzerà tutti i file nella directory corrente e nelle sue sottodirectory, ma puoi anche fare il "lint" di file e directory specifici passandoli come argomenti da linea di comando: Il comando sopra analizzerà tutto il codice nelle directory `src` e `test`. Se la regola `avoid_flutter_imports` è abilitata, qualsiasi file bloc o cubit che contiene un import flutter sarà segnalato come avviso: Puoi vedere l'avviso eseguendo il comando `bloc lint`: L'output dovrebbe essere: :::note Ecco tutte le regole di lint supportate: ::: ================================================ FILE: docs/src/content/docs/it/lint/customizing-rules.mdx ================================================ --- title: Personalizzare le Regole di Lint description: Personalizzare le regole di lint bloc sidebar: order: 4 --- import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import BlocLintEnablingRulesSnippet from '~/components/lint/BlocLintEnablingRulesSnippet.astro'; import BlocLintDisablingRulesSnippet from '~/components/lint/BlocLintDisablingRulesSnippet.astro'; import BlocLintChangingSeveritySnippet from '~/components/lint/BlocLintChangingSeveritySnippet.astro'; import ImportFlutterInfoSnippet from '~/components/lint/ImportFlutterInfoSnippet.mdx'; import ImportFlutterInfoOutputSnippet from '~/components/lint/ImportFlutterInfoOutputSnippet.astro'; import BlocLintExcludingFilesSnippet from '~/components/lint/BlocLintExcludingFilesSnippet.astro'; import BlocLintIgnoreForLineSnippet from '~/components/lint/BlocLintIgnoreForLineSnippet.astro'; import BlocLintIgnoreForFileSnippet from '~/components/lint/BlocLintIgnoreForFileSnippet.astro'; Puoi personalizzare il comportamento del linter bloc cambiando la gravità di regole individualmente, abilitando o disabilitando regole individualmente, ed escludendo file dall'analisi statica. ## Abilitare e Disabilitare Regole Il linter bloc supporta una lista crescente di regole di lint. Nota che le regole di lint non devono necessariamente essere compatibili tra loro. Ad esempio, alcuni sviluppatori potrebbero preferire usare bloc (`prefer_bloc`) mentre altri potrebbero preferire usare cubit (`prefer_cubit`). :::note A differenza dell'analisi statica, le regole di lint potrebbero contenere falsi positivi. Sentiti libero di segnalare eventuali falsi positivi o altri problemi [aprendo una "issue"](https://github.com/felangel/bloc/issues/new/choose). ::: ### Abilitare Regole Raccomandate La libreria bloc fornisce un set di regole di lint raccomandate come parte del pacchetto [`bloc_lint`](https://pub.dev/packages/bloc_lint). Per abilitare il set raccomandato di lint aggiungi il pacchetto `bloc_lint` come dipendenza di sviluppo: Poi modifica il tuo `analysis_options.yaml` per includere il set di regole: :::note Quando viene pubblicata una nuova versione di `bloc_lint`, il codice che precedentemente passava l'analisi statica potrebbe fallire. Raccomandiamo di aggiornare il tuo codice per adeguarsi alle nuove regole, oppure puoi anche opzionalmente abilitare o disabilitare le regole individualmente. ::: ### Abilitare Regole Individualmente Per abilitare regole individualmente, aggiungi `bloc:` al file `analysis_options.yaml` come chiave di primo livello e `rules:` come chiave di secondo livello. Nelle righe successive, specifica le regole che vuoi come una lista YAML (prefissate con trattini). Ad esempio: ### Disabilitare Regole Individualmente Se includi un set di regole esistente come il set `recommended`, potresti voler disabilitarne una o più di queste individualmente. Disabilitare regole è simile a abilitarle, ma richiede l'uso di una mappa YAML piuttosto che una lista. Ad esempio, il seguente include il set raccomandato di regole di lint eccetto `avoid_public_bloc_methods` e inoltre abilita la regola `prefer_bloc`: ## Personalizzare la Gravità delle Regole Puoi regolare la gravità di qualsiasi regola così: Ora la stessa regola di lint sarà segnalata con una gravità di `info` invece che di `warning`: L'output del comando `bloc lint` dovrebbe essere: Le opzioni di gravità supportate sono: | Gravità | Descrizione | | --------- | ------------------------------------------------------ | | `error` | Indica che il pattern non è permesso. | | `warning` | Indica che il pattern è sospetto ma permesso. | | `info` | Fornisce informazioni agli utenti ma non è un problema | | `hint` | Propone un modo migliore di ottenere un risultato. | ## Escludere File A volte va bene che l'analisi statica fallisca. Ad esempio, potresti voler ignorare avvisi o errori segnalati nel codice generato che non è stato scritto da te e dal tuo team. Proprio come con le regole di lint ufficiali di Dart, puoi usare l'opzione `exclude:` dell'analyzer per escludere file dall'analisi statica. Puoi elencare file individuali o usare pattern [`glob`](https://pub.dev/packages/glob). :::note Tutti gli usi di pattern glob dovrebbero essere relativi alla directory contenente il file `analysis_options.yaml` corrispondente. ::: Ad esempio, possiamo escludere tutto il codice Dart generato tramite le seguenti opzioni di analisi: ## Ignorare Regole Proprio come con le regole di lint ufficiali di Dart, puoi ignorare le regole di lint bloc per un dato file o riga di codice usando `// ignore_for_file` e `// ignore` rispettivamente. :::note Per ignorare più regole per una data riga o file, fornisci una lista separata da virgole. ::: ### Ignorare Righe Possiamo ignorare occorrenze specifiche di violazioni di regole aggiungendo un commento `ignore` sia direttamente sopra la riga incriminata che aggiungendolo alla fine della riga. Ad esempio, possiamo ignorare occorrenze specifiche di `prefer_file_naming_conventions` in un dato file: ### Ignorare File Possiamo ignorare tutte le occorrenze di violazioni di regole all'interno di un file aggiungendo un commento `ignore_for_file` ovunque nel file. Ad esempio, possiamo ignorare tutte le occorrenze di `prefer_file_naming_conventions` in un dato file: ================================================ FILE: docs/src/content/docs/it/lint/index.mdx ================================================ --- title: Panoramica del Linter description: Un'introduzione al linter bloc. sidebar: order: 1 --- import AvoidFlutterImportsWarningSnippet from '~/components/lint/ImportFlutterWarningSnippet.mdx'; import AvoidFlutterImportsWarningOutputSnippet from '~/components/lint/ImportFlutterWarningOutputSnippet.astro'; import InstallBlocToolsSnippet from '~/components/lint/InstallBlocToolsSnippet.astro'; import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import RunBlocLintInCurrentDirectorySnippet from '~/components/lint/RunBlocLintInCurrentDirectorySnippet.astro'; Il "linting" è il processo di analisi statica del codice per individuare potenziali bug in aggiunta a errori programmatici e stilistici. Bloc ha un linter integrato, che può essere usato tramite il tuo IDE o gli [strumenti da linea di comando bloc](https://pub.dev/packages/bloc_tools) con il comando `bloc lint`. Con l'aiuto del linter bloc, puoi migliorare la qualità dell'intero progetto e imporre coerenza senza eseguire una singola riga di codice. Ad esempio, forse hai accidentalmente importato una dipendenza Flutter nel tuo cubit: Se configurato correttamente, il linter bloc indicherà l'import e produrrà il seguente avviso: Nelle sezioni seguenti, copriremo come installare, configurare e personalizzare il linter bloc in modo da poter sfruttare l'analisi statica nel tuo progetto. ## Primi passi Inizia a usare il linter bloc in pochi passaggi rapidi e facili. :::note Per iniziare a usare bloc devi avere [Dart SDK](https://dart.dev/get-dart) installato sulla tua macchina. ::: 1. Installa gli [strumenti da linea di comando bloc](https://pub.dev/packages/bloc_tools); 1. Installa il pacchetto [bloc_lint](https://pub.dev/packages/bloc_lint); 1. Aggiungi un `analysis_options.yaml` alla radice del tuo progetto con le regole raccomandate; 1. Esegui il linter. Questo è tutto 🎉 Continua a leggere le altre sezioni per approfondire l'argomento su come configurare e personalizzare il linter bloc. ================================================ FILE: docs/src/content/docs/it/lint/installation.mdx ================================================ --- title: Installazione del Linter description: Installare il linter bloc. sidebar: order: 2 --- import { CardGrid } from '@astrojs/starlight/components'; import Card from '~/components/landing/Card.astro'; import InstallBlocToolsSnippet from '~/components/lint/InstallBlocToolsSnippet.astro'; import BlocToolsLintHelpOutputSnippet from '~/components/lint/BlocToolsLintHelpOutputSnippet.astro'; import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import BlocLintMultipleRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintMultipleRecommendedAnalysisOptionsSnippet.astro'; ## Strumenti da Linea di Comando Per usare il linter dalla linea di comando, installa [`package:bloc_tools`](https://pub.dev/packages/bloc_tools) tramite il seguente comando: Una volta che gli strumenti da linea di comando bloc sono stati installati, puoi eseguire il linter bloc tramite il comando `bloc lint`: ## Set di Regole Raccomandato Per installare il set di regole di lint raccomandato, installa [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) come dipendenza di sviluppo tramite il seguente comando: Poi, aggiungi un `analysis_options.yaml` alla radice del tuo progetto con il set di regole raccomandato: Se necessario, puoi includere più set di regole definendoli come una lista: ## Integrazioni IDE I seguenti IDE supportano ufficialmente il linter e il language server di bloc, fornendo diagnostica in tempo reale direttamente all'interno dell'ambiente di sviluppo. Il supporto per l'[Estensione Bloc VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) è disponibile nella v6.8.0. Il supporto per il [Plugin Bloc IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) è disponibile nella v4.1.0. ================================================ FILE: docs/src/content/docs/it/lint-rules/avoid_build_context_extensions.mdx ================================================ --- title: Evitare Estensioni BuildContext description: La regola avoid_build_context_extensions. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_build_context_extensions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_build_context_extensions/GoodSnippet.astro';
Evita di usare estensioni di `BuildContext` per accedere a istanze di `Bloc` o `Cubit`. :::note Questa regola di lint è stata introdotta nella versione `0.3.0` di [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Motivazione Per coerenza e per il bene di essere espliciti, preferisci usare direttamente i metodi di bloc rispetto le estensioni di `BuildContext`. Questo è anche vantaggioso per il testing dato che non è possibile mockare un metodo di una estensione. | estensione | metodo esplicito | | ---------------- | ------------------------------------------------------------------ | | `context.read` | `BlocProvider.of(context, listen: false)` | | `context.watch` | `BlocBuilder(...)` o `BlocProvider.of(context)` | | `context.select` | `BlocSelector(...)` | ## Esempi **Evita** di usare estensioni `BuildContext` per interagire con istanze di `Bloc` o `Cubit`. **Errato**: **Buono**: ## Abilita Per abilitare la regola `avoid_build_context_extensions`, aggiungila al tuo `analysis_options.yaml` sotto `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/it/lint-rules/avoid_flutter_imports.mdx ================================================ --- title: Evitare Import Flutter description: La regola di lint bloc avoid_flutter_imports. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_flutter_imports/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_flutter_imports/GoodSnippet.astro';
Evita di introdurre dipendenze su Flutter all'interno di componenti di logica applicativa (istanze `Bloc` o `Cubit`). ## Motivazione Stratificare un'applicazione è una parte chiave della costruzione di un progetto mantenibile e aiuta gli sviluppatori a iterare rapidamente e con fiducia. Ogni livello dovrebbe avere una singola responsabilità ed essere in grado di funzionare e di essere testato in modo isolato. Questo ti permette di contenere i cambiamenti a livelli specifici, minimizzando l'impatto su tutta l'applicazione. Di conseguenza, i componenti di logica applicativa dovrebbero generalmente gestire lo stato delle funzionalità ed essere disaccoppiati dal livello UI. Gli eventi dovrebbero fluire nei componenti di logica applicativa dal livello UI e lo stato dovrebbe fluire dal livello di logica applicativa nel livello UI. Mantenere i componenti di logica applicativa disaccoppiati da Flutter fornisce la capacità di riutilizzare la logica applicativa su più piattaforme/framework (ad es. Flutter, AngularDart, Jaspr, ecc.). ## Esempi **NON** importare Flutter all'interno dei tuoi componenti di logica applicativa. **Errato**: **Buono**: ## Abilita Per abilitare la regola `avoid_flutter_imports`, aggiungila al tuo `analysis_options.yaml` sotto `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/it/lint-rules/avoid_public_bloc_methods.mdx ================================================ --- title: Evitare Metodi Pubblici Bloc description: La regola avoid_public_bloc_methods. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_public_bloc_methods/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_public_bloc_methods/GoodSnippet.astro';
Evita di esporre metodi pubblici su istanze `Bloc`. ## Motivazione I bloc reagiscono a eventi in arrivo ed emettono stati in uscita. Di conseguenza, il modo raccomandato di comunicare con un'istanza di bloc è tramite il metodo `add`. Nella maggior parte dei casi, non c'è bisogno di creare astrazioni aggiuntive sopra l'API `add`. ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) ## Esempi **NON** esporre metodi pubblici su istanze bloc. **Errato**: **Buono**: ## Abilita Per abilitare la regola `avoid_public_bloc_methods`, aggiungila al tuo `analysis_options.yaml` sotto `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/it/lint-rules/avoid_public_fields.mdx ================================================ --- title: Evitare Campi Pubblici description: La regola avoid_public_fields. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_public_fields/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_public_fields/GoodSnippet.astro';
Evita di esporre campi pubblici su istanze `Bloc` e `Cubit`. ## Motivazione I componenti di logica applicativa mantengono il proprio `state` ed emettono cambiamenti di stato tramite l'API `emit`. Di conseguenza, tutto lo stato pubblico dovrebbe essere esposto tramite l'oggetto `state`. ## Esempi **NON** esporre campi pubblici su istanze bloc e cubit. **Errato**: **Buono**: ## Abilita Per abilitare la regola `avoid_public_fields`, aggiungila al tuo `analysis_options.yaml` sotto `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/it/lint-rules/prefer_bloc.mdx ================================================ --- title: Preferire Bloc description: La regola prefer_bloc. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_bloc/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_bloc/GoodSnippet.astro';
Preferisci usare istanze `Bloc` invece di istanze `Cubit`. ## Motivazione Questa regola è puramente una regola stilistica. In alcuni casi, i team potrebbero preferire standardizzare solo sull'uso di istanze `Bloc` in tutta la loro applicazione per coerenza. :::tip Scopri di più sui vantaggi di `Bloc` in [Concetti Bloc](/it/bloc-concepts/#vantaggi-del-bloc). ::: ## Esempi **Evita** di usare istanze `Cubit`. **Errato**: **Buono**: ## Abilita Per abilitare la regola `prefer_bloc`, aggiungila al tuo `analysis_options.yaml` sotto `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/it/lint-rules/prefer_build_context_extensions.mdx ================================================ --- title: Preferire Estensioni BuildContext description: La regola prefer_build_context_extensions. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_build_context_extensions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_build_context_extensions/GoodSnippet.astro';
Preferisci usare estensioni `BuildContext` per accedere a un'istanza di `Bloc` o `Repository`. :::note Questa regola di lint è stata introdotta nella versione `0.3.2` di [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Motivazione Per coerenza, preferisci usare estensioni `BuildContext` come `context.read`, `context.watch`, e `context.select` invece di `BlocProvider.of`, `RepositoryProvider.of`, `BlocBuilder` o `BlocSelector`. | metodo esplicito | estensione | | ------------------------------------------------------------------ | --------------------- | | `BlocProvider.of(context, listen: false)` | `context.read` | | `BlocBuilder(...)` o `BlocProvider.of(context)` | `context.watch` | | `BlocSelector(...)` | `context.select` | ## Esempi **Evita** di usare `BlocProvider.of(context)` per accedere a un'istanza di `Bloc`. **Errato**: **Buono**: ## Abilita Per abilitare la regola `prefer_build_context_extensions`, aggiungila al tuo `analysis_options.yaml` sotto `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/it/lint-rules/prefer_cubit.mdx ================================================ --- title: Preferire Cubit description: La regola prefer_cubit. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_cubit/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_cubit/GoodSnippet.astro';
Preferisci usare istanze `Cubit` invece di istanze `Bloc`. ## Motivazione Questa regola è puramente una regola stilistica. In alcuni casi, i team potrebbero preferire standardizzare solo sull'uso di istanze `Cubit` in tutta la loro applicazione per coerenza. :::tip Scopri di più sui vantaggi di `Cubit` in [Concetti Bloc](/it/bloc-concepts/#vantaggi-del-cubit). ::: ## Esempi **Evita** di usare istanze `Bloc`. **Errato**: **Buono**: ## Abilita Per abilitare la regola `prefer_cubit`, aggiungila al tuo `analysis_options.yaml` sotto `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/it/lint-rules/prefer_file_naming_conventions.mdx ================================================ --- title: Preferire Convenzioni di Nomenclatura dei File description: La regola prefer_file_naming_conventions. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_file_naming_conventions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_file_naming_conventions/GoodSnippet.astro';
Preferisci seguire le convenzioni di nomenclatura dei file. :::note Questa regola di lint è stata introdotta nella versione `0.3.0` di [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Motivazione Per coerenza, facilità di manutenzione e separazione delle responsabilità preferisci definire istanze bloc e cubit nei rispettivi file Dart invece di inserirle inline. :::tip Considera di usare il comando `bloc new ` da [package:bloc_tools](https://pub.dev/packages/bloc_tools) per generare rapidamente e coerentemente nuove istanze bloc/cubit. ::: ## Esempi **Preferisci** dichiarare istanze bloc/cubit nei loro rispettivi file. **Buono**: **Errato**: ## Abilita Per abilitare la regola `prefer_file_naming_conventions`, aggiungila al tuo `analysis_options.yaml` sotto `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/it/lint-rules/prefer_void_public_cubit_methods.mdx ================================================ --- title: Preferire Metodi Pubblici Void Cubit description: La regola prefer_void_public_cubit_methods. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_void_public_cubit_methods/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_void_public_cubit_methods/GoodSnippet.astro';
Preferisci metodi pubblici void su istanze `Cubit`. :::note Questa regola di lint è stata introdotta nella versione `0.2.0-dev.2` di [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Motivazione I metodi pubblici su istanze `Cubit` dovrebbero essere usati per notificare il `Cubit` e iniziare cambiamenti di stato tramite il metodo `emit`. Se il chiamante ha bisogno di accedere a qualsiasi informazione di stato, dovrebbero accedervi trammite `state`. :::note Le seguenti regole sono correlate e sono solitamente abilitate in combinazione con `prefer_void_public_cubit_methods`. - [`avoid_public_bloc_methods`](/it/lint-rules/avoid_public_bloc_methods); - [`avoid_public_fields`](/it/lint-rules/avoid_public_fields). ::: ## Esempi **Evita** metodi pubblici non-void su istanze `Cubit`. **Errato**: **Buono**: ## Abilita Per abilitare la regola `prefer_void_public_cubit_methods`, aggiungila al tuo `analysis_options.yaml` sotto `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/it/migration.mdx ================================================ --- title: Guida alla Migrazione description: Migrare all'ultima versione stabile di Bloc. --- import { Code, Tabs, TabItem } from '@astrojs/starlight/components'; :::tip Fai riferimento al [log delle release](https://github.com/felangel/bloc/releases) per maggiori informazioni riguardo a cosa è cambiato in ogni release. ::: ## v10.0.0 ### `package:bloc_test` #### ❗✨ Disaccoppiare `blocTest` da `BlocBase` :::note[Cosa è Cambiato?] In bloc_test v10.0.0, l'API `blocTest` non è più strettamente accoppiata a `BlocBase`. ::: ##### Motivazione `blocTest` dovrebbe usare le interfacce core di bloc quando possibile per aumentare flessibilità e riutilizzabilità. Precedentemente questo non era possibile perché `BlocBase` implementava `StateStreamableSource` che non era sufficiente per `blocTest` a causa della dipendenza interna sull'API `emit`. ### `package:hydrated_bloc` #### ❗✨ Supporto WebAssembly :::note[Cosa è Cambiato?] In hydrated_bloc v10.0.0, è stato aggiunto il supporto per compilare a WebAssembly (wasm). ::: ##### Motivazione Precedentemente non era possibile compilare app a wasm quando si usava `hydrated_bloc`. In v10.0.0, il pacchetto è stato modificato per permettere la compilazione a wasm. **v9.x.x** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` **v10.x.x** ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorageDirectory.web : HydratedStorageDirectory((await getTemporaryDirectory()).path), ); runApp(const App()); } ``` ## v9.0.0 ### `package:bloc` #### ❗🧹 Rimuovere API Deprecate :::note[Cosa è Cambiato?] In bloc v9.0.0, tutte le API precedentemente deprecate sono state rimosse. ::: ##### Riepilogo - `BlocOverrides` rimosso a favore di `Bloc.observer` e `Bloc.transformer`. #### ❗✨ Introdurre nuova Interfaccia `EmittableStateStreamableSource` :::note[Cosa è Cambiato?] In bloc v9.0.0, è stata introdotta una nuova interfaccia core `EmittableStateStreamableSource`. ::: ##### Motivazione `package:bloc_test` era precedentemente strettamente accoppiato a `BlocBase`. L'interfaccia `EmittableStateStreamableSource` è stata introdotta per permettere a `blocTest` di essere disaccoppiato dall'implementazione concreta di `BlocBase`. ### `package:hydrated_bloc` #### ✨ Reintrodurre API `HydratedBloc.storage` :::note[Cosa è Cambiato?] In hydrated_bloc v9.0.0, `HydratedBlocOverrides` è stato rimosso a favore dell'API `HydratedBloc.storage`. ::: ##### Motivazione Fai riferimento a [Motivazione per reintrodurre gli override di Bloc.observer e Bloc.transformer](/it/migration#-reintrodurre-api-blocobserver-e-bloctransformer). **v8.x.x** ```dart Future main() async { final storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); HydratedBlocOverrides.runZoned( () => runApp(App()), storage: storage, ); } ``` **v9.0.0** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` ## v8.1.0 ### `package:bloc` #### ✨ Reintrodurre API `Bloc.observer` e `Bloc.transformer` :::note[Cosa è Cambiato?] In bloc v8.1.0, `BlocOverrides` è stato deprecato a favore delle API `Bloc.observer` e `Bloc.transformer`. ::: ##### Motivazione L'API `BlocOverrides` è stata introdotta in v8.0.0 nel tentativo di supportare lo scoping di configurazioni specifiche del bloc come `BlocObserver`, `EventTransformer`, e `HydratedStorage`. Nelle applicazioni Dart pure, i cambiamenti hanno funzionato bene; tuttavia, nelle applicazioni Flutter la nuova API ha causato più problemi di quanti ne risolvesse. L'API `BlocOverrides` è stata ispirata da API simili in Flutter/Dart: - [HttpOverrides](https://api.flutter.dev/flutter/dart-io/HttpOverrides-class.html); - [IOOverrides](https://api.flutter.dev/flutter/dart-io/IOOverrides-class.html). **Problemi** Anche se non era il motivo principale per questi cambiamenti, l'API `BlocOverrides` ha introdotto complessità aggiuntiva per gli sviluppatori. Oltre ad aumentare la quantità di annidamento e righe di codice necessarie per ottenere lo stesso effetto, l'API `BlocOverrides` richiedeva agli sviluppatori di avere una solida comprensione delle [Zone](https://api.dart.dev/stable/2.17.6/dart-async/Zone-class.html) in Dart. Le `Zone` non sono un concetto adatto ai principianti e la non comprensione di come funzionano le Zone potrebbe portare all'introduzione di bug (come observer, trasformatori e istanze di storage non inizializzati). Ad esempio, molti sviluppatori avrebbero qualcosa del tipo: ```dart void main() { WidgetsFlutterBinding.ensureInitialized(); BlocOverrides.runZoned(...); } ``` Il codice sopra, anche se sembra innocuo, può effettivamente portare a molti bug difficili da tracciare. Qualunque sia la zona da cui viene inizialmente chiamato `WidgetsFlutterBinding.ensureInitialized`, sarà quella in cui verranno gestiti gli eventi gestuali (ad esempio callback `onTap`, `onPressed`) grazie a `GestureBinding.initInstances`. Questo è solo uno dei molti problemi causati dall'uso di `zoneValues`. Inoltre, Flutter fa molte cose dietro le quinte che coinvolgono fork/manipolazione di Zone (specialmente quando si eseguono test) che possono portare a comportamenti inaspettati (e in molti casi comportamenti che sono fuori dal controllo dello sviluppatore -- vedi "issue" sotto). A causa dell'uso di [runZoned](https://api.flutter.dev/flutter/dart-async/runZoned.html), la transizione all'API `BlocOverrides` ha portato alla scoperta di diversi bug/limitazioni in Flutter (specificamente intorno ai Test Widget e di Integrazione): - https://github.com/flutter/flutter/issues/96939 - https://github.com/flutter/flutter/issues/94123 - https://github.com/flutter/flutter/issues/93676 che hanno colpito molti sviluppatori che usavano la libreria bloc: - https://github.com/felangel/bloc/issues/3394 - https://github.com/felangel/bloc/issues/3350 - https://github.com/felangel/bloc/issues/3319 **v8.0.x** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` **v8.1.0** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` ## v8.0.0 ### `package:bloc` #### ❗✨ Introdurre nuova API `BlocOverrides` :::note[Cosa è Cambiato?] In bloc v8.0.0, `Bloc.observer` e `Bloc.transformer` sono stati rimossi a favore dell'API `BlocOverrides`. ::: ##### Motivazione L'API precedente usata per sovrascrivere il `BlocObserver` di default e `EventTransformer` si basava su un singleton globale sia per `BlocObserver` che per `EventTransformer`. Di conseguenza, non era possibile: - Avere più implementazioni di `BlocObserver` o `EventTransformer` con ambito su parti diverse dell'applicazione; - Limitare l'ambito delle sostituzioni di `BlocObserver` o `EventTransformer` a un pacchetto - Se un pacchetto dipendesse da `package:bloc` e registrasse il proprio `BlocObserver`, qualsiasi consumatore del pacchetto dovrebbe sovrascrivere il `BlocObserver` del pacchetto o segnalarlo al `BlocObserver` del pacchetto. Era anche più difficile testare a causa dello stato globale condiviso tra test. Bloc v8.0.0 introduce una classe `BlocOverrides` che permette agli sviluppatori di sovrascrivere `BlocObserver` e/o `EventTransformer` per una specifica `Zone` piuttosto che affidarsi a un singleton globale mutabile. **v7.x.x** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` **v8.0.0** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` Le istanze `Bloc` useranno il `BlocObserver` e/o `EventTransformer` per la `Zone` corrente tramite `BlocOverrides.current`. Se non ci sono `BlocOverrides` per la zona, useranno i default interni esistenti (nessun cambiamento nel comportamento/funzionalità). Questo permette a ogni `Zone` di funzionare indipendentemente con i propri `BlocOverrides`. ```dart BlocOverrides.runZoned( () { // BlocObserverA e eventTransformerA final overrides = BlocOverrides.current; // I Bloc in questa zona segnalano a BlocObserverA // e usano eventTransformerA come trasformatore di default. // ... // Più tardi... BlocOverrides.runZoned( () { // BlocObserverB e eventTransformerB final overrides = BlocOverrides.current; // I Bloc in questa zona segnalano a BlocObserverB // e usano eventTransformerB come trasformatore di default. // ... }, blocObserver: BlocObserverB(), eventTransformer: eventTransformerB(), ); }, blocObserver: BlocObserverA(), eventTransformer: eventTransformerA(), ); ``` #### ❗✨ Migliorare Gestione e Segnalazione degli Errori :::note[Cosa è Cambiato?] In bloc v8.0.0, `BlocUnhandledErrorException` è rimosso. Inoltre, qualsiasi eccezione non gestita è sempre segnalata a `onError` e rilanciata (indipendentemente dalla modalità debug o release). L'API `addError` segnala errori a `onError`, ma non tratta gli errori segnalati come eccezioni non gestite. ::: ##### Motivazione L'obiettivo di questi cambiamenti è: - rendere le eccezioni interne non gestite estremamente ovvie preservando ancora la funzionalità bloc; - supportare `addError` senza interrompere il flusso di controllo. Precedentemente, la gestione e segnalazione degli errori variava a seconda che l'applicazione fosse in esecuzione in modalità debug o release. Inoltre, gli errori segnalati tramite `addError` erano trattati come eccezioni non gestite in modalità debug il che portava a una cattiva esperienza per lo sviluppatore quando si usava l'API `addError` (specialmente quando si scrivevano test unitari). In v8.0.0, `addError` può essere usato in sicurezza per segnalare errori e `blocTest` può essere usato per verificare che gli errori siano segnalati. Tutti gli errori sono ancora segnalati a `onError`, tuttavia, solo le eccezioni non gestite sono rilanciate (indipendentemente dalla modalità debug o release). #### ❗🧹 Rendere `BlocObserver` astratto :::note[Cosa è Cambiato?] In bloc v8.0.0, `BlocObserver` è stato convertito in una classe `abstract` il che significa che un'istanza di `BlocObserver` non può essere istanziata. ::: ##### Motivazione `BlocObserver` era inteso per essere un'interfaccia. Poiché l'implementazione API di default sono "no-op", `BlocObserver` è ora una classe `abstract` per comunicare chiaramente che la classe è pensata per essere estesa e non istanziata direttamente. **v7.x.x** ```dart void main() { // Era possibile creare un'istanza della classe base. final observer = BlocObserver(); } ``` **v8.0.0** ```dart class MyBlocObserver extends BlocObserver {...} void main() { // Non è possibile istanziare la classe base. final observer = BlocObserver(); // ERRORE // Estendi `BlocObserver` invece. final observer = MyBlocObserver(); // OK } ``` #### ❗✨ `add` lancia `StateError` se Bloc è chiuso :::note[Cosa è Cambiato?] In bloc v8.0.0, chiamare `add` su un bloc chiuso risulterà in un `StateError`. ::: ##### Motivazione Precedentemente, era possibile chiamare `add` su un bloc chiuso e l'errore interno veniva ingoiato, rendendo difficile il debug del perché l'evento aggiunto non era processato. Per rendere questo scenario più visibile, in v8.0.0, chiamare `add` su un bloc chiuso lancerà un `StateError` che sarà segnalato come eccezione non gestita e propagato a `onError`. #### ❗✨ `emit` lancia `StateError` se Bloc è chiuso :::note[Cosa è Cambiato?] In bloc v8.0.0, chiamare `emit` all'interno di un bloc chiuso risulterà in un `StateError`. ::: ##### Motivazione Precedentemente, era possibile chiamare `emit` all'interno di un bloc chiuso e nessun cambiamento di stato si verificava ma non c'era anche alcuna indicazione di cosa fosse andato storto, rendendo difficile il debug. Per rendere questo scenario più visibile, in v8.0.0, chiamare `emit` all'interno di un bloc chiuso lancerà un `StateError` che sarà segnalato come eccezione non gestita e propagato a `onError`. #### ❗🧹 Rimuovere API Deprecate :::note[Cosa è Cambiato?] In bloc v8.0.0, tutte le API precedentemente deprecate sono state rimosse. ::: ##### Riepilogo - `mapEventToState` rimosso a favore di `on`; - `transformEvents` rimosso a favore dell'API `EventTransformer`; - `TransitionFunction` typedef rimosso a favore dell'API `EventTransformer`; - `listen` rimosso a favore di `stream.listen`. ### `package:bloc_test` #### ✨ `MockBloc` e `MockCubit` non richiedono più `registerFallbackValue` :::note[Cosa è Cambiato?] In bloc_test v9.0.0, gli sviluppatori non devono più chiamare esplicitamente `registerFallbackValue` quando usano `MockBloc` o `MockCubit`. ::: ##### Riepilogo `registerFallbackValue` è necessario solo quando si usa il matcher `any()` da `package:mocktail` per un tipo personalizzato. Precedentemente, `registerFallbackValue` era necessario per ogni `Event` e `State` quando si usava `MockBloc` o `MockCubit`. **v8.x.x** ```dart class FakeMyEvent extends Fake implements MyEvent {} class FakeMyState extends Fake implements MyState {} class MyMockBloc extends MockBloc implements MyBloc {} void main() { setUpAll(() { registerFallbackValue(FakeMyEvent()); registerFallbackValue(FakeMyState()); }); // Test... } ``` **v9.0.0** ```dart class MyMockBloc extends MockBloc implements MyBloc {} void main() { // Test... } ``` ### `package:hydrated_bloc` #### ❗✨ Introdurre nuova API `HydratedBlocOverrides` :::note[Cosa è Cambiato?] In hydrated_bloc v8.0.0, `HydratedBloc.storage` è stato rimosso a favore dell'API `HydratedBlocOverrides`. ::: ##### Motivazione Precedentemente, veniva usato un singleton globale per sovrascrivere l'implementazione `Storage`. Di conseguenza, non era possibile avere più implementazioni `Storage` con ambito in diverse parti dell'applicazione. Era anche più difficile testare a causa dello stato globale condiviso tra test. `HydratedBloc` v8.0.0 introduce una classe `HydratedBlocOverrides` che permette agli sviluppatori di sovrascrivere `Storage` per una specifica `Zone` piuttosto che affidarsi a un singleton globale mutabile. **v7.x.x** ```dart void main() async { HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); // ... } ``` **v8.0.0** ```dart void main() { final storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); HydratedBlocOverrides.runZoned( () { // ... }, storage: storage, ); } ``` Le istanze `HydratedBloc` useranno il `Storage` per la `Zone` corrente tramite `HydratedBlocOverrides.current`. Questo permette a ogni `Zone` di funzionare indipendentemente con i propri `BlocOverrides`. ## v7.2.0 ### `package:bloc` #### ✨ Introdurre nuova API `on` :::note[Cosa è Cambiato?] In bloc v7.2.0, `mapEventToState` è stato deprecato a favore di `on`. `mapEventToState` sarà rimosso in bloc v8.0.0. ::: ##### Motivazione L'API `on` è stata introdotta come parte di [[Proposta] Sostituire mapEventToState con on\ in Bloc](https://github.com/felangel/bloc/issues/2526). A causa di [un problema in Dart](https://github.com/dart-lang/sdk/issues/44616) non è sempre ovvio quale sarà il valore di `state` quando si ha a che fare con generatori asincroni annidati (`async*`). Anche se ci sono modi per aggirare il problema, uno dei principi fondamentali della libreria bloc è essere prevedibile. L'API `on` è stata creata per rendere la libreria il più sicura possibile da usare e per eliminare qualsiasi incertezza quando si tratta di cambiamenti di stato. :::tip Per maggiori informazioni, [leggi la proposta completa](https://github.com/felangel/bloc/issues/2526). ::: **Riepilogo** `on` ti permette di registrare un gestore di eventi per tutti gli eventi di tipo `E`. Di default, gli eventi saranno processati concorrentemente quando si usa `on` al contrario di `mapEventToState` che processa eventi `sequenzialmente`. **v7.1.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0); @override Stream mapEventToState(CounterEvent event) async* { if (event is Increment) { yield state + 1; } } } ``` **v7.2.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } } ``` :::note Ogni `EventHandler` registrato funziona indipendentemente quindi è importante registrare gestori di eventi in base al tipo di trasformatore che vuoi applicato. ::: Se vuoi mantenere esattamente lo stesso comportamento di v7.1.0 puoi registrare un singolo gestore di eventi per tutti gli eventi e applicare un trasformatore `sequential`: ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; class MyBloc extends Bloc { MyBloc() : super(MyState()) { on(_onEvent, transformer: sequential()) } FutureOr _onEvent(MyEvent event, Emitter emit) async { // TODO: la logica va qui... } } ``` Puoi anche sovrascrivere il `EventTransformer` di default per tutti i bloc nella tua applicazione: ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; void main() { Bloc.transformer = sequential(); ... } ``` #### ✨ Introdurre nuova API `EventTransformer` :::note[Cosa è Cambiato?] In bloc v7.2.0, `transformEvents` è stato deprecato a favore dell'API `EventTransformer`. `transformEvents` sarà rimosso in bloc v8.0.0. ::: ##### Motivazione L'API `on` ha aperto la porta alla possibilità di fornire un trasformatore di eventi personalizzato per gestore di eventi. È stato introdotto un nuovo typedef `EventTransformer` che permette agli sviluppatori di trasformare lo stream di eventi in arrivo per ogni gestore di eventi piuttosto che dover specificare un singolo trasformatore di eventi per tutti gli eventi. **Riepilogo** Un `EventTransformer` è responsabile di prendere lo stream di eventi in arrivo insieme a un `EventMapper` (il tuo gestore di eventi) e restituire un nuovo stream di eventi. ```dart typedef EventTransformer = Stream Function(Stream events, EventMapper mapper) ``` Il `EventTransformer` di default processa tutti gli eventi concorrentemente e assomiglia a qualcosa del tipo: ```dart EventTransformer concurrent() { return (events, mapper) => events.flatMap(mapper); } ``` :::tip Dai un'occhiata a [package:bloc_concurrency](https://pub.dev/packages/bloc_concurrency) per un set opinionato di trasformatori di eventi personalizzati ::: **v7.1.0** ```dart @override Stream> transformEvents(events, transitionFn) { return events .debounceTime(const Duration(milliseconds: 300)) .flatMap(transitionFn); } ``` **v7.2.0** ```dart /// Definisci un `EventTransformer` personalizzato EventTransformer debounce(Duration duration) { return (events, mapper) => events.debounceTime(duration).flatMap(mapper); } MyBloc() : super(MyState()) { /// Applica il `EventTransformer` personalizzato all'`EventHandler` on(_onEvent, transformer: debounce(const Duration(milliseconds: 300))) } ``` #### ⚠️ Deprecare API `transformTransitions` :::note[Cosa è Cambiato?] In bloc v7.2.0, `transformTransitions` è stato deprecato a favore di sovrascrivere l'API `stream`. `transformTransitions` sarà rimosso in bloc v8.0.0. ::: ##### Motivazione Il getter `stream` su `Bloc` rende facile sovrascrivere lo stream in uscita di stati quindi non è più prezioso mantenere un'API separata `transformTransitions`. **Riepilogo** **v7.1.0** ```dart @override Stream> transformTransitions( Stream> transitions, ) { return transitions.debounceTime(const Duration(milliseconds: 42)); } ``` **v7.2.0** ```dart @override Stream get stream => super.stream.debounceTime(const Duration(milliseconds: 42)); ``` ## v7.0.0 ### `package:bloc` #### ❗ Bloc e Cubit estendono BlocBase ##### Motivazione Come sviluppatore, la relazione tra bloc e cubit era un po' scomoda. Quando cubit è stato introdotto per la prima volta è iniziato come classe base per bloc il che aveva senso perché aveva un sottoinsieme della funzionalità e i bloc avrebbero semplicemente esteso Cubit e definito API aggiuntive. Questo ha portato ad alcuni svantaggi: - Tutte le API dovrebbero essere o rinominate per accettare un cubit per accuratezza o dovrebbero essere mantenute come bloc per coerenza anche se gerarchicamente è inaccurato ([#1708](https://github.com/felangel/bloc/issues/1708), [#1560](https://github.com/felangel/bloc/issues/1560)); - Cubit avrebbe bisogno di estendere Stream e implementare EventSink per avere una base comune su cui widget come BlocBuilder, BlocListener, ecc. possono essere implementati ([#1429](https://github.com/felangel/bloc/issues/1429)). Più tardi, abbiamo sperimentato invertendo la relazione e rendendo bloc la classe base che ha parzialmente risolto il primo punto sopra ma ha introdotto altri problemi: - L'API cubit è gonfia a causa delle API bloc sottostanti come mapEventToState, add, ecc. ([#2228](https://github.com/felangel/bloc/issues/2228)) - Gli sviluppatori possono tecnicamente invocare queste API e rompere le cose; - Abbiamo ancora lo stesso problema di cubit che espone l'intera API stream come prima. ([#1429](https://github.com/felangel/bloc/issues/1429)) Per affrontare questi problemi abbiamo introdotto una classe base per entrambi `Bloc` e `Cubit` chiamata `BlocBase` così che i componenti upstream possano ancora interoperare con entrambe le istanze bloc e cubit ma senza esporre l'intera API `Stream` e `EventSink` direttamente. **Riepilogo** **BlocObserver** **v6.1.x** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(Cubit cubit) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(Cubit cubit, Object event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(Cubit cubit, Object error, StackTrace stackTrace) {...} @override void onClose(Cubit cubit) {...} } ``` **v7.0.0** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(BlocBase bloc) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(BlocBase bloc, Object? event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) {...} @override void onClose(BlocBase bloc) {...} } ``` **Bloc/Cubit** **v6.1.x** ```dart final bloc = MyBloc(); bloc.listen((state) {...}); final cubit = MyCubit(); cubit.listen((state) {...}); ``` **v7.0.0** ```dart final bloc = MyBloc(); bloc.stream.listen((state) {...}); final cubit = MyCubit(); cubit.stream.listen((state) {...}); ``` ### `package:bloc_test` #### ❗seed restituisce una funzione per supportare valori dinamici ##### Motivazione Per supportare avere un valore seed mutabile che può essere aggiornato dinamicamente in `setUp`, `seed` restituisce una funzione. **Riepilogo** **v7.x.x** ```dart blocTest( '...', seed: MyState(), ... ); ``` **v8.0.0** ```dart blocTest( '...', seed: () => MyState(), ... ); ``` #### ❗expect restituisce una funzione per supportare valori dinamici e include supporto matcher ##### Motivazione Per supportare avere un'aspettativa mutabile che può essere aggiornata dinamicamente in `setUp`, `expect` restituisce una funzione. `expect` supporta anche `Matchers`. **Riepilogo** **v7.x.x** ```dart blocTest( '...', expect: [MyStateA(), MyStateB()], ... ); ``` **v8.0.0** ```dart blocTest( '...', expect: () => [MyStateA(), MyStateB()], ... ); // Può anche essere un `Matcher` blocTest( '...', expect: () => contains(MyStateA()), ... ); ``` #### ❗errors restituisce una funzione per supportare valori dinamici e include supporto matcher ##### Motivazione Per supportare avere errori mutabili che possono essere aggiornati dinamicamente in `setUp`, `errors` restituisce una funzione. `errors` supporta anche `Matchers`. **Riepilogo** **v7.x.x** ```dart blocTest( '...', errors: [MyError()], ... ); ``` **v8.0.0** ```dart blocTest( '...', errors: () => [MyError()], ... ); // Può anche essere un `Matcher` blocTest( '...', errors: () => contains(MyError()), ... ); ``` #### ❗MockBloc e MockCubit ##### Motivazione Per supportare lo stubbing di varie API core, `MockBloc` e `MockCubit` sono esportati come parte del pacchetto `bloc_test`. Precedentemente, `MockBloc` doveva essere usato per entrambe le istanze `Bloc` e `Cubit` il che non era intuitivo. **Riepilogo** **v7.x.x** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockBloc implements MyBloc {} ``` **v8.0.0** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockCubit implements MyCubit {} ``` #### ❗Integrazione Mocktail ##### Motivazione A causa di varie limitazioni del null-safe [package:mockito](https://pub.dev/packages/mockito) descritte [qui](https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#problems-with-typical-mocking-and-stubbing), [package:mocktail](https://pub.dev/packages/mocktail) è usato da `MockBloc` e `MockCubit`. Questo permette agli sviluppatori di continuare a usare un'API di mocking familiare senza la necessità di scrivere implementazioni simulate (stub) manualmente o affidarsi alla generazione di codice. **Riepilogo** **v7.x.x** ```dart import 'package:mockito/mockito.dart'; ... when(bloc.state).thenReturn(MyState()); verify(bloc.add(any)).called(1); ``` **v8.0.0** ```dart import 'package:mocktail/mocktail.dart'; ... when(() => bloc.state).thenReturn(MyState()); verify(() => bloc.add(any())).called(1); ``` > Fai riferimento a [#347](https://github.com/dart-lang/mockito/issues/347) così > come alla > [documentazione mocktail](https://github.com/felangel/mocktail/tree/main/packages/mocktail) > per maggiori informazioni. ### `package:flutter_bloc` #### ❗ rinominare parametro `cubit` a `bloc` ##### Motivazione Come risultato del refactoring in `package:bloc` per introdurre `BlocBase` che `Bloc` e `Cubit` estendono, i parametri di `BlocBuilder`, `BlocConsumer`, e `BlocListener` sono stati rinominati da `cubit` a `bloc` perché i widget operano sul tipo `BlocBase`. Questo si allinea anche ulteriormente con il nome della libreria e speriamo migliori la leggibilità. **Riepilogo** **v6.1.x** ```dart BlocBuilder( cubit: myBloc, ... ) BlocListener( cubit: myBloc, ... ) BlocConsumer( cubit: myBloc, ... ) ``` **v7.0.0** ```dart BlocBuilder( bloc: myBloc, ... ) BlocListener( bloc: myBloc, ... ) BlocConsumer( bloc: myBloc, ... ) ``` ### `package:hydrated_bloc` #### ❗storageDirectory è richiesto quando si chiama HydratedStorage.build ##### Motivazione Per rendere `package:hydrated_bloc` un pacchetto Dart puro, la dipendenza su [package:path_provider](https://pub.dev/packages/path_provider) è stata rimossa e il parametro `storageDirectory` quando si chiama `HydratedStorage.build` è richiesto e non ha più default a `getTemporaryDirectory`. **Riepilogo** **v6.x.x** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` **v7.0.0** ```dart import 'package:path_provider/path_provider.dart'; ... HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getTemporaryDirectory(), ); ``` ## v6.1.0 ### `package:flutter_bloc` #### ❗context.bloc e context.repository sono deprecati a favore di context.read e context.watch ##### Motivazione `context.read`, `context.watch`, e `context.select` sono stati aggiunti per allinearsi con l'API esistente di [provider](https://pub.dev/packages/provider) con cui molti sviluppatori hanno familiarità e per affrontare problemi che sono stati sollevati dalla comunità. Per migliorare la sicurezza del codice e mantenere la coerenza, `context.bloc` è stato deprecato perché può essere sostituito con `context.read` o `context.watch` a seconda che sia usato direttamente all'interno di `build`. **context.watch** `context.watch` affronta la richiesta di avere un [MultiBlocBuilder](https://github.com/felangel/bloc/issues/538) perché possiamo guardare diversi bloc all'interno di un singolo `Builder` per renderizzare UI basata su stati multipli: ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // restituisci un Widget che dipende dallo stato di BlocA, BlocB e BlocC } ); ``` **context.select** `context.select` permette agli sviluppatori di renderizzare/aggiornare UI basata su una parte di uno stato bloc e affronta la richiesta di avere un [buildWhen più semplice](https://github.com/felangel/bloc/issues/1521). ```dart final name = context.select((UserBloc bloc) => bloc.state.user.name); ``` Il frammento sopra ci permette di accedere e ricostruire il widget solo quando il nome dell'utente corrente cambia. **context.read** Anche se sembra che `context.read` sia identico a `context.bloc` ci sono alcune differenze sottili ma significative. Entrambi ti permettono di accedere a un bloc con un `BuildContext` e non risultano in aggiornamenti; tuttavia, `context.read` non può essere chiamato direttamente all'interno di un metodo `build`. Ci sono due ragioni principali per usare `context.bloc` all'interno di `build`: 1. **Per accedere allo stato del bloc** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` L'uso sopra è soggetto a errori perché il widget `Text` non sarà ricostruito se lo stato del bloc cambia. In questo scenario, dovrebbe essere usato un `BlocBuilder` o `context.watch`. ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` o ```dart @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) => Text('$state'), ); } ``` :::note Usare `context.watch` alla radice del metodo `build` risulterà nel aggiornamento dell'intero widget quando lo stato del bloc cambia. Se l'intero widget non ha bisogno di essere ricostruito, usa `BlocBuilder` per avvolgere le parti che dovrebbero essere ricostruite, usa un `Builder` con `context.watch` per limitare gli aggiornamenti, o scomponi il widget in widget più piccoli. ::: 2. **Per accedere al bloc così che un evento possa essere aggiunto** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` L'uso sopra è inefficiente perché risulta in una ricerca del bloc su ogni aggiornamento quando il bloc è necessario solo quando l'utente tocca l'`ElevatedButton`. In questo scenario, preferisci usare `context.read` per accedere al bloc direttamente dove è necessario (in questo caso, nel callback `onPressed`). ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` **Riepilogo** **v6.0.x** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` ?> Se accedi a un bloc per aggiungere un evento, esegui l'accesso al bloc usando `context.read` nel callback dove è necessario. **v6.0.x** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` ?> Usa `context.watch` quando accedi allo stato del bloc per assicurarti che il widget sia ricostruito quando lo stato cambia. ## v6.0.0 ### `package:bloc` #### ❗BlocObserver onError prende Cubit ##### Motivazione A causa dell'integrazione di `Cubit`, `onError` è ora condiviso tra entrambe le istanze `Bloc` e `Cubit`. Poiché `Cubit` è la base, `BlocObserver` accetterà un tipo `Cubit` piuttosto che un tipo `Bloc` nell'override `onError`. **v5.x.x** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Bloc bloc, Object error, StackTrace stackTrace) { super.onError(bloc, error, stackTrace); } } ``` **v6.0.0** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Cubit cubit, Object error, StackTrace stackTrace) { super.onError(cubit, error, stackTrace); } } ``` #### ❗Bloc non emette ultimo stato su sottoscrizione ##### Motivazione Questo cambiamento è stato fatto per allineare `Bloc` e `Cubit` con il comportamento `Stream` integrato in `Dart`. Inoltre, conformarsi al vecchio comportamento nel contesto di `Cubit` ha portato a molti effetti collaterali non intenzionali e in generale ha complicato le implementazioni interne di altri pacchetti come `flutter_bloc` e `bloc_test` inutilmente (richiedendo `skip(1)`, ecc...). **v5.x.x** ```dart final bloc = MyBloc(); bloc.listen(print); ``` Precedentemente, il frammento sopra avrebbe emesso lo stato iniziale del bloc seguito da cambiamenti di stato successivi. **v6.x.x** In v6.0.0, il frammento sopra non emette lo stato iniziale e emette solo cambiamenti di stato successivi. Il comportamento precedente può essere ottenuto con il seguente: ```dart final bloc = MyBloc(); print(bloc.state); bloc.listen(print); ``` ?> **Nota**: Questo cambiamento influenzerà solo il codice che si affida a sottoscrizioni bloc diretti. Quando si usa `BlocBuilder`, `BlocListener`, o `BlocConsumer` non ci sarà alcun cambiamento evidente nel comportamento. ### `package:bloc_test` #### ❗MockBloc richiede solo tipo State ##### Motivazione Non è necessario ed elimina codice extra rendendo anche `MockBloc` compatibile con `Cubit`. **v5.x.x** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` **v6.0.0** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` #### ❗whenListen richiede solo tipo State ##### Motivazione Non è necessario ed elimina codice extra rendendo anche `whenListen` compatibile con `Cubit`. **v5.x.x** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` **v6.0.0** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` #### ❗blocTest non richiede tipo Event ##### Motivazione Non è necessario ed elimina codice extra rendendo anche `blocTest` compatibile con `Cubit`. **v5.x.x** ```dart blocTest( 'emits [1] when increment is called', build: () async => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` **v6.0.0** ```dart blocTest( 'emits [1] when increment is called', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` #### ❗blocTest skip default a 0 ##### Motivazione Poiché le istanze `bloc` e `cubit` non emetteranno più lo stato più recente per nuove sottoscrizioni, non era più necessario impostare `skip` di default a `1`. **v5.x.x** ```dart blocTest( 'emits [0] when skip is 0', build: () async => CounterBloc(), skip: 0, expect: const [0], ); ``` **v6.0.0** ```dart blocTest( 'emits [] when skip is 0', build: () => CounterBloc(), skip: 0, expect: const [], ); ``` Lo stato iniziale di un bloc o cubit può essere testato con il seguente: ```dart test('initial state is correct', () { expect(MyBloc().state, InitialState()); }); ``` #### ❗blocTest rendere build sincrono ##### Motivazione Precedentemente, `build` era reso `async` così che varie preparazioni potessero essere fatte per mettere il bloc sotto test in uno stato specifico. Non è più necessario e risolve anche diversi problemi a causa della latenza aggiunta tra la build e l' sottoscrizione internamente. Invece di fare preparazione async per ottenere un bloc in uno stato desiderato possiamo ora impostare lo stato del bloc concatenando `emit` con lo stato desiderato. **v5.x.x** ```dart blocTest( 'emits [2] when increment is added', build: () async { final bloc = CounterBloc(); bloc.add(CounterEvent.increment); await bloc.take(2); return bloc; } act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` **v6.0.0** ```dart blocTest( 'emits [2] when increment is added', build: () => CounterBloc()..emit(1), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` :::note `emit` è visibile solo per il testing e non dovrebbe mai essere usato al di fuori dei test. ::: ### `package:flutter_bloc` #### ❗BlocBuilder parametro bloc rinominato a cubit ##### Motivazione Per rendere `BlocBuilder` interoperabile con istanze `bloc` e `cubit` il parametro `bloc` è stato rinominato a `cubit` (poiché `Cubit` è la classe base). **v5.x.x** ```dart BlocBuilder( bloc: myBloc, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocBuilder( cubit: myBloc, builder: (context, state) {...} ) ``` #### ❗BlocListener parametro bloc rinominato a cubit ##### Motivazione Per rendere `BlocListener` interoperabile con istanze `bloc` e `cubit` il parametro `bloc` è stato rinominato a `cubit` (poiché `Cubit` è la classe base). **v5.x.x** ```dart BlocListener( bloc: myBloc, listener: (context, state) {...} ) ``` **v6.0.0** ```dart BlocListener( cubit: myBloc, listener: (context, state) {...} ) ``` #### ❗BlocConsumer parametro bloc rinominato a cubit ##### Motivazione Per rendere `BlocConsumer` interoperabile con istanze `bloc` e `cubit` il parametro `bloc` è stato rinominato a `cubit` (poiché `Cubit` è la classe base). **v5.x.x** ```dart BlocConsumer( bloc: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocConsumer( cubit: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` --- ## v5.0.0 ### `package:bloc` #### ❗initialState è stato rimosso ##### Motivazione Come sviluppatore, dover sovrascrivere `initialState` quando si crea un bloc presenta due problemi principali: - Lo `initialState` del bloc può essere dinamico e può anche essere referenziato in un momento successivo (anche al di fuori del bloc stesso). In alcuni modi, questo può essere visto come perdita di informazioni interne del bloc al livello UI; - È verboso. **v4.x.x** ```dart class CounterBloc extends Bloc { @override int get initialState => 0; ... } ``` **v5.0.0** ```dart class CounterBloc extends Bloc { CounterBloc() : super(0); ... } ``` ?> Per maggiori informazioni controlla [#1304](https://github.com/felangel/bloc/issues/1304) #### ❗BlocDelegate rinominato a BlocObserver ##### Motivazione Il nome `BlocDelegate` non era una descrizione accurata del ruolo che la classe giocava. `BlocDelegate` suggerisce che la classe gioca un ruolo attivo mentre in realtà il ruolo inteso del `BlocDelegate` era che fosse un componente passivo che semplicemente osserva tutti i bloc in un'applicazione. :::note Non dovrebbe idealmente esserci funzionalità o caratteristiche user-facing gestite all'interno di `BlocObserver`. ::: **v4.x.x** ```dart class MyBlocDelegate extends BlocDelegate { ... } ``` **v5.0.0** ```dart class MyBlocObserver extends BlocObserver { ... } ``` #### ❗BlocSupervisor è stato rimosso ##### Motivazione `BlocSupervisor` era un altro componente che gli sviluppatori dovevano conoscere e interagire per il solo scopo di specificare un `BlocDelegate` personalizzato. Con il cambiamento a `BlocObserver` abbiamo sentito che migliorava l'esperienza dello sviluppatore impostare l'observer direttamente sul bloc stesso. ?> Questo cambiamento ci ha anche permesso di disaccoppiare altri add-on bloc come `HydratedStorage` dal `BlocObserver`. **v4.x.x** ```dart BlocSupervisor.delegate = MyBlocDelegate(); ``` **v5.0.0** ```dart Bloc.observer = MyBlocObserver(); ``` ### `package:flutter_bloc` #### ❗BlocBuilder condition rinominato a buildWhen ##### Motivazione Quando si usa `BlocBuilder`, potevamo precedentemente specificare una `condition` per determinare se il `builder` dovrebbe essere ricostruito. ```dart BlocBuilder( condition: (previous, current) { // restituisci true/false per determinare se chiamare builder }, builder: (context, state) {...} ) ``` Il nome `condition` non è molto auto-esplicativo o ovvio e più importante, quando si interagisce con un `BlocConsumer` l'API è diventata inconsistente perché gli sviluppatori possono fornire due condizioni (una per `builder` e una per `listener`). Di conseguenza, l'API `BlocConsumer` espone un `buildWhen` e `listenWhen` ```dart BlocConsumer( listenWhen: (previous, current) { // restituisci true/false per determinare se chiamare listener }, listener: (context, state) {...}, buildWhen: (previous, current) { // restituisci true/false per determinare se chiamare builder }, builder: (context, state) {...}, ) ``` Per allineare l'API e fornire un'esperienza più coerente per lo sviluppatore, `condition` è stato rinominato a `buildWhen`. **v4.x.x** ```dart BlocBuilder( condition: (previous, current) { // restituisci true/false per determinare se chiamare builder }, builder: (context, state) {...} ) ``` **v5.0.0** ```dart BlocBuilder( buildWhen: (previous, current) { // restituisci true/false per determinare se chiamare builder }, builder: (context, state) {...} ) ``` #### ❗BlocListener condition rinominato a listenWhen ##### Motivazione Per le stesse ragioni descritte sopra, anche la `condition` di `BlocListener` è stata rinominata. **v4.x.x** ```dart BlocListener( condition: (previous, current) { // restituisci true/false per determinare se chiamare listener }, listener: (context, state) {...} ) ``` **v5.0.0** ```dart BlocListener( listenWhen: (previous, current) { // restituisci true/false per determinare se chiamare listener }, listener: (context, state) {...} ) ``` ### `package:hydrated_bloc` #### ❗HydratedStorage e HydratedBlocStorage rinominati ##### Motivazione Per migliorare il riutilizzo del codice tra [hydrated_bloc](https://pub.dev/packages/hydrated_bloc) e [hydrated_cubit](https://pub.dev/packages/hydrated_cubit), l'implementazione storage concreta di default è stata rinominata da `HydratedBlocStorage` a `HydratedStorage`. Inoltre, l'interfaccia `HydratedStorage` è stata rinominata da `HydratedStorage` a `Storage`. **v4.0.0** ```dart class MyHydratedStorage implements HydratedStorage { ... } ``` **v5.0.0** ```dart class MyHydratedStorage implements Storage { ... } ``` #### ❗HydratedStorage disaccoppiato da BlocDelegate ##### Motivazione Come menzionato prima, `BlocDelegate` è stato rinominato a `BlocObserver` ed è stato impostato direttamente come parte del `bloc` tramite: ```dart Bloc.observer = MyBlocObserver(); ``` Il seguente cambiamento è stato fatto per: - Rimanere coerenti con la nuova API bloc observer; - Mantenere lo storage con ambito solo a `HydratedBloc`; - Disaccoppiare il `BlocObserver` da `Storage`. **v4.0.0** ```dart BlocSupervisor.delegate = await HydratedBlocDelegate.build(); ``` **v5.0.0** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` #### ❗Inizializzazione Semplificata ##### Motivazione Precedentemente, gli sviluppatori dovevano chiamare manualmente `super.initialState ?? DefaultInitialState()` per impostare le loro istanze `HydratedBloc`. Questo è goffo e verboso e anche incompatibile con i cambiamenti breaking a `initialState` in `bloc`. Di conseguenza, in v5.0.0 l'inizializzazione di `HydratedBloc` è identica all'inizializzazione normale di `Bloc`. **v4.0.0** ```dart class CounterBloc extends HydratedBloc { @override int get initialState => super.initialState ?? 0; } ``` **v5.0.0** ```dart class CounterBloc extends HydratedBloc { CounterBloc() : super(0); ... } ``` ================================================ FILE: docs/src/content/docs/it/modeling-state.mdx ================================================ --- title: Modellare lo Stato description: Una panoramica di diversi modi per modellare gli stati quando si usa package:bloc. --- import ConcreteClassAndStatusEnumSnippet from '~/components/modeling-state/ConcreteClassAndStatusEnumSnippet.astro'; import SealedClassAndSubclassesSnippet from '~/components/modeling-state/SealedClassAndSubclassesSnippet.astro'; Ci sono molti approcci diversi quando si tratta di strutturare lo stato dell'applicazione. Ognuno ha i suoi vantaggi e svantaggi. In questa sezione, daremo un'occhiata a diversi approcci, i loro pro e contro, e quando usare ciascuno. I seguenti approcci sono semplicemente raccomandazioni e sono completamente opzionali. Sentiti libero di usare qualsiasi approccio preferisci. Potresti notare che alcuni esempi o parti della documentazione non seguono rigorosamente questi approcci, principalmente per ragioni di semplicità e brevità. :::tip I seguenti frammenti di codice si concentrano sulla struttura dello stato. All'atto pratico, potresti anche voler: - Estendere `Equatable` da [`package:equatable`](https://pub.dev/packages/equatable); - Annotare la classe con `@Data()` da [`package:data_class`](https://pub.dev/packages/data_class); - Annotare la classe con **@immutable** da [`package:meta`](https://pub.dev/packages/meta); - Implementare un metodo `copyWith`; - Usare la parola chiave `const` per i costruttori. ::: ## Classe Concreta e Enum di Stato Questo approccio consiste in una **singola classe concreta** per tutti gli stati con un parametro `enum` che rappresenta diversi stati. Le proprietà sono rese nullable e sono gestite in base allo stato corrente. Questo approccio funziona meglio per stati che non sono strettamente esclusivi e/o contengono molte proprietà condivise. #### Pro - **Semplice**: Facile gestire una singola classe e un enum di stato e tutte le proprietà sono facilmente accessibili; - **Conciso**: Generalmente richiede meno righe di codice rispetto ad altri approcci. #### Contro - **Non "Type Safe"**: Richiede controllare lo `status` prima di accedere alle proprietà. È possibile emittare (`emit`) uno stato malformato che può portare a bug. Le proprietà per stati specifici sono nullable; questo può rendere la gestione macchinosa, poiché richiede continui controlli sui null o l'uso del "force unwrap". Alcuni di questi svantaggi possono essere mitigati scrivendo unit test e definendo dei costruttori nominativi specializzati; - **Sovraccarico**: Il rischio è di avere un unico stato che, con il passare del tempo, può sovraccaricarsi di proprietà. #### Verdetto Questo approccio funziona meglio per stati semplici o quando i requisiti richiedono stati che non sono esclusivi (ad es. mostrare uno snackbar quando si verifica un errore mentre si mostrano ancora i vecchi dati dall'ultimo stato di successo). Questo approccio fornisce flessibilità e concisione al costo della "type safety". ## Classe Sealed e Sottoclassi Questo approccio consiste in una **classe sealed** che contiene qualsiasi proprietà condivisa e più sottoclassi per gli stati separati. Questo approccio è ottimo per creare stati separati con proprietà esclusive. #### Pro - **"Type Safe"**: Il codice è sicuro in fase di compilazione e non è possibile accedere accidentalmente a una proprietà non valida. Ogni sottoclasse contiene le sue proprietà, rendendo chiaro quali proprietà appartengono a quale stato; - **Esplicito:** Separa le proprietà condivise dalle proprietà specifiche dello stato; - **Esaustivo**: Usare un'istruzione `switch` ci assicura che ogni stato sia gestito esplicitamente. - Se non vuoi [switch esaustivi](https://dart.dev/language/branches#exhaustiveness-checking) o vuoi essere in grado di aggiungere sottotipi in seguito senza rompere l'API, usa il modificatore [final](https://dart.dev/language/class-modifiers#final). - Vedi la [documentazione delle classi sealed](https://dart.dev/language/class-modifiers#sealed) per maggiori dettagli. #### Contro - **Verboso**: Richiede più codice (una classe base e una sottoclasse per stato). Inoltre può richiedere codice duplicato per proprietà condivise tra alcune sottoclassi; - **Complesso**: Aggiungere nuove proprietà richiede aggiornare ogni sottoclasse e la classe base, il che può essere scomodo e portare ad aumenti nella complessità dello stato. Inoltre, può richiedere controlli di tipo non necessari/eccessivi per accedere alle proprietà. #### Verdetto Questo approccio funziona meglio per stati ben definiti ed esclusivi con proprietà uniche. Questo approccio fornisce sicurezza sui tipi, controlli di esaustività e enfatizza la sicurezza rispetto a concisione e semplicità. ================================================ FILE: docs/src/content/docs/it/naming-conventions.mdx ================================================ --- title: Convenzioni di Nomenclatura description: Panoramica delle convenzioni di nomenclatura raccomandate quando si usa bloc. --- import EventExamplesGood1 from '~/components/naming-conventions/EventExamplesGood1Snippet.astro'; import EventExamplesBad1 from '~/components/naming-conventions/EventExamplesBad1Snippet.astro'; import StateExamplesGood1Snippet from '~/components/naming-conventions/StateExamplesGood1Snippet.astro'; import SingleStateExamplesGood1Snippet from '~/components/naming-conventions/SingleStateExamplesGood1Snippet.astro'; import StateExamplesBad1Snippet from '~/components/naming-conventions/StateExamplesBad1Snippet.astro'; Le seguenti convenzioni di nomenclatura sono semplicemente raccomandazioni e sono completamente opzionali. Sentiti libero di usare qualsiasi convenzione di nomenclatura preferisci. Potresti notare che alcuni esempi o parti della documentazione non seguono le convenzioni di nomenclatura, principalmente per ragioni di semplicità e brevità. Tuttavia, queste convenzioni sono caldamente raccomandate per progetti di grandi dimensioni che coinvolgono più sviluppatori. ## Convenzioni per gli Eventi Gli eventi dovrebbero essere nominati al **tempo passato** perché gli eventi sono cose che sono già accadute dalla prospettiva del bloc. ### Anatomia `BlocSubject` + `Sostantivo (opzionale)` + `Verbo (evento)` Gli eventi di caricamento iniziale dovrebbero seguire la convenzione: `BlocSubject` + `Started` :::note La classe evento base dovrebbe essere nominata: `BlocSubject` + `Event`. ::: ### Esempi ✅ **Buono** ❌ **Errato** ## Convenzioni per gli Stati Gli stati dovrebbero essere sostantivi perché uno stato è solo un'istantanea in un punto particolare nel tempo. Ci sono due modi comuni per rappresentare lo stato: usando sottoclassi o usando una singola classe. ### Anatomia #### Sottoclassi `BlocSubject` + `Verbo (azione)` + `State` Quando si rappresenta lo stato come più sottoclassi `State` dovrebbe essere uno dei seguenti: `Initial` | `Success` | `Failure` | `InProgress` :::note Gli stati iniziali dovrebbero seguire la convenzione: `BlocSubject` + `Initial`. ::: #### Classe Singola `BlocSubject` + `State` Quando si rappresenta lo stato come una singola classe base un enum chiamato `BlocSubject` + `Status` dovrebbe essere usato per rappresentare la condizione dello stato: `initial` | `success` | `failure` | `loading`. :::note La classe stato base dovrebbe sempre essere nominata: `BlocSubject` + `State`. ::: ### Esempi ✅ **Buono** ##### Sottoclassi ##### Classe Singola ❌ **Errato** ================================================ FILE: docs/src/content/docs/it/testing.mdx ================================================ --- title: Testing description: Le basi su come scrivere test per i tuoi bloc. --- import CounterBlocSnippet from '~/components/testing/CounterBlocSnippet.astro'; import AddDevDependenciesSnippet from '~/components/testing/AddDevDependenciesSnippet.astro'; import CounterBlocTestImportsSnippet from '~/components/testing/CounterBlocTestImportsSnippet.astro'; import CounterBlocTestMainSnippet from '~/components/testing/CounterBlocTestMainSnippet.astro'; import CounterBlocTestSetupSnippet from '~/components/testing/CounterBlocTestSetupSnippet.astro'; import CounterBlocTestInitialStateSnippet from '~/components/testing/CounterBlocTestInitialStateSnippet.astro'; import CounterBlocTestBlocTestSnippet from '~/components/testing/CounterBlocTestBlocTestSnippet.astro'; Bloc è stato progettato per essere estremamente facile da testare. In questa sezione, vedremo come testare un bloc con i test unitari. Per semplicità, scriviamo i test per il `CounterBloc` che abbiamo creato in [Concetti Bloc](/it/bloc-concepts). Ricapitolando, l'implementazione del `CounterBloc` è la seguente: ## Configurazione Prima di iniziare a scrivere i nostri test avremo bisogno di aggiungere un framework di test alle nostre dipendenze. Dobbiamo aggiungere [test](https://pub.dev/packages/test) e [bloc_test](https://pub.dev/packages/bloc_test) al nostro progetto. ## Testing Iniziamo creando il file `counter_bloc_test.dart` per i nostri test del `CounterBloc` e importando il pacchetto test. Successivamente, dobbiamo creare il nostro `main` così come il nostro gruppo di test. :::note I gruppi servono sia per organizzare i singoli test, sia per creare un contesto in cui è possibile condividere le operazioni di `setUp` e `tearDown` tra tutti i test contenuti. ::: Iniziamo creando un'istanza del nostro `CounterBloc` che sarà usata in tutti i nostri test. Ora possiamo iniziare a scrivere i nostri test individuali. :::note Possiamo eseguire tutti i nostri test con il comando `dart test`. ::: A questo punto dovremmo avere il nostro primo test che passa! Ora scriviamo un test più complesso usando il pacchetto [bloc_test](https://pub.dev/packages/bloc_test). A questo punto, possiamo eseguire i test e verificare che siano tutti superati. Questo è tutto, il testing dovrebbe essere semplice e dovremmo sentirci fiduciosi quando facciamo cambiamenti e "refactoring" del nostro codice. Puoi fare riferimento all' [App Meteo](https://github.com/felangel/bloc/tree/master/examples/flutter_weather) per un esempio di un'applicazione completamente testata. ================================================ FILE: docs/src/content/docs/it/tutorials/flutter-counter.mdx ================================================ --- title: Flutter Counter description: Una guida approfondita su come costruire un'app Flutter counter con bloc. sidebar: order: 1 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-counter/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) In questo tutorial, costruiremo un contatore in Flutter usando la libreria bloc. ![demo](~/assets/tutorials/flutter-counter.gif) ## Argomenti Chiave - Osservare i cambiamenti di stato con [BlocObserver](/it/bloc-concepts#blocobserver); - [BlocProvider](/it/flutter-bloc-concepts#blocprovider), widget Flutter che fornisce un bloc ai suoi figli; - [BlocBuilder](/it/flutter-bloc-concepts#blocbuilder), widget Flutter che gestisce la costruzione del widget in risposta a nuovi stati; - Usare Cubit invece di Bloc. [Qual è la differenza?](/it/bloc-concepts#cubit-vs-bloc); - Aggiungere eventi con [context.read](/it/flutter-bloc-concepts#contextread). ## Configurazione Inizieremo creando un nuovo progetto Flutter Possiamo poi sostituire il contenuto di `pubspec.yaml` con e poi installare tutte le nostre dipendenze ## Struttura del Progetto ``` ├── lib │ ├── app.dart │ ├── counter │ │ ├── counter.dart │ │ ├── cubit │ │ │ └── counter_cubit.dart │ │ └── view │ │ ├── counter_page.dart │ │ ├── counter_view.dart │ │ └── view.dart │ ├── counter_observer.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` L'applicazione usa una struttura di directory guidata dalle funzionalità. Questa struttura ci permette di scalare il progetto avendo funzionalità autonome. In questo esempio avremo solo una singola funzionalità (il counter stesso) ma in applicazioni più complesse possiamo avere centinaia di funzionalità diverse. ## BlocObserver La prima cosa che vedremo è come creare un `BlocObserver` che ci aiuterà a osservare tutti i cambiamenti di stato nell'applicazione. Creiamo `lib/counter_observer.dart`: In questo caso, stiamo solo sovrascrivendo `onChange` per vedere tutti i cambiamenti di stato che si verificano. :::note `onChange` funziona allo stesso modo per entrambe le istanze `Bloc` e `Cubit`. ::: ## main.dart Successivamente, sostituiamo il contenuto di `lib/main.dart` con: Stiamo inizializzando il `CounterObserver` che abbiamo appena creato e chiamando `runApp` con il widget `CounterApp` che vedremo dopo. ## Counter App Creiamo `lib/app.dart`: `CounterApp` sarà un `MaterialApp` che specifica `CounterPage` come `home`. :::note Stiamo estendendo `MaterialApp` perché `CounterApp` _è_ un `MaterialApp`. Nella maggior parte dei casi, creeremo istanze di `StatelessWidget` o `StatefulWidget` e comporremo widget in `build`, ma in questo caso non ci sono widget da comporre quindi è più semplice estendere `MaterialApp`. ::: Diamo un'occhiata a `CounterPage`! ## Counter Page Creiamo `lib/counter/view/counter_page.dart`: Il widget `CounterPage` è responsabile di creare un `CounterCubit` (che vedremo dopo) e fornirlo a `CounterView`. :::note È importante separare o disaccoppiare la creazione di un `Cubit` dal consumo di un `Cubit` per avere codice molto più testabile e riutilizzabile. ::: ## Counter Cubit Creiamo `lib/counter/cubit/counter_cubit.dart`: La classe `CounterCubit` esporrà due metodi: - `increment`: aggiunge 1 allo stato corrente; - `decrement`: sottrae 1 dallo stato corrente. Il tipo di stato che il `CounterCubit` sta gestendo è solo un `int` e lo stato iniziale è `0`. :::tip Usa l' [Estensione VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) o [Plugin IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) per creare nuovi cubit automaticamente. ::: Successivamente, diamo un'occhiata a `CounterView` che sarà responsabile di consumare lo stato e interagire con il `CounterCubit`. ## Counter View Creiamo `lib/counter/view/counter_view.dart`: `CounterView` è responsabile di renderizzare il conteggio corrente e renderizzare due `FloatingActionButton` per incrementare e decrementare il counter. Un `BlocBuilder` è usato per avvolgere il widget `Text` per aggiornare il testo ogni volta che lo stato del `CounterCubit` cambia. Inoltre, `context.read()` è usato per recuperare l'istanza `CounterCubit` più vicina. :::note Solo il widget `Text` è avvolto in un `BlocBuilder` perché è l'unico widget che deve essere ricostruito in risposta ai cambiamenti di stato del `CounterCubit`. Evita di avvolgere inutilmente widget che non hanno bisogno di essere ricostruiti quando lo stato cambia. ::: ## Barrel Crea `lib/counter/view/view.dart`: Aggiungi `view.dart` per esportare tutte le parti pubbliche della vista del counter. Creiamo `lib/counter/counter.dart`: Aggiungi `counter.dart` per esportare tutte le parti pubbliche della funzionalità counter. Questo è tutto! Abbiamo separato il livello di presentazione dal livello di logica applicativa. `CounterView` non ha idea di cosa succede quando un utente preme un pulsante; semplicemente notifica il `CounterCubit`. Inoltre, il `CounterCubit` non ha idea di cosa sta succedendo con lo stato (valore del counter); sta semplicemente emettendo nuovi stati in risposta ai metodi chiamati. Possiamo eseguire l'app con `flutter run` e visualizzarla sul nostro dispositivo o simulatore/emulatore. Il codice sorgente completo (inclusi test unitari e widget) per questo esempio può essere trovato [qui](https://github.com/felangel/Bloc/tree/master/examples/flutter_counter). ================================================ FILE: docs/src/content/docs/it/tutorials/flutter-firebase-login.mdx ================================================ --- title: Flutter Firebase Login description: Guida completa alla creazione di un flusso di login Flutter con bloc e Firebase. sidebar: order: 7 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-firebase-login/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) In questo tutorial costruiremo in Flutter un flusso di accesso al sistema tramite Firebase usando la libreria bloc. ![demo](~/assets/tutorials/flutter-firebase-login.gif) ## Argomenti Chiave - [BlocProvider](/it/flutter-bloc-concepts#blocprovider), widget Flutter che fornisce un'istanza di bloc ai suoi figli; - Usare Cubit invece di Bloc. [Qual è la differenza?](/it/bloc-concepts#cubit-vs-bloc); - Aggiungere eventi con [context.read](/it/flutter-bloc-concepts#contextread); - Prevenire aggiornamenti non necessari con [Equatable](/it/faqs/#quando-usare-equatable); - [RepositoryProvider](/it/flutter-bloc-concepts#repositoryprovider), widget Flutter che fornisce un repository ai suoi figli; - [BlocListener](/it/flutter-bloc-concepts#bloclistener), widget Flutter che invoca codice in risposta ai cambiamenti di stato nel bloc; - Aggiungere eventi con [context.read](/it/flutter-bloc-concepts#contextselect). ## Configurazione Inizieremo creando un nuovo progetto Flutter. Proprio come nel [tutorial login](/it/tutorials/flutter-login), creeremo pacchetti interni per strutturare meglio l'architettura dell'applicazione e mantenere confini chiari, massimizzando la riutilizzabilità e migliorando la testabilità. In questo caso, i pacchetti [firebase_auth](https://pub.dev/packages/firebase_auth) e [google_sign_in](https://pub.dev/packages/google_sign_in) costituiranno il nostro livello dati; creeremo quindi un `AuthenticationRepository` per comporre i dati provenienti dai due client API. ## Authentication Repository L'`AuthenticationRepository` ha la responsabilità di astrarre i dettagli implementativi dell'autenticazione e del recupero delle informazioni utente. In questo caso si integrerà con Firebase, ma potremo cambiare l'implementazione interna in futuro senza influenzare il resto dell'applicazione. ### Configurazione Inizieremo creando la cartella `packages/authentication_repository` e un `pubspec.yaml` alla radice del progetto. Successivamente, possiamo installare le dipendenze eseguendo: nella directory `authentication_repository`. Come la maggior parte dei pacchetti, l'`authentication_repository` definirà la sua superficie API tramite `packages/authentication_repository/lib/authentication_repository.dart`. :::note Il pacchetto `authentication_repository` esporrà un `AuthenticationRepository` e i relativi modelli. ::: Diamo ora un'occhiata ai modelli. ### User Il modello `User` descriverà un utente nel contesto del dominio di autenticazione. Per gli scopi di questo esempio, un utente sarà composto da `email`, `id`, `name` e `photo`. :::note Sta a te definire come deve essere un utente nel contesto del tuo dominio. ::: [user.dart](https://raw.githubusercontent.com/felangel/bloc/master/examples/flutter_firebase_login/packages/authentication_repository/lib/src/models/user.dart ':include') :::note La classe `User` estende [equatable](https://pub.dev/packages/equatable) per sovrascrivere i confronti di uguaglianza, permettendoci di confrontare diverse istanze di `User` per valore. ::: :::tip È utile definire un `User` vuoto `static` per evitare di gestire `User` null e lavorare sempre con un oggetto `User` concreto. ::: ### Repository L'`AuthenticationRepository` ha il compito di astrarre l'implementazione sottostante dell'autenticazione e di recuperare l'utente. L'`AuthenticationRepository` espone uno `Stream` a cui possiamo sottoscriverci per essere notificati quando un `User` cambia. Inoltre, espone metodi per `signUp`, `logInWithGoogle`, `logInWithEmailAndPassword` e `logOut`. :::note L'`AuthenticationRepository` è anche responsabile della gestione degli errori di basso livello che possono verificarsi nel livello dati, esponendo un set pulito e semplice di errori allineati con il dominio. ::: Questo è tutto per l'`AuthenticationRepository`. Vediamo ora come integrarlo nel progetto Flutter che abbiamo creato. ## Configurazione Firebase Dobbiamo seguire le [istruzioni di utilizzo firebase_auth](https://pub.dev/packages/firebase_auth#usage) per collegare la nostra applicazione a Firebase e abilitare [google_sign_in](https://pub.dev/packages/google_sign_in). :::caution Ricorda di aggiornare il `google-services.json` su Android e i file `GoogleService-Info.plist` e `Info.plist` su iOS, altrimenti l'applicazione andrà in crash. ::: ## Dipendenze del Progetto Possiamo sostituire il `pubspec.yaml` generato alla radice del progetto con il seguente: Nota che stiamo specificando una directory assets per tutti gli asset locali delle nostre applicazioni. Crea una directory `assets` alla radice del tuo progetto e aggiungi l'asset [bloc logo](https://github.com/felangel/bloc/blob/master/examples/flutter_firebase_login/assets/bloc_logo_small.png) (che useremo più tardi). Poi installa tutte le dipendenze: :::note Dipendiamo dal pacchetto `authentication_repository` tramite path; questo ci permetterà di iterare rapidamente mantenendo una separazione chiara. ::: ## main.dart Il file `main.dart` può essere sostituito con il seguente: Qui impostiamo alcune configurazioni globali per l'applicazione e chiamiamo `runApp` con un'istanza di `App`. :::note Stiamo iniettando una singola istanza di `AuthenticationRepository` nell'`App` come dipendenza esplicita del costruttore. ::: ## App Proprio come nel [tutorial login](/it/tutorials/flutter-login), il nostro file `app.dart` fornisce all'applicazione un'istanza di `AuthenticationRepository` tramite `RepositoryProvider` e crea un'istanza di `AuthenticationBloc` rendendola disponibile. Successivamente, `AppView` utilizza l'`AuthenticationBloc` per gestire l'aggiornamento della route corrente in base all'`AuthenticationState`. ## App Bloc L'`AppBloc` è responsabile della gestione dello stato globale dell'applicazione. Ha una dipendenza dall'`AuthenticationRepository` e si sottoscrive allo stream `user` per emettere nuovi stati in risposta ai cambiamenti dell'utente corrente. ### State L'`AppState` consiste in un `AppStatus` e un `User`. Il costruttore di default accetta un `User` opzionale e reindirizza al costruttore privato con lo stato di autenticazione appropriato. ### Event L'`AppEvent` ha due sottoclassi: - `AppUserSubscriptionRequested`: notifica il bloc di sottoscriversi allo stream utente; - `AppLogoutPressed`: notifica il bloc di un'azione di logout dell'utente. ### Bloc Nel corpo del costruttore, le sottoclassi di `AppEvent` sono mappate ai loro gestori di eventi corrispondenti. Nel gestore `_onUserSubscriptionRequested`, l'`AppBloc` usa `emit.onEach` per sottoscriversi allo stream utente dell'`AuthenticationRepository` ed emettere uno stato in risposta a ogni `User`. `emit.onEach` crea internamente una sottoscrizione allo stream e si occupa di cancellarla quando l'`AppBloc` o lo stream utente vengono chiusi. Se lo stream utente emette un errore, `addError` inoltra l'errore e lo stack trace a qualsiasi `BlocObserver` in ascolto. :::caution Se `onError` viene omesso, eventuali errori sullo stream utente sono considerati non gestiti e saranno lanciati da `onEach`. Di conseguenza, la sottoscrizione allo stream utente verrà cancellata. ::: :::tip Un [`BlocObserver`](/it/bloc-concepts/#blocobserver-1) è ottimo per registrare eventi Bloc, errori e cambiamenti di stato, specialmente nel contesto di analytics e crash reporting. ::: ## Modelli Modelli di input per `Email` e `Password` sono utili per incapsulare la logica di validazione e saranno usati sia nel `LoginForm` che nel `SignUpForm` (più avanti nel tutorial). Entrambi i modelli di input sono realizzati usando il pacchetto [formz](https://pub.dev/packages/formz) e ci permettono di lavorare con un oggetto validato piuttosto che con un tipo primitivo come `String`. ### Email ### Password ## Login Page La `LoginPage` è responsabile della creazione e fornitura di un'istanza di `LoginCubit` al `LoginForm`. :::tip È molto importante mantenere la creazione di bloc/cubit separata da dove vengono consumati. Questo ti permetterà di iniettare facilmente istanze fittizie (mock) e testare la tua vista in isolamento. ::: ## Login Cubit Il `LoginCubit` gestisce il `LoginState` del form. Espone API per `logInWithCredentials`, `logInWithGoogle`, e viene notificato quando email o password vengono aggiornate. ### State Il `LoginState` consiste in `Email`, `Password` e `FormzStatus`. I modelli `Email` e `Password` estendono `FormzInput` dal pacchetto [formz](https://pub.dev/packages/formz). ### Cubit Il `LoginCubit` ha una dipendenza dall'`AuthenticationRepository` per accedere all'utente tramite credenziali o tramite Google Sign In. :::note Abbiamo usato un `Cubit` invece di un `Bloc` perché il `LoginState` è piuttosto semplice e localizzato. Anche senza eventi, possiamo intuire cosa è successo osservando i cambiamenti di stato; inoltre, il nostro codice risulta più semplice e conciso. ::: ## Login Form Il `LoginForm` renderizza il form in base al `LoginState` e invoca metodi sul `LoginCubit` in risposta alle interazioni dell'utente. Il `LoginForm` renderizza anche un pulsante "Create Account" che naviga alla `SignUpPage`, dove un utente può creare un nuovo account. ## Sign Up Page La struttura `SignUp` rispecchia quella di `Login` e consiste in `SignUpPage`, `SignUpView` e `SignUpCubit`. La `SignUpPage` è responsabile solo di creare e fornire un'istanza del `SignUpCubit` al `SignUpForm` (esattamente come in `LoginPage`). :::note Così come il `LoginCubit`, anche il `SignUpCubit` dipende dall'`AuthenticationRepository` per la creazione di nuovi utenti. ::: ## Sign Up Cubit Il `SignUpCubit` gestisce lo stato del `SignUpForm` e comunica con l'`AuthenticationRepository` per creare nuovi utenti. ### State Il `SignUpState` riutilizza gli stessi modelli di input form `Email` e `Password` perché la logica di validazione è identica. ### Cubit Il `SignUpCubit` è estremamente simile al `LoginCubit`, con la principale eccezione che espone un'API per inviare il form di registrazione anziché di login. ## Sign Up Form Il `SignUpForm` è responsabile del rendering del form in risposta al `SignUpState` e invoca metodi sul `SignUpCubit` in risposta alle interazioni dell'utente. ## Home Page Dopo che un utente accede o si registra con successo, lo stream `user` sarà aggiornato. Questo porterà a un cambiamento di stato nell'`AuthenticationBloc` e farà sì che l'`AppView` mostri la `HomePage`. Dalla `HomePage`, l'utente può visualizzare le informazioni del proprio profilo e disconnettersi toccando l'icona di uscita nella `AppBar`. :::note Una directory `widgets` è stata creata insieme alla directory `view` all'interno della funzionalità `home` per componenti riutilizzabili specifici di quella funzionalità. In questo caso un semplice widget `Avatar` è esportato e usato all'interno della `HomePage`. ::: :::note Quando viene toccato l'`IconButton` di logout, un evento `AuthenticationLogoutRequested` viene aggiunto all'`AuthenticationBloc`, che disconnette l'utente e lo riporta alla `LoginPage`. ::: A questo punto abbiamo un'implementazione di login solida usando Firebase e abbiamo disaccoppiato il livello di presentazione dalla logica applicativa usando la Libreria Bloc. Il codice sorgente completo per questo esempio può essere trovato [qui](https://github.com/felangel/bloc/tree/master/examples/flutter_firebase_login). ================================================ FILE: docs/src/content/docs/it/tutorials/flutter-infinite-list.mdx ================================================ --- title: Flutter Infinite List description: Una guida approfondita su come costruire una lista infinita Flutter con bloc. sidebar: order: 3 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-infinite-list/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/flutter-infinite-list/FlutterPubGetSnippet.astro'; import PostsJsonSnippet from '~/components/tutorials/flutter-infinite-list/PostsJsonSnippet.astro'; import PostBlocInitialStateSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocInitialStateSnippet.astro'; import PostBlocOnPostFetchedSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocOnPostFetchedSnippet.astro'; import PostBlocTransformerSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocTransformerSnippet.astro'; ![intermediate](https://img.shields.io/badge/level-intermediate-orange.svg) In questo tutorial, usando Flutter e bloc, implementeremo un'app che recupera dati tramite la rete e li carica mentre l'utente scorre una lista. ![demo](~/assets/tutorials/flutter-infinite-list.gif) ## Argomenti Chiave - Osservare i cambiamenti di stato con [BlocObserver](/it/bloc-concepts#blocobserver); - [BlocProvider](/it/flutter-bloc-concepts#blocprovider), widget Flutter che fornisce un bloc ai suoi figli; - [BlocBuilder](/it/flutter-bloc-concepts#blocbuilder), widget Flutter che gestisce la costruzione del widget in risposta a nuovi stati; - Aggiungere eventi con [context.read](/it/flutter-bloc-concepts#contextread); - Prevenire aggiornamenti non necessari con [Equatable](/it/faqs/#quando-usare-equatable); - Usare il metodo `transformEvents` con Rx. ## Configurazione Inizieremo creando un nuovo progetto Flutter Possiamo poi sostituire il contenuto di pubspec.yaml con e poi installare tutte le nostre dipendenze ## Struttura del Progetto ``` ├── lib │ ├── posts │ │ ├── bloc │ │ │ └── post_bloc.dart │ | | └── post_event.dart │ | | └── post_state.dart │ | └── models │ | | └── models.dart* │ | | └── post.dart │ │ └── view │ │ | ├── posts_page.dart │ │ | └── posts_list.dart │ | | └── view.dart* │ | └── widgets │ | | └── bottom_loader.dart │ | | └── post_list_item.dart │ | | └── widgets.dart* │ │ ├── posts.dart* │ ├── app.dart │ ├── simple_bloc_observer.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` L'applicazione usa una struttura di directory guidata dalle funzionalità. Questa struttura del progetto ci permette di scalare il progetto avendo funzionalità autonome. In questo esempio avremo solo una singola funzionalità (la funzionalità post) ed è divisa in cartelle ognuna con rispettivamente un "barrel" file, indicati dall'asterisco (\*). ## REST API Per questa app demo, useremo [jsonplaceholder](http://jsonplaceholder.typicode.com) come nostra sorgente di dati. :::note jsonplaceholder è un'API REST online che serve dati falsi; è molto utile per costruire prototipi. ::: Apri una nuova scheda nel tuo browser e visita https://jsonplaceholder.typicode.com/posts?_start=0&_limit=2 per vedere cosa l'API restituisce. :::note Nel nostro URL abbiamo specificato `start` e `limit` come parametri di query nella richiesta GET. ::: Ottimo! Ora che sappiamo come saranno strutturati i nostri dati, creiamo il modello. ## Modello Dati Creiamo `post.dart` e iniziamo a definire il modello del nostro oggetto Post. `Post` è solo una classe con un `id`, `title`, e `body`. :::note Estendiamo [`Equatable`](https://pub.dev/packages/equatable) per poter confrontare gli oggetti `Post`. Senza questo, dovremmo modificare manualmente la classe per sovrascrivere `==` e `hashCode` per distinguere due oggetti `Post`. Vedi [il pacchetto](https://pub.dev/packages/equatable) per maggiori dettagli. ::: Ora che abbiamo definito il modello `Post`, iniziamo a lavorare sul componente di logica applicativa (bloc). ## Post Events Prima di immergerci nell'implementazione, dobbiamo definire cosa farà il nostro `PostBloc`. Ad alto livello, risponderà all'input dell'utente (scorrimento) e recupererà più post in modo che il livello di presentazione possa visualizzarli. Iniziamo creando il nostro `Event`. Il nostro `PostBloc` risponderà solo a un singolo evento: `PostFetched`, che verrà aggiunto dal livello di presentazione ogni volta che ha bisogno di più post da visualizzare. Poiché `PostFetched` è un tipo di `PostEvent`, possiamo creare `bloc/post_event.dart` e implementare l'evento in questo modo. Per ricapitolare, il nostro `PostBloc` riceverà `PostEvents` e li convertirà in `PostStates`. Abbiamo definito tutti i nostri `PostEvents` (`PostFetched`), quindi ora definiamo il nostro `PostState`. ## Post States Il nostro livello di presentazione avrà bisogno di diverse informazioni per disporre correttamente l'interfaccia: - `PostInitial` - indica al livello di presentazione che deve renderizzare un indicatore di caricamento mentre il batch iniziale di post viene caricato; - `PostSuccess` - indica al livello di presentazione che ha contenuto da renderizzare: - `posts` - sarà la `List` che verrà visualizzata; - `hasReachedMax` - indica al livello di presentazione se ha o meno raggiunto il numero massimo di post; - `PostFailure` - indica al livello di presentazione che si è verificato un errore durante il recupero dei post. Possiamo ora creare `bloc/post_state.dart` e implementarlo così. :::note Abbiamo implementato `copyWith` così possiamo copiare un'istanza di `PostSuccess` e aggiornare zero o più proprietà convenientemente (questo tornerà utile più tardi). ::: Ora che abbiamo implementato i nostri `Events` e `States`, possiamo creare il nostro `PostBloc`. ## Post Bloc Per semplicità, il nostro `PostBloc` avrà una dipendenza diretta su un client HTTP; tuttavia, in un'applicazione di produzione suggeriamo invece di iniettare un client API e usare il pattern repository [docs](/it/architecture). Creiamo `post_bloc.dart` e creiamo il nostro `PostBloc` vuoto. :::note Dalla dichiarazione della classe possiamo vedere che il nostro `PostBloc` riceverà `PostEvents` come input e produrrà `PostStates`. ::: Successivamente, dobbiamo registrare un gestore di eventi per gestire gli eventi `PostFetched` in arrivo. In risposta a un evento `PostFetched`, chiameremo `_fetchPosts` per recuperare i post dall'API. Il nostro `PostBloc` emetterà nuovi stati tramite l'`Emitter` fornito nel gestore di eventi. Consulta [concetti base](/it/bloc-concepts/#stream) per maggiori informazioni. Ogni volta che viene aggiunto un `PostEvent`, se è un evento `PostFetched` e ci sono più post da recuperare, il nostro `PostBloc` recupererà i prossimi 20 post. L'API restituirà un array vuoto se proviamo a recuperare oltre il numero massimo di post (100), quindi se otteniamo un array vuoto, il nostro bloc emetterà lo stato corrente impostando `hasReachedMax` a `true`. Se non possiamo recuperare i post, emettiamo `PostFailure`. Se riusciamo a recuperare i post, emettiamo `PostSuccess` con l'intera lista di post. Un'ottimizzazione che possiamo fare è applicare il `throttle` all'evento `PostFetched` per evitare di sovraccaricare inutilmente la nostra API. Possiamo farlo usando il parametro `transform` quando registriamo il gestore di eventi `_onFetched`. :::note Passare un `transformer` a `on` ci permette di personalizzare come gli eventi sono processati. ::: :::note Assicurati di importare [`package:stream_transform`](https://pub.dev/packages/stream_transform) per usare l'API `throttle`. ::: Il nostro `PostBloc` finito dovrebbe ora essere così: Ottimo! Ora che abbiamo finito di implementare la logica applicativa tutto ciò che resta da fare è implementare il livello di presentazione. ## Livello di Presentazione Nel nostro `main.dart` possiamo iniziare implementando la nostra funzione main e chiamando `runApp` per renderizzare il nostro widget root. Qui, possiamo anche includere il nostro bloc observer per registrare transizioni e eventuali errori. Nel nostro widget `App`, la radice del nostro progetto, possiamo poi impostare home a `PostsPage` Nel nostro widget `PostsPage`, usiamo `BlocProvider` per creare e fornire un'istanza di `PostBloc` al sottoalbero. Inoltre, aggiungiamo un evento `PostFetched` così che quando l'app si carica, richiede il batch iniziale di Posts. Successivamente, dobbiamo implementare la vista `PostsList` che presenterà i nostri post e si collegherà al `PostBloc`. :::note `PostsList` è un `StatefulWidget` perché dovrà mantenere un `ScrollController`. In `initState`, aggiungiamo un listener al `ScrollController` per poter rispondere agli eventi di scroll. Accediamo anche all'istanza `PostBloc` tramite `context.read()`. ::: Proseguendo, il nostro metodo build restituisce un `BlocBuilder`. `BlocBuilder` è un widget Flutter dal [pacchetto flutter_bloc](https://pub.dev/packages/flutter_bloc) che gestisce la costruzione di un widget in risposta a nuovi stati bloc. Ogni volta che lo stato del nostro `PostBloc` cambia, la nostra funzione builder sarà chiamata con il nuovo `PostState`. :::caution Dobbiamo ricordarci di pulire dopo di noi e eliminare il `ScrollController` quando lo `StatefulWidget` viene eliminato. ::: Ogni volta che l'utente scorre, calcoliamo quanto è stato scrollato nella pagina e se la distanza è ≥ 90% del `maxScrollExtent`, aggiungiamo un evento `PostFetched` per caricare più post. Successivamente, dobbiamo implementare il nostro widget `BottomLoader` che indicherà all'utente che stiamo caricando più post. Infine, dobbiamo implementare `PostListItem` che renderizzerà un singolo `Post`. A questo punto, dovremmo essere in grado di eseguire l'app e tutto dovrebbe funzionare; tuttavia, c'è un'altra cosa che possiamo fare. Un vantaggio aggiuntivo dell'usare la libreria bloc è che possiamo avere accesso a tutte le transizioni in un unico posto. Il cambiamento da uno stato a un altro è chiamato transizione. :::note Una `Transition` consiste dello stato corrente, dell'evento e dello stato successivo. ::: Anche se in questa applicazione abbiamo solo un bloc, è abbastanza comune in applicazioni più grandi avere molti bloc che gestiscono diverse parti dello stato dell'applicazione. Se vogliamo essere in grado di fare qualcosa in risposta a tutte le `Transitions` possiamo semplicemente creare il nostro `BlocObserver`. :::note Tutto ciò che dobbiamo fare è estendere `BlocObserver` e sovrascrivere il metodo `onTransition`. ::: Ora ogni volta che si verifica una transizione del bloc, possiamo vedere la transizione stampata nella console. :::note In pratica, puoi creare diversi `BlocObservers` e poiché ogni cambiamento di stato è registrato, siamo in grado di strumentare molto facilmente le nostre applicazioni e tracciare tutte le interazioni dell'utente e i cambiamenti di stato in un unico posto! ::: Questo è tutto! Abbiamo ora implementato con successo una lista infinita in Flutter usando i pacchetti [bloc](https://pub.dev/packages/bloc) e [flutter_bloc](https://pub.dev/packages/flutter_bloc) e abbiamo separato con successo il livello di presentazione dalla logica applicativa. La `PostsPage` non ha idea da dove vengono i `Post` o come sono recuperati. Al contrario, il `PostBloc` non ha idea di come lo stato viene renderizzato, converte semplicemente eventi in stati. Il codice sorgente completo per questo esempio può essere trovato [qui](https://github.com/felangel/Bloc/tree/master/examples/flutter_infinite_list). ================================================ FILE: docs/src/content/docs/it/tutorials/flutter-login.mdx ================================================ --- title: Flutter Login description: Una guida approfondita su come costruire un flusso di login Flutter con bloc. sidebar: order: 4 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-login/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![intermediate](https://img.shields.io/badge/level-intermediate-orange.svg) In questo tutorial, costruiremo un flusso per accedere al sistema, in Flutter, usando la libreria bloc. ![demo](~/assets/tutorials/flutter-login.gif) ## Argomenti Chiave - [BlocProvider](/it/flutter-bloc-concepts#blocprovider), widget Flutter che fornisce un bloc ai suoi figli; - Aggiungere eventi con [context.read](/it/flutter-bloc-concepts#contextread); - Prevenire aggiornamenti non necessari con [Equatable](/it/faqs/#quando-usare-equatable); - [RepositoryProvider](/it/flutter-bloc-concepts#repositoryprovider), widget Flutter che fornisce un repository ai suoi figli; - [BlocListener](/it/flutter-bloc-concepts#bloclistener), widget Flutter che invoca il codice listener in risposta ai cambiamenti di stato nel bloc; - Aggiornare l'UI basata su una parte di uno stato bloc con [context.select](/it/flutter-bloc-concepts#contextselect). ## Configurazione del Progetto Inizieremo creando un nuovo progetto Flutter Successivamente, possiamo installare tutte le nostre dipendenze ## Authentication Repository La prima cosa che faremo è creare un pacchetto `authentication_repository` che sarà responsabile di gestire il dominio della autenticazione. Inizieremo creando una directory `packages` alla radice del progetto che conterrà tutti i pacchetti interni. Dopodiché aggiungiamo la directory `packages/authentication_repository`. Ad alto livello, la struttura della directory dovrebbe essere così: ``` ├── android ├── ios ├── lib ├── packages │ └── authentication_repository └── test ``` Successivamente, possiamo creare un `pubspec.yaml` per il pacchetto `authentication_repository`: :::note `package:authentication_repository` sarà un pacchetto Dart puro senza alcuna dipendenza esterna. ::: Successivamente, dobbiamo implementare la classe `AuthenticationRepository` stessa che sarà in `packages/authentication_repository/lib/src/authentication_repository.dart`. L'`AuthenticationRepository` espone uno `Stream` di aggiornamenti `AuthenticationStatus` che sarà usato per notificare l'applicazione quando un utente accede o esce. Inoltre, ci sono metodi `logIn` e `logOut` che sono simulati (stub) per semplicità ma possono essere facilmente estesi per autenticare con `FirebaseAuth` per esempio o qualche altro provider di autenticazione. :::note Poiché stiamo mantenendo un `StreamController` internamente, un metodo `dispose` è esposto così che il controller possa essere chiuso quando non è più necessario. ::: Infine, dobbiamo creare `packages/authentication_repository/lib/authentication_repository.dart` che conterrà le esportazioni pubbliche: Questo è tutto per l'`AuthenticationRepository`, successivamente lavoreremo sul `UserRepository`. ## User Repository Proprio come con l'`AuthenticationRepository`, creeremo un pacchetto `user_repository` all'interno della directory `packages`. ``` ├── android ├── ios ├── lib ├── packages │ ├── authentication_repository │ └── user_repository └── test ``` Successivamente, creeremo il `pubspec.yaml` per il `user_repository`: Il `user_repository` sarà responsabile del dominio utente ed esporrà API per interagire con l'utente corrente. La prima cosa che definiremo è il modello utente in `packages/user_repository/lib/src/models/user.dart`: Per semplicità, un utente ha solo una proprietà `id` ma potremmo aggiungere proprietà quali `firstName`, `lastName`, `avatarUrl`, ecc... :::note [`package:equatable`](https://pub.dev/packages/equatable) è usato per confrontare le istanze `User` per valore e non per riferimento. ::: Successivamente, possiamo creare un `models.dart` in `packages/user_repository/lib/src/models` che esporterà tutti i modelli. In questo modo possiamo usare un singolo import per importare più modelli. Ora che i modelli sono stati definiti, possiamo implementare la classe `UserRepository` in `packages/user_repository/lib/src/user_repository.dart`. Per questo semplice esempio, il `UserRepository` espone un singolo metodo `getUser` che recupererà l'utente corrente. Stiamo simulando una risposta (stub) ma nella pratica sarebbe il posto dove interrogheremmo l'utente corrente dal backend. Abbiamo quasi finito con il pacchetto `user_repository` -- l'unica cosa che resta da fare è creare il file `user_repository.dart` in `packages/user_repository/lib` che definisce cosa esportare pubblicamente: Ora che abbiamo completato i pacchetti `authentication_repository` e `user_repository`, possiamo concentrarci sull'applicazione Flutter. ## Installare Dipendenze Iniziamo aggiornando il `pubspec.yaml` generato alla radice del nostro progetto: Possiamo installare le dipendenze eseguendo: ## Authentication Bloc L'`AuthenticationBloc` sarà responsabile di reagire ai cambiamenti nello stato di autenticazione (esposto dall'`AuthenticationRepository`) ed emetterà stati a cui possiamo reagire nel livello di presentazione. L'implementazione per l'`AuthenticationBloc` è all'interno di `lib/authentication` perché trattiamo l'autenticazione come una funzionalità nel nostro livello applicazione. ``` ├── lib │ ├── app.dart │ ├── authentication │ │ ├── authentication.dart │ │ └── bloc │ │ ├── authentication_bloc.dart │ │ ├── authentication_event.dart │ │ └── authentication_state.dart │ ├── main.dart ``` :::tip Usa l' [Estensione VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) o [Plugin IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) per creare bloc automaticamente. ::: ### authentication_event.dart Le istanze `AuthenticationEvent` saranno l'input dell'`AuthenticationBloc` e saranno processate e usate per emettere nuove istanze `AuthenticationState`. In questa applicazione, l'`AuthenticationBloc` reagirà a due diversi eventi: - `AuthenticationSubscriptionRequested`: evento iniziale che notifica il bloc di sottoscriversi allo stream `AuthenticationStatus`; - `AuthenticationLogoutPressed`: notifica il bloc di un'azione di logout dell'utente. Successivamente, diamo un'occhiata all'`AuthenticationState`. ### authentication_state.dart Le istanze `AuthenticationState` saranno l'output dell'`AuthenticationBloc` e saranno consumate dal livello di presentazione. La classe `AuthenticationState` ha tre costruttori nominati: - `AuthenticationState.unknown()`: lo stato di default che indica che il bloc non sa ancora se l'utente corrente è autenticato o meno; - `AuthenticationState.authenticated()`: lo stato che indica che l'utente è attualmente autenticato; - `AuthenticationState.unauthenticated()`: lo stato che indica che l'utente non è attualmente autenticato. Ora che abbiamo visto le implementazioni di `AuthenticationEvent` e `AuthenticationState` diamo un'occhiata all'`AuthenticationBloc`. ### authentication_bloc.dart L'`AuthenticationBloc` gestisce lo stato di autenticazione dell'applicazione. Tra i molteplici usi è adoperato per determinare la rotta, login o home page, dove l'utente farà l'accesso. L'`AuthenticationBloc` ha una dipendenza sia sull'`AuthenticationRepository` che sul `UserRepository` e definisce lo stato iniziale come `AuthenticationState.unknown()`. Nel corpo del costruttore, le sottoclassi di `AuthenticationEvent` sono mappate ai loro corrispondenti gestori di eventi. Nel gestore di eventi `_onSubscriptionRequested`, l'`AuthenticationBloc` usa `emit.onEach` per sottoscriversi allo stream `status` dell' `AuthenticationRepository` ed emettere uno stato in risposta a ogni `AuthenticationStatus`. `emit.onEach` crea una sottoscrizione internamente e si occupa di cancellarla quando l'`AuthenticationBloc` o lo stream `status` è chiuso. Se lo stream `status` emette un errore, `addError` inoltra l'errore e lo stackTrace a qualsiasi `BlocObserver` in ascolto. :::caution Se `onError` è omesso, eventuali errori sullo stream `status` sono considerati non gestiti, e saranno lanciati da `onEach`. Di conseguenza, la sottoscrizione allo stream `status` sarà cancellato. ::: :::tip Un [`BlocObserver`](/it/bloc-concepts/#blocobserver-1) è ottimo per registrare ad eventi Bloc, errori e cambiamenti di stato specialmente nel contesto di analytics e crash reporting.; ::: Quando lo stream `status` emette `AuthenticationStatus.unknown` o `unauthenticated`, viene emesso lo `AuthenticationState` corrispondente. Quando viene emesso `AuthenticationStatus.authenticated`, l'`AuthentictionBloc` interroga l'utente tramite il `UserRepository`. ## main.dart Successivamente, possiamo sostituire il `main.dart` di default con: ## App `app.dart` conterrà il widget `App` root per l'intera applicazione. :::note `app.dart` è diviso in due parti `App` e `AppView`. `App` è responsabile di creare/fornire l'`AuthenticationBloc` che sarà consumato dal `AppView`. Questo disaccoppiamento ci permetterà in seguito di testare facilmente sia il widget `App` che `AppView`. ::: :::note `RepositoryProvider` è usato per fornire la singola istanza di `AuthenticationRepository` all'intera applicazione. Più tardi vedremo come tornerà utile. ::: Di default, `BlocProvider` è lazy e non chiama `create` fino alla prima volta in cui viene richiesto l'accesso al Bloc. Poiché `AuthenticationBloc` dovrebbe sempre sottoscriversi immediatamente allo stream `AuthenticationStatus` (tramite l'evento `AuthenticationSubscriptionRequested`), possiamo esplicitamente rifiutare questo comportamento impostando `lazy: false`. `AppView` è un `StatefulWidget` perché mantiene una `GlobalKey` che è usata per accedere al `NavigatorState`. Di default, `AppView` renderizzerà la `SplashPage` (che vedremo dopo) e usa `BlocListener` per navigare a pagine diverse in base ai cambiamenti nell'`AuthenticationState`. ## Splash La funzionalità splash conterrà solo una vista semplice che sarà renderizzata quando l'app viene avviata mentre l'app determina se l'utente è autenticato. ``` lib └── splash ├── splash.dart └── view └── splash_page.dart ``` :::tip `SplashPage` espone una `Route` statica che rende molto facile navigare tramite `Navigator.of(context).push(SplashPage.route())`; ::: ## Login La funzionalità login contiene una `LoginPage`, `LoginForm` e `LoginBloc` e permette agli utenti di inserire un username e password per accedere all'applicazione. ``` ├── lib │ ├── login │ │ ├── bloc │ │ │ ├── login_bloc.dart │ │ │ ├── login_event.dart │ │ │ └── login_state.dart │ │ ├── login.dart │ │ ├── models │ │ │ ├── models.dart │ │ │ ├── password.dart │ │ │ └── username.dart │ │ └── view │ │ ├── login_form.dart │ │ ├── login_page.dart │ │ └── view.dart ``` ### Modelli Login Stiamo usando [`package:formz`](https://pub.dev/packages/formz) per creare modelli riutilizzabili e standard per `username` e `password`. #### Username Per semplicità, stiamo solo validando l'username per assicurarci che non sia vuoto ma nella pratica puoi imporre l'uso di caratteri speciali, lunghezza, ecc... #### Password Anche per la password stiamo semplicemente eseguendo un controllo per assicurarci non sia vuota. #### Barrel Modelli Proprio come prima, c'è un "barrel" `models.dart` per rendere facile importare i modelli `Username` e `Password` con un singolo import. ### Login Bloc Il `LoginBloc` gestisce lo stato del `LoginForm` e si occupa di validare l'input username e password così come lo stato del form. #### login_event.dart In questa applicazione ci sono tre diversi tipi di `LoginEvent`: - `LoginUsernameChanged`: notifica il bloc che l'username è stato modificato; - `LoginPasswordChanged`: notifica il bloc che la password è stata modificata; - `LoginSubmitted`: notifica il bloc che il form è stato inviato. #### login_state.dart Il `LoginState` conterrà lo stato del form così come gli stati di input username e password. :::note I modelli `Username` e `Password` sono usati come parte del `LoginState` e lo status fa anche parte di [package:formz](https://pub.dev/packages/formz). ::: #### login_bloc.dart Il `LoginBloc` è responsabile di reagire alle interazioni dell'utente nel `LoginForm` e gestire la validazione e l'invio del form. Il `LoginBloc` ha una dipendenza sull'`AuthenticationRepository` perché quando il form viene inviato, invoca `logIn`. Lo stato iniziale del bloc è `pure` il che significa che non c'è stata alcuna interazione né con gli input né con il form. Ogni volta che `username` o `password` cambiano, il bloc creerà una variante `dirty` del modello `Username`/`Password` e aggiornerà lo stato del form tramite l'API `Formz.validate`. Quando viene aggiunto l'evento `LoginSubmitted`, se lo stato corrente del form è valido, il bloc fa una chiamata a `logIn` e aggiorna lo stato in base al risultato della richiesta. Diamo ora un'occhiata alla `LoginPage` e alla `LoginForm`. ### Login Page La `LoginPage` è responsabile di esporre la `Route` così come creare e fornire il `LoginBloc` al `LoginForm`. :::note `context.read()` è usato per cercare l'istanza di `AuthenticationRepository` tramite il `BuildContext`. ::: ### Login Form Il `LoginForm` notifica gli eventi generati dall'utente al `LoginBloc` risponde ai cambiamenti di stato attraverso `BlocBuilder` e `BlocListener`. `BlocListener` è usato per mostrare uno `SnackBar` se l'invio del login fallisce. In aggiunta, `context.select` è usato in ogni widget per accedere efficientemente a parti specifiche del `LoginState`, prevenendo aggiornamenti non necessari. La callback `onChanged` è usato per notificare il `LoginBloc` dei cambiamenti a username/password. Il widget `_LoginButton` è abilitato solo se lo stato del form è valido e un `CircularProgressIndicator` è mostrato al suo posto mentre il form viene inviato. ## Home Dopo una richiesta `logIn` di successo, lo stato dell'`AuthenticationBloc` cambierà a `authenticated` e l'utente sarà riportato alla `HomePage` dove visualizziamo l'`id` dell'utente così come un pulsante per disconnettersi. ``` ├── lib │ ├── home │ │ ├── home.dart │ │ └── view │ │ └── home_page.dart ``` ### Home Page La `HomePage` può accedere all'id utente corrente tramite `context.select((AuthenticationBloc bloc) => bloc.state.user.id)` e lo visualizza tramite un widget `Text`. Inoltre, quando viene toccato il pulsante logout, un evento `AuthenticationLogoutPressed` viene aggiunto all'`AuthenticationBloc`. :::note `context.select((AuthenticationBloc bloc) => bloc.state.user.id)` renderizzerà nuovamente `Text` solo se l'id utente cambia. ::: A questo punto abbiamo un'implementazione di login abbastanza solida e abbiamo disaccoppiato il nostro livello di presentazione dal livello di logica applicativa usando Bloc. Il codice sorgente completo per questo esempio (inclusi test unitari e widget) può essere trovato [qui](https://github.com/felangel/Bloc/tree/master/examples/flutter_login). ================================================ FILE: docs/src/content/docs/it/tutorials/flutter-timer.mdx ================================================ --- title: Flutter Timer description: Una guida approfondita su come costruire un'app Flutter timer con bloc. sidebar: order: 2 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-timer/FlutterCreateSnippet.astro'; import TimerBlocEmptySnippet from '~/components/tutorials/flutter-timer/TimerBlocEmptySnippet.astro'; import TimerBlocInitialStateSnippet from '~/components/tutorials/flutter-timer/TimerBlocInitialStateSnippet.astro'; import TimerBlocTickerSnippet from '~/components/tutorials/flutter-timer/TimerBlocTickerSnippet.astro'; import TimerBlocOnStartedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnStartedSnippet.astro'; import TimerBlocOnTickedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnTickedSnippet.astro'; import TimerBlocOnPausedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnPausedSnippet.astro'; import TimerBlocOnResumedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnResumedSnippet.astro'; import TimerPageSnippet from '~/components/tutorials/flutter-timer/TimerPageSnippet.astro'; import ActionsSnippet from '~/components/tutorials/flutter-timer/ActionsSnippet.astro'; import BackgroundSnippet from '~/components/tutorials/flutter-timer/BackgroundSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) In questo tutorial vedremo come costruire un timer usando la libreria bloc. L'applicazione risultante dovrebbe essere così: ![demo](~/assets/tutorials/flutter-timer.gif) ## Argomenti Chiave - Osservare i cambiamenti di stato con [BlocObserver](/it/bloc-concepts#blocobserver); - [BlocProvider](/it/flutter-bloc-concepts#blocprovider), widget Flutter che fornisce un bloc ai suoi figli; - [BlocBuilder](/it/flutter-bloc-concepts#blocbuilder), widget Flutter che gestisce la costruzione del widget in risposta a nuovi stati; - Prevenire aggiornamenti non necessari con [Equatable](/it/faqs/#quando-usare-equatable); - Imparare a usare `StreamSubscription` in un Bloc; - Prevenire aggiornamenti non necessari con `buildWhen`. ## Configurazione Inizieremo creando un nuovo progetto Flutter: Possiamo poi sostituire il contenuto di pubspec.yaml con: :::note Useremo i pacchetti [flutter_bloc](https://pub.dev/packages/flutter_bloc) e [equatable](https://pub.dev/packages/equatable) in questa app. ::: Successivamente, esegui `flutter pub get` per installare tutte le dipendenze. ## Struttura del Progetto ``` ├── lib | ├── timer │ │ ├── bloc │ │ │ └── timer_bloc.dart | | | └── timer_event.dart | | | └── timer_state.dart │ │ └── view │ │ | ├── timer_page.dart │ │ ├── timer.dart │ ├── app.dart │ ├── ticker.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` ## Ticker Il ticker sarà la sorgente di dati per l'applicazione timer. Esporrà uno stream di "tick" a cui possiamo sottoscriverci e reagire. Inizia creando `ticker.dart`. Tutto ciò che fa la classe `Ticker` è esporre una funzione `tick` che prende il numero di tick (secondi) che vogliamo e restituisce uno stream che emette i secondi rimanenti ogni secondo. Successivamente, dobbiamo creare il `TimerBloc` che consumerà il `Ticker`. ## Timer Bloc ### TimerState Inizieremo definendo i `TimerStates` in cui il `TimerBloc` può trovarsi. Lo stato del `TimerBloc` può essere uno dei seguenti: - `TimerInitial`: pronto per iniziare il conteggio alla rovescia dalla durata specificata. - `TimerRunInProgress`: sta attivamente contando alla rovescia dalla durata specificata. - `TimerRunPause`: in pausa a una durata rimanente. - `TimerRunComplete`: completato con una durata rimanente di 0. Ognuno di questi stati avrà un'implicazione sull'interfaccia utente e sulle azioni che l'utente può eseguire. Ad esempio: - se lo stato è `TimerInitial` l'utente sarà in grado di avviare il timer; - se lo stato è `TimerRunInProgress` l'utente sarà in grado di mettere in pausa e resettare il timer così come vedere la durata rimanente; - se lo stato è `TimerRunPause` l'utente sarà in grado di riprendere il timer e resettare il timer; - se lo stato è `TimerRunComplete` l'utente sarà in grado di resettare il timer. Per mantenere tutti i nostri file bloc insieme, creiamo una directory bloc con `bloc/timer_state.dart`. :::tip Puoi usare le [IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc-code-generator) o [VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) estensioni per autogenerare i seguenti file bloc per te. ::: Nota che tutti i `TimerStates` estendono la classe base astratta `TimerState` che ha una proprietà duration. Questo è perché non importa in quale stato il nostro `TimerBloc` sia, vogliamo sempre sapere quanto tempo rimane. Inoltre, `TimerState` estende `Equatable` per ottimizzare il nostro codice assicurandoci che la nostra app non attivi aggiornamenti se viene emesso uno stato uguale al precedente. Successivamente, definiamo e implementiamo i `TimerEvents` che il nostro `TimerBloc` processerà. ### TimerEvent Il nostro `TimerBloc` dovrà sapere come processare i seguenti eventi: - `TimerStarted`: informa il TimerBloc che il timer dovrebbe essere avviato; - `TimerPaused`: informa il TimerBloc che il timer dovrebbe essere messo in pausa; - `TimerResumed`: informa il TimerBloc che il timer dovrebbe essere ripreso; - `TimerReset`: informa il TimerBloc che il timer dovrebbe essere resettato allo stato originale; - `_TimerTicked`: informa il TimerBloc che si è verificato un tick e che deve aggiornare il suo stato di conseguenza. Se non hai usato le [IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc-code-generator) o [VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) estensioni, crea `bloc/timer_event.dart` e implementiamo quegli eventi. Successivamente, implementiamo il `TimerBloc`! ### TimerBloc Se non l'hai già fatto, crea `bloc/timer_bloc.dart` e crea un `TimerBloc` vuoto. La prima cosa che dobbiamo fare è definire lo stato iniziale del nostro `TimerBloc`. In questo caso, vogliamo che il `TimerBloc` inizi nello stato `TimerInitial` con una durata preimpostata di 1 minuto (60 secondi). Successivamente, dobbiamo definire la dipendenza sul nostro `Ticker`. Stiamo anche definendo una `StreamSubscription` per il nostro `Ticker` che implementeremo tra poco. A questo punto, tutto ciò che resta da fare è implementare i gestori di eventi. Per migliorare la leggibilità, mi piace separare ogni gestore di eventi in una funzione dedicata. Inizieremo con l'evento `TimerStarted`. Se il `TimerBloc` riceve un evento `TimerStarted`, emette uno stato `TimerRunInProgress` con la durata iniziale. Inoltre, se c'era già una `_tickerSubscription` aperta dobbiamo cancellarla per deallocare la memoria. Dobbiamo anche sovrascrivere il metodo `close` sul nostro `TimerBloc` così che possiamo cancellare la `_tickerSubscription` quando il `TimerBloc` è chiuso. Infine, ascoltiamo lo stream `_ticker.tick` e su ogni tick aggiungiamo un evento `_TimerTicked` con la durata rimanente. Successivamente, implementiamo il gestore di eventi `_TimerTicked`. Ogni volta che viene ricevuto un evento `_TimerTicked`, se la durata del tick è maggiore di 0, dobbiamo emettere uno stato `TimerRunInProgress` aggiornato con la nuova durata. Altrimenti, se la durata del tick è 0, il nostro timer è terminato e dobbiamo emettere uno stato `TimerRunComplete`. Ora implementiamo il gestore di eventi `TimerPaused`. In `_onPaused` se lo `state` del nostro `TimerBloc` è `TimerRunInProgress`, allora possiamo mettere in pausa la `_tickerSubscription` ed emettere uno stato `TimerRunPause` con la durata del timer corrente. Successivamente, implementiamo il gestore di eventi `TimerResumed` così possiamo riprendere il timer. Il gestore di eventi `TimerResumed` è molto simile al gestore di eventi `TimerPaused`. Se il `TimerBloc` ha uno `state` di `TimerRunPause` e riceve un evento `TimerResumed`, riprende la `_tickerSubscription` ed emette uno stato `TimerRunInProgress` con la durata corrente. Infine, dobbiamo implementare il gestore di eventi `TimerReset`. Se il `TimerBloc` riceve un evento `TimerReset`, deve cancellare la corrente `_tickerSubscription` così che non viene notificato di tick aggiuntivi ed emette uno stato `TimerInitial` con la durata originale. Questo è tutto per il `TimerBloc`. Ora tutto ciò che resta è implementare l'UI per il nostro Timer. ## UI dell'Applicazione ### MyApp Possiamo iniziare cancellando il contenuto di `main.dart` e sostituendolo con il seguente. Successivamente, creiamo il nostro widget `App` in `app.dart`, che sarà la radice della nostra applicazione. Successivamente, dobbiamo implementare il nostro widget `Timer`. ### Timer Il nostro widget `Timer` (`lib/timer/view/timer_page.dart`) sarà responsabile di visualizzare il tempo rimanente insieme ai pulsanti appropriati che permetteranno agli utenti di avviare, mettere in pausa e resettare il timer. Finora, stiamo solo usando `BlocProvider` per accedere all'istanza del nostro `TimerBloc`. Successivamente, implementeremo il nostro widget `Actions` che avrà le azioni appropriate (avvia, pausa e reset). ### Barrel Per pulire i nostri import dalla sezione `Timer`, dobbiamo creare un file "barrel" `timer/timer.dart`. ### Actions Il widget `Actions` è solo un altro `StatelessWidget` che usa un `BlocBuilder` per ricostruire la UI ogni volta che otteniamo un nuovo `TimerState`. `Actions` usa `context.read()` per accedere all'istanza del `TimerBloc` e restituisce diversi `FloatingActionButtons` in base allo stato corrente del `TimerBloc`. Ognuno dei `FloatingActionButtons` aggiunge un evento nel suo callback `onPressed` per notificare il `TimerBloc`. Se vuoi un controllo granulare su quando la funzione `builder` viene chiamata puoi fornire opzionalmente un `buildWhen` a `BlocBuilder`. Il `buildWhen` prende lo stato precedente del bloc e lo stato corrente del bloc e restituisce un `booleano`. Se `buildWhen` restituisce `true`, `builder` sarà chiamato con `state` e il widget si ricostruirà. Se `buildWhen` restituisce `false`, `builder` non sarà chiamato con `state` e non si verificherà alcun aggiornamento. In questo caso, non vogliamo che il widget `Actions` sia ricostruito su ogni tick perché sarebbe inefficiente. Invece, vogliamo che `Actions` si ricostruisca solo se il `runtimeType` del `TimerState` cambia (TimerInitial => TimerRunInProgress, TimerRunInProgress => TimerRunPause, ecc...). Di conseguenza, se colorassimo casualmente i widget su ogngli aggiornamenti, sarebbe: ![BlocBuilder buildWhen demo](https://cdn-images-1.medium.com/max/1600/1*YyjpH1rcZlYWxCX308l_Ew.gif) :::note Anche se il widget `Text` è ricostruito su ogni tick, ricostruiamo le `Actions` solo se necessario. ::: ### Background Infine, aggiungi il widget background come segue: ### Riassumendo Questo è tutto! A questo punto abbiamo un'applicazione timer abbastanza solida che ricostruisce efficientemente i widget solo quando necessario. Il codice sorgente completo per questo esempio può essere trovato [qui](https://github.com/felangel/Bloc/tree/master/examples/flutter_timer). ================================================ FILE: docs/src/content/docs/it/tutorials/flutter-todos.mdx ================================================ --- title: Flutter Todos description: Guida completa alla creazione di un'app Flutter todos con bloc. sidebar: order: 6 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-todos/FlutterCreateSnippet.astro'; import ActivateVeryGoodCLISnippet from '~/components/tutorials/flutter-todos/ActivateVeryGoodCLISnippet.astro'; import FlutterCreatePackagesSnippet from '~/components/tutorials/flutter-todos/FlutterCreatePackagesSnippet.astro'; import ProjectStructureSnippet from '~/components/tutorials/flutter-todos/ProjectStructureSnippet.astro'; import VeryGoodPackagesGetSnippet from '~/components/tutorials/flutter-todos/VeryGoodPackagesGetSnippet.astro'; import HomePageTreeSnippet from '~/components/tutorials/flutter-todos/HomePageTreeSnippet.astro'; import TodosOverviewPageTreeSnippet from '~/components/tutorials/flutter-todos/TodosOverviewPageTreeSnippet.astro'; import StatsPageTreeSnippet from '~/components/tutorials/flutter-todos/StatsPageTreeSnippet.astro'; import EditTodosPageTreeSnippet from '~/components/tutorials/flutter-todos/EditTodosPageTreeSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) In questo tutorial costruiremo una applicazione in Flutter per spuntare le cose da fare usando la libreria bloc. ![demo](~/assets/tutorials/flutter-todos.gif) ## Argomenti Chiave - [Bloc e Cubit](/it/bloc-concepts#cubit-vs-bloc) per gestire i vari stati delle funzionalità; - [Architettura a Livelli](/it/architecture) per separazione delle responsabilità e per facilitare la riutilizzabilità; - [BlocObserver](/it/bloc-concepts#blocobserver) per osservare i cambiamenti di stato; - [BlocProvider](/it/flutter-bloc-concepts#blocprovider), widget Flutter che fornisce un bloc ai suoi figli; - [BlocBuilder](/it/flutter-bloc-concepts#blocbuilder), widget Flutter che gestisce la costruzione del widget in risposta a nuovi stati; - [BlocListener](/it/flutter-bloc-concepts#bloclistener), widget Flutter che gestisce l'esecuzione di effetti collaterali (side-effects) in risposta ai cambiamenti di stato; - [RepositoryProvider](/it/flutter-bloc-concepts#repositoryprovider), widget Flutter per fornire un repository ai suoi figli; - [Equatable](/it/faqs/#quando-usare-equatable) per prevenire aggiornamenti non necessari; - [MultiBlocListener](/it/flutter-bloc-concepts#multibloclistener), widget Flutter che riduce l'annidamento quando si usano più BlocListener. ## Configurazione Inizieremo creando un nuovo progetto Flutter usando [very_good_cli](https://pub.dev/packages/very_good_cli). :::note Installa `very_good_cli` usando il seguente comando: ::: Successivamente creeremo i pacchetti `todos_api`, `local_storage_todos_api` e `todos_repository` usando `very_good_cli`: Possiamo poi sostituire il contenuto di `pubspec.yaml` con: Infine, installiamo tutte le dipendenze: ## Struttura del Progetto La struttura del progetto della nostra applicazione dovrebbe essere: Dividiamo il progetto in più pacchetti per mantenere esplicite le dipendenze di ognuno e chiari i confini, rispettando il [principio della singola responsabilità](https://it.wikipedia.org/wiki/Principio_di_singola_responsabilit%C3%A0). Modularizzare il progetto in questo modo offre molti vantaggi, inclusi: - facilitare il riutilizzo dei pacchetti tra più progetti; - migliorare CI/CD in termini di efficienza (eseguire controlli solo sul codice modificato); - facilità nella manutenzione dei pacchetti, con suite di test dedicate, versionamento semantico e cicli di rilascio indipendenti. ## Architettura ![Todos Architecture Diagram](~/assets/tutorials/todos-architecture.png) La stratificazione del codice è fondamentale per iterare rapidamente e in sicurezza. Ogni strato ha una singola responsabilità e può essere usato e testato in isolamento. Questo ci permette di contenere le modifiche in uno strato specifico, minimizzando l'impatto sull'intera applicazione. Inoltre, stratificare l'applicazione ci consente di riutilizzare facilmente le librerie tra più progetti (specialmente per quanto riguarda il livello dati). La nostra applicazione consiste in tre strati principali: - livello dati; - livello dominio; - livello funzionalità (feature layer). - presentazione/UI (widget) - logica applicativa (bloc/cubit) **Livello Dati** Questo è il livello più basso ed è responsabile del recupero di dati grezzi da sorgenti esterne come database, API e altro. I pacchetti nel livello dati non dovrebbero dipendere da alcuna UI e possono essere riutilizzati e persino pubblicati su [pub.dev](https://pub.dev) come pacchetti standalone. In questo esempio, il nostro livello dati consiste nei pacchetti `todos_api` e `local_storage_todos_api`. **Livello Dominio** Questo strato combina uno o più data provider e applica "regole di business" ai dati. Ogni componente in questo strato è chiamato repository e ogni repository gestisce generalmente un singolo dominio. I pacchetti nel livello repository dovrebbero interagire solo con il livello dati. In questo esempio, il livello repository consiste nel pacchetto `todos_repository`. **Livello Funzionalità** Questo strato contiene tutte le funzionalità e i casi d'uso specifici dell'applicazione. Ogni funzionalità è generalmente composta da una parte UI e dalla logica applicativa. Le funzionalità dovrebbero essere indipendenti tra loro per poter essere aggiunte o rimosse facilmente senza impattare il resto del progetto. All'interno di ogni funzionalità, lo stato e la logica applicativa sono gestiti dai bloc. I bloc interagiscono con zero o più repository, reagiscono agli eventi ed emettono stati che attivano cambiamenti nella UI. I widget all'interno di ogni funzionalità dovrebbero dipendere solo dal bloc corrispondente e renderizzare l'interfaccia basata sullo stato corrente. La UI può notificare il bloc dell'input utente tramite eventi. In questo esempio, l'applicazione consisterà delle funzionalità `home`, `todos_overview`, `stats` e `edit_todos`. Ora che abbiamo visto gli strati ad alto livello, iniziamo a costruire la nostra applicazione partendo dal livello dati! ## Livello Dati Il livello dati è lo strato più basso dell'applicazione e consiste in data provider grezzi. I pacchetti in questo strato si occupano principalmente della provenienza e del recupero dei dati. In questo caso il livello dati consisterà del `TodosApi`, che è un'interfaccia, e del `LocalStorageTodosApi`, un'implementazione del `TodosApi` basata su `shared_preferences`. ### TodosApi Il pacchetto `todos_api` esporterà un'interfaccia generica per interagire e gestire i todos. Successivamente implementeremo il `TodosApi` usando `shared_preferences`. Avere un'astrazione renderà facile supportare altre implementazioni senza dover modificare altre parti dell'applicazione. Ad esempio, potremmo aggiungere un `FirestoreTodosApi` (che usa `cloud_firestore` invece di `shared_preferences`) con modifiche minime al resto del codice. #### Modello Todo Successivamente definiremo il modello `Todo`. La prima cosa da notare è che il modello `Todo` non vive nell'app, ma è parte del pacchetto `todos_api`. Questo perché il `TodosApi` definisce API che restituiscono e accettano oggetti `Todo`. Il modello è una rappresentazione Dart dell'oggetto Todo grezzo che sarà memorizzato e recuperato. Il modello `Todo` usa [json_serializable](https://pub.dev/packages/json_serializable) per gestire la (de)serializzazione JSON. Se stai seguendo il tutorial, dovrai eseguire la [generazione del codice](https://pub.dev/packages/json_serializable#running-the-code-generator) per risolvere gli errori del compilatore. `json_map.dart` fornisce un `typedef` per il controllo del codice e il linting. Il modello `Todo` è definito in `todos_api/models/todo.dart` ed è esportato da `package:todos_api/todos_api.dart`. #### Aggiornare Esportazioni Il modello `Todo` e il `TodosApi` sono esportati tramite file "barrel". Nota come non importiamo il modello direttamente, ma lo importiamo in `lib/src/todos_api.dart` con un riferimento al file "barrel" del pacchetto: `import 'package:todos_api/todos_api.dart';`. Aggiorna i file "barrel" per risolvere eventuali errori di import rimanenti: #### Stream vs Futures In una versione precedente di questo tutorial, il `TodosApi` era basato su `Future` piuttosto che su `Stream`. Per un esempio di API basata su `Future`, vedi [l'implementazione di Brian Egan nei suoi Architecture Samples](https://github.com/brianegan/flutter_architecture_samples/tree/master/todos_repository_core). Un'implementazione basata su `Future` potrebbe consistere in due metodi: `loadTodos` e `saveTodos` (nota il plurale). Questo significa che una lista completa di todos deve essere fornita al metodo ogni volta. - Una limitazione di questo approccio è che le operazioni CRUD standard (Create, Read, Update e Delete) richiedono l'invio dell'intera lista di todos a ogni chiamata. Ad esempio, in una schermata "Aggiungi Todo", non si può semplicemente inviare il singolo todo aggiunto. Dobbiamo tenere traccia dell'intera lista e fornirla completa quando persistiamo i dati. - Una seconda limitazione è che `loadTodos` è un recupero dati _una tantum_. L'app deve contenere logica per richiedere aggiornamenti periodicamente. Nell'implementazione corrente, il `TodosApi` espone uno `Stream>` tramite `getTodos()` che segnalerà aggiornamenti in tempo reale a tutti i sottoscritti quando la lista di todos cambia. Inoltre, i todos possono essere creati, eliminati o aggiornati individualmente. Ad esempio, sia eliminare che salvare un todo viene fatto passando solo il `todo` come argomento. Non è necessario fornire la lista appena aggiornata di tutti i todos ogni volta. ### LocalStorageTodosApi Questo pacchetto implementa il `todos_api` usando il pacchetto [`shared_preferences`](https://pub.dev/packages/shared_preferences). ## Livello Repository Un [repository](/it/architecture#repository) è parte del livello applicativo. Un repository dipende da uno o più data provider privi di logica di dominio e combina le loro API pubbliche in API che forniscono valore applicativo ("business value"). In aggiunta, avere un livello repository aiuta ad astrarre l'acquisizione dati dal resto dell'applicazione, permettendoci di cambiare dove e come i dati vengono memorizzati senza influenzare altre parti dell'app. ### TodosRepository Per istanziare il repository è necessario specificare un `TodosApi`, che abbiamo discusso prima in questo tutorial; l'abbiamo quindi aggiunto come dipendenza nel `pubspec.yaml`: #### Esportazioni Libreria Oltre a esportare la classe `TodosRepository`, esportiamo anche il modello `Todo` dal pacchetto `todos_api`. Questo passo previene l'accoppiamento stretto tra l'applicazione e i data provider. Abbiamo deciso di riesportare lo stesso modello `Todo` dal `todos_api`, piuttosto che ridefinire un modello separato nel `todos_repository`, perché in questo caso abbiamo controllo completo del modello dati. In molti casi, il data provider non sarà qualcosa sotto il tuo controllo. In quei casi, diventa importante mantenere le proprie definizioni di modello nel livello repository per mantenere il pieno controllo dell'interfaccia e del contratto API. ## Livello Funzionalità ### Entrypoint Il punto di ingresso della nostra app è `main.dart`. In questo caso, ci sono tre versioni: La cosa più notevole è che l'implementazione concreta del `local_storage_todos_api` viene istanziata all'interno di ogni entrypoint. ### Bootstrapping `bootstrap.dart` carica il `BlocObserver` e crea l'istanza di `TodosRepository`. ### App `App` avvolge un widget `RepositoryProvider` che fornisce il repository a tutti i figli. Poiché sia i sottoalberi `EditTodoPage` che `HomePage` sono discendenti, tutti i bloc e cubit possono accedere al repository. `AppView` crea la `MaterialApp` e configura tema e localizzazioni. ### Theme Fornisce la definizione del tema per la modalità chiara e scura. ### Home La funzionalità home è responsabile della gestione della tab attualmente selezionata e della visualizzazione del sottoalbero corretto. #### HomeState Ci sono solo due stati associati alle due schermate: `todos` e `stats`. :::note `EditTodo` è una route separata, quindi non fa parte dello `HomeState`. ::: #### HomeCubit Un cubit è appropriato in questo caso data la semplicità della logica applicativa. Abbiamo un metodo `setTab` per cambiare la tab selezionata. #### HomeView `view.dart` è un file "barrel" che esporta tutti i componenti UI rilevanti per la funzionalità home. `home_page.dart` contiene la UI per la pagina root che l'utente vedrà all'avvio dell'app. Una rappresentazione semplificata dell'albero dei widget per la `HomePage` è: La `HomePage` fornisce un'istanza di `HomeCubit` a `HomeView`. `HomeView` usa `context.select` per ricostruire selettivamente la view ogni volta che la tab cambia. Questo ci permette di testare facilmente il widget `HomeView` fornendo un `HomeCubit` fittizio (mock) e simulando lo stato. Il `BottomAppBar` contiene widget `HomeTabButton` che chiamano `setTab` sul `HomeCubit`. L'istanza del cubit viene recuperata tramite `context.read` e il metodo appropriato viene invocato. :::caution `context.read` non ascolta i cambiamenti, è usato solo per accedere al `HomeCubit` e chiamare `setTab`. ::: ### TodosOverview La funzionalità "todos overview" permette agli utenti di gestire i propri todos creando, modificando, eliminando e filtrando gli elementi. #### TodosOverviewEvent Creiamo `todos_overview/bloc/todos_overview_event.dart` e definiamo gli eventi. - `TodosOverviewSubscriptionRequested`: Questo è l'evento di avvio. In risposta, il bloc si sottoscrive allo stream di todos dal `TodosRepository`; - `TodosOverviewTodoDeleted`: Elimina un Todo; - `TodosOverviewTodoCompletionToggled`: Attiva/disattiva lo stato completato di un todo; - `TodosOverviewToggleAllRequested`: Attiva/disattiva il completamento per tutti i todos; - `TodosOverviewClearCompletedRequested`: Elimina tutti i todos completati; - `TodosOverviewUndoDeletionRequested`: Annulla un'eliminazione di todo (es. eliminazione accidentale); - `TodosOverviewFilterChanged`: Prende un `TodosViewFilter` come argomento e cambia la vista applicando un filtro. #### TodosOverviewState Creiamo `todos_overview/bloc/todos_overview_state.dart` e definiamo lo stato. `TodosOverviewState` terrà traccia di una lista di todos, del filtro attivo, del `lastDeletedTodo` e dello status. :::note Oltre ai getter e setter di default, abbiamo un getter personalizzato chiamato `filteredTodos`. L'interfaccia usa `BlocBuilder` per accedere a `state.filteredTodos` o `state.todos`. ::: #### TodosOverviewBloc Creiamo `todos_overview/bloc/todos_overview_bloc.dart`. :::note Il bloc non crea un'istanza del `TodosRepository` internamente. Si affida invece a un'istanza del repository iniettata tramite costruttore. ::: ##### onSubscriptionRequested Quando viene aggiunto `TodosOverviewSubscriptionRequested`, il bloc inizia emettendo uno stato `loading`. In risposta, la UI può renderizzare un indicatore di caricamento. Successivamente, usiamo `emit.forEach>( ... )` che crea una sottoscrizione allo stream di todos dal `TodosRepository`. :::caution `emit.forEach()` non è lo stesso `forEach()` usato dalle liste. Questo `forEach` permette al bloc di sottoscriversi a uno `Stream` ed emettere un nuovo stato per ogni aggiornamento dello stream. ::: :::note `stream.listen` non è mai chiamato direttamente in questo tutorial. Usare `await emit.forEach()` è un pattern più recente per sottoscriversi a uno stream che permette al bloc di gestire la sottoscrizione internamente. ::: Ora che la sottoscrizione è attiva, gestiremo gli altri eventi come aggiungere, modificare ed eliminare i todos. ##### onTodoSaved `_onTodoSaved` chiama semplicemente `_todosRepository.saveTodo(event.todo)`. :::note `emit` non è mai chiamato dentro `onTodoSaved` e in molti altri gestori di eventi. Invece, questi notificano il repository che emette una lista aggiornata tramite lo stream todos. Vedi la sezione [flusso dati](#flusso-dati) per maggiori informazioni. ::: ##### Undo La funzionalità undo permette agli utenti di ripristinare l'ultimo elemento eliminato. `_onTodoDeleted` fa due cose: 1. Emette un nuovo stato con il `Todo` da eliminare (per tenerne traccia). 2. Elimina il `Todo` tramite una chiamata al repository. `_onUndoDeletionRequested` viene eseguito quando arriva la richiesta di annullamento dalla interfaccia utente. Questo metodo: - Salva temporaneamente una copia dell'ultimo todo eliminato; - Aggiorna lo stato rimuovendo il `lastDeletedTodo`; - Ripristina l'eliminazione chiamando il repository. ##### Filtraggio `_onFilterChanged` emette un nuovo stato con il nuovo filtro dell'evento. #### Modelli C'è un file modello che si occupa del filtraggio della vista. `todos_view_filter.dart` è un enum che rappresenta i tre filtri di vista e i metodi per applicarli. `models.dart` è il file "barrel" per le esportazioni. Passiamo ora alla `TodosOverviewPage`. #### TodosOverviewPage Una rappresentazione semplificata dell'albero dei widget per la `TodosOverviewPage` è: Proprio come con la funzionalità `Home`, `TodosOverviewPage` fornisce un'istanza del `TodosOverviewBloc` al sottoalbero tramite `BlocProvider`. Questo limita il `TodosOverviewBloc` solo ai widget sotto `TodosOverviewPage`. Ci sono tre widget che ascoltano i cambiamenti nel `TodosOverviewBloc`: 1. Un `BlocListener` che ascolta gli errori. Il `listener` sarà chiamato solo quando `listenWhen` restituisce `true`. Se lo status è `TodosOverviewStatus.failure`, viene visualizzato uno `SnackBar`; 2. Un secondo `BlocListener` che ascolta le eliminazioni. Quando un todo viene eliminato, viene visualizzato uno `SnackBar` con un pulsante undo. Se l'utente tocca undo, l'evento `TodosOverviewUndoDeletionRequested` sarà aggiunto al bloc; 3. Infine, usiamo un `BlocBuilder` per costruire la ListView che visualizza i todos. L'`AppBar` contiene due azioni (dropdown) per filtrare e manipolare i todos. :::note `TodosOverviewTodoCompletionToggled` e `TodosOverviewTodoDeleted` vengono aggiunti al bloc tramite `context.read`. ::: `view.dart` è il file "barrel" che esporta `todos_overview_page.dart`. #### Widget `widgets.dart` è un altro file "barrel" che esporta tutti i componenti usati all'interno della funzionalità `todos_overview`. `todo_list_tile.dart` è il `ListTile` per ogni elemento todo. `todos_overview_options_button.dart` espone due opzioni per manipolare i todos: - `toggleAll`; - `clearCompleted`. `todos_overview_filter_button.dart` espone tre opzioni di filtro: - `all`; - `activeOnly`; - `completedOnly`. ### Stats La funzionalità stats visualizza statistiche sui todos attivi e completati. #### StatsState `StatsState` tiene traccia delle informazioni di riepilogo e dello `StatsStatus` corrente. #### StatsEvent `StatsEvent` ha solo un evento chiamato `StatsSubscriptionRequested`: #### StatsBloc `StatsBloc` dipende dal `TodosRepository` proprio come `TodosOverviewBloc`. Si sottoscrive allo stream todos tramite `_todosRepository.getTodos`. #### Stats View `view.dart` è il file "barrel" per `stats_page`. `stats_page.dart` contiene la interffacia utente per la pagina che visualizza le statistiche dei todos. Una rappresentazione semplificata dell'albero dei widget per la `StatsPage` è: :::caution `TodosOverviewBloc` e `StatsBloc` comunicano entrambi con il `TodosRepository`, ma è importante notare che non c'è comunicazione diretta tra i bloc. Vedi la sezione [flusso dati](#flusso-dati) per maggiori informazioni. ::: ### EditTodo La funzionalità `EditTodo` permette agli utenti di modificare un elemento todo esistente e salvare le modifiche. #### EditTodoState `EditTodoState` tiene traccia delle informazioni necessarie durante la modifica di un todo. #### EditTodoEvent Gli eventi a cui il bloc reagirà sono: - `EditTodoTitleChanged`; - `EditTodoDescriptionChanged`; - `EditTodoSubmitted`. #### EditTodoBloc `EditTodoBloc` dipende dal `TodosRepository`, proprio come `TodosOverviewBloc` e `StatsBloc`. :::caution A differenza degli altri bloc, `EditTodoBloc` non si sottoscrive a `_todosRepository.getTodos`. È un bloc di "sola scrittura", il che significa che non ha bisogno di leggere informazioni dal repository. ::: ##### Flusso Dati Anche se ci sono molte funzionalità che dipendono dalla stessa lista di todos, non c'è comunicazione bloc-to-bloc. Invece, tutte le funzionalità sono indipendenti tra loro e si affidano al `TodosRepository` per ascoltare i cambiamenti nella lista di todos ed eseguire aggiornamenti. Ad esempio, `EditTodo` non sa nulla delle funzionalità `TodosOverview` o `Stats`. Quando la UI invia un evento `EditTodoSubmitted`: 1. `EditTodoBloc` gestisce la logica applicativa per aggiornare il `TodosRepository`; 2. `TodosRepository` notifica `TodosOverviewBloc` e `StatsBloc`; 3. `TodosOverviewBloc` e `StatsBloc` notificano l'interfaccia che si aggiorna con il nuovo stato. #### EditTodoPage Come per le funzionalità precedenti, `EditTodoPage` fornisce un'istanza del `EditTodoBloc` tramite `BlocProvider`. A differenza delle altre funzionalità, `EditTodoPage` è una route separata, motivo per cui espone un metodo `static` `route`. Questo rende facile spingere `EditTodoPage` sullo stack di navigazione tramite `Navigator.of(context).push(...)`. Una rappresentazione semplificata dell'albero dei widget per `EditTodoPage` è: ## Riepilogo Questo è tutto, abbiamo completato il tutorial! 🎉 Il codice sorgente completo per questo esempio, inclusi test unitari e widget, può essere trovato [qui](https://github.com/felangel/bloc/tree/master/examples/flutter_todos). ================================================ FILE: docs/src/content/docs/it/tutorials/flutter-weather.mdx ================================================ --- title: Flutter Weather description: Una guida approfondita su come costruire un'app Flutter weather con bloc. sidebar: order: 5 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-weather/FlutterCreateSnippet.astro'; import FeatureTreeSnippet from '~/components/tutorials/flutter-weather/FeatureTreeSnippet.astro'; import FlutterCreateApiClientSnippet from '~/components/tutorials/flutter-weather/FlutterCreateApiClientSnippet.astro'; import OpenMeteoModelsTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoModelsTreeSnippet.astro'; import LocationJsonSnippet from '~/components/tutorials/flutter-weather/LocationJsonSnippet.astro'; import LocationDartSnippet from '~/components/tutorials/flutter-weather/LocationDartSnippet.astro'; import WeatherJsonSnippet from '~/components/tutorials/flutter-weather/WeatherJsonSnippet.astro'; import WeatherDartSnippet from '~/components/tutorials/flutter-weather/WeatherDartSnippet.astro'; import OpenMeteoModelsBarrelTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoModelsBarrelTreeSnippet.astro'; import OpenMeteoLibrarySnippet from '~/components/tutorials/flutter-weather/OpenMeteoLibrarySnippet.astro'; import BuildRunnerBuildSnippet from '~/components/tutorials/flutter-weather/BuildRunnerBuildSnippet.astro'; import OpenMeteoApiClientTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoApiClientTreeSnippet.astro'; import LocationSearchMethodSnippet from '~/components/tutorials/flutter-weather/LocationSearchMethodSnippet.astro'; import GetWeatherMethodSnippet from '~/components/tutorials/flutter-weather/GetWeatherMethodSnippet.astro'; import FlutterTestCoverageSnippet from '~/components/tutorials/flutter-weather/FlutterTestCoverageSnippet.astro'; import FlutterCreateRepositorySnippet from '~/components/tutorials/flutter-weather/FlutterCreateRepositorySnippet.astro'; import RepositoryModelsBarrelTreeSnippet from '~/components/tutorials/flutter-weather/RepositoryModelsBarrelTreeSnippet.astro'; import WeatherRepositoryLibrarySnippet from '~/components/tutorials/flutter-weather/WeatherRepositoryLibrarySnippet.astro'; import WeatherCubitTreeSnippet from '~/components/tutorials/flutter-weather/WeatherCubitTreeSnippet.astro'; import WeatherBarrelDartSnippet from '~/components/tutorials/flutter-weather/WeatherBarrelDartSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) In questo tutorial, costruiremo un'app per vedere le previsioni meteo in Flutter che dimostra come gestire più cubit per implementare temi dinamici, pull-to-refresh e molto altro. L'app meteo recupererà dati meteo in tempo reale dall' API pubblica OpenMeteo e dimostrerà come separare l'applicazione in livelli (dati, repository, logica applicativa e presentazione). ![demo](~/assets/tutorials/flutter-weather.gif) ## Requisiti del Progetto L'app dovrebbe permettere agli utenti di - Cercare una città su una pagina di ricerca dedicata; - Vedere una piacevole rappresentazione dei dati meteo restituiti da [Open Meteo API](https://open-meteo.com); - Cambiare le unità visualizzate (metriche vs imperiali). Inoltre, - Il tema dell'applicazione dovrebbe riflettere il meteo per la città scelta; - Lo stato dell'applicazione dovrebbe persistere tra le sessioni: cioè, l'app dovrebbe ricordare il suo stato dopo la chiusura e la riapertura (usando [HydratedBloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc)). ## Concetti Chiave - Osservare i cambiamenti di stato con [BlocObserver](/it/bloc-concepts#blocobserver); - [BlocProvider](/it/flutter-bloc-concepts#blocprovider), widget Flutter che fornisce un bloc ai suoi figli; - [BlocBuilder](/it/flutter-bloc-concepts#blocbuilder), widget Flutter che gestisce la costruzione del widget in risposta a nuovi stati; - Prevenire aggiornamenti non necessari con [Equatable](/it/faqs/#quando-usare-equatable); - [RepositoryProvider](/it/flutter-bloc-concepts#repositoryprovider), widget Flutter che fornisce un repository ai suoi figli; - [BlocListener](/it/flutter-bloc-concepts#bloclistener), widget Flutter che invoca il codice listener in risposta ai cambiamenti di stato nel bloc; - [MultiBlocProvider](/it/flutter-bloc-concepts#multiblocprovider), widget Flutter che unisce più widget BlocProvider in uno; - [BlocConsumer](/it/flutter-bloc-concepts#blocconsumer), widget Flutter che espone un builder e listener per reagire a nuovi stati; - [HydratedBloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) per gestire e persistere lo stato. ## Configurazione Per iniziare, crea un nuovo progetto flutter ### Struttura del Progetto L'app consisterà di funzionalità isolate in directory corrispondenti. Questo ci permette di scalare man mano che il numero di funzionalità aumenta e permette agli sviluppatori di lavorare su funzionalità diverse in parallelo. L'app può essere suddivisa in quattro funzionalità principali: **search, settings, theme, weather**. Creiamo quelle directory. ### Architettura Seguendo le linee guida dell'[architettura bloc](/it/architecture), l'applicazione consisterà di diversi livelli. In questo tutorial, i diversi livelli avranno i seguenti compiti: - **Dati**: recuperare dati meteo grezzi dall'API; - **Repository**: astrarre il livello dati ed esporre modelli di dominio per l'applicazione da consumare; - **Logica Applicativa**: gestire lo stato di ogni funzionalità (informazioni unità, dettagli città, temi, ecc.); - **Presentazione**: visualizzare informazioni meteo e raccogliere input dagli utenti (pagina impostazioni, pagina ricerca ecc.). ## Livello Dati Per questa applicazione useremo l' [API Open Meteo](https://open-meteo.com). Ci concentreremo su due endpoint: - `https://geocoding-api.open-meteo.com/v1/search?name=$city&count=1` per ottenere una posizione per un dato nome città; - `https://api.open-meteo.com/v1/forecast?latitude=$latitude&longitude=$longitude¤t_weather=true` per ottenere il meteo per una data posizione. Apri [https://geocoding-api.open-meteo.com/v1/search?name=chicago&count=1](https://geocoding-api.open-meteo.com/v1/search?name=chicago&count=1) nel tuo browser per vedere la risposta per la città di Chicago. Useremo la `latitude` e `longitude` nella risposta per chiamare l'endpoint meteo. La `latitude`/`longitutde` per Chicago è `41.85003`/`-87.65005`. Naviga a [https://api.open-meteo.com/v1/forecast?latitude=43.0389&longitude=-87.90647¤t_weather=true](https://api.open-meteo.com/v1/forecast?latitude=43.0389&longitude=-87.90647¤t_weather=true) nel tuo browser e vedrai che la risposta per il meteo a Chicago conterrà tutti i dati di cui avremo bisogno per la nostra app. ### OpenMeteo API Client L'OpenMeteo API Client è indipendente dalla nostra applicazione. Di conseguenza, lo creeremo come pacchetto interno (e potremmo persino pubblicarlo su [pub.dev](https://pub.dev)). Possiamo poi usare il pacchetto aggiungendolo al `pubspec.yaml` per il livello repository, che gestirà le richieste dati per la nostra applicazione meteo principale. Crea una nuova directory a livello progetto chiamata `packages`. Questa directory memorizzerà tutti i nostri pacchetti interni. All'interno di questa directory, esegui il comando `flutter create` per creare un nuovo pacchetto chiamato `open_meteo_api` per il nostro client API. ### Modello Dati Meteo Successivamente, creiamo `location.dart` e `weather.dart` che conterranno i modelli per le risposte degli endpoint API `location` e `weather`. #### Modello Location Il modello `location.dart` dovrà memorizzare i dati restituiti dall'API location, la cui risposta dovrebbe essere simile alla seguente: Ecco il file `location.dart` che memorizza la risposta mostrata sopra: #### Modello Weather Successivamente, lavoriamo su `weather.dart`. Il nostro modello meteo dovrebbe memorizzare i dati restituiti dall'API meteo, la cui risposta dovrebbe essere simile alla seguente: Ecco il file `weather.dart` che memorizza la risposta di cui sopra: ### File Barrel Creiamo rapidamente un [file "barrel"](https://adrianfaciu.dev/posts/barrel-files/) `models.dart` per avere un unico file di esportazione per tutti i modelli. Creiamo anche un file "barrel" a livello pacchetto, `open_meteo_api.dart` Nel livello superiore, `open_meteo_api.dart` esportiamo i modelli: ### Configurazione Dobbiamo essere in grado di [serializzare e deserializzare](https://en.wikipedia.org/wiki/Serialization) i nostri modelli per lavorare con i dati delle API. Per fare questo, aggiungeremo i metodi `toJson` e `fromJson` ai nostri modelli. Inoltre, abbiamo bisogno di un modo per [fare richieste HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) per recuperare i dati da un'API remota. Fortunatamente, ci sono numerosi pacchetti popolari per fare proprio questo. Useremo i pacchetti [json_annotation](https://pub.dev/packages/json_annotation), [json_serializable](https://pub.dev/packages/json_serializable), e [build_runner](https://pub.dev/packages/build_runner) per generare le implementazioni `toJson` e `fromJson` per noi. Successivamente, useremo anche il pacchetto [http](https://pub.dev/packages/http) per inviare richieste di rete all'API meteo permettendo alla nostra applicazione di visualizzare i dati meteo correnti. Aggiungiamo queste dipendenze al `pubspec.yaml`. :::note Ricorda di eseguire `flutter pub get` dopo aver aggiunto le dipendenze. ::: ### (De)Serializzazione Affinché la generazione del codice funzioni, dobbiamo annotare il nostro codice usando il seguente: - `@JsonSerializable` per etichettare classi che possono essere serializzate; - `@JsonKey` per fornire la rappresentazione dei nomi dei campi; - `@JsonValue` per fornire la rappresentazioni dei valori dei campi; - Implementare `JSONConverter` per creare dei convertitori ad-hoc. Per ogni file dobbiamo anche: - Importare `json_annotation`; - Includere il codice generato usando la parola chiave [part](https://dart.dev/tools/pub/create-packages#organizing-a-package); - Includere il metodo `fromJson` per la deserializzazione. #### Modello Location Ecco il nostro file `location.dart` completo: #### Modello Weather Ecco il nostro file `weather.dart` completo: #### Crea File Build Nella cartella `open_meteo_api`, crea un file `build.yaml`. Lo scopo di questo file è gestire discrepanze tra convenzioni di nomenclatura nei nomi dei campi `json_serializable`. #### Generazione Codice Usiamo `build_runner` per generare il codice. `build_runner` dovrebbe generare i file `location.g.dart` e `weather.g.dart`. ### OpenMeteo API Client Creiamo il nostro client API in `open_meteo_api_client.dart` all'interno della directory `src` . La struttura del nostro progetto dovrebbe ora essere così: Possiamo ora usare il pacchetto [http](https://pub.dev/packages/http) che abbiamo aggiunto prima al file `pubspec.yaml` per fare richieste HTTP all'API meteo e usare queste informazioni nella nostra applicazione. Il nostro client API esporrà due metodi: - `locationSearch` che restituisce un `Future`; - `getWeather` che restituisce un `Future`. #### Location Search Il metodo `locationSearch` chiama l'API location e in caso di errore lancia il fallimento `LocationRequestFailure`. Il metodo completato è riportato di seguito: #### Get Weather Allo stesso modo, il metodo `getWeather` chiama l'API meteo e in caso di errore lancia `WeatherRequestFailure`. Il metodo completato è così definito: L'intero file dovrebbe risultare così: #### Aggiornamenti File Barrel Concludiamo questo pacchetto aggiungendo il nostro client API al file "barrel". ### Test Unitari È particolarmente importante scrivere test unitari per il livello dati poiché è la fondazione della nostra applicazione. I test unitari ci daranno fiducia che il pacchetto si comporti come previsto. #### Configurazione Prima, abbiamo aggiunto il pacchetto [test](https://pub.dev/packages/test) al nostro pubspec.yaml che permette di scrivere facilmente test unitari. Creeremo un file di test per il client API così come per i due modelli. #### Test Location #### Test Weather #### Test API Client Successivamente, testiamo il nostro client API. Per assicurarci il corretto funzionamento dovremmo testare che il nostro client API gestisca entrambe le chiamate API correttamente, inclusi casi limite. :::note Non vogliamo che i nostri test facciano chiamate API reali poiché il nostro obiettivo è testare la logica del client API (inclusi tutti i casi limite) e non l'API stessa. Per avere un ambiente di test consistente e controllato, useremo [mocktail](https://github.com/felangel/mocktail) (che abbiamo aggiunto al file pubspec.yaml prima) per mockare il client `http`. ::: #### Copertura Test Infine, verifichiamo la copertura dei test per assicurarci che ogni riga di codice sia testata almeno una volta. ## Livello Repository L'obiettivo del nostro livello repository è astrarre il nostro livello dati e facilitare la comunicazione con il livello bloc. Facendo questo, il resto del nostro progetto dipenderà solo dalle funzioni esposte e non dalle implementazioni specifiche dei data provider. Questo ci permette di cambiare data provider senza invalidare alcuna implementazione al livello applicativo. Ad esempio, se decidiamo di migrare da questa particolare API meteo, dovremmo essere in grado di creare un nuovo client API e sostituirlo senza dover fare cambiamenti all'API pubblica del repository o al livello applicativo. ### Configurazione All'interno della directory packages, esegui il seguente comando: Useremo gli stessi pacchetti del pacchetto `open_meteo_api` incluso il pacchetto `open_meteo_api` definito nel passo precedente. Aggiorna il tuo `pubspec.yaml` ed esegui `flutter pub get`. :::note Stiamo usando un `path` per specificare la posizione di `open_meteo_api` che ci permette di trattarlo proprio come un pacchetto esterno da `pub.dev`. ::: ### Modelli Weather Repository Creeremo un nuovo file `weather.dart` per esporre un modello meteo specifico del dominio. Questo modello conterrà solo dati rilevanti per i nostri casi applicativi -- in altre parole dovrebbe essere completamente disaccoppiato dal client API . Come al solito, creeremo anche un file "barrel" `models.dart`. Questa volta, il nostro modello meteo memorizzerà solo le proprietà `location, temperature, condition`. Continueremo anche ad annotare il nostro codice per generare il codice di serializzazione e deserializzazione. Aggiorna il file "barrel" che abbiamo creato prima per includere i modelli. #### Crea File Build Come prima, dobbiamo creare un file `build.yaml` con il seguente contenuto: #### Generazione Codice Come abbiamo fatto prima, esegui il seguente comando per generare l' implementazione (de)serializzazione. #### File Barrel Creiamo anche un file "barrel" a livello di pacchetto chiamato `packages/weather_repository/lib/weather_repository.dart` per esportare i nostri modelli: ### Weather Repository L'obiettivo principale del `WeatherRepository` è fornire un'interfaccia che astrae il data provider. In questo caso, il `WeatherRepository` avrà una dipendenza sul `WeatherApiClient` ed esporrà un singolo metodo pubblico, `getWeather(String city)`. :::note I consumatori del `WeatherRepository` non sono a conoscenza dei dettagli di implementazione sottostanti come il fatto che vengono fatte due richieste di rete all' API meteo. L'obiettivo del `WeatherRepository` è separare il "cosa" dal "come" -- in altre parole, vogliamo avere un modo per recuperare il meteo per una data città, ma non ci interessa come o da dove provengono quei dati. ::: #### Configurazione Creiamo il file `weather_repository.dart` all'interno della directory `src` del nostro pacchetto e lavoriamo sull'implementazione del repository. Il metodo principale su cui ci concentreremo è `getWeather(String city)`. Possiamo implementarlo usando due chiamate al client API come segue: #### File Barrel Aggiorna il file "barrel" che abbiamo creato prima. ### Test Unitari Proprio come con il livello dati, è critico testare il livello repository per assicurarci che la logica a livello dominio sia corretta. Per testare il nostro `WeatherRepository`, useremo la libreria [mocktail](https://github.com/felangel/mocktail). Mockeremo il client API sottostante per testare unitariamente la logica `WeatherRepository` in un ambiente isolato e controllato. ## Livello Logica Applicativa Nel livello di logica applicativa, consumeremo il modello meteo di dominio dal `WeatherRepository` ed esporremo un nuovo modello a livello di funzionalità che sarà mostrato all'utente tramite l'interfaccia. :::note Questo è il terzo diverso tipo di modello meteo che stiamo implementando. Nel client API , il nostro modello meteo conteneva tutte le informazioni restituite dall'API. Nel livello repository, il nostro modello meteo conteneva solo il modello astratto basato sul nostro caso applicativo. In questo strato, il nostro modello meteo conterrà le sole informazioni rilevanti necessarie per il set di funzionalità corrente. ::: ### Configurazione Poiché il nostro livello di logica applicativa risiede nella nostra app principale, dobbiamo modificare il `pubspec.yaml` per l'intero progetto `flutter_weather` e includere tutti i pacchetti che useremo. - Usare [equatable](https://pub.dev/packages/equatable) permetterà alle istanze delle classi di essere confrontate (`==`) per valore. Sotto il cofano, bloc confronterà i nostri stati per vedere se sono uguali, e se non lo sono, attiverà un aggiornamento. Questo garantisce che il nostro albero dei widget si ricostruisca solo quando necessario per mantenere le prestazioni veloci e reattive; - Possiamo ravvivare la nostra interfaccia utente con [google_fonts](https://pub.dev/packages/google_fonts); - [HydratedBloc](https://pub.dev/packages/hydrated_bloc) ci permette di persistere lo stato dell'applicazione quando l'app viene chiusa e riaperta; - Includeremo il pacchetto `weather_repository` che abbiamo appena creato per permetterci di recuperare i dati meteo correnti. Per i test, vorremmo includere il solito pacchetto `test`, insieme a `mocktail` per mockare dipendenze e [bloc_test](https://pub.dev/packages/bloc_test), per abilitare test facili di unità di logica applicativa, o bloc! Successivamente, lavoreremo sul livello applicativo all'interno della directory `weather` . ### Modello Weather L'obiettivo del nostro modello meteo è tenere traccia dei dati meteo visualizzati dalla nostra app, così come l'unità di misura della temperatura (Celsius o Fahrenheit). Crea `flutter_weather/lib/weather/models/weather.dart`: ### Crea File Build Crea un file `build.yaml` per il livello di logica applicativa. ### Generazione Codice Esegui `build_runner` per generare le implementazioni (de)serializzazione. ### File Barrel Esportiamo i nostri modelli dal file "barrel" (`flutter_weather/lib/weather/models/models.dart`): Poi, creiamo un file "barrel" weather di livello superiore (`flutter_weather/lib/weather/weather.dart`); ### Weather Useremo `HydratedCubit` per abilitare la nostra app a ricordare il suo stato dell'applicazione, anche dopo che è stata chiusa e riaperta. :::note `HydratedCubit` è un'estensione di `Cubit` che gestisce la persistenza e ripristino dello stato tra le sessioni. ::: #### Weather State Usando l' [Estensione Bloc VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) o [Plugin Bloc IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc), fai clic destro sulla directory `weather` e crea un nuovo cubit chiamato `Weather`. La struttura del progetto dovrebbe essere così: Ci sono quattro stati in cui la nostra app meteo può essere: - `initial` prima che qualcosa si carichi; - `loading` durante la chiamata API; - `success` se la chiamata API ha successo; - `failure` se la chiamata API non ha successo. L'enum `WeatherStatus` rappresenterà quanto sopra. Lo stato meteo completo dovrebbe essere così: #### Weather Cubit Ora che abbiamo definito il `WeatherState`, scriviamo il `WeatherCubit` che esporrà i seguenti metodi: - `fetchWeather(String? city)` usa il nostro weather repository per provare a recuperare un oggetto meteo per la città data; - `refreshWeather()` recupera un nuovo oggetto meteo usando il weather repository dato lo stato meteo corrente; - `toggleUnits()` attiva/disattiva lo stato tra Celsius e Fahrenheit; - `fromJson(Map json)`, `toJson(WeatherState state)` usati per persistenza. :::note Ricorda di generare il codice (de)serializzazione tramite: ::: ### Test Unitari Simile ai livelli dati e repository, è critico testare unitariamente il livello di logica applicativa per assicurarci che la logica a livello di funzionalità si comporti come ci aspettiamo. Ci affideremo a [bloc_test](https://pub.dev/packages/bloc_test) in aggiunta a `mocktail` e `test`. Aggiungiamo i pacchetti `test`, `bloc_test`, e `mocktail` alle `dev_dependencies`. :::note Il pacchetto [bloc_test](https://pub.dev/packages/bloc_test) ci permette di preparare facilmente i nostri bloc per i test, gestire i cambiamenti di stato e controllare i risultati in un modo consistente. ::: #### Test Weather Cubit ## Livello Presentazione ### Weather Page Inizieremo con la `WeatherPage` che usa `BlocProvider` per fornire un'istanza del `WeatherCubit` all'albero dei widget. Noterai che la pagina dipende dai widget `SettingsPage` e `SearchPage`, che creeremo successivamente. ### SettingsPage La pagina impostazioni permette agli utenti di aggiornare le loro preferenze per l'unità di misura della temperatura. ### SearchPage La pagina ricerca permette agli utenti di inserire il nome della loro città desiderata e fornisce il risultato della ricerca alla route precedente tramite `Navigator.of(context).pop`. ### Widget Weather L'app visualizzerà componenti diversi a seconda dei quattro possibili stati del `WeatherCubit`. #### WeatherEmpty Questo widget verrà mostrato quando non ci sono dati da visualizzare perché l'utente non ha ancora selezionato una città. #### WeatherError Questo componente verrà visualizzato in presenza di un errore. #### WeatherLoading Questo widget verrà visualizzato mentre l'applicazione recupera i dati. #### WeatherPopulated Questa componente verrà visualizzato una volta che l'utente ha selezionato una città e vengono recuperati i dati. ### File Barrel Aggiungiamo questi widget a un file "barrel" per pulire i nostri import. ### Entrypoint Il nostro file `main.dart` dovrebbe inizializzare la nostra `WeatherApp` e `BlocObserver` (per scopi di debug), così come impostare il nostro `HydratedStorage` per persistere lo stato tra le sessioni. Il nostro widget `app.dart` gestirà la costruzione della vista `WeatherPage` che abbiamo precedentemente creato e userà `BlocProvider` per iniettare il nostro `WeatherCubit`. ### Test Widget La libreria [`bloc_test`](https://pub.dev/packages/bloc_test) espone anche `MockBlocs` e `MockCubits` che rendono facile testare l'UI. Possiamo mockare gli stati dei vari cubit e assicurarci che l'UI reagisca correttamente. :::note Stiamo usando un `MockWeatherCubit` insieme all'API `when` da `mocktail` per simulare (stub) lo stato del cubit in ognuno dei casi di test. Questo ci permette di simulare tutti gli stati e verificare che l'interfaccia si comporti correttamente in tutte le circostanze. ::: ## Riepilogo Questo è tutto, abbiamo completato il tutorial! 🎉 Possiamo eseguire l'app finale usando il comando `flutter run`. Il codice sorgente completo per questo esempio, inclusi test unitari e widget, può essere trovato [qui](https://github.com/felangel/bloc/tree/master/examples/flutter_weather). ================================================ FILE: docs/src/content/docs/it/tutorials/github-search.mdx ================================================ --- title: GitHub Search description: Guida completa alla creazione di un'app GitHub Search in Flutter e AngularDart con bloc. sidebar: order: 9 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import SetupSnippet from '~/components/tutorials/github-search/SetupSnippet.astro'; import DartPubGetSnippet from '~/components/tutorials/github-search/DartPubGetSnippet.astro'; import FlutterCreateSnippet from '~/components/tutorials/github-search/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; import StagehandSnippet from '~/components/tutorials/github-search/StagehandSnippet.astro'; import ActivateStagehandSnippet from '~/components/tutorials/github-search/ActivateStagehandSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) In questo tutorial costruiremo un'app GitHub Search in Flutter e AngularDart per dimostrare come condividere i livelli dati e la logica applicativa tra i due progetti. ![demo](~/assets/tutorials/flutter-github-search.gif) ![demo](~/assets/tutorials/ngdart-github-search.gif) ## Argomenti Chiave - [BlocProvider](/it/flutter-bloc-concepts#blocprovider), widget Flutter che fornisce un'istanza di bloc ai suoi figli; - [BlocBuilder](/it/flutter-bloc-concepts#blocbuilder), widget Flutter che gestisce la costruzione del widget in risposta a nuovi stati; - Usare Cubit invece di Bloc. [Qual è la differenza?](/it/bloc-concepts#cubit-vs-bloc); - Prevenire aggiornamenti non necessari con [Equatable](/it/faqs/#quando-usare-equatable); - Usare un `EventTransformer` personalizzato con [`bloc_concurrency`](https://pub.dev/packages/bloc_concurrency); - Eseguire richieste di rete usando il pacchetto `http`. ## Libreria Condivisa GitHub Search La libreria "Common GitHub Search" conterrà i modelli, il data provider, il repository e il bloc che saranno condivisi tra AngularDart e Flutter. ### Configurazione Inizieremo creando una nuova directory per l'applicazione. :::note La directory `common_github_search` conterrà la libreria condivisa. ::: Dobbiamo creare un `pubspec.yaml` con le dipendenze richieste. Infine, installiamo le dipendenze. Questo è tutto per il setup del progetto! Ora possiamo iniziare a lavorare sulla costruzione del pacchetto `common_github_search`. ### Github Client Il `GithubClient` fornirà i dati grezzi dall'[API GitHub](https://developer.github.com/v3/). :::note Puoi vedere un esempio dei dati che otterremo [qui](https://api.github.com/search/repositories?q=dartlang). ::: Creiamo `github_client.dart`. :::note Il nostro `GithubClient` esegue semplicemente una richiesta di rete all'API Repository Search di Github e converte il risultato in un `SearchResult` o `SearchResultError` come `Future`. ::: :::note L'implementazione di `GithubClient` dipende da `SearchResult.fromJson`, che non abbiamo ancora implementato. ::: Successivamente dobbiamo definire i nostri modelli `SearchResult` e `SearchResultError`. #### Modello Search Result Crea `search_result.dart`, che rappresenta una lista di `SearchResultItems` basata sulla query dell'utente: :::note L'implementazione di `SearchResult` dipende da `SearchResultItem.fromJson`, che non abbiamo ancora implementato. ::: :::note Non includiamo proprietà che non verranno utilizzate nel nostro modello. ::: #### Modello Search Result Item Successivamente, creeremo `search_result_item.dart`. :::note Anche qui, l'implementazione di `SearchResultItem` dipende da `GithubUser.fromJson`, che non abbiamo ancora implementato. ::: #### Modello GitHub User Creiamo `github_user.dart`. A questo punto abbiamo finito di implementare `SearchResult` e le sue dipendenze. Ora passeremo a `SearchResultError`. #### Modello Search Result Error Crea `search_result_error.dart`. Il nostro `GithubClient` è finito. Passeremo ora al `GithubCache`, che sarà responsabile della [memoizzazione](https://it.wikipedia.org/wiki/Memoizzazione) per ottimizzare le prestazioni. ### GitHub Cache Il nostro `GithubCache` sarà responsabile di ricordare tutte le query passate, evitando così di fare richieste di rete non necessarie all'API GitHub. Questo aiuterà anche a migliorare le prestazioni della nostra applicazione. Crea `github_cache.dart`. Ora siamo pronti a creare il nostro `GithubRepository`! ### GitHub Repository Il Github Repository ha il compito di creare un'astrazione tra il livello dati (`GithubClient`) e il livello di logica applicativa (`Bloc`). È qui che utilizzeremo il nostro `GithubCache`. Crea `github_repository.dart`. :::note Il `GithubRepository` ha una dipendenza da `GithubCache` e `GithubClient` e astrae l'implementazione sottostante. La nostra applicazione non deve mai sapere come i dati vengono recuperati o da dove provengono. Possiamo cambiare il funzionamento del repository in qualsiasi momento e, finché non cambiamo l'interfaccia, non dovremo modificare alcun codice client. ::: A questo punto abbiamo completato il livello data provider e il livello repository; siamo pronti a passare al livello di logica applicativa. ### GitHub Search Event Il nostro Bloc sarà notificato quando un utente digita il nome di un repository; rappresenteremo questa azione come un `GithubSearchEvent` di tipo `TextChanged`. Crea `github_search_event.dart`. :::note Estendiamo [`Equatable`](https://pub.dev/packages/equatable) per poter confrontare per valore istanze di `GithubSearchEvent`. Di default, l'operatore di uguaglianza restituisce true se e solo se le istanze puntano allo stesso riferimento in memoria. ::: ### Github Search State Il nostro livello di presentazione necessita di diverse informazioni per renderizzarsi correttamente: - `SearchStateEmpty`: indica al livello di presentazione che l'utente non ha fornito input; - `SearchStateLoading`: indica al livello di presentazione di visualizzare un indicatore di caricamento; - `SearchStateSuccess`: indica che ci sono dati da presentare. - `items`: la `List` che sarà visualizzata; - `SearchStateError`: indica che si è verificato un errore durante il recupero dei repository. - `error`: l'errore esatto che si è verificato. Possiamo ora creare `github_search_state.dart` e implementarlo così: :::note Estendiamo [`Equatable`](https://pub.dev/packages/equatable) per poter confrontare istanze di `GithubSearchState` per valore e non per riferimento. ::: Ora che abbiamo implementato i nostri Eventi e Stati, possiamo creare il nostro `GithubSearchBloc`. ### GitHub Search Bloc Crea `github_search_bloc.dart`: :::note Il nostro `GithubSearchBloc` converte `GithubSearchEvent` in `GithubSearchState` e ha una dipendenza da `GithubRepository`. ::: :::note Creiamo un `EventTransformer` personalizzato per il ["debounce"](https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/debounce.html) dei `GithubSearchEvent`. Una delle ragioni per cui abbiamo creato un `Bloc` invece di un `Cubit` è proprio per sfruttare i trasformatori di stream. ::: Fantastico! Abbiamo finito con il nostro pacchetto `common_github_search`. Il prodotto finito dovrebbe essere simile a [questo](https://github.com/felangel/bloc/tree/master/examples/github_search/common_github_search). Successivamente, lavoreremo sull'implementazione Flutter. ## Flutter GitHub Search Flutter Github Search sarà un'applicazione Flutter che riutilizza modelli, data provider, repository e bloc da `common_github_search` per implementare la ricerca GitHub. ### Configurazione Iniziamo creando un nuovo progetto Flutter nella nostra directory `github_search` allo stesso livello di `common_github_search`. Dobbiamo aggiornare il nostro `pubspec.yaml` per includere tutte le dipendenze necessarie. :::note Stiamo includendo la nostra libreria `common_github_search` appena creata come dipendenza. ::: Ora installiamo le dipendenze. Questo è tutto per il setup del progetto. Poiché il pacchetto `common_github_search` contiene il nostro livello dati e la logica applicativa, tutto ciò che dobbiamo costruire è il livello di presentazione. ### Search Form Dovremo creare un form con i widget `_SearchBar` e `_SearchBody`. - `_SearchBar` sarà responsabile della gestione dell'input utente; - `_SearchBody` sarà responsabile della visualizzazione dei risultati di ricerca, indicatori di caricamento ed errori. Creiamo `search_form.dart`. Il nostro `SearchForm` sarà un `StatelessWidget` che renderizza i widget `_SearchBar` e `_SearchBody`. `_SearchBar` sarà uno `StatefulWidget` perché dovrà mantenere il proprio `TextEditingController` per tracciare l'input dell'utente. `_SearchBody` è un `StatelessWidget` responsabile di mostrare risultati, errori e caricamenti. Sarà il consumatore del `GithubSearchBloc`. Se il nostro stato è `SearchStateSuccess`, renderizziamo `_SearchResults` (che implementeremo successivamente). `_SearchResults` è un `StatelessWidget` che prende una `List` e la visualizza come lista di `_SearchResultItem`. `_SearchResultItem` è un `StatelessWidget` responsabile del rendering delle informazioni per un singolo risultato. Gestisce anche l'interazione dell'utente, navigando all'url del repository al tocco. :::note `_SearchBar` accede a `GitHubSearchBloc` tramite `context.read()` e notifica il bloc degli eventi `TextChanged`. ::: :::note `_SearchBody` usa `BlocBuilder` per ricostruire la UI in risposta ai cambiamenti di stato. Poiché il parametro bloc dell'oggetto `BlocBuilder` è stato omesso, `BlocBuilder` eseguirà automaticamente un lookup usando `BlocProvider` e il `BuildContext` corrente. Leggi di più [qui](/it/flutter-bloc-concepts#blocbuilder). ::: :::note Usiamo `ListView.builder` per costruire una lista scrollabile di `_SearchResultItem`. ::: :::note Usiamo il pacchetto [url_launcher](https://pub.dev/packages/url_launcher) per aprire url esterni. ::: ### Riassumendo Ora tutto ciò che resta da fare è implementare la nostra app principale in `main.dart`. :::note Il nostro `GithubRepository` è creato in `main` e iniettato nel nostro `App`. Il nostro `SearchForm` è avvolto in un `BlocProvider` che è responsabile di inizializzare, chiudere e rendere disponibile l'istanza di `GithubSearchBloc` al widget `SearchForm` e ai suoi figli. ::: Questo è tutto! Abbiamo implementato con successo un'app di ricerca GitHub in Flutter usando i pacchetti [bloc](https://pub.dev/packages/bloc) e [flutter_bloc](https://pub.dev/packages/flutter_bloc), separando il livello di presentazione dalla logica applicativa. Il codice sorgente completo può essere trovato [qui](https://github.com/felangel/bloc/tree/master/examples/github_search/flutter_github_search). Infine, costruiremo la nostra app AngularDart GitHub Search. ## AngularDart GitHub Search AngularDart GitHub Search sarà un'applicazione AngularDart che riutilizza i modelli, data provider, repository e bloc da `common_github_search` per implementare Github Search. ### Configurazione Dobbiamo iniziare creando un nuovo progetto AngularDart nella nostra directory github_search, allo stesso livello di `common_github_search`. :::note Puoi installare `stagehand` tramite: ::: Possiamo poi sostituire il contenuto di `pubspec.yaml` con: ### Search Form Proprio come nella nostra app Flutter, dovremo creare un `SearchForm` con un componente `SearchBar` e `SearchBody`. Il nostro componente `SearchForm` implementerà `OnInit` e `OnDestroy` perché dovrà creare e chiudere un `GithubSearchBloc`. - `SearchBar` sarà responsabile della gestione dell'input utente; - `SearchBody` sarà responsabile della visualizzazione dei risultati di ricerca, indicatori di caricamento ed errori. Creiamo `search_form_component.dart`. :::note Il `GithubRepository` è iniettato nel `SearchFormComponent`. ::: :::note Il `GithubSearchBloc` è creato e chiuso dal `SearchFormComponent`. ::: Il nostro template (`search_form_component.html`) sarà: Successivamente, implementeremo il componente `SearchBar`. ### Search Bar `SearchBar` è un componente responsabile di prendere l'input dell'utente e notificare il `GithubSearchBloc` dei cambiamenti di testo. Crea `search_bar_component.dart`. :::note `SearchBarComponent` ha una dipendenza su `GitHubSearchBloc` perché è responsabile di notificare il bloc degli eventi `TextChanged`. ::: Successivamente, possiamo creare `search_bar_component.html`. Abbiamo finito con `SearchBar`, ora passiamo a `SearchBody`. ### Search Body `SearchBody` è il componente responsabile della visualizzazione di risultati, errori e indicatori di caricamento. Sarà il consumatore del `GithubSearchBloc`. Crea `search_body_component.dart`. :::note `SearchBodyComponent` ha una dipendenza su `GithubSearchState` che viene fornito dal `GithubSearchBloc` usando la bloc pipe di `angular_bloc`. ::: Crea `search_body_component.html`. Se il nostro stato `isSuccess`, renderizziamo `SearchResults`. Lo implementeremo ora. ### Search Results `SearchResults` è un componente che prende una `List` e la visualizza come una lista di `SearchResultItem`. Crea `search_results_component.dart`. Successivamente creeremo `search_results_component.html`. :::note Usiamo `ngFor` per costruire una lista di componenti `SearchResultItem`. ::: È ora di implementare `SearchResultItem`. ### Search Result Item `SearchResultItem` è un componente responsabile del rendering delle informazioni per un singolo risultato di ricerca. Gestisce anche l'interazione dell'utente e la navigazione all'URL del repository. Crea `search_result_item_component.dart`. e il template corrispondente in `search_result_item_component.html`. ### Riassumendo Abbiamo tutti i nostri componenti ed è il momento di assemblarli nel nostro `app_component.dart`. :::note Stiamo creando il `GithubRepository` nell'`AppComponent` e lo iniettiamo nel componente `SearchForm`. ::: Questo è tutto! Abbiamo implementato con successo un'app di ricerca GitHub in AngularDart usando i pacchetti `bloc` e `angular_bloc`, separando il livello di presentazione dalla logica applicativa. Il codice sorgente completo può essere trovato [qui](https://github.com/felangel/bloc/tree/master/examples/github_search/angular_github_search). ## Riepilogo In questo tutorial abbiamo creato un'app Flutter e una AngularDart condividendo tutti i modelli, data provider e bloc tra le due. L'unica cosa che abbiamo dovuto scrivere due volte è stato il livello di presentazione (UI), il che è fantastico in termini di efficienza e velocità di sviluppo. Inoltre, è piuttosto comune che app web e mobile abbiano esperienze utente e stili diversi; questo approccio dimostra quanto sia facile costruire due app che appaiono totalmente diverse ma condividono gli stessi livelli di dati e logica applicativa. Il codice sorgente completo può essere trovato [qui](https://github.com/felangel/bloc/tree/master/examples/github_search). ================================================ FILE: docs/src/content/docs/it/tutorials/ngdart-counter.mdx ================================================ --- title: AngularDart Counter description: Una guida approfondita su come costruire un'app AngularDart counter con bloc. sidebar: order: 8 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import ActivateStagehandSnippet from '~/components/tutorials/ngdart-counter/ActivateStagehandSnippet.astro'; import StagehandSnippet from '~/components/tutorials/ngdart-counter/StagehandSnippet.astro'; import InstallDependenciesSnippet from '~/components/tutorials/ngdart-counter/InstallDependenciesSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) In questo tutorial, costruiremo un counter in AngularDart usando la libreria bloc. ![demo](~/assets/tutorials/ngdart-counter.gif) ## Configurazione Inizieremo creando un nuovo progetto AngularDart con [stagehand](https://github.com/dart-lang/stagehand). Se non hai stagehand installato, attivalo tramite: Poi genera un nuovo progetto tramite: Possiamo poi sostituire il contenuto di `pubspec.yaml` con: e poi installare tutte le nostre dipendenze La nostra app counter avrà solo due pulsanti per incrementare/decrementare il valore del counter e un elemento per visualizzare il valore corrente. Iniziamo progettando i `CounterEvents`. ## Counter Bloc Poiché lo stato del nostro counter può essere rappresentato da un intero non abbiamo bisogno di creare una classe personalizzata e possiamo co-localizzare gli eventi e il bloc. :::note Solo dalla dichiarazione della classe possiamo dire che il nostro `CounterBloc` prenderà `CounterEvents` come input e produrrà interi. ::: ## Counter App Ora che abbiamo il nostro `CounterBloc` completamente implementato, possiamo iniziare creando il nostro Componente App AngularDart. Il nostro `app.component.dart` dovrebbe essere: e il nostro `app.component.html` dovrebbe essere: ## Counter Page Infine, tutto quello che resta è costruire il nostro Componente Counter Page. Il nostro `counter_page_component.dart` dovrebbe essere: :::note Siamo in grado di accedere all'istanza del `CounterBloc` usando il sistema di dependency injection di AngularDart. Poiché l'abbiamo registrato come `Provider`, AngularDart può risolvere correttamente `CounterBloc`. ::: :::note Stiamo chiudendo il `CounterBloc` in `ngOnDestroy`. ::: :::note Stiamo importando il `BlocPipe` così possiamo usarlo nel nostro template. ::: Infine, il nostro `counter_page_component.html` dovrebbe essere: :::note Stiamo usando il `BlocPipe` così possiamo visualizzare lo stato del nostro `CounterBloc` mentre viene aggiornato. ::: Questo è tutto! Abbiamo separato il nostro livello di presentazione dal nostro livello di logica applicativa. Il nostro `CounterPageComponent` non ha idea di cosa succede quando un utente preme un pulsante; aggiunge semplicemente un evento per notificare il `CounterBloc`. Inoltre, il nostro `CounterBloc` non ha idea di cosa sta succedendo con lo stato (valore del counter); sta semplicemente convertendo i `CounterEvents` in interi. Possiamo eseguire la nostra app con `webdev serve` e visualizzarla localmente. Il codice sorgente completo per questo esempio può essere trovato [qui](https://github.com/felangel/bloc/tree/master/examples/angular_counter). ================================================ FILE: docs/src/content/docs/it/why-bloc.mdx ================================================ --- title: Perché Bloc? description: Una panoramica di cosa rende Bloc una soluzione solida per la gestione dello stato. sidebar: order: 1 --- Bloc rende facile separare la presentazione dalla logica applicativa, rendendo il tuo codice _veloce_, _facile da testare_ e _riutilizzabile_. Nella realizzazione di applicazioni "ready-for-production", la gestione dello stato diventa un aspetto cruciale. Come sviluppatori vogliamo: - sapere in qualsiasi momento in quale stato si trova l'applicazione; - testare facilmente ogni caso per assicurarci che l'app risponda appropriatamente; - registrare ogni singola interazione dell'utente nell'applicazione in modo da poter prendere decisioni "data-driven"; - lavorare nel modo più efficiente possibile e riutilizzare componenti sia all'interno dell'applicazione che attraverso altre applicazioni; - avere molti sviluppatori che lavorano senza problemi all'interno di un'unico sorgente seguendo gli stessi pattern e convenzioni; - sviluppare app veloci e reattive. Bloc è stato progettato per soddisfare tutte queste esigenze e molte altre ancora. Ci sono molte soluzioni di "state management" e decidere quale usare può essere un compito scoraggiante. Non esiste una soluzione perfetta di "state management"! Quello che è importante è che tu scelga quella che funziona meglio per il tuo team e per il tuo progetto. Bloc è stato progettato con tre valori fondamentali in mente: - **Semplice:** Facile da capire e utilizzabile da sviluppatori con diversi livelli di competenza; - **Potente:** Aiuta a creare applicazioni fantastiche e complesse componendole con elementi più piccoli; - **Testabile:** Permette di testare facilmente ogni aspetto di un'applicazione così da poter evolvere il progetto in totale sicurezza. L'obiettivo di Bloc è rendere i cambiamenti di stato deterministici: lo fa controllando quando questi possono avvenire e garantendo che esista un solo modo per aggiornare lo stato nell'intera app. ================================================ FILE: docs/src/content/docs/ja/bloc-concepts.mdx ================================================ --- title: Blocのコンセプト description: package:blocを構成する主要なコンセプトの概要です。 sidebar: order: 1 --- import CountStreamSnippet from '~/components/concepts/bloc/CountStreamSnippet.astro'; import SumStreamSnippet from '~/components/concepts/bloc/SumStreamSnippet.astro'; import StreamsMainSnippet from '~/components/concepts/bloc/StreamsMainSnippet.astro'; import CounterCubitSnippet from '~/components/concepts/bloc/CounterCubitSnippet.astro'; import CounterCubitInitialStateSnippet from '~/components/concepts/bloc/CounterCubitInitialStateSnippet.astro'; import CounterCubitInstantiationSnippet from '~/components/concepts/bloc/CounterCubitInstantiationSnippet.astro'; import CounterCubitIncrementSnippet from '~/components/concepts/bloc/CounterCubitIncrementSnippet.astro'; import CounterCubitBasicUsageSnippet from '~/components/concepts/bloc/CounterCubitBasicUsageSnippet.astro'; import CounterCubitStreamUsageSnippet from '~/components/concepts/bloc/CounterCubitStreamUsageSnippet.astro'; import CounterCubitOnChangeSnippet from '~/components/concepts/bloc/CounterCubitOnChangeSnippet.astro'; import CounterCubitOnChangeUsageSnippet from '~/components/concepts/bloc/CounterCubitOnChangeUsageSnippet.astro'; import CounterCubitOnChangeOutputSnippet from '~/components/concepts/bloc/CounterCubitOnChangeOutputSnippet.astro'; import SimpleBlocObserverOnChangeSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeSnippet.astro'; import SimpleBlocObserverOnChangeUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeUsageSnippet.astro'; import SimpleBlocObserverOnChangeOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeOutputSnippet.astro'; import CounterCubitOnErrorSnippet from '~/components/concepts/bloc/CounterCubitOnErrorSnippet.astro'; import SimpleBlocObserverOnErrorSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnErrorSnippet.astro'; import CounterCubitOnErrorOutputSnippet from '~/components/concepts/bloc/CounterCubitOnErrorOutputSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/bloc/CounterBlocSnippet.astro'; import CounterBlocEventHandlerSnippet from '~/components/concepts/bloc/CounterBlocEventHandlerSnippet.astro'; import CounterBlocIncrementSnippet from '~/components/concepts/bloc/CounterBlocIncrementSnippet.astro'; import CounterBlocUsageSnippet from '~/components/concepts/bloc/CounterBlocUsageSnippet.astro'; import CounterBlocStreamUsageSnippet from '~/components/concepts/bloc/CounterBlocStreamUsageSnippet.astro'; import CounterBlocOnChangeSnippet from '~/components/concepts/bloc/CounterBlocOnChangeSnippet.astro'; import CounterBlocOnChangeUsageSnippet from '~/components/concepts/bloc/CounterBlocOnChangeUsageSnippet.astro'; import CounterBlocOnChangeOutputSnippet from '~/components/concepts/bloc/CounterBlocOnChangeOutputSnippet.astro'; import CounterBlocOnTransitionSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionSnippet.astro'; import CounterBlocOnTransitionOutputSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionOutputSnippet.astro'; import SimpleBlocObserverOnTransitionSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionSnippet.astro'; import SimpleBlocObserverOnTransitionUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionUsageSnippet.astro'; import SimpleBlocObserverOnTransitionOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionOutputSnippet.astro'; import CounterBlocOnEventSnippet from '~/components/concepts/bloc/CounterBlocOnEventSnippet.astro'; import SimpleBlocObserverOnEventSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventSnippet.astro'; import SimpleBlocObserverOnEventOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventOutputSnippet.astro'; import CounterBlocOnErrorSnippet from '~/components/concepts/bloc/CounterBlocOnErrorSnippet.astro'; import CounterBlocOnErrorOutputSnippet from '~/components/concepts/bloc/CounterBlocOnErrorOutputSnippet.astro'; import CounterCubitFullSnippet from '~/components/concepts/bloc/CounterCubitFullSnippet.astro'; import CounterBlocFullSnippet from '~/components/concepts/bloc/CounterBlocFullSnippet.astro'; import AuthenticationStateSnippet from '~/components/concepts/bloc/AuthenticationStateSnippet.astro'; import AuthenticationTransitionSnippet from '~/components/concepts/bloc/AuthenticationTransitionSnippet.astro'; import AuthenticationChangeSnippet from '~/components/concepts/bloc/AuthenticationChangeSnippet.astro'; import DebounceEventTransformerSnippet from '~/components/concepts/bloc/DebounceEventTransformerSnippet.astro'; :::note [`package:bloc`](https://pub.dev/packages/bloc)を使用して作業する前に、本セクションを必ずお読みください。 ::: blocパッケージを使いこなすには、押さえておくべき重要なコンセプト(概念)がいくつかあります。 本セクションでは、それぞれのコンセプトについて詳しく説明し、実際にカウンターアプリを実装していくことで、その仕組みがどのように使われるのかを解説していきます。 ## Stream :::note `Stream`の詳細については、公式の[Dartドキュメント](https://dart.dev/tutorials/language/streams) を確認してください。 ::: `Stream`とは、非同期なデータが順番に届く仕組みです。 blocパッケージを使用するためには、`Stream`の基本的な仕組みを理解する必要があります。 `Stream`に馴染みがない方は、水が流れているパイプを想像してみてください。パイプが`Stream`であり、水が非同期なデータです。 Dartでは、`async*`(async generator)関数を書くことで`Stream`を作成できます。 関数に`async*`を付けることで`yield`キーワードが使用できるようになり、データの`Stream`が返せるようになります。上記の例では、`0`から引数`max`に渡された値の1つ前まで、int型の値が順番に流れる`Stream`を返しています。 `async*`関数内で`yield`するたびに、その値を`Stream`に送り出しています。 上記の`Stream`はいくつかの方法で使用できます。このint型の`Stream`の合計値を返す関数を書きたい場合、次のようになります。 上記の関数に`async`を付けることで`await`キーワードが使用できるようになり、int型の`Future`を返すことが出来ます。この例では、`Stream`から値が流れてくるまで待機(`await`)し、流れてきたすべての整数の合計を返しています。 さて、これでDartにおける`Stream`の基本的な仕組みについて理解できたので、blocパッケージの中核となるコンポーネントである`Cubit`について学ぶ準備が整いました。 ## Cubit `Cubit`は`BlocBase`を継承するクラスで、さまざまな型の状態(state)を管理するために拡張することが出来ます。 ![Cubit Architecture](~/assets/concepts/cubit_architecture_full.png) Cubitは、状態を変化させるための関数を公開できます。この状態は`Cubit`の出力であり、アプリケーションの状態の一部を表します。UIコンポーネントは状態が変化したという通知を受け取り、現在の状態に基づいて自身の一部を再描画することが出来ます。 :::note `Cubit`の起源について知りたい方は [こちらのIssue](https://github.com/felangel/cubit/issues/69) をご覧ください。 ::: ### Cubitの作成 `CounterCubit`は以下のようにして作成できます。 `Cubit`を作成するためには、`Cubit`が管理する状態の型を定義する必要があります。上記の`CounterCubit`の場合、状態は`int`型で十分ですが、もっと複雑なケースでは、 `int`型のようなプリミティブ型の代わりに`class`を使用する必要があるかもしれません。 また、初期状態も指定する必要があります。これは初期値の`super`を呼び出すことで行えます。上記のスニペットでは、初期状態を内部的に`0`に設定していますが、外部の値を受け入れることで`Cubit`をもっと柔軟にすることもできます。 これにより、異なる初期状態で`CounterCubit`をインスタンス化できるようになります。 ### Cubitの状態変化 各`Cubit`は`emit`を使用することで新しい状態を出力する機能を持っています。 上記のスニペットでは、`CounterCubit`が`increment`というメソッドを公開しています。このメソッドは外部から呼び出すことが可能で、 `CounterCubit`に対し、状態をインクリメント(`1`を加算)するよう通知します。 `increment`が呼び出されると、`getter`である`state`を介して`Cubit`の現在の状態にアクセスし、現在の状態に`1`を加えることで新しい状態を`emit`できます。 :::caution `emit`メソッドはprotectedであり、`Cubit`の内部でのみ使用されるべきです。 ::: ### Cubitを使用する それでは、実装した`CounterCubit`を実際に使ってみましょう! #### 基本的な使い方 上記のスニペットでは、まず`CounterCubit`のインスタンスを作成しています。その次の行の`print`では、まだ新しい状態が`emit`されていないため、 `CounterCubit`の初期状態である`0`が出力されます。その後、`increment`関数を呼び出すことで状態変化を引き起こしています。その次の`print`で`Cubit`の状態が`0`から`1`に変わっていることが確認でき、最後に`Cubit`内の`Stream`を閉じるために`close`を呼び出しています。 #### Streamの使い方 `Cubit`は`Stream`を公開しており、購読することで状態の更新をリアルタイムに受け取れます。 上記のスニペットでは、`CounterCubit`を購読することで、状態が変化するたびに`print`が呼び出されるようにしています。次に、新しい状態を`emit`する`increment`関数を呼び出しています。最後に、更新の受信が不要になったので購読をキャンセルしてから`Cubit`を閉じています。 :::note この例の`await Future.delayed(Duration.zero)`は、購読がすぐにキャンセルされてしまうのを避けるために追加されています。 ::: :::caution `Cubit`の`listen`を呼び出した際、それまでの状態変化は受け取れず、呼び出した以降の状態変化のみが受信できます。 ::: ### Cubitの監視 `Cubit`が新しい状態を`emit`すると、状態変化として`Change`が発生します。 `onChange`をオーバーライドすることで、特定の`Cubit`のすべての`Change`を監視できます。 これで、`Cubit`を使用することで、すべての変更がコンソールに出力されるのを確認できるようになります。 上記の例は以下のように出力されます。 :::note `Change`は`Cubit`の状態が更新される直前に発生します。 `Change`は`currentState`と`nextState`から構成されます。 ::: #### BlocObserver blocパッケージの使用には、すべての状態変化(`Change`)に一箇所でアクセスできるという利点もあります。このアプリケーションでは`Cubit`が1つしかありませんが、大規模なアプリケーションでは、さまざまな状態を管理するいくつもの`Cubit`を持つことが一般的です。 すべての`Change`に反応して何らかの処理を実行したい場合、独自の`BlocObserver`を作成するだけで実現できます。 :::note 必要なのは、`BlocObserver`を拡張して`onChange`メソッドをオーバーライドすることだけです。 ::: `SimpleBlocObserver`を使用するには、`main`関数を少しだけ調整する必要があります。 上記のスニペットは次のように出力されます。 :::note オーバーライドされた`onChange`メソッドが最初に呼び出され、そこで `super.onChange`を実行することで`BlocObserver`内の`onChange`メソッドを呼び出しています。 ::: :::tip `BlocObserver`では、`Change`に加えて`Cubit`インスタンス自体にもアクセスできます。 ::: ### Cubitのエラーハンドリング すべての`Cubit`には、エラーを通知する`addError`メソッドが用意されています。 :::note `onError`を`Cubit`内でオーバーライドすることで、その`Cubit`のすべてのエラーを処理できます。 ::: `onError`は`BlocObserver`でもオーバーライド可能なので、アプリケーション内で通知されたすべてのエラーを処理できます。 プログラムを再度実行すると、以下の出力が表示されるはずです。 ## Bloc `Bloc`は、関数の代わりにイベント(`event`)を使って状態(`state`)の変化を引き起こす、 `Cubit`よりも高度なクラスです。 `Bloc`は`BlocBase`を継承しているため、`Cubit`と同様の公開メソッドを持っています。しかし、`Bloc`上で関数を呼び出して直接新しい状態を`emit`するのではなく、 `Bloc`はイベントを受け取り、受信したイベントを状態に変換します。 ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) ### Blocの作成 `Bloc`の作成は`Cubit`の作成と似ていますが、管理する状態(`state`)を定義することに加えて、 `Bloc`が処理できるイベント(`event`)も定義する必要があります。 イベントは`Bloc`への入力です。これらは通常、ボタンタップなどのユーザー操作や、ページ読み込みなどのライフサイクルイベントに応答する形で発生します。 `CounterCubit`を作成した時と同様に、`super`を通じて親クラスに初期状態を渡す必要があります。 ### Blocの状態変化 `Bloc`では、`Cubit`の関数とは異なり、`on`APIを使用してイベントハンドラーを登録する必要があります。このイベントハンドラーが、受信したイベントを新しい状態(`state`)に変換します。一つのイベントによって発生する状態変化は一つだけとは限らず、イベントの処理中に二回以上状態が変化することや、逆に一度も状態変化しないこともあります。 :::tip `EventHandler`は、受け取ったイベントにアクセスでき、また、受け取ったイベントを状態(`state`)に変換する役割を担う`Emitter`にもアクセスできます。 ::: では、`CounterIncrementPressed`イベントを処理できるように`EventHandler`を更新してみましょう。 上記のスニペットでは、すべての`CounterIncrementPressed`イベントを管理するための`EventHandler`を登録しています。受信した各`CounterIncrementPressed`イベントに対して、 `getter`である`state`を介して`CounterBloc`の現在の状態にアクセスし、 `emit(state + 1)`を実行しています。 :::note `Bloc`クラスは`BlocBase`を継承しているため、`Cubit`と同様に、 `getter`である`state`を介していつでもBlocの現在の状態にアクセスできます。 ::: :::caution `Bloc`は新しい状態を直接`emit`してはいけません。その代わり、すべての状態変化は`EventHandler`内で行い、受信したイベントに応答する形で`emit`される必要があります。 ::: :::caution `Bloc`と`Cubit`は両方とも、重複した状態を無視します。 `state == nextState`となるような`State nextState`を`emit`した場合、状態変化は発生しません。 ::: ### Blocを使用する これで、`CounterBloc`のインスタンスを作成して使用できるようになりました! #### 基本的な使い方 上記のスニペットでは、まず`CounterBloc`のインスタンスを作成しています。その次の行で`Bloc`の現在の状態を`print`していますが、ここではまだ新しい状態が`emit`されていないため、初期値の`0`が出力されます。次に、状態変化に応じた処理を行うために`CounterIncrementPressed`イベントを発生させています。最後に、`0`から`1`に変化した`Bloc`の状態を`print`し、 `Bloc`内の`Stream`を閉じるために`close`を呼び出しています。 :::note `await Future.delayed(Duration.zero)`を追加することで、処理を一旦終了し、次の処理サイクルで再開しています。これにより、`EventHandler`に処理の順番をゆずり、状態変化が適切に反映されるようになります。 ::: #### Streamの使い方 `Cubit`と同様に、`Bloc`は特殊な`Stream`であるため、状態のリアルタイム更新のためにBlocを購読することも出来ます。 上記のスニペットでは、`CounterBloc`を購読し、状態変化が起きるたびに`print`を呼び出しています。次に、`CounterIncrementPressed`イベントを発生させることで、登録された`on`ハンドラーが実行され、新しい状態を`emit`します。最後に、更新の受信が不要になったので、購読をキャンセルしてから`Bloc`を閉じています。 :::note この例における`await Future.delayed(Duration.zero)`は、購読がすぐにキャンセルされてしまうのを避けるために追加されています。 ::: ### Blocの監視 `Bloc`は`BlocBase`を継承しているため、`onChange`を使用することで`Bloc`のすべての状態変化を監視できます。 この実装により、`main.dart`を以下のように更新できます。 上記のスニペットを実行すると、出力は以下のようになります。 `Bloc`と`Cubit`の重要な違いは、`Bloc`がイベント駆動であるため、状態変化を引き起こしたイベントの情報も取得できる点です。 これは`onTransition`をオーバーライドすることで実現できます。 ある状態から別の状態への変化は遷移(`Transition`)と呼ばれます。 `Transition`は現在の状態、イベント、次の状態で構成されます。 前と同じ`main.dart`スニペットを再実行すると、以下の出力が表示されるはずです。 :::note `onTransition`は`onChange`の前に呼び出され、 `currentState`から`nextState`への変化を引き起こしたイベントも保持しています。 ::: #### BlocObserver 前回と同様に、カスタマイズされた`BlocObserver`内で`onTransition`をオーバーライドすることで、発生するすべての遷移を一箇所で監視できます。 以前と同じように`SimpleBlocObserver`を初期化します。 上記のスニペットを実行すると、出力は以下のようになるはずです。 :::note `onTransition`が最初に呼び出され(`BlocObserver`よりも`Bloc`の`onTransition`が先)、その後に`onChange`が続きます。 ::: `Bloc`インスタンスのもう一つのユニークな機能は、 `Bloc`に新しいイベントが発生するたびに呼び出される`onEvent`をオーバーライドできることです。 `onChange`や`onTransition`と同様に、`onEvent`も`Bloc`と`BlocObserver`の両方でオーバーライドできます。 前と同じ`main.dart`を実行すると、以下のような出力が表示されるはずです。 :::note `onEvent`はイベントが追加されるとすぐに呼び出されます。 `Bloc`内の`onEvent`は`BlocObserver`内の`onEvent`よりも先に呼び出されます。 ::: ### Blocのエラーハンドリング `Cubit`と同様に、各`Bloc`には`addError`と`onError`メソッドがあります。 `addError`を呼び出すことで、`Bloc`内のどこからでもエラーが発生したことを通知できます。そして、`Cubit`と同じように`onError`をオーバーライドすることで、すべてのエラーに対応できます。 前回と同じ`main.dart`を再実行すると、エラーが通知されたときの様子が確認できます。 :::note `Bloc`内の`onError`が最初に呼び出され、その後に`BlocObserver`内の`onError`が呼び出されます。 ::: :::note `onError`と`onChange`の動作は、`Bloc`でも`Cubit`でも同じです。 ::: :::caution `EventHandler`内で発生した未処理の例外(`Unhandled exception`)も`onError`に通知されます。 ::: ## Cubit vs Bloc `Cubit`と`Bloc`クラスの基本について説明しましたが、いつ`Cubit`を使用し、いつ`Bloc`を使用するべきか悩む人がいるかもしれません。 ### Cubitの利点 #### シンプルさ `Cubit`を使用する最大の利点に「シンプルさ」があります。 `Cubit`を作成する際は、状態と、状態を変化させるために公開する関数を定義するだけです。一方、`Bloc`を作成する場合は、状態、イベント、そして`EventHandler`の実装を定義する必要があります。なので、`Cubit`の方が理解しやすく、コード量も少なくて済みます。 以下の`CounterCubit`と`CounterBloc`の実装を見てみましょう。 ##### CounterCubit ##### CounterBloc `Cubit`の実装の方が簡潔です。イベントを別途定義する必要がなく、関数がイベントのように動作します。さらに、`Cubit`の場合、`emit`を呼ぶだけで、どこからでも簡単に状態変化を発生させられます。 ### Blocの利点 #### トレーサビリティ(状態遷移の可視化) `BLoC`が特に優れているのは、状態がどう変わったか、何がきっかけだったのかを明確に把握できる点です。アプリケーションの機能における重要な状態の場合、イベント駆動型のように、すべてのイベントと状態変化を把握する手法が非常に有効かもしれません。 よくある例としては`AuthenticationState`の管理があります。実装を簡単にするため、`AuthenticationState`を`enum`で表現したとしましょう。 しかし、アプリケーションの状態が`authenticated`から`unauthenticated`に変わる理由は多数考えられます。例えば、ユーザーがログアウトボタンをタップし、アプリケーションからサインアウトすることを要求したかもしれません。他には、ユーザーのアクセストークンが取り消され、強制的にログアウトされた可能性もあります。 `Bloc`を使用すると、アプリケーションの状態がどのようにしてその状態に至ったのかを明確に追跡できます。 上記の`Transition`は、なぜ状態が変化したのかを知るために必要なすべての情報を提供します。もし`Cubit`を使用して`AuthenticationState`を管理していた場合、ログは以下のようになります。 このログはユーザーがログアウトしたことを示していますが、なぜログアウトしたのかは分かりません。これはデバッグや、アプリケーションの状態がどのように変化したのかを理解する上で問題になるかもしれません。 #### 高度なイベント変換 `buffer`、`debounceTime`、`throttle`などのリアクティブ演算子を活用する必要のある場合も、 `Bloc`の方が`Cubit`より優れています。 :::tip `Stream`の変換については [`package:stream_transform`](https://pub.dev/packages/stream_transform)と [`package:rxdart`](https://pub.dev/packages/rxdart)を参照してください。 ::: `Bloc`にはイベントの`Sink`があるので、受信したイベントの流れを制御したり変換したりできます。 例えば、リアルタイム検索を実装する際には、レート制限を避けるためや、バックエンドのコストや負荷を抑えるために、リクエストにデバウンス処理を適用したくなるはずです。 `Bloc`を使用する場合、独自の`EventTransformer`を定義することで、`Bloc`が受け取ったイベントの処理を変更できます。 上記のように、わずかなコードの追加で、簡単にイベントの受信時にデバウンスできるようになります。 :::tip [`package:bloc_concurrency`](https://pub.dev/packages/bloc_concurrency)は、よく使われる`EventTransformer`を集めたパッケージです。 ::: どちらを使うべきか迷った場合は、まず`Cubit`から始めましょう。後から必要に応じて`Bloc`へのリファクタリングや拡張が可能です。 ================================================ FILE: docs/src/content/docs/ja/flutter-bloc-concepts.mdx ================================================ --- title: Flutter Blocのコンセプト description: package:flutter_blocを構成する主要なコンセプトの概要です。 sidebar: order: 2 --- import BlocBuilderSnippet from '~/components/concepts/flutter-bloc/BlocBuilderSnippet.astro'; import BlocBuilderExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocBuilderExplicitBlocSnippet.astro'; import BlocBuilderConditionSnippet from '~/components/concepts/flutter-bloc/BlocBuilderConditionSnippet.astro'; import BlocSelectorSnippet from '~/components/concepts/flutter-bloc/BlocSelectorSnippet.astro'; import BlocProviderSnippet from '~/components/concepts/flutter-bloc/BlocProviderSnippet.astro'; import BlocProviderEagerSnippet from '~/components/concepts/flutter-bloc/BlocProviderEagerSnippet.astro'; import BlocProviderValueSnippet from '~/components/concepts/flutter-bloc/BlocProviderValueSnippet.astro'; import BlocProviderLookupSnippet from '~/components/concepts/flutter-bloc/BlocProviderLookupSnippet.astro'; import NestedBlocProviderSnippet from '~/components/concepts/flutter-bloc/NestedBlocProviderSnippet.astro'; import MultiBlocProviderSnippet from '~/components/concepts/flutter-bloc/MultiBlocProviderSnippet.astro'; import BlocListenerSnippet from '~/components/concepts/flutter-bloc/BlocListenerSnippet.astro'; import BlocListenerExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocListenerExplicitBlocSnippet.astro'; import BlocListenerConditionSnippet from '~/components/concepts/flutter-bloc/BlocListenerConditionSnippet.astro'; import NestedBlocListenerSnippet from '~/components/concepts/flutter-bloc/NestedBlocListenerSnippet.astro'; import MultiBlocListenerSnippet from '~/components/concepts/flutter-bloc/MultiBlocListenerSnippet.astro'; import BlocConsumerSnippet from '~/components/concepts/flutter-bloc/BlocConsumerSnippet.astro'; import BlocConsumerConditionSnippet from '~/components/concepts/flutter-bloc/BlocConsumerConditionSnippet.astro'; import RepositoryProviderSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderSnippet.astro'; import RepositoryProviderLookupSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderLookupSnippet.astro'; import RepositoryProviderDisposeSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderDisposeSnippet.astro'; import NestedRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/NestedRepositoryProviderSnippet.astro'; import MultiRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/MultiRepositoryProviderSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/flutter-bloc/CounterBlocSnippet.astro'; import CounterMainSnippet from '~/components/concepts/flutter-bloc/CounterMainSnippet.astro'; import CounterPageSnippet from '~/components/concepts/flutter-bloc/CounterPageSnippet.astro'; import WeatherRepositorySnippet from '~/components/concepts/flutter-bloc/WeatherRepositorySnippet.astro'; import WeatherMainSnippet from '~/components/concepts/flutter-bloc/WeatherMainSnippet.astro'; import WeatherAppSnippet from '~/components/concepts/flutter-bloc/WeatherAppSnippet.astro'; import WeatherPageSnippet from '~/components/concepts/flutter-bloc/WeatherPageSnippet.astro'; :::note [`package:flutter_bloc`](https://pub.dev/packages/flutter_bloc)を使用して作業する前に、本セクションを必ずお読みください。 ::: :::note `flutter_bloc`パッケージによって公開されているすべてのウィジェットは、`Cubit`と`Bloc`の両方に対応しています。 ::: ## Bloc Widgets ### BlocBuilder **BlocBuilder**は、`Bloc`と`builder`関数を必要とするFlutterウィジェットです。 `BlocBuilder`は、最新の状態(state)に応じてウィジェットを構築(build)し、再描画する役割を持ちます。 `StreamBuilder`と似ていますが、こちらの方がボイラープレート(冗長的なコード)が少ないため短く書けます。 `builder`は、状態に応じてウィジェットを返す [純粋関数](https://en.wikipedia.org/wiki/Pure_function)である必要があります。 状態の変化に応じて画面遷移を行ったり、ダイアログを表示したりといった「何らかの処理」を実行したい場合は`BlocListener`をご覧ください。 `bloc`引数を指定しない場合、`BlocBuilder`は現在の`BuildContext`をもとに、親の`BlocProvider`から提供された`Bloc`を自動的に取得します。 親の`BlocProvider`や現在の`BuildContext`から取得できない、単一のウィジェットに限定された`Bloc`を渡したい時にのみ、`bloc`引数を指定してください。 オプションの`buildWhen`引数に関数を渡すと、 `builder`関数の呼び出されるタイミングが細かく制御できます。 `buildWhen`関数では、前回の`Bloc`の状態と現在の`Bloc`の状態を引数として受け取れ、戻り値として`bool`を返します。 `buildWhen`が`true`を返した場合、 `builder`は`state`(現在の状態)とともに呼び出され、ウィジェットは再構築(rebuild)されます。`false`の場合は再構築されません。 ### BlocSelector **BlocSelector**は`BlocBuilder`に類似したFlutterのウィジェットですが、現在の`Bloc`の状態に基づいた値を選択し、戻り値として返すことで、更新のフィルタリングが行える特徴があります。 選択した値が変わらない限り、再構築は行われません。 `BlocSelector`が`builder`を再度呼び出すべきかどうかを正確に判定できるように、選択される値は不変(immutable)である必要があります。 `bloc`引数を指定しない場合、`BlocSelector`は現在の`BuildContext`をもとに、親の`BlocProvider`から提供された`Bloc`を自動的に取得します。 ### BlocProvider **BlocProvider**は、`BlocProvider.of(context)`を通じて子ウィジェットに`Bloc`を提供(provide)するFlutterウィジェットです。依存性注入(DI)ウィジェットとして使用され、サブツリー内の様々なウィジェットに対し、単一の`Bloc`インスタンスが提供できます。 ほとんどの場合、`BlocProvider`を使って新しい`Bloc`を作成し、サブツリーの残りの部分で利用できるようにするべきです。 `BlocProvider`を用いて`Bloc`を作成した場合、破棄も自動的に行われるためです。 デフォルトでは、`BlocProvider`は`Bloc`を遅延生成します。つまり、`create`は`BlocProvider.of(context)`を通じて `Bloc`が参照された際に初めて実行されます。 `create`を即座に実行させたい場合は、`lazy`を`false`に設定します。 `BlocProvider`を使って既存の`Bloc`を他のウィジェットツリーに提供することも可能です。これは、既存の`Bloc`を新しいルート(route)で利用したいときに最もよく使われます。この場合、`BlocProvider`自身がその`Bloc`を作成した訳ではないため、自動的に破棄されません。 `ChildA`と`ScreenA`のどちらからでも、以下のようにすることで`BlocA`が取得できます。 ### MultiBlocProvider `MultiBlocProvider`はFlutterのウィジェットで、二つ以上の`BlocProvider`ウィジェットを一つにまとめるためものです。いくつもの`BlocProvider`をネストする必要がなくなるため、可読性が向上します。 `MultiBlocProvider`を使えば、以下のように書けます。 :::caution `MultiBlocProvider`内で定義された`BlocProvider`の`child`は無視されます。 ::: ### BlocListener **BlocListener**は、`BlocWidgetListener`とオプションの`Bloc`を受け取り、 `Bloc`の状態変化に応じて`listener`を呼び出すFlutterウィジェットです。ナビゲーション、`SnackBar`の表示、`Dialog`の表示など、状態変化ごとに一度だけ処理を実行したい場合に使用します。 `listener`は、`BlocBuilder`の`builder`とは異なり、各状態変化に対して一度だけ呼び出されます(ただし初期状態は**含みません**)。また、`void`関数です。 `bloc`引数を指定しない場合、`BlocListener`は現在の`BuildContext`をもとに、親の`BlocProvider`から提供された`Bloc`を自動的に取得します。 現在の`BuildContext`では取得できない`Bloc`を渡したい場合にのみ、 `bloc`引数を指定してください。 `listener`関数が呼び出されるタイミングを細かく制御するには、オプションの`listenWhen`を指定します。 `listenWhen`は前回の`Bloc`の状態と現在の`Bloc`の状態を受け取り、戻り値として`bool`を返します。 `listenWhen`が`true`を返した場合、 `listener`は`state`(現在の状態)とともに呼び出されます。`false`を返した場合は呼び出されません。 ### MultiBlocListener `MultiBlocListener`はFlutterのウィジェットで、二つ以上の`BlocListener`ウィジェットを1つにまとめるためのものです。いくつもの`BlocListener`をネストする必要がなくなるため、可読性が向上します。 `MultiBlocListener`を使用すると、上記のコードを以下のように簡略化できます。 :::caution `MultiBlocListener`内で定義された`BlocListener`の`child`は無視されます。 ::: ### BlocConsumer **BlocConsumer**は、状態変化に応じて処理を行う`listener`と、ウィジェットを再構築する`builder`を引数に持ちます。 `BlocConsumer`はネストされた`BlocListener`と`BlocBuilder`に相当しますが、ボイラープレート(冗長的なコード)が少ないため短く書けます。 `BlocConsumer`は`Bloc`の状態変化に対する何らかの処理と、ウィジェットの再構築の両方が必要な場合にのみ使用してください。 `BlocConsumer`は、必須の`BlocWidgetBuilder`と`BlocWidgetListener`、およびオプションの`bloc`、`BlocBuilderCondition`、`BlocListenerCondition`を受け取ります。 `bloc`引数を指定しない場合、`BlocConsumer`は現在の`BuildContext`をもとに、親の`BlocProvider`から提供された`Bloc`を自動的に取得します。 `listener`と`builder`が呼び出されるタイミングを細かく制御するために、オプションの`listenWhen`と`buildWhen`が利用できます。 `listenWhen`と`buildWhen`は、`Bloc`の状態が変化するたびに呼び出されます。それぞれ前回の状態と現在の状態を受け取り、 `builder`や`listener`関数を呼び出すかどうかを決定する`bool`を返す必要があります。前回の状態の初期値は、`BlocConsumer`が初期化された時点の`Bloc`の状態になります。 `listenWhen`引数と`buildWhen`引数はオプションであり、指定されていない場合は常に`true`を返しているとみなされます。 ### RepositoryProvider **RepositoryProvider**は、`RepositoryProvider.of(context)`を通じて子ウィジェットにリポジトリーを提供(provide)するFlutterウィジェットです。依存性注入(DI)ウィジェットとして使用され、サブツリー内の様々なウィジェットに対し、単一のリポジトリーインスタンスが提供できます。 `Bloc`の提供には`BlocProvider`を使用し、`RepositoryProvider`はリポジトリーの提供にのみ使用してください。 **BlocProvider**は、`BlocProvider.of(context)`を通じて子ウィジェットに`Bloc`を提供するFlutterウィジェットです。依存性注入(DI)ウィジェットとして使用され、サブツリー内の様々なウィジェットに対し、単一の`Bloc`インスタンスを提供することが出来ます。 `ChildA`からは、以下のようにして`Repository`インスタンスが取得できます。 リポジトリが管理するリソースの破棄が必要な場合は、`dispose`コールバックで処理できます。 ### MultiRepositoryProvider `MultiRepositoryProvider`はFlutterのウィジェットで、二つ以上の`RepositoryProvider`ウィジェットを1つにまとめるためのものです。いくつもの`RepositoryProvider`をネストする必要がなくなるため、可読性が向上します。 `MultiRepositoryProvider`を使用すると、上記のコードを以下のように簡略化できます。 :::caution `MultiRepositoryProvider`内で定義された`RepositoryProvider`の`child`は無視されます。 ::: ## BlocProviderの使い方 `BlocProvider`を使って`CounterBloc`を`CounterPage`に提供し、 `BlocBuilder`で状態変化に応じた処理をする方法を見ていきましょう。 この時点で、プレゼンテーション層とビジネスロジック層の分離に成功しました。 `CounterPage`ウィジェットは、ユーザーがボタンをタップしたときに何が起こるかについて一切関知しません。このウィジェットは単に、ユーザーが+ボタンまたは-ボタンを押したことを`CounterBloc`に伝えるだけです。 ## RepositoryProviderの使い方 [`flutter_weather`][flutter_weather_link]の例を通して、`RepositoryProvider`の使い方を見ていきましょう。 `main.dart`では、`WeatherApp`ウィジェットを引数として`runApp`を呼び出します。 `RepositoryProvider`を使って、`WeatherRepository`のインスタンスをウィジェットツリーに注入します。 `Bloc`をインスタンス化する際には、`context.read`でリポジトリーのインスタンスにアクセスし、コンストラクターを通じて`Bloc`にリポジトリーを注入できます。 :::tip 二つ以上のリポジトリーがある場合は、`MultiRepositoryProvider`を使うことで、サブツリーにまとめてリポジトリーインスタンスが提供できます。 ::: :::note `RepositoryProvider`がアンマウントされる際にリソースを解放するには、`dispose`コールバックを使用してください。 ::: [flutter_weather_link]: https://github.com/felangel/bloc/blob/master/examples/flutter_weather ## 拡張関数(extension) Dart 2.7で導入された[拡張関数](https://dart.dev/guides/language/extension-methods)は、既存のライブラリーに機能を追加する方法です。このセクションでは、`package:flutter_bloc`に含まれる拡張関数と、その使い方について見ていきます。 `flutter_bloc`は[package:provider](https://pub.dev/packages/provider)に依存しており、これにより[`InheritedWidget`](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html)の利用が簡潔になっています。 内部的には、`package:flutter_bloc`は`package:provider`を使って、 `BlocProvider`、`MultiBlocProvider`、`RepositoryProvider`、`MultiRepositoryProvider`の各ウィジェットを実装しています。また、`package:flutter_bloc`は`package:provider`の `ReadContext`、`WatchContext`、`SelectContext`拡張を`export`しています。 :::note `package:provider`の詳細は[こちら](https://pub.dev/packages/provider)をご覧ください。 ::: ### context.read `context.read()`は、呼び出された地点から最も近い位置で提供された型`T`のインスタンスを返します。機能的には`BlocProvider.of(context)`と同等です。 `context.read`は、`onPressed`コールバック内でイベントを追加するために `Bloc`インスタンスを取得する用途で最もよく使われます。 :::note `context.read()`は`T`を`listen`しません。そのため、型`T`の提供されたオブジェクトが変更されても、 `context.read`はウィジェットの再構築を引き起こしません。 ::: #### 使い方 ✅ コールバック内でイベントを追加するときに`context.read`を使用してください。 ```dart onPressed() { context.read().add(CounterIncrementPressed()), } ``` ❌ `build`メソッド内で状態を取得するために`context.read`を使用しないでください。 ```dart @override Widget build(BuildContext context) { final state = context.read().state; return Text('$state'); } ``` 上記の使い方はエラーが発生しやすくなります。 `Bloc`の状態が変化しても `Text`ウィジェットが再構築されないためです。 :::caution 状態の変化に応じて再構築するには、代わりに`BlocBuilder`または`context.watch`を使用してください。 ::: ### context.watch `context.read()`と同様に、 `context.watch()`は呼び出された地点から最も近い位置で提供された型`T`のインスタンスを返しますが、そのインスタンスの変更も監視します。機能的には `BlocProvider.of(context, listen: true)`と同等です。 提供された型`T`の`Object`が変更されると、 `context.watch`は再構築を引き起こします。 :::caution `context.watch`は`StatelessWidget`または`State`クラスの`build`メソッド内でのみ使用できます。 ::: #### 使い方 ✅ 再構築の範囲を明示的に限定するには、 `context.watch`の代わりに`BlocBuilder`を使用してください。 ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (context, state) { // stateが変更されるたびに、Textのみが再構築されます。 return Text(state.value); }, ), ), ); } ``` あるいは、`Builder`を使って再構築する範囲を限定することもできます。 ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // stateが変更されるたびに、Textのみが再構築されます。 final state = context.watch().state; return Text(state.value); }, ), ), ); } ``` ✅ `Builder`と`context.watch`を`MultiBlocBuilder`として使用してください。 ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // BlocA、BlocB、BlocC のstateに依存するWidgetを返す } ); ``` ❌ `build`メソッド内の親ウィジェットが`state`に依存していない場合、`context.watch`は使用しないでください。 ```dart @override Widget build(BuildContext context) { // 実際にはTextウィジェットでしか使われていないのに、 // stateが変更されるたびにMaterialAppが再構築されてしまいます。 final state = context.watch().state; return MaterialApp( home: Scaffold( body: Text(state.value), ), ); } ``` :::caution `build`メソッドの先頭で`context.watch`を使用すると、 `Bloc`の状態が変更されるたびに、そのウィジェット全体が再構築されてしまいます。 ::: ### context.select `context.watch()`と同様に、`context.select(R function(T value))`は呼び出された地点から最も近い位置で提供された型`T`のインスタンスを返し、`T`の変更を監視します。 `context.watch`とは異なり、`context.select`は`Bloc`の状態内の限られた部分の変更のみを監視することが可能です。 ```dart Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); } ``` 上記のコードは、`ProfileBloc`の`state`の`name`プロパティーが変更されたときにのみウィジェットを再構築します。 #### 使い方 ✅ 再構築の範囲を明示的に限定するために、`context.select`の代わりに`BlocSelector`を使用してください。 ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector( selector: (state) => state.name, builder: (context, name) { // state.nameが変更されるたびに、Textのみが再構築されます。 return Text(name); }, ), ), ); } ``` あるいは、`Builder`を使って再構築の範囲を限定することも出来ます。 ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // state.nameが変更されるたびに、Textのみが再構築されます。 final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), ); } ``` ❌ `build`メソッド内の親ウィジェットが`state`に依存していない場合、 `context.select`を使用することは避けてください。 ```dart @override Widget build(BuildContext context) { // nameはTextウィジェットでしか使われていないのに、 // state.nameが変更されるたびにMaterialAppが再構築されてしまいます。 final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), ); } ``` :::caution `build`メソッドの先頭で`context.select`を使用すると、 `Bloc`の状態内の選択した値が変更されるたびに、そのウィジェット全体が再構築されてしまいます。 ::: ================================================ FILE: docs/src/content/docs/ja/getting-started.mdx ================================================ --- title: はじめに description: Blocで開発を始めるためのすべてが揃っています。 --- import InstallationTabs from '~/components/getting-started/InstallationTabs.astro'; import ImportTabs from '~/components/getting-started/ImportTabs.astro'; ## パッケージ blocのエコシステムは以下のパッケージで構成されています。 | Package | Description | Link | | ------------------------------------------------------------------------------------------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | AngularDartコンポーネント | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | コアDart API | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | イベントのトランスフォーマー | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | カスタムLinter | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | APIのテスト | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | コマンドラインツール | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | Flutterウィジェット | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | キャッシュ/永続化のサポート | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | Undo/Redo機能のサポート | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | ## インストール :::note Blocを使って開発を始めるにはまず[Dart SDK](https://dart.dev/get-dart) をインストールする必要があります。 ::: ## importする blocが正常にインストールされたので、`main.dart`を作成し、それぞれの`bloc`パッケージを`import`できます。 ================================================ FILE: docs/src/content/docs/ja/index.mdx ================================================ --- template: splash title: Bloc 状態管理ライブラリ description: 状態管理ライブラリBlocの公式ドキュメントです。Dart、Flutter、AngularDartをサポートしています。サンプルとチュートリアルが含まれています。 banner: content: | ✨ Blocショップにアクセス ✨ editUrl: false lastUpdated: false hero: title: Bloc v9.2.0 tagline: 思い通りに動くDartの状態管理用ライブラリ image: alt: Bloc logo file: ~/assets/bloc.svg actions: - text: はじめる link: /ja/getting-started/ variant: primary icon: rocket - text: GitHubで詳細を見る link: https://github.com/felangel/bloc icon: github variant: secondary --- import { CardGrid } from '@astrojs/starlight/components'; import SponsorsGrid from '~/components/landing/SponsorsGrid.astro'; import Card from '~/components/landing/Card.astro'; import ListCard from '~/components/landing/ListCard.astro'; import SplitCard from '~/components/landing/SplitCard.astro'; import Discord from '~/components/landing/Discord.astro';
```sh # プロジェクトにblocを追加します。 dart pub add bloc ``` [はじめに](/ja/getting-started)では、数分でBlocを使い始められるようになるための方法が順番に解説されています。 [公式チュートリアル](/ja/tutorials/flutter-counter)でベストプラクティスを学び、 Blocを利用したさまざまなアプリを作成してみましょう。 カウンター、タイマー、無限にスクロール可能なリスト、天気、ToDoなど、完全にテストされた高品質な [サンプルアプリ](https://github.com/felangel/bloc/tree/master/examples) をご覧ください! - [なぜBloc?](/ja/why-bloc) - [基本的なコンセプト](/ja/bloc-concepts) - [アーキテクチャー](/ja/architecture) - [テスト](/ja/testing) - [命名規則](/ja/naming-conventions) - [よくある質問](/ja/faqs) - [VSCode拡張機能](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) - [IntelliJ拡張機能](https://plugins.jetbrains.com/plugin/12129-bloc) - [Neovim拡張機能](https://github.com/wa11breaker/flutter-bloc.nvim) - [Mason拡張機能](https://github.com/felangel/bloc/blob/master/bricks/README.md) - [カスタムテンプレート](https://brickhub.dev/search?q=bloc) - [開発者ツール](https://github.com/felangel/bloc/blob/master/packages/bloc_tools/README.md) ================================================ FILE: docs/src/content/docs/ja/why-bloc.mdx ================================================ --- title: なぜBloc? description: Blocが優れた状態管理手法である理由を解説しています。 sidebar: order: 1 --- Blocはプレゼンテーション層からビジネスロジックを簡単に分離できるので、コードが*高速で*、_テストしやすく_、 *再利用可能に*なります。 本番環境向けのアプリケーション開発において、状態管理は非常に重要です。 開発者としては - アプリケーションの状態を常に把握したい - 全てのケースにおいて正しく動作しているかを簡単にテストしたい - アプリケーション内でユーザーが行った操作を全て記録し、データに基づいた意思決定を可能にしたい - 特定のアプリケーションのコンポーネントを再利用することで、ほかのアプリケーションでも効率よく開発をしたい - 大勢が携わる開発でも同じパターンとルールに従って、単一のコードベースでもシームレスに作業したい - サクサク動くリアクティブなアプリケーションを開発したい ですよね。 Blocはそんなニーズに応えるために開発されました。 世の中には多数の状態管理手法があり、どれを使うか判断するのは大変です。 Blocは以下の三つのコアバリューを元に作成されました。 - シンプル - 簡単に使え、開発者の技術レベルを問わず使ってもらえる - パワフル - 複雑なアプリケーションを小さなコンポーネントに分けることで、高品質なアプリケーションを作ることが出来る - テストのしやすさ - 品質に自信を持って開発を進められるように、アプリケーションを構築する全ての要素を簡単にテストできるようにする まとめると、Blocは状態変化のタイミングを制御し、アプリケーション全体で統一された方法で状態を管理することで、状態の変化を予測可能にします。 ================================================ FILE: docs/src/content/docs/ko/architecture.mdx ================================================ --- title: 아키텍쳐 description: Bloc을 사용할 때 권장되는 아키텍쳐(디자인) 패턴에 대한 개요입니다. --- import DataProviderSnippet from '~/components/architecture/DataProviderSnippet.astro'; import RepositorySnippet from '~/components/architecture/RepositorySnippet.astro'; import BusinessLogicComponentSnippet from '~/components/architecture/BusinessLogicComponentSnippet.astro'; import BlocTightCouplingSnippet from '~/components/architecture/BlocTightCouplingSnippet.astro'; import BlocLooseCouplingPresentationSnippet from '~/components/architecture/BlocLooseCouplingPresentationSnippet.astro'; import AppIdeasRepositorySnippet from '~/components/architecture/AppIdeasRepositorySnippet.astro'; import AppIdeaRankingBlocSnippet from '~/components/architecture/AppIdeaRankingBlocSnippet.astro'; import PresentationComponentSnippet from '~/components/architecture/PresentationComponentSnippet.astro'; ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) Bloc 라이브러리를 사용하면 애플리케이션을 세 가지 레이어로 분리할 수 있습니다: - Presentation - Business Logic - Data - Repository - Data Provider 이제 가장 아래 수준(사용자 인터페이스로부터 가장 멀리 떨어진)의 레이어 부터 시작하여 Presentation 레이어까지 작업해 보겠습니다. ## Data Layer Data 레이어는 하나 이상의 소스에서 데이터를 검색/조작하는 책임을 맡습니다. 따라서 Data 레이어는 두 부분으로 나뉠 수 있습니다: - Repository - Data Provider 이 레이어는 애플리케이션의 가장 아래 수준이며 데이터베이스, 네트워크 요청 및 기타 비동기 데이터 소스와 상호 작용합니다. ### Data Provider Data Provider는 원시(raw) 데이터를 제공하는 것입니다. Data Provider는 일반적이고 가변적이어야 합니다. 일반적으로 Data Provider는 간단한 API를 노출하여 [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) 작업을 수행합니다. 따라서 Data 레이어의 일부로 `createData`, `readData`, `updateData`, 그리고 `deleteData` 메서드가 있을 수 있습니다. ### Repository Repository는 Bloc 계층이 통신하는 하나 이상의 Data Provider를 감싸는 래퍼(Wrapper) 입니다. 보이시는 바와 같이, Repository 계층은 여러 Data Provider와 상호 작용하고, 데이터에 대한 변환을 수행한 후 그 결과를 Business Logic 레이어로 전달할 수 있습니다. ## Business Logic Layer Business Logic 레이어는 Presentation 레이어의 입력에 대해 새로운 상태로 응답하는 책임을 갖습니다. Business Logic 레이어를 사용자 인터페이스(Presentation 레이어)와 Data 레이어 사이의 다리라고 생각해도 좋습니다. Business Logic 레이어는 Presentation 레이어로부터 events/actions에 대한 알림을 받은 다음, Repository와 통신하여 Presentation 레이어가 사용할 새 state를 구축합니다. ### Bloc간 통신 Bloc은 스트림(Stream)을 노출하기 때문에 다른 bloc을 수신하는 bloc을 만들고 싶은 경우가 있을 수 있겠습니다만, **절대로** 이렇게 하면 안 됩니다. 아래에 소개되는 코드보다 더 나은 대안이 있습니다. 위에 소개된 코드에 오류는 없지만(심지어 스트림 구독 해체 로직도 있지만) 더 큰 문제가 존재합니다: 두 bloc간 종속성을 생성하는 문제 일반적으로, 동일한 아키텍쳐 레이어이 있는 두 엔티티(Entity)간 남매 종속성(Sibling dependencies)은 유지보수하기 어려운 긴밀한 결합을 생성하기 때문에 어떤 대가를 치르더라도 반드시 피해야 합니다. Bloc은 Business Logic 아키텍쳐 레이어에 존재하기 때문에 어떠한 bloc도 다른 bloc에 대해 알면 안 됩니다. ![Application Architecture Layers](~/assets/architecture/architecture.png) Bloc은 events와 종속성 주입된 Repository(즉, 생성자에서 bloc에 주입된 repository)를 통해서만 정보를 수신해야 합니다. 한 bloc이 다른 bloc에 응답해야 하는 상황에 처한 경우 두 가지 다른 대안이 있습니다. 연결 문제를 한 레이어 위로(Presentation 레이어로) 올리거나, 한 레이어 아래로(Domain 레이어로) 내릴 수 있습니다. #### Presentation를 통한 bloc 연결 `BlocListener`를 사용하여 한 bloc(FirstBloc)을 수신하고, 이 bloc이 변경될 때 마다 다른 bloc(SecondBloc)에 event를 추가할 수 있습니다. 위의 코드는 `SecondBloc`이 `FirstBloc`에 대해 알 필요가 없도록 하여 느슨한 결합을 유도합니다. [flutter_weather](/ko/tutorials/flutter-weather) 애플리케이션은 [이 기법을 사용하여](https://github.com/felangel/bloc/blob/b4c8db938ad71a6b60d4a641ec357905095c3965/examples/flutter_weather/lib/weather/view/weather_page.dart#L38-L42) 수신된 날씨 정보에 따라 앱 테마를 변경합니다. 어떤 상황에서는 Presentation 레이어에서 두 bloc을 연결하고 싶지 않을 수 있습니다. 이런 경우에는 두 bloc이 동일한 데이터 소스를 공유하고 데이터가 변경될 때 마다 업데이트하는 것이 합리적일 수 있습니다. #### Domain을 통한 bloc 연결 두 bloc은 Repository에서 Stream을 수신하고 Repository 데이터가 변경될 때 마다 서로 독립적으로 상태를 업데이트 할 수 있습니다. Reactive repository를 사용하여 state를 동기화 하는 것은 대규모 기업체 애플리케이션에서 흔히 볼 수 있습니다. 우선, 데이터 `Stream`을 제공하는 Repository를 만들거나 사용합니다. 예를 들어, 아래에 소개되는 Repository는 몇 가지 앱 아이디어에 대한 무한히 반복되는 Stream을 노출합니다: 새로운 앱 아이디어에 반응해야 하는 각 bloc에 동일한 Repository를 종속성 주입할 수 있습니다. 아래의 코드는 위의 Repository에서 들어오는 각 앱 아이디어에 대한 state를 yield하는 `AppIdeaRankingBloc`입니다: Bloc에서 Stream을 사용하는 방법에 대한 자세한 내용은 [Streams 그리고 Concurrency에서 Bloc을 사용하는 방법](https://verygood.ventures/blog/how-to-use-bloc-with-streams-and-concurrency)을 참조하세요. ## Presentation Layer Presentation 레이어는 하나 이상의 bloc state를 기반하여 렌더링하는 방법을 알아내야 하는 책임을 갖습니다. 대부분의 애플리케이션 흐름은 애플리케이션이 사용자에게 표시할 일부 데이터를 가져오도록 촉발하는 `AppStart` event로부터 시작합니다. 이 시나리오에서 Presentation 레이어는 `AppStart` event를 추가합니다. 또한, Presentation 레이어는 bloc 레이어의 state를 기반으로 화면에 렌더링할 내용을 파악해야 합니다. 지금까지 몇 가지 코드 스니펫을 살펴봤지만, 이 모든것은 상당히 높은 수준이었습니다. 튜토리얼 섹션에서는 여러 가지 예제 앱을 빌드하면서 이 모든 것을 종합해 보겠습니다. ================================================ FILE: docs/src/content/docs/ko/bloc-concepts.mdx ================================================ --- title: 핵심 컨셉 description: package:bloc의 핵심 개념에 대한 개요입니다. sidebar: order: 1 --- import CountStreamSnippet from '~/components/concepts/bloc/CountStreamSnippet.astro'; import SumStreamSnippet from '~/components/concepts/bloc/SumStreamSnippet.astro'; import StreamsMainSnippet from '~/components/concepts/bloc/StreamsMainSnippet.astro'; import CounterCubitSnippet from '~/components/concepts/bloc/CounterCubitSnippet.astro'; import CounterCubitInitialStateSnippet from '~/components/concepts/bloc/CounterCubitInitialStateSnippet.astro'; import CounterCubitInstantiationSnippet from '~/components/concepts/bloc/CounterCubitInstantiationSnippet.astro'; import CounterCubitIncrementSnippet from '~/components/concepts/bloc/CounterCubitIncrementSnippet.astro'; import CounterCubitBasicUsageSnippet from '~/components/concepts/bloc/CounterCubitBasicUsageSnippet.astro'; import CounterCubitStreamUsageSnippet from '~/components/concepts/bloc/CounterCubitStreamUsageSnippet.astro'; import CounterCubitOnChangeSnippet from '~/components/concepts/bloc/CounterCubitOnChangeSnippet.astro'; import CounterCubitOnChangeUsageSnippet from '~/components/concepts/bloc/CounterCubitOnChangeUsageSnippet.astro'; import CounterCubitOnChangeOutputSnippet from '~/components/concepts/bloc/CounterCubitOnChangeOutputSnippet.astro'; import SimpleBlocObserverOnChangeSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeSnippet.astro'; import SimpleBlocObserverOnChangeUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeUsageSnippet.astro'; import SimpleBlocObserverOnChangeOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeOutputSnippet.astro'; import CounterCubitOnErrorSnippet from '~/components/concepts/bloc/CounterCubitOnErrorSnippet.astro'; import SimpleBlocObserverOnErrorSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnErrorSnippet.astro'; import CounterCubitOnErrorOutputSnippet from '~/components/concepts/bloc/CounterCubitOnErrorOutputSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/bloc/CounterBlocSnippet.astro'; import CounterBlocEventHandlerSnippet from '~/components/concepts/bloc/CounterBlocEventHandlerSnippet.astro'; import CounterBlocIncrementSnippet from '~/components/concepts/bloc/CounterBlocIncrementSnippet.astro'; import CounterBlocUsageSnippet from '~/components/concepts/bloc/CounterBlocUsageSnippet.astro'; import CounterBlocStreamUsageSnippet from '~/components/concepts/bloc/CounterBlocStreamUsageSnippet.astro'; import CounterBlocOnChangeSnippet from '~/components/concepts/bloc/CounterBlocOnChangeSnippet.astro'; import CounterBlocOnChangeUsageSnippet from '~/components/concepts/bloc/CounterBlocOnChangeUsageSnippet.astro'; import CounterBlocOnChangeOutputSnippet from '~/components/concepts/bloc/CounterBlocOnChangeOutputSnippet.astro'; import CounterBlocOnTransitionSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionSnippet.astro'; import CounterBlocOnTransitionOutputSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionOutputSnippet.astro'; import SimpleBlocObserverOnTransitionSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionSnippet.astro'; import SimpleBlocObserverOnTransitionUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionUsageSnippet.astro'; import SimpleBlocObserverOnTransitionOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionOutputSnippet.astro'; import CounterBlocOnEventSnippet from '~/components/concepts/bloc/CounterBlocOnEventSnippet.astro'; import SimpleBlocObserverOnEventSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventSnippet.astro'; import SimpleBlocObserverOnEventOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventOutputSnippet.astro'; import CounterBlocOnErrorSnippet from '~/components/concepts/bloc/CounterBlocOnErrorSnippet.astro'; import CounterBlocOnErrorOutputSnippet from '~/components/concepts/bloc/CounterBlocOnErrorOutputSnippet.astro'; import CounterCubitFullSnippet from '~/components/concepts/bloc/CounterCubitFullSnippet.astro'; import CounterBlocFullSnippet from '~/components/concepts/bloc/CounterBlocFullSnippet.astro'; import AuthenticationStateSnippet from '~/components/concepts/bloc/AuthenticationStateSnippet.astro'; import AuthenticationTransitionSnippet from '~/components/concepts/bloc/AuthenticationTransitionSnippet.astro'; import AuthenticationChangeSnippet from '~/components/concepts/bloc/AuthenticationChangeSnippet.astro'; import DebounceEventTransformerSnippet from '~/components/concepts/bloc/DebounceEventTransformerSnippet.astro'; :::note [`package:bloc`](https://pub.dev/packages/bloc)으로 작업하기 전에 다음 섹션을 주의 깊게 읽어주세요. ::: Bloc 패키지 사용 방법을 이해하는 데 중요한 몇 가지 핵심 개념이 있습니다. 다음 섹션에서는 각 항목에 대해 자세하게 살펴보고 카운터 앱에 적용하는 방법을 살펴보겠습니다. ## Streams :::note `Streams`에 대한 자세한 내용은 [Dart 문서](https://dart.dev/tutorials/language/streams)를 참조하세요. ::: Stream은 연속적인 비동기 데이터입니다. Bloc 라이브러리를 사용하려면 `Streams`과 그 작동 방식에 대한 기본적인 이해가 필요합니다. 만약 당신이 `Streams`이 익숙하지 않다면, 물이 흐르는 파이프를 생각하면 됩니다. 파이프는 `Streams`이고 물은 비동기 데이터 입니다. `async*` (비동기 생성기) 함수를 작성하여 Dart에서 `Stream`을 생성할 수 있습니다. 함수를 `async*`로 표시하면 `yield` 키워드를 사용하여 데이터의 `Stream`을 반환할 수 있습니다. 위 예시에서는 `max` 정수 파라미터까지의 정수 `Stream`을 반환하고 있습니다. `async*` 함수에서 `yield` 할 때 마다 해당 데이터를 `Stream`을 통해 푸쉬합니다. 위의 `Stream`을 여러 가지 방법으로 사용할 수 있습니다. 만약 정수로 이루어진 `Stream`의 합계를 반환하는 함수를 작성하고 싶다면 다음과 같이 작성할 수 있습니다: 위의 함수를 `async`로 작성하면 `await` 키워드를 사용하여 정수의 `Future`를 반환할 수 있습니다. 이 예제에서는 Stream의 각 값을 기다렸다가 Stream에 있는 모든 정수의 합을 반환합니다. 위 모든 코드를 다음과 같이 사용할 수 있습니다: 이제 Dart에서 `Streams`이 어떻게 작동하는지 기본적인 이해를 했으니, Bloc 패키지의 핵심 구성 요소: `Cubit`에 대해 알아볼 준비가 되었습니다. ## Cubit `Cubit`은 `BlocBase`를 extends한 클래스로, 모든 유형의 state를 관리하도록 확장할 수 있습니다. ![Cubit Architecture](~/assets/concepts/cubit_architecture_full.png) `Cubit`은 state의 변경을 촉발하기 위해 호출할 수 있는 함수를 외부로 노출시킬 수 있습니다. State는 `Cubit`의 출력이며 애플리케이션 state의 일부를 나타냅니다. UI 컴포넌트는 state에 대한 notify를 받고, 현재 state에 따라 일부를 다시 그릴 수 있습니다. :::note `Cubit`의 기원에 대한 자세한 내용은 [해당 Issue](https://github.com/felangel/cubit/issues/69)에서 확인하세요. ::: ### Cubit 만들기 다음과 같은 `CounterCubit`을 만들 수 있습니다: `Cubit`을 생성할 때, `Cubit`이 관리할 상태의 타입을 정의해야 합니다. 위의 `CounterCubit`의 경우 state 타입은 `int`로 표현할 수 있지만, 더 복잡한 경우에는 Primitive type 대신 `class`를 사용해야 할 수도 있습니다. `Cubit`을 생성할 때 두 번째로 해야 할 일은 초기 상태를 지정하는 것입니다. 초기 상태의 값으로 `super`를 호출하여 이를 수행할 수 있습니다. 위의 예시 코드는 내부적으로 초기 상태를 `0`으로 설정하고 있지만, 다음과 같이 외부의 값을 허용하여 `Cubit`이 더 유연하게 작동하도록 할 수도 있습니다. 이렇게 하면 다음과 같이 다양한 초기 상태를 가진 `CounterCubit` 인스턴스를 만들 수 있습니다. ### Cubit의 state변화 각 `Cubit`은 `emit`을 통해 새로운 state를 출력할 수 있습니다. 위의 예시 코드에서 `CounterCubit`은 외부에서 호출하여 `CounterCubit`의 state를 증가시킬 수 있는 `increment` 라는 public 메서드를 노출하고 있습니다. `increment`가 호출되면 `state` getter를 통해 `Cubit`의 현재 state에 접근하고, 현재 상태에 1을 더하여 새로운 state를 `emit`할 수 있습니다. :::caution `emit` 메서드는 protected 이므로 `Cubit` 내부에서만 사용해야 합니다. ::: ### Cubit 사용하기 이제 우리가 구현한 `CounterCubit`을 실제로 사용할 수 있습니다! #### 기본 사용법 위의 예시 코드에서는 먼저 `CounterCubit`의 인스턴스를 생성합니다. 그런 다음 초기 state인 Cubit의 현재 state를 출력합니다 (아직 새로운 state가 emit되지 않았으므로). 다음으로 `increment` 함수를 호출하여 state의 변경을 촉발합니다. 마지막으로 `0`에서 `1`로 바뀐 `Cubit`의 state를 다시 출력하고 `Cubit`의 `close`를 호출하여 내부 state stream을 닫습니다. #### Stream 사용법 `Cubit`은 실시간 state 업데이트를 받을 수 있는 `Stream`을 노출합니다: 위의 예시 코드에서는 `CounterCubit`을 구독하고 각 state 변경 시마다 print를 호출하고 있습니다. 그런 다음 새로운 state를 출력하는 `increment` 함수를 호출하고 있습니다. 마지막으로, 더 이상 업데이트를 받고 싶지 않을 때 `subscription`의 `cancel`을 호출하고 `Cubit`을 닫습니다. :::note 해당 예제에서는 구독이 즉시 취소되지 않도록 `await Future.delayed(Duration.zero)`을 추가했습니다. ::: :::caution `Cubit`에서 `listen`을 호출할 때는 후속 상태 변경 사항만을 수신합니다. ::: ### Cubit 관찰하기 `Cubit`이 새로운 state를 emit하면 `Change`가 발생합니다. `onChange`를 override하여 주어진 `Cubit`에 대한 모든 변경 사항을 관찰할 수 있습니다. 그런 다음 `Cubit`과 상호 작용하고, 콘솔로 출력되는 모든 변경 사항을 관찰해봅시다. 위 예시는 다음과 같이 출력됩니다: :::note `Change`는 `Cubit`의 state가 업데이트되기 직전에 발생합니다. `Change`는 `currentState`와 `nextState`로 구성됩니다. ::: #### BlocObserver Bloc 라이브러리를 사용하면 한 곳에서 모든 `Change`에 접근할 수 있다는 장점이 있습니다. 이 애플리케이션은 하나의 `Cubit`만 있지만, 대규모 애플리케이션에서는 애플리케이션 state의 여러 부분을 관리하는 많은 `Cubit`을 사용하는 것이 일반적입니다. 모든 `Change`에 대응하여 무언가를 할 수 있도록 하려면 자체적으로 `BlocObserver`를 만들면 됩니다. :::note `BlocObserver`를 extends 하고 `onChange`메서드를 override 하기만 하면 됩니다. ::: `SimpleBlocObserver`를 사용하려면 `main`함수를 조금만 수정하면 됩니다: 위 예시 코드에 대한 출력입니다: :::note 내부의 `onChange` override가 먼저 호출되어 `BlocObserver`의 `onChange`를 알리는 `super.onChange`를 호출합니다. ::: :::tip `BlocObserver`에서는 `Change` 그 자체 외에도 `Cubit` 인스턴스에 접근할 수 있습니다. ::: ### Cubit의 에러 처리 모든 `Cubit`에는 에러가 발생했음을 나타내는데 사용할 수 있는 `addError` 메서드가 있습니다. :::note 특정 `Cubit`에 대한 모든 에러를 처리하기 위해 `onError`를 `Cubit` 내에서 override할 수 있습니다. ::: `BlocObserver`에서 `onError`를 override하여 보고된 모든 에러를 전역적으로 처리할 수도 있습니다. 동일한 프로그램을 다시 실행하면 다음과 같은 출력을 볼 수 있습니다: ## Bloc `Bloc`은 함수가 아닌 `event`에 의존하여 `state` 변경을 촉발하는 고급 클래스 입니다. `Bloc`은 또한 `BlocBase`를 extends하여 `Cubit`과 유사한 공용 API를 갖고 있습니다. 그러나, `Bloc`에서 `함수`를 호출하여 새로운 `state`를 직접 emit하는 대신, `Bloc`은 `event`를 수신하고 수신된 `event`를 나가는 `state`로 변환합니다. ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) ### Bloc 만들기 `Bloc`을 생성하는 것은 `Cubit`를 생성하는 것과 비슷하지만, 관리할 state를 정의하는 것 외에 `Bloc`이 처리할 event도 정의해야 한다는 점이 다릅니다. Event는 Bloc에 대한 입력입니다. 일반적으로 버튼을 누름과 같은 사용자 상호 작용이나, 페이지 로드와 같은 생명 주기 이벤트에 대한 응답으로 추가됩니다. `CounterCubit`을 생성할 때와 마찬가지로 `super`를 통해 부모 클래스에 전달하여 초기 state를 지정해야 합니다. ### Bloc의 state변화 `Bloc`은 `Cubit`의 함수가 아닌 `on` API를 통해 이벤트 핸들러를 등록해야 합니다. 이벤트 핸들러는 들어오는 모든 event를 0개 이상의 나가는 state로 변환하는 역할을 수행합니다. :::tip `EventHandler`는 추가된 event뿐 만 아니라 수신되는 event에 대한 응답으로 0개 이상의 상태를 emit하는데 사용할 수 있는 `Emitter`에 접근할 수 있습니다. ::: 그런 다음 `EventHandler`를 업데이트하여 `CounterIncrementPressed` event를 처리할 수 있습니다: 위의 예시 코드에서는 모든 `CounterIncrementPressed` event를 관리하기 위해 `EventHandler`를 등록했습니다. 들어오는 각 `CounterIncrementPressed` event에 대해 `state` getter와 `emit(state + 1)`를 통해 bloc의 현재 상태에 접근할 수 있습니다. :::note `Bloc` 클래스는 `BlocBase`를 extends 하기 떄문에 `Cubit`와 마찬가지로 `state` getter를 통해 언제든지 bloc의 현재 state에 접근할 수 있습니다. ::: :::caution Bloc은 새로운 state를 직접 `emit`해서는 안 됩니다. 대신 모든 state 변경은 `EventHandler` 내에서 들어오는 event에 대한 응답으로 출력되어야 합니다. ::: :::caution Bloc과 Cubit 모두 복제된 상태를 무시합니다. `state == nextState`에서 `State nextState`를 emit하면 state 변경이 발생하지 않습니다. ::: ### Bloc 사용하기 이 시점에서 `CounterBloc`의 인스턴스를 생성하여 사용할 수 있습니다! #### 기본 사용법 위의 예시 코드에서는 먼저 `CounterBloc`의 인스턴스를 생성합니다. 그런 다음 초기 state인 `Bloc`의 현재 state를 출력합니다 (아직 새로운 state가 emit되지 않았으므로). 다음으로 state 변경을 촉발하기 위해 `CounterIncrementPressed` event를 추가합니다. 마지막으로 `0`에서 `1`로 변경된 `Bloc`의 state를 다시 출력하고 `Bloc`의 `close`룰 호출하여 내부 state stream을 닫습니다. :::note 다음 event-loop를 기다리기 위해 `await Future.delayed(Duration.zero)`가 추가됩니다 (`EventHandler`가 event를 처리할 수 있도록 허용). ::: #### Stream 사용법 `Cubit`과 마찬가지로 `Bloc`은 `Stream`의 특수한 유형으로, `Bloc`을 구독하여 state를 실시간으로 업데이트 할 수도 있습니다: 위의 예시 코드에서는 `CounterBloc`을 구독하고 각 state 변경 시마다 print를 호출하고 있습니다. 그런 다음 `on` `EvnetHandler`를 촉발하고 새 state를 emit하는 `CounterIncrementPressed` event를 추가하고 있습니다. 마지막으로, 더 이상 업데이트를 받지 않으려면 `subscription`의 `cancel`을 호출하고 `Bloc`을 닫습니다. :::note 해당 예제에서는 구독이 즉시 취소되지 않도록 `await Future.delayed(Duration.zero)`을 추가했습니다. ::: ### Bloc 관찰하기 `Bloc`은 `BlocBase`를 extends 하기 때문에 `onChange`를 사용하여 `Bloc`의 모든 state 변화를 관찰할 수 있습니다. 그런 다음 `main.dart`를 다음과 같이 업데이트 합니다: 위 예시 코드에 대한 출력입니다: `Bloc`과 `Cubit`의 주요 차별화 요소 중 하나는 `Bloc`이 event 기반이기 때문에 state 변화를 유발한 원인에 대한 정보도 캡처할 수 있다는 점입니다. 이 작업은 `onTransition`을 override하여 수행할 수 있습니다. 한 state에서 다른 state로 변경되는 것을 `Transition`이라고 합니다. `Transition`은 현재 state, event, 다음 state로 구성됩니다. 그런 다음 이전과 동일한 `main.dart` 예시 코드를 다시 실행하면 다음과 같은 출력이 표시됩니다. :::note `onTransition`은 `onChange` 이전에 호출되며 `currentState`에서 `nextState`로 변경을 촉발한 event를 포함합니다. ::: #### BlocObserver 이전과 마찬가지로 커스텀 `BlocObserver`에서 `onTransition`을 override하여 단일 위치에서 발생하는 모든 Transition을 관찰할 수 있습니다. 이전과 마찬가지로 `SimpleBlocObserver`를 초기화 할 수 있습니다: 이제 위의 예시 코드를 실행하면 다음과 같은 출력을 얻을 수 있습니다: :::note `onTransition` 이 먼저 호출되고(global보다 local이 먼저) 그 다음에 `onChange`가 호출됩니다. ::: `Bloc` 인스턴스의 또 다른 독특한 특징은 `Bloc`에 새 event가 추가될 때마다 호출되는 `onEvnet`를 override할 수 있다는 점입니다. `onChange` 및 `onTransition`과 마찬가지로 `onEvent`는 전역뿐만 아니라 로컬에서도 override할 수 있습니다. 이전과 동일한 `main.dart`를 실행하면 다음과 같은 출력을 볼 수 있습니다: :::note `onEvent`는 event가 추가되는 즉시 호출됩니다. local `onEvent`는 `BlocObserver`의 global `onEvent`보다 먼저 호출됩니다. ::: ### Bloc의 에러 처리 `Cubit`과 마찬가지로 각 `Bloc`에는 `addError`와 `onError` 메서드가 있습니다. `Bloc` 내부 어디에서나 `addError`를 호출하여 에러가 발생했음을 알릴 수 있습니다. 그런 다음 `Cubit`과 마찬가지로 `onError`를 override 하여 모든 에러에 대응할 수 있습니다. 이전과 동일한 `main.dart`를 다시 실행하면 에러가 보고될 때 어떤 모습인지 확인할 수 있습니다: :::note local `onError`가 먼저 호출된 후 `BlocObserver`의 global `onError`가 호출됩니다. ::: :::note `onError`와 `onChange`는 `Bloc`과 `Cubit` 인스턴스 모두에 대해 똑같은 방식으로 작동합니다. ::: :::caution `EventHandler` 내에서 발생하는 처리되지 않은 exception도 `onError`에서 보고됩니다. ::: ## Cubit vs. Bloc 이제 `Cubit`과 `Bloc` 클래스의 기본 사항을 살펴봤으니 언제 `Cubit`을 사용해야 하는지, 언제 `Bloc`을 사용해야 하는지 궁금하실 것입니다. ### Cubit 장점 #### 단순성 `Cubit` 사용의 가장 큰 장점 중 하나는 단순성입니다. `Cubit`을 생성할 때는 state와 state를 변경하기 위해 노출할 함수만 정의하면 됩니다. 반면에 `Bloc`을 생성할 때는 state, event, `EventHandler` 구현을 정의해야 합니다. 따라서 `Cubit`을 더 쉽게 이해할 수 있고 관련된 코드도 더 적습니다. 이제 두 가지 카운터 구현을 살펴보겠습니다: ##### CounterCubit ##### CounterBloc `Cubit` 구현은 더 간결하며 event를 별도로 정의하는 대신 함수가 event처럼 작동합니다. 또한, `Cubit`을 사용할 때는 어디서든 `emit`을 호출하여 state 변경을 촉발할 수 있습니다. ### Bloc 장점 #### 추적가능성 `Bloc` 사용의 가장 큰 장점 중 하나는 state 변경의 순서와 그 변경을 유발한 원인을 정확히 파악할 수 있다는 점입니다. 애플리케이션의 기능에 중요한 state의 경우 state 변경 외에도 모든 event를 캡처하기 위해, 보다 event 중심적인 접근 방식을 사용하는 것이 매우 유용할 수 있습니다. 일반적인 사용 사례는 `AuthenticationState`를 관리하는 것입니다. 간단히 설명하기 위해 `enum`을 통해 `AuthenticationState`를 표현할 수 있다고 가정해 보겠습니다: 애플리케이션의 state가 `authenticated`에서 `unauthenticated`로 변경되는 이유는 여러 가지가 있을 수 있습니다. 예로 들어 사용자가 로그아웃 버튼을 탭하고 애플리케이션에서 로그아웃을 요청했을 수 있습니다. 반면에 사용자의 access token이 해지되어 강제로 로그아웃되었을 수도 있습니다. `Bloc`을 사용하면 애플리케이션 state가 특정 state에 도달한 경로를 명확하게 추적할 수 있습니다. 위의 `Transition`은 state가 변경된 이유를 이해하는데 필요한 모든 정보를 제공합니다. 만약 `Cubit`을 사용해 `AuthenticationState`를 관리했다면, 로그는 다음과 같이 보일 것입니다: 이는 사용자가 로그아웃되었다는 사실을 알려주지만, 시간이 지남에 따라 상태가 어떻게 변하는지 디버깅하고 이해하는데 무엇이 중요한지 설명하지 않습니다. #### 고급 Event Transformations `Bloc`이 `Cubit`보다 뛰어난 또 다른 영역은 `buffer`, `debounceTime`, `throttle` 등과 같은 반응형 연산자를 활용해야 하는 경우입니다. `Bloc`에는 들어오는 event의 흐름을 제어하고 변환할 수 있는 event sink가 있습니다. 예를 들어, 실시간 검색을 구축하는 경우 속도 제한을 피하고 백엔드의 비용/부하를 줄이기 위해 백엔드에 대한 요청을 debouncing하고 싶을 것입니다. `Bloc`을 사용하면 `Bloc`이 수신 event를 처리하는 방식을 변경하는 커스텀 `EventTransformer`를 제공할 수 있습니다. 위의 코드를 사용하면 추가 코드를 거의 추가하지 않고도 수신 event를 쉽게 debounce할 수 있습니다. :::tip EventTransformer에 대한 자세한 내용은 [`package:bloc_concurrency`](https://pub.dev/packages/bloc_concurrency)를 확인하세요. ::: 어떤 것을 사용해야 할지 잘 모르겠다면 `Cubit`으로 시작하고 나중에 필요에 따라 `Bloc`으로 리펙토링하거나 스케일업할 수 있습니다. ================================================ FILE: docs/src/content/docs/ko/faqs.mdx ================================================ --- title: 자주 묻는 질문 description: Bloc 라이브러리와 관련하여 자주 묻는 질문에 대한 답변입니다. --- import StateNotUpdatingGood1Snippet from '~/components/faqs/StateNotUpdatingGood1Snippet.astro'; import StateNotUpdatingGood2Snippet from '~/components/faqs/StateNotUpdatingGood2Snippet.astro'; import StateNotUpdatingGood3Snippet from '~/components/faqs/StateNotUpdatingGood3Snippet.astro'; import StateNotUpdatingBad1Snippet from '~/components/faqs/StateNotUpdatingBad1Snippet.astro'; import StateNotUpdatingBad2Snippet from '~/components/faqs/StateNotUpdatingBad2Snippet.astro'; import StateNotUpdatingBad3Snippet from '~/components/faqs/StateNotUpdatingBad3Snippet.astro'; import EquatableEmitSnippet from '~/components/faqs/EquatableEmitSnippet.astro'; import EquatableBlocTestSnippet from '~/components/faqs/EquatableBlocTestSnippet.astro'; import NoEquatableBlocTestSnippet from '~/components/faqs/NoEquatableBlocTestSnippet.astro'; import SingleStateSnippet from '~/components/faqs/SingleStateSnippet.astro'; import SingleStateUsageSnippet from '~/components/faqs/SingleStateUsageSnippet.astro'; import BlocProviderGood1Snippet from '~/components/faqs/BlocProviderGood1Snippet.astro'; import BlocProviderGood2Snippet from '~/components/faqs/BlocProviderGood2Snippet.astro'; import BlocProviderBad1Snippet from '~/components/faqs/BlocProviderBad1Snippet.astro'; import BlocInternalAddEventSnippet from '~/components/faqs/BlocInternalAddEventSnippet.astro'; import BlocInternalEventSnippet from '~/components/faqs/BlocInternalEventSnippet.astro'; import BlocExternalForEachSnippet from '~/components/faqs/BlocExternalForEachSnippet.astro'; ## State가 업데이트되지 않아요 ❔ **Question**: Bloc에서 state를 emit하는데 UI가 업데이트되지 않아요. 무엇이 문제인가요? 💡 **Answer**: 만약 Equatable을 사용하는 경우 모든 프로퍼티를 props getter에 전달해야 합니다. ✅ **GOOD** ❌ **BAD** 또한, Bloc에서 state의 새 인스턴스를 emit하고 있는지 확인하세요. ✅ **GOOD** ❌ **BAD** :::caution `Equatable` 프로퍼티는 항상 수정하지 말고 복사해야 합니다. `Equatable` 클래스에 `List` 또는 `Map`이 프로퍼티로 있는 경우, 참조가 아닌 프로퍼티 값을 기준으로 동등성이 평가되도록 `List.of` 또는 `Map.of`을 각각 사용해야 합니다. ::: ## 언제 Equatable를 사용해야 하나요 ❔**Question**: Equatable은 언제 사용해야 하나요? 💡**Answer**: 위의 시나리오에서 `StateA`가 `Equatable`을 extends 한다면 하나의 state 변경만 발생합니다 (두 번째 emit은 무시됩니다). 일반적으로 코드를 최적화하여 리빌드 횟수를 줄이려면 `Equatable`을 사용해야 합니다. 동일한 state가 연속적으로 여러 Transition을 촉발하려면 `Equatable`을 사용하면 안 됩니다. 또한, `Equatable`을 사용하면 `Matchers`나 `Predicates`를 사용하는 것보다 Bloc state의 특정 인스턴스를 예상할 수 있으므로 Bloc을 훨씬 쉽게 테스트할 수 있습니다. `Equatable`이 없다면 위의 테스트는 실패할 것이고, 다음과 같이 다시 작성해야 합니다: ## 에러 처리 ❔ **Question**: 이전 데이터를 계속 표시하면서 에러를 처리하려면 어떻게 해야 하나요? 💡 **Answer**: 이는 Bloc의 state가 어떻게 모델링되었는지에 따라 크게 달라집니다. 에러가 발생하더라도 데이터를 계속 유지해야 하는 경우에는 단일 state 클래스를 사용하는 것이 좋습니다. 이렇게 하면 위젯이 `데이터` 및 `에러` 프로퍼티에 동시에 접근할 수 있으며, Bloc은 `state.copyWith`을 사용하여 에러가 발생한 경우에도 이전 데이터를 유지할 수 있습니다. ## Bloc vs. Redux ❔ **Question**: Bloc과 Redux의 차이점은 무엇인가요? 💡 **Answer**: BLoC 은 다음 규칙에 의해 정의되는 디자인 패턴입니다: 1. BLoC의 입력과 출력은 간단한 Streams과 Sinks 입니다. 2. 종속성은 주입이 가능하고 플렛폼에 구애받지 않아야 합니다. 3. 플랫폼별 분기은 허용되지 않습니다. 4. 위의 규칙을 따르는 한 원하는 대로 구현할 수 있습니다. UI 가이드라인은 다음과 같습니다: 1. "충분히 복잡한" 각 컴포넌트에는 해당하는 BLoC이 있습니다. 2. 컴포넌트는 입력을 "있는 그대로" 보내야 합니다. 3. 컴포넌트는 가능한 "있는 그대로"에 가까운 출력을 표시해야 합니다. 4. 모든 분기는 간단한 BLoC boolean 출력을 기반으로 해야 합니다. Bloc 라이브러리는 BLoC 디자인 패턴을 구현하며 개발자 경험을 단순화하기 위해 RxDart를 추상화하는 것을 목표로 합니다. Redux의 세 원칙은 다음과 같습니다: 1. 신뢰할 수 있는 단일 소스 2. State는 읽기 전용 3. 순수 함수로 Change가 이루어짐 Bloc 라이브러리는 bloc state가 여러 bloc에 분산되어 있기 때문에 첫 번째 원칙을 위반합니다. 또한 bloc에는 미들웨어라는 개념이 없으며 bloc은 비동기 상태 변경을 매우 쉽게 할 수 있도록 설계되어 단일 event에 대해 여러 state를 emit할 수 있습니다. ## Bloc vs. Provider ❔ **Question**: Bloc과 Provider의 차이점은 무엇인가요? 💡 **Answer**: `provider`는 종속성 주입을 위해 설계되었습니다 (`InheritedWidget`을 래핑합니다). 여전히 state를 관리하는 방법을 알아내야 합니다 (`ChangeNotifier`, `Bloc`, `Mobx` 등을 통해). Bloc 라이브러리는 내부적으로 `provider`를 사용하여 위젯 트리 전체에서 bloc을 쉽게 제공하고 접근할 수 있도록 합니다. ## BlocProvider.of()가 Bloc을 못 찾아요 ❔ **Question**: `BlocProvider.of(context)`을 사용할 때 bloc을 찾을 수 없어요. 어떻게 고치면 될까요? 💡 **Answer**: Bloc이 제공한 context와 동일한 context에서는 bloc에 접근할 수 없으므로, 하위 `BuildContext` 내에서 `BlocProvider.of()`가 호출되는지 확인해야 합니다. ✅ **GOOD** ❌ **BAD** ## 프로젝트 구조 ❔ **Question**: 프로젝트를 어떻게 구조화하는게 좋을까요? 💡 **Answer**: 이 질문에 대한 정답은 없지만, 몇 가지 권장되는 참고 자료는 다음과 같습니다: - [I/O Photobooth](https://github.com/flutter/photobooth) - [I/O Pinball](https://github.com/flutter/pinball) - [Flutter News Toolkit](https://github.com/flutter/news_toolkit) 가장 중요한 것은 **일관성**있고 **의도적인** 프로젝트 구조를 갖는 것입니다. ## Bloc 내에서 Event 추가하기 ❔ **Question**: Bloc 내에서 event를 추가해도 괜찮은가요? 💡 **Answer**: 대부분의 경우, event는 외부에서 추가해야 하지만 일부 경우에는 event를 내부적으로 추가하는 것이 합리적일 수 있습니다. 내부 event가 사용되는 가장 일반적인 상황은 Repository의 실시간 업데이트에 대한 응답으로 state의 변경이 발생해야 하는 경우입니다. 이러한 상황에서 Repository는 버튼 탭과 같은 외부 event 대신 state 변경에 대한 자극이 됩니다. 다음 예시에서 `MyBloc`의 state는 `UserRepository`의 `Stream`를 통해 노출되는 현재 사용자에 따라 달라집니다. `MyBloc`은 현재 사용자의 변경 사항을 수신하고, 사용자가 사용자 stream에서 방출될 때 마다 내부 `_UserChanged` event를 추가합니다. 내부 event를 추가함으로써 event에 대한 커스텀 `transformer`를 지정하여 여러 `_UserChanged` event가 처리되는 방식을 결정할 수도 있습니다. 기본적으로 event는 동시에 처리됩니다. 내부 event는 private로 정의되는 것을 강력히 권장합니다. 이는 특정 event가 bloc 자체 내에서만 사용한다는 것을 명시적으로 알리는 방법이며, 외부 컴포넌트가 event에 대해 아는 것을 방지합니다. 또한 외부 `Started` event를 정의하고 `emit.forEach` API를 사용하여 실시간 사용자 업데이트에 대한 반응을 처리할 수 있습니다. 위 접근 방식의 장점은 다음과 같습니다: - 내부 `_UserChanged` event가 필요하지 않습니다. - `StreamSubscription`을 수동으로 관리할 필요가 없습니다. - Bloc이 사용자 업데이트 steram을 구독하는 시기를 완전히 제어할 수 있습니다. 위 접근 방식의 단점은 다음과 같습니다: - 구독을 쉽게 `pause` 하거나 `resume`할 수 없습니다. - 외부적으로 추가해야 하는 공개 `Started` event를 노출해야 합니다. - 사용자 업데이트에 반응하는 방식을 조정하기 위해 커스텀 `transformer`를 사용할 수 없습니다. ## Public 메서드 노출 ❔ **Question**: Bloc 및 Cubit 인스턴스에 public 메서드를 노출해도 괜찮을까요? 💡 **Answer** Cubit을 생성할 때 state 변경을 촉발할 목적으로만 public 메서드를 노출하는 것이 좋습니다. 결과적으로, 일반적인 cubit 인스턴스의 모든 public 메서드는 `void` 또는 `Future`를 반환해야 합니다. Bloc을 생성할 때 커스텀 public 메서드를 노출하지 않고, 대신 `add`를 호출하여 event를 bloc에 알리는 것이 좋습니다. ================================================ FILE: docs/src/content/docs/ko/flutter-bloc-concepts.mdx ================================================ --- title: Flutter Bloc 핵심 컨셉 description: package:flutter_bloc의 핵심 개념에 대한 개요입니다. sidebar: order: 2 --- import BlocBuilderSnippet from '~/components/concepts/flutter-bloc/BlocBuilderSnippet.astro'; import BlocBuilderExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocBuilderExplicitBlocSnippet.astro'; import BlocBuilderConditionSnippet from '~/components/concepts/flutter-bloc/BlocBuilderConditionSnippet.astro'; import BlocSelectorSnippet from '~/components/concepts/flutter-bloc/BlocSelectorSnippet.astro'; import BlocProviderSnippet from '~/components/concepts/flutter-bloc/BlocProviderSnippet.astro'; import BlocProviderEagerSnippet from '~/components/concepts/flutter-bloc/BlocProviderEagerSnippet.astro'; import BlocProviderValueSnippet from '~/components/concepts/flutter-bloc/BlocProviderValueSnippet.astro'; import BlocProviderLookupSnippet from '~/components/concepts/flutter-bloc/BlocProviderLookupSnippet.astro'; import NestedBlocProviderSnippet from '~/components/concepts/flutter-bloc/NestedBlocProviderSnippet.astro'; import MultiBlocProviderSnippet from '~/components/concepts/flutter-bloc/MultiBlocProviderSnippet.astro'; import BlocListenerSnippet from '~/components/concepts/flutter-bloc/BlocListenerSnippet.astro'; import BlocListenerExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocListenerExplicitBlocSnippet.astro'; import BlocListenerConditionSnippet from '~/components/concepts/flutter-bloc/BlocListenerConditionSnippet.astro'; import NestedBlocListenerSnippet from '~/components/concepts/flutter-bloc/NestedBlocListenerSnippet.astro'; import MultiBlocListenerSnippet from '~/components/concepts/flutter-bloc/MultiBlocListenerSnippet.astro'; import BlocConsumerSnippet from '~/components/concepts/flutter-bloc/BlocConsumerSnippet.astro'; import BlocConsumerConditionSnippet from '~/components/concepts/flutter-bloc/BlocConsumerConditionSnippet.astro'; import RepositoryProviderSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderSnippet.astro'; import RepositoryProviderLookupSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderLookupSnippet.astro'; import NestedRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/NestedRepositoryProviderSnippet.astro'; import MultiRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/MultiRepositoryProviderSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/flutter-bloc/CounterBlocSnippet.astro'; import CounterMainSnippet from '~/components/concepts/flutter-bloc/CounterMainSnippet.astro'; import CounterPageSnippet from '~/components/concepts/flutter-bloc/CounterPageSnippet.astro'; import WeatherRepositorySnippet from '~/components/concepts/flutter-bloc/WeatherRepositorySnippet.astro'; import WeatherMainSnippet from '~/components/concepts/flutter-bloc/WeatherMainSnippet.astro'; import WeatherAppSnippet from '~/components/concepts/flutter-bloc/WeatherAppSnippet.astro'; import WeatherPageSnippet from '~/components/concepts/flutter-bloc/WeatherPageSnippet.astro'; :::note [`package:flutter_bloc`](https://pub.dev/packages/flutter_bloc)을 사용하기 전에 다음 섹션을 주의 깊게 읽어주세요. ::: :::note `flutter_bloc` 패키지에서 export된 모든 위젯은 `Cubit` 및 `Bloc` 인스턴스와 모두 통합됩니다. ::: ## Bloc Widgets ### BlocBuilder **BlocBuilder**는 `Bloc` 및 `builder` 함수 기능이 필요한 Flutter 위젯입니다. `BlocBuilder`는 새로운 state에 대한 응답으로 위젯 빌드를 처리합니다. `BlocBuilder`는 `StreamBuilder`와 매우 유사하지만 필요한 보일러플레이트 코드의 양을 줄이기 위해 더 간단한 API를 갖고 있습니다. `builder` 함수는 잠재적으로 여러 번 호출될 수 있으며 state에 응답하여 위젯을 반환하는 [순수 함수](https://en.wikipedia.org/wiki/Pure_function)여야 합니다. Navigation, Dialog 표시 등과 같은 state 변경에 대한 응답으로 무엇이든 간에 "수행"하려면 `BlocListener`를 참조하세요. 만약 `bloc` 파라미터가 생략되면 `BlocBuilder`는 `BlocProvider`와 현재 `BuildContext`를 사용하여 자동으로 bloc을 조회합니다. 단일 위젯으로 범위가 지정되고, 상위 `BlocProvider` 및 현재 `BuildContext`를 통해 접근할 수 없는 bloc을 제공하려는 경우에만 bloc 파라미터를 지정하세요. `builder` 함수가 호출되는 시점을 세밀하게 제어하기 위해 선택적 `buildWhen` 파라미터가 제공됩니다. `buildWhen`은 이전 bloc state와 현재 bloc state를 가져온 후 boolean을 반환합니다. `buildWhen`이 true를 반환하면 `builder`가 `state`와 함께 호출되고 위젯이 다시 빌드됩니다. `buildWhen`이 false를 반환하면 `builder`는 `state`와 함께 호출되지 않으며 리빌드는 일어나지 않습니다. ### BlocSelector **BlocSelector**는 `BlocBuilder`와 유사하지만 개발자가 현재 bloc state에 따라 새 값을 선택하여 업데이트를 필터링할 수 있는 Flutter 위젯입니다. 선택한 값이 변경되지 않으면 불필요한 빌드가 방지됩니다. `BlocSelector`가 `builder`를 다시 호출해야 하는지 여부를 정확하게 결정하려면 선택한 값을 변경할 수 없어야(immutable) 합니다. 만약 `bloc` 파라미터가 생략되면 `BlocBuilder`는 `BlocProvider`와 현재 `BuildContext`를 사용하여 자동으로 bloc을 조회합니다. ### BlocProvider **BlocProvider**는 `BlocProvider.of(context)`를 통해 child에게 bloc을 제공하는 Flutter 위젯입니다. 이는 bloc의 단일 인스턴스가 하위 트리 내의 여러 위젯에 제공될 수 있도록 종속성 주입(DI) 위젯으로 사용됩니다. 대부분의 경우, `BlocProvider`를 사용하여 나머지 하위 트리에서 사용할 수 있는 새 bloc을 생성해야 합니다. 이 경우 `BlocProvider`가 bloc 생성을 담당하기 때문에 자동으로 bloc을 close하는 것도 처리합니다. 기본적으로, `BlocProvider`는 bloc을 lazy하게 생성합니다. 즉, `BlocProvider.of(context)`을 통해 bloc을 조회할 때 `create`가 실행된다는 의미입니다. 이 동작을 무시하고 `create`가 즉시 실행되도록 하려면 `lazy`를 `false`로 설정하면 됩니다. 어떤 경우에는 `BlocProvider`를 사용하여 위젯 트리의 새 부분에 기존 bloc을 제공할 수 잇습니다. 이는 기존 bloc을 새 route에서 사용할 수 있도록 해야 할 때 가장 일반적으로 사용됩니다. 이 경우 `BlocProvider`는 bloc을 생성하지 않았으므로 자동으로 bloc을 close하지 않습니다. 그런 다음 `ChildA` 또는 `ScreenA`애서 다음을 사용하여 `BlocA`를 찾을 수 있습니다: ### MultiBlocProvider **MultiBlocProvider**는 여러 `BlocProvider` 위젯을 하나로 병합하는 Flutter 위젯입니다. `MultiBlocProvider`는 가독성을 향상시키고 여러 `BlocProvider`를 중첩할 필요성을 제거합니다. `MultiBlocProvider`를 사용하면 다음과 같던 코드를: 다음과 같이 변경할 수 있습니다: ### BlocListener **BlocListener**는 필수 `BlocWidgetListener`와 선택적 `Bloc` 파라미터를 사용하고 bloc의 state 변경에 대한 응답으로 `listener`를 호출하는 Flutter 위젯입니다. navigation, `Snackbar` 표시, `Dialog` 표시 등과 같이 state 변경당 한 번 발생해야 하는 기능에 사용해야 합니다. `listener`는 `BlocBuilder`의 `builder`와 달리 각 state 변경 (초기 state를 포함하지 **않음**)에 대해 한 번만 호출되며 `void` 함수입니다. 만약 `bloc` 파라미터가 생략되면 `BlocBuilder`는 `BlocProvider`와 현재 `BuildContext`를 사용하여 자동으로 bloc을 조회합니다. `BlocProvider` 및 현재 `BuildContext`를 통해 접근할 수 없는 bloc을 제공하려는 경우에만 bloc 파라미터를 지정하세요. `listner` 함수가 호출되는 시점을 세밀하게 제어하기 위해 선택적 `listenWhen` 파라미터가 제공됩니다. `listenWhen`은 이전 bloc state와 현재 bloc state를 가져온 후 boolean을 반환합니다. `listenWhen`이 true를 반환하면 `listener`는 `state`와 함께 호출됩니다. `listenWhen`이 false를 반환하면 `listener`는 `state`와 함께 호출되지 않습니다. ### MultiBlocListener **MultiBlocListener**는 여러 `BlocListener` 위젯을 하나로 병합하는 Flutter 위젯입니다. `MultiBlocListener`는 가독성을 향상시키고 여러 `BlocListener`를 중첩할 필요성을 제거합니다. `MultiBlocListener`를 사용하면 다음과 같던 코드를: 다음과 같이 변경할 수 있습니다: ### BlocConsumer **BlocConsumer**는 새로운 state에 반응하기 위해 `builder`와 `listener`를 노출합니다. `BlocConsumer`는 중첩된 `BlocListener` 및 `BlocBuilder`와 유사하지만, 필요한 보일러플레이트 코드의 양을 줄입니다. `BlocConsumer`는 UI를 다시 빌드하고 `bloc`의 상태 변경에 대한 다른 반응을 실행해야 하는 경우에만 사용해야 합니다. `BlocConsumer`는 필수 `BlocWidgetBuilder` 및 `BlocWidgetListener`와 선택적인 `bloc`, `BlocBuilderCondition`, `BlocListenerCondition` 파라미터를 사용합니다. 만약 `bloc` 파라미터가 생략되면 `BlocBuilder`는 `BlocProvider`와 현재 `BuildContext`를 사용하여 자동으로 bloc을 조회합니다. 선택적 파라미터인 `listenWhen` 및 `buildWhen`을 구현하면 `listener` 및 `builder`가 호출되는 시점을 더욱 세밀하게 제어할 수 있습니다. `listenWhen` 및 `buildWhen`은 각 `bloc` `state` 변경 시 호출됩니다. 이들은 각각 이전 `state`와 현재 `state`를 취하고 `builder` 및/또는 `listener` 함수가 호출되는지 여부를 결정하는 `bool`을 반환해야 합니다. `BlocConsumer`가 초기화되면 이전 `state`는 `bloc`의 `state`로 초기화됩니다. `listenWhen` 및 `buildWhen`은 선택사항이며 구현되지 않은 경우 기본값은 `true` 입니다. ### RepositoryProvider **RepositoryProvider**는 `RepositoryProvider.of(context)`를 통해 child에게 repository을 제공하는 Flutter 위젯입니다. 이는 repository의 단일 인스턴스가 하위 트리 내의 여러 위젯에 제공될 수 있도록 종속성 주입(DI) 위젯으로 사용됩니다. `BlocProvider`는 bloc을 제공하는 데 사용해야 하는 반면, `RepositoryProvider`는 repository를 제공하는 데에만 사용해야 합니다. 그런 다음 `ChildA`에서 다음을 사용하여 `Repository` 인스턴스를 찾을 수 있습니다: ### MultiRepositoryProvider **MultiRepositoryProvider**는 여러 `RepositoryProvider` 위젯을 하나로 병합하는 Flutter 위젯입니다. `MultiRepositoryProvider`는 가독성을 향상시키고 여러 `RepositoryProvider`를 중첩할 필요성을 제거합니다. `MultiRepositoryProvider`를 사용하면 다음과 같던 코드를: 다음과 같이 변경할 수 있습니다: ## BlocProvider 사용법 `BlocProvider`를 사용하여 `CounterPage`에 `CounterBloc`을 제공하고 `BlocBuilder`를 사용하여 state 변경에 대한 반응을 살펴보겠습니다. 이 시점에서 우리는 Presentation 레이어를 Business Logic 레이어에서 성공적으로 분리했습니다. `CounterPage` 위젯은 사용자가 버튼을 탭할 때 어떤 일이 발생하는지 전혀 모릅니다. 위젯은 단순히 사용자가 증가 또는 감소 버튼을 눌렀음을 `CounterBloc`에 알려줄 뿐 입니다. ## RepositoryProvider 사용법 [`flutter_weather`][flutter_weather_link] 예시의 맥락에서 `RepositoryProvider`를 사용하는 방법을 살펴보겠습니다. 앱이 `WeatherRepository`에 명시적으로 종속되어 있으므로 생성자를 통해 인스턴스를 주입합니다. 이를 통해 빌드 Flavor나 환경에 따라 `WeatherRepository`의 다양한 인스턴스를 주입할 수 있습니다. 우리 앱에는 하나의 Repository만 있으므로 `RepositoryProvider.value`를 통해 이를 위젯 트리에 삽입합니다. Repository가 두 개 이상인 경우 `MultiRepositoryProvider`를 사용하여 하위 트리에 여러 repository 인스턴스를 제공할 수 있습니다. 대부분의 경우, Root 앱 위젯은 `RepositoryProvider`를 통해 하위 트리에 하나 이상의 repository를 노출합니다. 이제 bloc을 인스턴스화 할 때, `context.read`를 통해 repository의 인스턴스에 접근하고 생성자를 통해 repository를 bloc에 주입할 수 있습니다. [flutter_weather_link]: https://github.com/felangel/bloc/blob/master/examples/flutter_weather ## Extension Methods Dart 2.7에 도입된 [Extension methods](https://dart.dev/guides/language/extension-methods)는 기존 라이브러리에 기능을 추가하는 방법입니다. 이번 섹션에서는 `package:flutter_bloc`에 포함된 확장 메서드와 이를 사용하는 방법을 살펴보겠습니다. `flutter_bloc`은 [`InheritedWidget`](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html)의 사용을 단순화하는 [package:provider](https://pub.dev/packages/provider)에 대한 종속성이 있습니다. 내부적으로, `package:flutter_bloc`은 `package:provider`를 사용하여 `BlocProvider`, `MultiBlocProvider`, `RepositoryProvider` 그리고 `MultiRepositoryProvider` 위젯을 구현합니다. `package:flutter_bloc`은 `package:provider`의 확장인 `ReadContext`, `WatchContext` 그리고 `SelectContext`를 export 합니다. :::note [`package:provider`](https://pub.dev/packages/provider)에 대해 자세히 알아보세요. ::: ### context.read `context.read()`는 `T`타입에 가장 가까운 상위 인스턴스를 조회하며 기능적으로 `BlocProvider.of(context)`와 동일합니다. `context.read`는 `onPressed` 콜백 내에 event를 추가하기 위해 bloc 인스턴스를 검색하는 데 가장 일반적으로 사용됩니다. :::note `context.read()`는 `T`를 listen하지 않습니다. 제공된 `T` 타입의 `Object`가 변경되면 `context.read`는 위젯 리빌드를 촉발하지 않습니다. ::: #### 사용법 ✅ **DO** 콜백에 event를 추가하려면 `context.read`를 사용하세요. ```dart onPressed() { context.read().add(CounterIncrementPressed()), } ``` ❌ **AVOID** `context.read`를 사용하여 `build` 메서드 내에서 상태를 찾지 마세요. ```dart @override Widget build(BuildContext context) { final state = context.read().state; return Text('$state'); } ``` 위의 사용법은 bloc의 state가 변경되어도 `Text` 위젯이 다시 리빌드되지 않기 때문에 오류가 발생하기 쉽습니다. :::caution State 변경에 따라 다시 빌드하려면 `BlocBuilder` 또는 `context.watch` 를 대신 사용하세요. ::: ### context.watch `context.read()`와 마찬가지로, `context.watch()`는 `T`타입에 가장 가까운 상위 인스턴스를 조회하며 인스턴스의 변경 사항도 listen 합니다. 기능적으로는 `BlocProvider.of(context, listening: true)`와 동일합니다. 제공된 `T` 타입의 `Object`가 변경되면 `context.watch`는 위젯 리빌드를 촉발합니다. :::caution `context.watch`는 `StatelessWidget` 또는 `State` 클래스의 `build` 메서드 내에서만 접근할 수 있습니다. ::: #### 사용법 ✅ **DO** 명시적으로 리빌드 scope를 지정하려면 `context.watch` 대신 `BlocBuilder`를 사용하세요. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (context, state) { // Whenever the state changes, only the Text is rebuilt. return Text(state.value); }, ), ), ); } ``` 또는, `Builder`를 사용하여 리빌드 scope를 제한하세요. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Whenever the state changes, only the Text is rebuilt. final state = context.watch().state; return Text(state.value); }, ), ), ); } ``` ✅ **DO** `Builder`와 `context.watch`를 `MultiBlocBuilder`처럼 사용하세요. ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // return a Widget which depends on the state of BlocA, BlocB, and BlocC } ); ``` ❌ **AVOID** `build` 메서드 내의 상위 위젯이 state에 의존하지 않는 경우 `context.watch`를 사용하지 마세요. ```dart @override Widget build(BuildContext context) { // Whenever the state changes, the MaterialApp is rebuilt // even though it is only used in the Text widget. final state = context.watch().state; return MaterialApp( home: Scaffold( body: Text(state.value), ), ); } ``` :::caution `build` 메서드의 root에서 `context.watch`를 사용하면 bloc state가 변경될 때 전체 위젯이 다시 빌드됩니다. ::: ### context.select `context.watch()`와 마찬가지로, `context.select(R function(T value))`는 `T`타입에 가장 가까운 상위 인스턴스를 조회하며 인스턴스의 변경 사항도 listen 합니다. `context.watch`와 달리 `context.select`를 사용하면 state의 작은 부분(일부)에서의 변경 사항을 listen할 수 있습니다. ```dart Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); } ``` 위의 내용은 `ProfileBloc` state의 `name` 프로퍼티가 변경될 때만 위젯을 다시 빌드합니다. #### 사용법 ✅ **DO** 명시적으로 리빌드 scope를 지정하려면 `context.select` 대신 `BlocSelector`를 사용하세요. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector( selector: (state) => state.name, builder: (context, name) { // Whenever the state.name changes, only the Text is rebuilt. return Text(name); }, ), ), ); } ``` 또는, `Builder`를 사용하여 리빌드 scope를 제한하세요. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Whenever state.name changes, only the Text is rebuilt. final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), ); } ``` ❌ **AVOID** `build` 메서드 내의 상위 위젯이 state에 의존하지 않는 경우 `context.select`를 사용하지 마세요. ```dart @override Widget build(BuildContext context) { // Whenever the state.value changes, the MaterialApp is rebuilt // even though it is only used in the Text widget. final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), ); } ``` :::caution `build` 메서드의 root에서 `context.select`를 사용하면 bloc state가 변경될 때 전체 위젯이 다시 빌드됩니다. ::: ================================================ FILE: docs/src/content/docs/ko/getting-started.mdx ================================================ --- title: 시작하기 가이드 description: Bloc으로 구축을 시작하는 데 필요한 모든 것. --- import InstallationTabs from '~/components/getting-started/InstallationTabs.astro'; import ImportTabs from '~/components/getting-started/ImportTabs.astro'; ## 패키지 Bloc 생태계는 아래에 나열된 여러 패키지들로 구성됩니다: | 패키지 | Description | Link | | ------------------------------------------------------------------------------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | AngularDart Components | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | Core Dart APIs | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | Event Transformers | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | Custom Linter | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | Testing APIs | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | Command-line Tools | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | Flutter Widgets | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | Caching/Persistence Support | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | Undo/Redo Support | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | ## 설치 :::note Bloc 사용을 시작하려면 장치에 [Dart SDK](https://dart.dev/get-dart)가 설치되어 있어야 합니다. ::: ## Imports 이제 bloc을 성공적으로 설치했으므로 `main.dart`를 만들고 해당 `bloc` 패키지를 가져올 수 있습니다. ================================================ FILE: docs/src/content/docs/ko/index.mdx ================================================ --- template: splash title: Bloc 상태 관리 라이브러리 description: Bloc 상태 관리 라이브러리에 대한 공식 문서입니다. Dart, Flutter, 그리고 AngularDart를 지원합니다. 예제 및 튜토리얼이 포함되어 있습니다. banner: content: | ✨ Bloc Shop 을 방문해보세요✨ editUrl: false lastUpdated: false hero: title: Bloc v9.2.0 tagline: Dart를 위한 예측 가능한 상태관리 라이브러리. image: alt: Bloc logo file: ~/assets/bloc.svg actions: - text: 시작하기 link: /ko/getting-started/ variant: primary icon: rocket - text: GitHub에서 보기 link: https://github.com/felangel/bloc icon: github variant: secondary --- import { CardGrid } from '@astrojs/starlight/components'; import SponsorsGrid from '~/components/landing/SponsorsGrid.astro'; import Card from '~/components/landing/Card.astro'; import ListCard from '~/components/landing/ListCard.astro'; import SplitCard from '~/components/landing/SplitCard.astro'; import Discord from '~/components/landing/Discord.astro';
```sh # 프로젝트에 bloc을 추가하세요. dart pub add bloc ``` 여기 [시작하기 가이드](/ko/getting-started) 에는 빠르게 Bloc을 사용하는 방법에 대한 단계별 지침이 나와 있습니다. [공식 튜토리얼](/ko/tutorials/flutter-counter) 을 완료하여 모범 사례를 배우고, Bloc을 사용하는 다양한 앱을 빌드하세요. 카운터, 타이머, 무한 스크롤, 날씨, todo 등과 같은 고품질의, 완벽한 테스트를 거친 [예시 앱](https://github.com/felangel/bloc/tree/master/examples)들을 살펴보세요! - [왜 Bloc인가?](/ko/why-bloc) - [핵심 컨셉](/ko/bloc-concepts) - [아키텍쳐](/ko/architecture) - [테스팅](/ko/testing) - [작명 규칙](/ko/naming-conventions) - [FAQs](/ko/faqs) - [VSCode Integration](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) - [IntelliJ Integration](https://plugins.jetbrains.com/plugin/12129-bloc) - [Neovim Integration](https://github.com/wa11breaker/flutter-bloc.nvim) - [Mason CLI Integration](https://github.com/felangel/bloc/blob/master/bricks/README.md) - [Custom Templates](https://brickhub.dev/search?q=bloc) - [Developer Tools](https://github.com/felangel/bloc/blob/master/packages/bloc_tools/README.md) ================================================ FILE: docs/src/content/docs/ko/modeling-state.mdx ================================================ --- title: Modeling State description: package:bloc을 사용할 때 상태를 모델링하는 다양한 방법에 대한 개요입니다. --- import ConcreteClassAndStatusEnumSnippet from '~/components/modeling-state/ConcreteClassAndStatusEnumSnippet.astro'; import SealedClassAndSubclassesSnippet from '~/components/modeling-state/SealedClassAndSubclassesSnippet.astro'; 애플리케이션의 상태를 구조화할 때는 여러 가지 다른 접근 방식이 있습니다. 각각은 고유한 장점과 단점이 있습니다. 이 섹션에서는 여러 접근 방식, 장단점, 그리고 각각을 언제 사용해야 하는지 살펴보겠습니다. 다음 접근 방식들은 단순히 권장사항이며 완전히 선택사항입니다. 원하는 접근 방식을 자유롭게 사용하세요. 일부 예제/문서가 주로 간단함/간결함을 위해 이러한 접근 방식을 따르지 않을 수 있음을 알 수 있을 것입니다. :::tip 다음 코드 스니펫들은 상태 구조에 중점을 둡니다. 실제로는 다음도 원할 수 있습니다: - [`package:equatable`](https://pub.dev/packages/equatable)의 `Equatable` 확장 - [`package:data_class`](https://pub.dev/packages/data_class)의 `@Data()`로 클래스 주석 처리 - [`package:meta`](https://pub.dev/packages/meta)의 **@immutable**로 클래스 주석 처리 - `copyWith` 메서드 구현 - 생성자에 `const` 키워드 사용 ::: ## Concrete Class and Status Enum 이 접근 방식은 모든 상태에 대한 **single concreate class**와 다양한 상태를 나타내는 `enum`으로 구성됩니다. 속성들은 nullable이며 현재 상태에 따라 처리됩니다. 이 접근 방식은 엄격하게 배타적이지 않고/또는 많은 공유 속성을 포함하는 상태에 가장 적합합니다. #### 장점 - **간단함**: 단일 클래스와 상태 enum을 관리하기 쉽고 모든 속성에 쉽게 접근할 수 있습니다. - **간결함**: 일반적으로 다른 접근 방식에 비해 더 적은 코드 라인이 필요합니다. #### 단점 - **타입 안전하지 않음**: 속성에 접근하기 전에 `상태`를 확인해야 합니다. `emit`으로 잘못된 상태를 발생시킬 가능성이 있어 버그로 이어질 수 있습니다. 특정 상태의 속성들은 nullable이므로 처리하기 번거로울 수 있고 강제 언래핑이나 null 검사를 수행해야 합니다. 이러한 단점 중 일부는 단위 테스트와 특수화된 명명된 생성자를 작성하여 완화할 수 있습니다. - **비대함**: 시간이 지남에 따라 많은 속성으로 비대해질 수 있는 단일 상태로 이어집니다. #### 결론 이 접근 방식은 간단한 상태나 요구사항이 배타적이지 않은 상태를 호출할 때(예: 오류가 발생했을 때 스낵바를 표시하면서 마지막 성공한 상태의 이전 데이터를 계속 표시) 가장 잘 작동합니다. 이 접근 방식은 타입 안전성을 희생하여 유연성과 간결성을 제공합니다. ## Sealed Class and Subclasses 이 접근 방식은 공유 속성을 포함하는 **sealed class**와 분리된 상태에 대한 여러 하위 클래스로 구성됩니다. 이 접근 방식은 분리되고 배타적인 상태에 이상적입니다. #### 장점 - **타입 안전함**: 코드는 컴파일 타임에 안전하며 잘못된 속성에 실수로 접근할 수 없습니다. 각 하위 클래스는 자체 속성을 포함하므로 어떤 속성이 어떤 상태에 속하는지 명확합니다. - **명시적**: 공유 속성을 상태별 속성과 분리합니다. - **완전함**: 완전한 검사를 위한 `switch` 문 사용은 각 상태가 명시적으로 처리되도록 보장합니다. - [완전성 검사](https://dart.dev/language/branches#exhaustiveness-checking)를 원하지 않거나 나중에 API를 깨뜨리지 않고 하위 타입을 추가할 수 있게 하려면 [final](https://dart.dev/language/class-modifiers#final) 수정자를 사용하세요. - 자세한 내용은 [봉인 클래스 문서](https://dart.dev/language/class-modifiers#sealed)를 참조하세요. #### 단점 - **장황함**: 더 많은 코드가 필요합니다(상태당 하나의 기본 클래스와 하위 클래스). 또한 하위 클래스 간 공유 속성에 대한 중복 코드가 필요할 수 있습니다. - **복잡성**: 새 속성을 추가하려면 각 하위 클래스와 기본 클래스를 업데이트해야 하므로 번거로울 수 있고 상태의 복잡성이 증가할 수 있습니다. 또한 속성에 접근하기 위해 불필요한/과도한 타입 검사가 필요할 수 있습니다. #### 결론 이 접근 방식은 고유한 속성을 가진 잘 정의되고 배타적인 상태에 가장 잘 작동합니다. 이 접근 방식은 간결함과 단순성을 희생하여 타입 안전성과 완전한 검사를 제공하며 안전성을 강조합니다. ================================================ FILE: docs/src/content/docs/ko/naming-conventions.mdx ================================================ --- title: 작명 규칙 description: Bloc 사용 시 권장되는 작명 규칙 개요입니다. --- import EventExamplesGood1 from '~/components/naming-conventions/EventExamplesGood1Snippet.astro'; import EventExamplesBad1 from '~/components/naming-conventions/EventExamplesBad1Snippet.astro'; import StateExamplesGood1Snippet from '~/components/naming-conventions/StateExamplesGood1Snippet.astro'; import SingleStateExamplesGood1Snippet from '~/components/naming-conventions/SingleStateExamplesGood1Snippet.astro'; import StateExamplesBad1Snippet from '~/components/naming-conventions/StateExamplesBad1Snippet.astro'; 다음의 작명 규칙은 단순히 권장 사항일 뿐이며 완진한 선택 사항입니다. 원하는 작명 규칙을 자유롭게 사용하세요. 일부 예제/문서는 주로 단순성/간결성을 위해 작명 규칙을 따르지 않을 수 있습니다. 이러한 규칙은 개발자가 여러 명인 대규모 프로젝트에 강력히 권장됩니다. ## Event 규칙 이벤트는 bloc의 관점에서 이미 발생한 일이므로 **과거형**으로 이름을 지정해야 합니다. ### 해부 `BlocSubject` + `명사 (선택)` + `동사 (event)` 초기 로드 event는 다음의 규칙을 따라야 합니다: `BlocSubject` + `Started` :::note 기본이 되는 클래스 이름은 다음과 같아야 합니다: `BlocSubject` + `Event`. ::: ### 예시 ✅ **Good** ❌ **Bad** ## State 규칙 State는 특정 시점의 스냅샷일 뿐이므로 state는 명사여야 합니다. State를 나타내는 두 가지 일반적인 방법은 하위 클래스를 사용하거나 단일 클래스를 사용하는 것입니다. ### 해부 #### 하위 클래스 `BlocSubject` + `동사 (동작)` + `State` State를 여러 하위 클래스로 표현하는 경우 `State`는 다음 중 하나여야 합니다: `Initial` | `Success` | `Failure` | `InProgress` :::note 초기 state는 다음의 규칙을 따라야 합니다: `BlocSubject` + `Initial`. ::: #### 단일 클래스 `BlocSubject` + `State` State를 단일 기본 클래스로 표시할 때 `BlocSubject` + `Status`라는 enum을 사용하여 다음과 같은 상태를 표시해야 합니다: `initial` | `success` | `failure` | `loading`. :::note 기본이 되는 클래스 이름은 항상 다음과 같아야 합니다: `BlocSubject` + `State`. ::: ### 예시 ✅ **Good** ##### 하위 클래스 ##### 단일 클래스 ❌ **Bad** ================================================ FILE: docs/src/content/docs/ko/testing.mdx ================================================ --- title: 테스팅 description: Bloc에 대한 테스트 작성 방법에 대한 기본 사항입니다. --- import CounterBlocSnippet from '~/components/testing/CounterBlocSnippet.astro'; import AddDevDependenciesSnippet from '~/components/testing/AddDevDependenciesSnippet.astro'; import CounterBlocTestImportsSnippet from '~/components/testing/CounterBlocTestImportsSnippet.astro'; import CounterBlocTestMainSnippet from '~/components/testing/CounterBlocTestMainSnippet.astro'; import CounterBlocTestSetupSnippet from '~/components/testing/CounterBlocTestSetupSnippet.astro'; import CounterBlocTestInitialStateSnippet from '~/components/testing/CounterBlocTestInitialStateSnippet.astro'; import CounterBlocTestBlocTestSnippet from '~/components/testing/CounterBlocTestBlocTestSnippet.astro'; Bloc은 테스트하기가 매우 쉽도록 설계되었습니다. 이 섹션에서는 bloc을 unit test 하는 방법을 살펴보겠습니다. 단순화를 위해, [Core Concepts](/ko/bloc-concepts)에서 만든 `CounterBloc`에 대한 테스트를 작성해보겠습니다. 복습을 위해 요약하면, `CounterBloc` 구현은 다음과 같습니다: ## 준비 테스트 작성을 시작하기 전에 종속성에 테스트 프레임워크를 추가해야 합니다. 프로젝트에 [test](https://pub.dev/packages/test) 및 [bloc_test](https://pub.dev/packages/bloc_test)를 추가합니다. ## 테스팅 `CounterBloc` 테스트용 파일인 `counter_bloc_test.dart`를 만들고 테스트 패키지를 가져오는 것부터 시작해 보겠습니다. 다음으로, `main`와 테스트 그룹을 만들어야 합니다. :::note 그룹은 개별 테스트를 구성하고 모든 개별 테스트에서 공통 `setUp` 및 `tearDown`을 공유할 수 있는 context를 생성하기 위한 것입니다. ::: 모든 테스트에서 사용될 `CounterBloc`의 인스턴스를 생성하는 것부터 시작해 보겠습니다. 이제 개별 테스트 작성을 시작하겠습니다. :::note `dart test` 명령어를 사용하여 모든 테스트를 실행할 수 있습니다. ::: 이 시점에서 우리는 첫 번째 통과된 테스트를 받아야 합니다! 이제 [bloc_test](https://pub.dev/packages/bloc_test) 패키지를 사용하여 좀 더 복잡한 테스트를 작성해 보겠습니다. 이제 우리는 테스트를 실행하고 모든 테스트가 통과되는지 확인할 수 있어야 합니다. 이게 전부입니다. 테스트는 쉬워야 하며 코드를 변경하고 리펙토링할 때 자신감을 가질 수 있어야 합니다. 완전히 테스트된 애플리케이션의 예제는 [Weather App](https://github.com/felangel/bloc/tree/master/examples/flutter_weather)을 참조 바랍니다. ================================================ FILE: docs/src/content/docs/ko/tutorials/flutter-counter.mdx ================================================ --- title: Flutter Counter description: Bloc을 사용한 Flutter 카운터 앱 만들기 튜토리얼입니다. sidebar: order: 1 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-counter/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) 이 튜토리얼에서는 Bloc 라이브러리를 사용해서 Flutter로 카운터 앱을 만들어 봅니다. ![demo](~/assets/tutorials/flutter-counter.gif) ## 핵심 주제 - [BlocObserver](/ko/bloc-concepts#blocobserver)로 상태 변화 관찰하기. - [BlocProvider](/ko/flutter-bloc-concepts#blocprovider)로 하위 위젯에 bloc 제공하기. - [BlocBuilder](/ko/flutter-bloc-concepts#blocbuilder)로 상태 변화에 따라 위젯 다시 그리기. - Bloc 대신 Cubit 사용하기. [차이점이 뭔가요?](/ko/bloc-concepts#cubit-vs-bloc) - [context.read](/ko/flutter-bloc-concepts#contextread)로 이벤트 추가하기. ## 프로젝트 설정 먼저 새로운 Flutter 프로젝트를 생성합니다. 그 다음 `pubspec.yaml` 파일을 아래 내용으로 교체합니다. 의존성을 설치합니다. ## 프로젝트 구조 ``` ├── lib │ ├── app.dart │ ├── counter │ │ ├── counter.dart │ │ ├── cubit │ │ │ └── counter_cubit.dart │ │ └── view │ │ ├── counter_page.dart │ │ ├── counter_view.dart │ │ └── view.dart │ ├── counter_observer.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` 이 프로젝트는 기능 기반 디렉토리 구조를 사용합니다. 이런 구조를 사용하면 각 기능이 독립적으로 구성되어 프로젝트 확장이 쉬워집니다. 이 예제에서는 카운터 기능 하나만 있지만, 실제 복잡한 앱에서는 수백 개의 기능이 있을 수 있습니다. ## BlocObserver 먼저 `BlocObserver`를 만들어 봅니다. 이걸 사용하면 앱 전체의 상태 변화를 관찰할 수 있습니다. `lib/counter_observer.dart` 파일을 생성합니다: 여기서는 `onChange`만 override해서 모든 상태 변화를 확인합니다. :::note `onChange`는 `Bloc`과 `Cubit` 인스턴스 모두에서 동일하게 동작합니다. ::: ## main.dart 다음으로 `lib/main.dart` 파일을 아래 내용으로 교체합니다: 방금 만든 `CounterObserver`를 초기화하고, `runApp`에 다음에 만들 `CounterApp` 위젯을 전달합니다. ## Counter App `lib/app.dart` 파일을 생성합니다: `CounterApp`은 `MaterialApp`이고 `home`으로 `CounterPage`를 지정합니다. :::note `CounterApp`이 `MaterialApp`을 상속하는 이유는 `CounterApp` 자체가 `MaterialApp`이기 때문입니다. 대부분의 경우 `StatelessWidget`이나 `StatefulWidget`을 만들고 `build` 메서드에서 위젯을 조합하지만, 여기서는 조합할 위젯이 없어서 `MaterialApp`을 직접 상속하는 게 더 간단합니다. ::: 다음은 `CounterPage`를 살펴봅니다. ## Counter Page `lib/counter/view/counter_page.dart` 파일을 생성합니다: `CounterPage` 위젯은 `CounterCubit`(다음에 살펴볼 예정)을 생성하고 `CounterView`에 제공하는 역할을 합니다. :::note `Cubit`의 생성과 사용을 분리하는 게 중요합니다. 이렇게 하면 코드 테스트와 재사용이 훨씬 쉬워집니다. ::: ## Counter Cubit `lib/counter/cubit/counter_cubit.dart` 파일을 생성합니다: `CounterCubit` 클래스는 두 가지 메서드를 제공합니다: - `increment`: 현재 상태에 1을 더합니다. - `decrement`: 현재 상태에서 1을 뺍니다. `CounterCubit`이 관리하는 상태 타입은 `int`이고 초기 상태는 `0`입니다. :::tip [VSCode Extension](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc)이나 [IntelliJ Plugin](https://plugins.jetbrains.com/plugin/12129-bloc)을 사용하면 Cubit을 자동으로 생성할 수 있습니다. ::: 다음으로 상태를 사용하고 `CounterCubit`과 상호작용하는 `CounterView`를 살펴봅니다. ## Counter View `lib/counter/view/counter_view.dart` 파일을 생성합니다: `CounterView`는 현재 카운트 값을 표시하고, 카운터를 증가/감소시키는 두 개의 FloatingActionButton을 렌더링합니다. `BlocBuilder`로 `Text` 위젯을 감싸서 `CounterCubit` 상태가 바뀔 때마다 텍스트를 업데이트합니다. 또한 `context.read()`을 사용해서 가장 가까운 `CounterCubit` 인스턴스를 찾습니다. :::note `BlocBuilder`로 `Text` 위젯만 감싼 이유는 `CounterCubit` 상태 변화에 반응해서 다시 그려야 하는 위젯이 `Text`뿐이기 때문입니다. 상태 변화에 다시 그릴 필요가 없는 위젯은 불필요하게 감싸지 마세요. ::: ## Barrel 파일 `lib/counter/view/view.dart` 파일을 생성합니다: `view.dart`를 추가해서 counter view의 공개 부분을 export합니다. `lib/counter/counter.dart` 파일을 생성합니다: `counter.dart`를 추가해서 counter 기능의 모든 공개 부분을 export합니다. 끝입니다! 프레젠테이션 레이어와 비즈니스 로직 레이어를 분리했습니다. `CounterView`는 사용자가 버튼을 누를 때 무슨 일이 일어나는지 모릅니다. 단지 `CounterCubit`에 알릴 뿐입니다. 마찬가지로 `CounterCubit`은 상태(카운터 값)가 어떻게 표시되는지 모릅니다. 메서드 호출에 대한 응답으로 새로운 상태를 emit할 뿐입니다. `flutter run`으로 앱을 실행하면 기기나 시뮬레이터/에뮬레이터에서 확인할 수 있습니다. 이 예제의 전체 소스 코드(단위 테스트와 위젯 테스트 포함)는 [여기](https://github.com/felangel/Bloc/tree/master/examples/flutter_counter)에서 확인할 수 있습니다. ================================================ FILE: docs/src/content/docs/ko/tutorials/flutter-firebase-login.mdx ================================================ --- title: Flutter Firebase Login description: Bloc과 Firebase를 사용한 Flutter 로그인 플로우 튜토리얼입니다. sidebar: order: 7 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-firebase-login/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) 이 튜토리얼에서는 Bloc 라이브러리를 사용해서 Flutter에서 Firebase 로그인 플로우를 만들어 봅니다. ![demo](~/assets/tutorials/flutter-firebase-login.gif) ## 핵심 주제 - [BlocProvider](/ko/flutter-bloc-concepts#blocprovider)로 하위 위젯에 bloc 제공하기. - Bloc 대신 Cubit 사용하기. [차이점이 뭔가요?](/ko/bloc-concepts#cubit-vs-bloc) - [context.read](/ko/flutter-bloc-concepts#contextread)로 이벤트 추가하기. - [Equatable](/ko/faqs#언제-equatable를-사용해야-하나요)로 불필요한 rebuild 방지하기. - [RepositoryProvider](/ko/flutter-bloc-concepts#repositoryprovider)로 하위 위젯에 repository 제공하기. - [BlocListener](/ko/flutter-bloc-concepts#bloclistener)로 상태 변화에 반응하기. - [context.read](/ko/flutter-bloc-concepts#contextselect)로 이벤트 추가하기. ## 프로젝트 설정 새로운 Flutter 프로젝트를 생성합니다. [로그인 튜토리얼](/ko/tutorials/flutter-login)처럼 내부 패키지를 만들어서 앱 아키텍처를 더 잘 계층화하고, 명확한 경계를 유지하며, 재사용성과 테스트 용이성을 높입니다. 이 경우 [firebase_auth](https://pub.dev/packages/firebase_auth)와 [google_sign_in](https://pub.dev/packages/google_sign_in) 패키지가 데이터 레이어가 되므로, 두 API 클라이언트의 데이터를 조합하는 `AuthenticationRepository`만 만들면 됩니다. ## Authentication Repository `AuthenticationRepository`는 사용자 인증과 사용자 정보 가져오기의 내부 구현 세부사항을 추상화합니다. 지금은 Firebase와 통합하지만, 나중에 내부 구현을 바꿔도 앱에는 영향이 없습니다. ### 설정 프로젝트 루트에 `packages/authentication_repository`와 `pubspec.yaml`을 생성합니다. `authentication_repository` 디렉토리에서 다음을 실행해서 의존성을 설치합니다: 대부분의 패키지처럼 `authentication_repository`는 `packages/authentication_repository/lib/authentication_repository.dart`를 통해 API를 노출합니다. :::note `authentication_repository` 패키지는 `AuthenticationRepository`와 모델들을 노출합니다. ::: 다음으로 모델을 살펴봅니다. ### User `User` 모델은 인증 도메인에서 사용자를 설명합니다. 이 예제에서 사용자는 `email`, `id`, `name`, `photo`로 구성됩니다. :::note 도메인에서 사용자가 어떻게 생겼는지 정의하는 건 전적으로 여러분에게 달렸습니다. ::: [user.dart](https://raw.githubusercontent.com/felangel/bloc/master/examples/flutter_firebase_login/packages/authentication_repository/lib/src/models/user.dart ':include') :::note `User` 클래스는 [equatable](https://pub.dev/packages/equatable)을 상속해서 다른 `User` 인스턴스를 값으로 비교할 수 있습니다. ::: :::tip `null` User를 처리하지 않고 항상 구체적인 `User` 객체로 작업할 수 있도록 `static` empty `User`를 정의하는 게 유용합니다. ::: ### Repository `AuthenticationRepository`는 사용자 인증과 사용자 정보 가져오기의 구현을 추상화합니다. `AuthenticationRepository`는 `User`가 바뀔 때 알림을 받을 수 있도록 구독할 수 있는 `Stream`를 노출합니다. 또한 `signUp`, `logInWithGoogle`, `logInWithEmailAndPassword`, `logOut` 메서드를 노출합니다. :::note `AuthenticationRepository`는 데이터 레이어에서 발생할 수 있는 저수준 에러도 처리하고, 도메인에 맞는 깔끔하고 간단한 에러 집합을 노출합니다. ::: `AuthenticationRepository`는 여기까지입니다. 다음으로 만든 Flutter 프로젝트에 어떻게 통합하는지 살펴봅니다. ## Firebase 설정 앱을 Firebase에 연결하고 [google_sign_in](https://pub.dev/packages/google_sign_in)을 활성화하려면 [firebase_auth 사용 설명서](https://pub.dev/packages/firebase_auth#usage)를 따라야 합니다. :::caution Android에서 `google-services.json`을, iOS에서 `GoogleService-Info.plist`와 `Info.plist`를 업데이트하는 걸 잊지 마세요. 그렇지 않으면 앱이 크래시합니다. ::: ## 프로젝트 의존성 프로젝트 루트에 생성된 `pubspec.yaml`을 다음으로 교체합니다: 모든 로컬 에셋을 위한 assets 디렉토리를 지정하고 있습니다. 프로젝트 루트에 `assets` 디렉토리를 만들고 [bloc 로고](https://github.com/felangel/bloc/blob/master/examples/flutter_firebase_login/assets/bloc_logo_small.png) 에셋을 추가합니다(나중에 사용). 그 다음 모든 의존성을 설치합니다: :::note 명확한 분리를 유지하면서 빠르게 반복할 수 있도록 path를 통해 `authentication_repository` 패키지에 의존하고 있습니다. ::: ## main.dart `main.dart` 파일을 다음으로 교체합니다: 앱의 전역 설정을 하고 `App` 인스턴스와 함께 `runApp`을 호출합니다. :::note `App`에 `AuthenticationRepository`의 단일 인스턴스를 주입하고 있고, 이건 명시적인 생성자 의존성입니다. ::: ## App [로그인 튜토리얼](/ko/tutorials/flutter-login)처럼 `app.dart`는 `RepositoryProvider`를 통해 앱에 `AuthenticationRepository` 인스턴스를 제공하고, `AuthenticationBloc` 인스턴스도 생성해서 제공합니다. 그런 다음 `AppView`가 `AuthenticationBloc`을 사용하고 `AuthenticationState`에 따라 현재 라우트를 업데이트합니다. ## App Bloc `AppBloc`은 앱의 전역 상태를 관리합니다. `AuthenticationRepository`에 의존하고 현재 사용자의 변경에 대한 응답으로 새 상태를 emit하기 위해 `user` Stream을 구독합니다. ### State `AppState`는 `AppStatus`와 `User`로 구성됩니다. 기본 생성자는 선택적 `User`를 받아서 적절한 인증 상태와 함께 private 생성자로 리다이렉트합니다. ### Event `AppEvent`는 두 개의 하위 클래스가 있습니다: - `AppUserSubscriptionRequested`: bloc에게 user 스트림을 구독하라고 알립니다. - `AppLogoutPressed`: bloc에게 사용자 로그아웃 액션을 알립니다. ### Bloc 생성자 본문에서 `AppEvent` 하위 클래스가 해당 이벤트 핸들러에 매핑됩니다. `_onUserSubscriptionRequested` 이벤트 핸들러에서 `AppBloc`은 `emit.onEach`를 사용해서 `AuthenticationRepository`의 user 스트림을 구독하고 각 `User`에 대한 응답으로 상태를 emit합니다. `emit.onEach`는 내부적으로 스트림 구독을 생성하고 `AppBloc`이나 user 스트림이 닫히면 취소를 처리합니다. user 스트림이 에러를 emit하면 `addError`가 에러와 스택 트레이스를 listen하고 있는 `BlocObserver`에 전달합니다. :::caution `onError`가 생략되면 user 스트림의 모든 에러는 처리되지 않은 것으로 간주되어 `onEach`에서 throw됩니다. 결과적으로 user 스트림에 대한 구독이 취소됩니다. ::: :::tip [`BlocObserver`](/ko/bloc-concepts/#blocobserver-1)는 특히 분석과 크래시 리포팅에서 Bloc 이벤트, 에러, 상태 변화를 로깅하는 데 좋습니다. ::: ## Models `Email`과 `Password` 입력 모델은 유효성 검사 로직을 캡슐화하는 데 유용하고 `LoginForm`과 `SignUpForm`(튜토리얼 후반부) 모두에서 사용됩니다. 두 입력 모델은 [formz](https://pub.dev/packages/formz) 패키지를 사용해서 만들어지고, `String` 같은 원시 타입 대신 유효성 검사된 객체로 작업할 수 있게 해줍니다. ### Email ### Password ## Login Page `LoginPage`는 `LoginCubit` 인스턴스를 생성하고 `LoginForm`에 제공합니다. :::tip bloc/cubit의 생성과 사용을 분리하는 게 매우 중요합니다. 이렇게 하면 mock 인스턴스를 쉽게 주입하고 뷰를 독립적으로 테스트할 수 있습니다. ::: ## Login Cubit `LoginCubit`은 폼의 `LoginState`를 관리합니다. `logInWithCredentials`, `logInWithGoogle` API를 노출하고 email/password가 업데이트될 때 알림을 받습니다. ### State `LoginState`는 `Email`, `Password`, `FormzStatus`로 구성됩니다. `Email`과 `Password` 모델은 [formz](https://pub.dev/packages/formz) 패키지의 `FormzInput`을 상속합니다. ### Cubit `LoginCubit`은 credentials나 google 로그인을 통해 사용자를 로그인시키기 위해 `AuthenticationRepository`에 의존합니다. :::note `LoginState`가 상당히 단순하고 지역적이기 때문에 여기서는 `Bloc` 대신 `Cubit`을 사용했습니다. 이벤트 없이도 한 상태에서 다른 상태로의 변화를 보는 것만으로 무슨 일이 일어났는지 꽤 잘 알 수 있고, 코드가 훨씬 단순하고 간결합니다. ::: ## Login Form `LoginForm`은 `LoginState`에 대한 응답으로 폼을 렌더링하고 사용자 상호작용에 대한 응답으로 `LoginCubit`의 메서드를 호출합니다. `LoginForm`은 사용자가 새 계정을 만들 수 있는 `SignUpPage`로 이동하는 "Create Account" 버튼도 렌더링합니다. ## Sign Up Page `SignUp` 구조는 `Login` 구조를 미러링하고 `SignUpPage`, `SignUpView`, `SignUpCubit`으로 구성됩니다. `SignUpPage`는 `SignUpCubit` 인스턴스를 생성하고 `SignUpForm`에 제공합니다(`LoginPage`와 정확히 같습니다). :::note `LoginCubit`처럼 `SignUpCubit`도 새 사용자 계정을 만들기 위해 `AuthenticationRepository`에 의존합니다. ::: ## Sign Up Cubit `SignUpCubit`은 `SignUpForm`의 상태를 관리하고 새 사용자 계정을 만들기 위해 `AuthenticationRepository`와 통신합니다. ### State `SignUpState`는 유효성 검사 로직이 같기 때문에 같은 `Email`과 `Password` 폼 입력 모델을 재사용합니다. ### Cubit `SignUpCubit`은 `LoginCubit`과 매우 비슷하지만 login 대신 폼을 submit하는 API를 노출한다는 주요 차이점이 있습니다. ## Sign Up Form `SignUpForm`은 `SignUpState`에 대한 응답으로 폼을 렌더링하고 사용자 상호작용에 대한 응답으로 `SignUpCubit`의 메서드를 호출합니다. ## Home Page 사용자가 로그인하거나 가입에 성공하면 `user` 스트림이 업데이트되고, 이는 `AuthenticationBloc`에서 상태 변화를 트리거하여 `AppView`가 네비게이션 스택에 `HomePage` 라우트를 push하게 됩니다. `HomePage`에서 사용자는 프로필 정보를 보고 `AppBar`의 exit 아이콘을 탭해서 로그아웃할 수 있습니다. :::note `home` 기능 내에서 `view` 디렉토리와 함께 해당 기능에 특화된 재사용 가능한 컴포넌트를 위한 `widgets` 디렉토리가 만들어졌습니다. 이 경우 간단한 `Avatar` 위젯이 export되어 `HomePage` 내에서 사용됩니다. ::: :::note logout `IconButton`이 탭되면 `AuthenticationBloc`에 `AuthenticationLogoutRequested` 이벤트가 추가되어 사용자를 로그아웃시키고 `LoginPage`로 다시 이동합니다. ::: 이 시점에서 Firebase를 사용한 꽤 괜찮은 로그인 구현이 있고, Bloc 라이브러리를 사용해서 프레젠테이션 레이어와 비즈니스 로직 레이어를 분리했습니다. 이 예제의 전체 소스 코드는 [여기](https://github.com/felangel/bloc/tree/master/examples/flutter_firebase_login)에서 확인할 수 있습니다. ================================================ FILE: docs/src/content/docs/ko/tutorials/flutter-infinite-list.mdx ================================================ --- title: Flutter Infinite List description: Bloc을 사용한 Flutter 무한 스크롤 리스트 만들기 튜토리얼입니다. sidebar: order: 3 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-infinite-list/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/flutter-infinite-list/FlutterPubGetSnippet.astro'; import PostsJsonSnippet from '~/components/tutorials/flutter-infinite-list/PostsJsonSnippet.astro'; import PostBlocInitialStateSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocInitialStateSnippet.astro'; import PostBlocOnPostFetchedSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocOnPostFetchedSnippet.astro'; import PostBlocTransformerSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocTransformerSnippet.astro'; ![intermediate](https://img.shields.io/badge/level-intermediate-orange.svg) 이 튜토리얼에서는 Flutter와 bloc 라이브러리를 사용해서 사용자가 스크롤할 때 네트워크에서 데이터를 가져와 로드하는 앱을 만들어 봅니다. ![demo](~/assets/tutorials/flutter-infinite-list.gif) ## 핵심 주제 - [BlocObserver](/ko/bloc-concepts#blocobserver)로 상태 변화 관찰하기. - [BlocProvider](/ko/flutter-bloc-concepts#blocprovider)로 하위 위젯에 bloc 제공하기. - [BlocBuilder](/ko/flutter-bloc-concepts#blocbuilder)로 상태 변화에 따라 위젯 다시 그리기. - [context.read](/ko/flutter-bloc-concepts#contextread)로 이벤트 추가하기. - [Equatable](/ko/faqs#언제-equatable를-사용해야-하나요)로 불필요한 rebuild 방지하기. - Rx로 `transformEvents` 메서드 사용하기. ## 프로젝트 설정 새로운 Flutter 프로젝트를 생성합니다. pubspec.yaml 파일을 아래 내용으로 교체합니다. 의존성을 설치합니다. ## 프로젝트 구조 ``` ├── lib | ├── posts │ │ ├── bloc │ │ │ └── post_bloc.dart | | | └── post_event.dart | | | └── post_state.dart | | └── models | | | └── models.dart* | | | └── post.dart │ │ └── view │ │ | ├── posts_page.dart │ │ | └── posts_list.dart | | | └── view.dart* | | └── widgets | | | └── bottom_loader.dart | | | └── post_list_item.dart | | | └── widgets.dart* │ │ ├── posts.dart* │ ├── app.dart │ ├── simple_bloc_observer.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` 이 프로젝트는 기능 기반 디렉토리 구조를 사용합니다. 이런 구조를 사용하면 각 기능이 독립적으로 구성되어 프로젝트 확장이 쉬워집니다. 이 예제에서는 post 기능 하나만 있고, 별표(\*)로 표시된 barrel 파일과 함께 각각의 폴더로 나뉘어 있습니다. ## REST API 이 데모 앱에서는 데이터 소스로 [jsonplaceholder](http://jsonplaceholder.typicode.com)를 사용합니다. :::note jsonplaceholder는 가짜 데이터를 제공하는 온라인 REST API입니다. 프로토타입을 만들 때 매우 유용합니다. ::: 브라우저에서 새 탭을 열고 https://jsonplaceholder.typicode.com/posts?_start=0&_limit=2 에 접속해서 API가 뭘 반환하는지 확인해 보세요. :::note URL에서 start와 limit를 GET 요청의 쿼리 파라미터로 지정했습니다. ::: 이제 데이터가 어떻게 생겼는지 알았으니 모델을 만들어 봅니다. ## 데이터 모델 `post.dart`를 생성하고 Post 객체 모델을 만듭니다. `Post`는 `id`, `title`, `body`를 가진 간단한 클래스입니다. :::note `Post`를 비교할 수 있도록 [`Equatable`](https://pub.dev/packages/equatable)을 상속합니다. 이게 없으면 두 `Post` 객체의 차이를 알 수 있도록 equality와 hashCode를 직접 override해야 합니다. 자세한 내용은 [패키지 문서](https://pub.dev/packages/equatable)를 참고하세요. ::: 이제 `Post` 객체 모델이 있으니 비즈니스 로직 컴포넌트(bloc)를 만들어 봅니다. ## Post Events 구현에 들어가기 전에 `PostBloc`이 뭘 할 건지 정의해야 합니다. 간단히 말하면 사용자 입력(스크롤)에 반응해서 프레젠테이션 레이어가 표시할 수 있도록 더 많은 post를 가져옵니다. `Event`부터 만들어 봅니다. `PostBloc`은 하나의 이벤트에만 반응합니다. `PostFetched`는 프레젠테이션 레이어가 더 많은 Post를 표시해야 할 때마다 추가됩니다. `PostFetched` 이벤트는 `PostEvent`의 한 종류이므로 `bloc/post_event.dart`를 생성하고 이벤트를 구현합니다. 정리하면 `PostBloc`은 `PostEvent`를 받아서 `PostState`로 변환합니다. 모든 `PostEvent`(PostFetched)를 정의했으니 이제 `PostState`를 정의합니다. ## Post States 프레젠테이션 레이어가 제대로 렌더링하려면 다음 정보가 필요합니다: - `PostInitial`: 초기 배치의 post가 로드되는 동안 로딩 인디케이터를 렌더링해야 함을 알립니다. - `PostSuccess`: 렌더링할 콘텐츠가 있음을 알립니다. - `posts`: 표시할 `List` - `hasReachedMax`: 최대 post 수에 도달했는지 여부 - `PostFailure`: post를 가져오는 중 에러가 발생했음을 알립니다. 이제 `bloc/post_state.dart`를 생성하고 구현합니다. :::note `PostSuccess` 인스턴스를 복사하고 0개 이상의 속성을 편리하게 업데이트할 수 있도록 `copyWith`를 구현했습니다(나중에 유용하게 쓰입니다). ::: 이제 `Event`와 `State`가 구현됐으니 `PostBloc`을 만들 수 있습니다. ## Post Bloc 단순함을 위해 `PostBloc`은 `http client`에 직접 의존합니다. 하지만 프로덕션 앱에서는 api client를 주입하고 repository 패턴([문서](/ko/architecture))을 사용하는 것을 권장합니다. `post_bloc.dart`를 생성하고 빈 `PostBloc`을 만듭니다. :::note 클래스 선언만 봐도 PostBloc이 PostEvent를 입력으로 받아서 PostState를 출력한다는 걸 알 수 있습니다. ::: 다음으로 들어오는 `PostFetched` 이벤트를 처리할 이벤트 핸들러를 등록해야 합니다. `PostFetched` 이벤트에 대한 응답으로 API에서 post를 가져오는 `_fetchPosts`를 호출합니다. `PostBloc`은 이벤트 핸들러에 제공된 `Emitter`를 통해 새로운 상태를 `emit`합니다. 자세한 내용은 [핵심 개념](/ko/bloc-concepts#streams)을 참고하세요. 이제 `PostEvent`가 추가될 때마다 `PostFetched` 이벤트이고 가져올 post가 더 있으면 `PostBloc`이 다음 20개의 post를 가져옵니다. 최대 post 수(100)를 초과해서 가져오려고 하면 API가 빈 배열을 반환합니다. 빈 배열을 받으면 bloc은 `hasReachedMax`를 true로 설정한 currentState를 `emit`합니다. post를 가져올 수 없으면 `PostStatus.failure`를 emit합니다. post를 가져올 수 있으면 `PostStatus.success`와 전체 post 목록을 emit합니다. API에 불필요하게 스팸을 보내는 것을 방지하기 위해 `PostFetched` 이벤트를 `throttle`하는 최적화를 할 수 있습니다. `_onFetched` 이벤트 핸들러를 등록할 때 `transform` 파라미터를 사용하면 됩니다. :::note `on`에 `transformer`를 전달하면 이벤트가 처리되는 방식을 커스터마이즈할 수 있습니다. ::: :::note `throttle` API를 사용하려면 [`package:stream_transform`](https://pub.dev/packages/stream_transform)을 import해야 합니다. ::: 완성된 `PostBloc`은 다음과 같습니다: 비즈니스 로직 구현이 끝났으니 이제 프레젠테이션 레이어만 구현하면 됩니다. ## 프레젠테이션 레이어 `main.dart`에서 main 함수를 구현하고 `runApp`을 호출해서 루트 위젯을 렌더링합니다. 여기서 transition과 에러를 로깅하기 위한 bloc observer도 포함할 수 있습니다. 프로젝트의 루트인 `App` 위젯에서 home을 `PostsPage`로 설정합니다. `PostsPage` 위젯에서 `BlocProvider`를 사용해서 `PostBloc` 인스턴스를 생성하고 하위 트리에 제공합니다. 또한 앱이 로드될 때 초기 배치의 Post를 요청하도록 `PostFetched` 이벤트를 추가합니다. 다음으로 post를 표시하고 `PostBloc`에 연결할 `PostsList` 뷰를 구현합니다. :::note `PostsList`는 `ScrollController`를 유지해야 하므로 `StatefulWidget`입니다. `initState`에서 스크롤 이벤트에 반응할 수 있도록 `ScrollController`에 listener를 추가합니다. 또한 `context.read()`을 통해 `PostBloc` 인스턴스에 접근합니다. ::: build 메서드는 `BlocBuilder`를 반환합니다. `BlocBuilder`는 [flutter_bloc 패키지](https://pub.dev/packages/flutter_bloc)의 Flutter 위젯으로, 새로운 bloc 상태에 대한 응답으로 위젯을 빌드합니다. `PostBloc` 상태가 바뀔 때마다 새로운 `PostState`와 함께 builder 함수가 호출됩니다. :::caution StatefulWidget이 dispose될 때 `ScrollController`를 dispose해서 정리하는 것을 잊지 마세요. ::: 사용자가 스크롤할 때마다 페이지를 얼마나 스크롤했는지 계산하고, 거리가 `maxScrollExtent`의 90% 이상이면 더 많은 post를 로드하기 위해 `PostFetched` 이벤트를 추가합니다. 다음으로 더 많은 post를 로드 중임을 사용자에게 알려주는 `BottomLoader` 위젯을 구현합니다. 마지막으로 개별 `Post`를 렌더링할 `PostListItem`을 구현합니다. 이 시점에서 앱을 실행하면 모든 게 동작합니다. 하지만 한 가지 더 할 수 있는 게 있습니다. bloc 라이브러리를 사용하면 모든 `Transition`에 한 곳에서 접근할 수 있다는 추가적인 이점이 있습니다. 한 상태에서 다른 상태로의 변경을 `Transition`이라고 합니다. :::note `Transition`은 현재 상태, 이벤트, 다음 상태로 구성됩니다. ::: 이 앱에는 bloc이 하나뿐이지만, 큰 앱에서는 앱 상태의 다른 부분을 관리하는 많은 bloc이 있는 게 일반적입니다. 모든 `Transition`에 대해 뭔가를 하고 싶다면 간단히 `BlocObserver`를 만들면 됩니다. :::note `BlocObserver`를 상속하고 `onTransition` 메서드를 override하기만 하면 됩니다. ::: 이제 Bloc `Transition`이 발생할 때마다 콘솔에 transition이 출력되는 걸 볼 수 있습니다. :::note 실제로 다른 `BlocObserver`를 만들 수 있고, 모든 상태 변경이 기록되기 때문에 모든 사용자 상호작용과 상태 변경을 한 곳에서 쉽게 추적할 수 있습니다! ::: 이게 전부입니다! [bloc](https://pub.dev/packages/bloc)과 [flutter_bloc](https://pub.dev/packages/flutter_bloc) 패키지를 사용해서 Flutter에서 무한 리스트를 성공적으로 구현했고, 프레젠테이션 레이어와 비즈니스 로직을 분리했습니다. `PostsPage`는 `Post`가 어디서 오는지, 어떻게 가져오는지 모릅니다. 반대로 `PostBloc`은 `State`가 어떻게 렌더링되는지 모르고, 단순히 이벤트를 상태로 변환합니다. 이 예제의 전체 소스 코드는 [여기](https://github.com/felangel/Bloc/tree/master/examples/flutter_infinite_list)에서 확인할 수 있습니다. ================================================ FILE: docs/src/content/docs/ko/tutorials/flutter-login.mdx ================================================ --- title: Flutter Login description: Bloc을 사용한 Flutter 로그인 플로우 튜토리얼입니다. sidebar: order: 4 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-login/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![intermediate](https://img.shields.io/badge/level-intermediate-orange.svg) 이 튜토리얼에서는 Bloc 라이브러리를 사용해서 Flutter에서 로그인 플로우를 만들어 봅니다. ![demo](~/assets/tutorials/flutter-login.gif) ## 핵심 주제 - [BlocProvider](/ko/flutter-bloc-concepts#blocprovider)로 하위 위젯에 bloc 제공하기. - [context.read](/ko/flutter-bloc-concepts#contextread)로 이벤트 추가하기. - [Equatable](/ko/faqs#언제-equatable를-사용해야-하나요)로 불필요한 rebuild 방지하기. - [RepositoryProvider](/ko/flutter-bloc-concepts#repositoryprovider)로 하위 위젯에 repository 제공하기. - [BlocListener](/ko/flutter-bloc-concepts#bloclistener)로 상태 변화에 반응하기. - [context.select](/ko/flutter-bloc-concepts#contextselect)로 bloc 상태의 일부에 따라 UI 업데이트하기. ## 프로젝트 설정 새로운 Flutter 프로젝트를 생성합니다. 의존성을 설치합니다. ## Authentication Repository 먼저 인증 도메인을 관리하는 `authentication_repository` 패키지를 만듭니다. 프로젝트 루트에 모든 내부 패키지가 들어갈 `packages/authentication_repository` 디렉토리를 생성합니다. 대략적으로 디렉토리 구조는 다음과 같습니다: ``` ├── android ├── ios ├── lib ├── packages │ └── authentication_repository └── test ``` 다음으로 `authentication_repository` 패키지의 `pubspec.yaml`을 생성합니다: :::note `package:authentication_repository`는 외부 의존성이 없는 순수 Dart 패키지입니다. ::: 다음으로 `AuthenticationRepository` 클래스 자체를 `packages/authentication_repository/lib/src/authentication_repository.dart`에 구현합니다. `AuthenticationRepository`는 사용자가 로그인하거나 로그아웃할 때 앱에 알리는 데 사용할 `AuthenticationStatus` 업데이트 `Stream`을 노출합니다. 또한 단순화를 위해 stub으로 처리된 `logIn`과 `logOut` 메서드가 있지만, `FirebaseAuth`나 다른 인증 프로바이더로 쉽게 확장할 수 있습니다. :::note 내부적으로 `StreamController`를 유지하고 있으므로 컨트롤러가 더 이상 필요하지 않을 때 닫을 수 있도록 `dispose` 메서드를 노출합니다. ::: 마지막으로 public exports를 포함할 `packages/authentication_repository/lib/authentication_repository.dart`를 생성합니다: `AuthenticationRepository`는 여기까지입니다. 다음으로 `UserRepository`를 만듭니다. ## User Repository `AuthenticationRepository`처럼 `packages` 디렉토리 안에 `user_repository` 패키지를 만듭니다. ``` ├── android ├── ios ├── lib ├── packages │ ├── authentication_repository │ └── user_repository └── test ``` 다음으로 `user_repository`의 `pubspec.yaml`을 생성합니다: `user_repository`는 사용자 도메인을 담당하고 현재 사용자와 상호작용하기 위한 API를 노출합니다. 먼저 `packages/user_repository/lib/src/models/user.dart`에 user 모델을 정의합니다: 단순화를 위해 user는 `id` 속성만 가지지만, 실제로는 `firstName`, `lastName`, `avatarUrl` 등의 추가 속성이 있을 수 있습니다. :::note [`package:equatable`](https://pub.dev/packages/equatable)을 사용해서 `User` 객체의 값 비교를 활성화합니다. ::: 다음으로 `packages/user_repository/lib/src/models`에 `models.dart`를 생성해서 단일 import로 여러 모델을 가져올 수 있도록 합니다. 이제 모델이 정의됐으니 `packages/user_repository/lib/src/user_repository.dart`에 `UserRepository` 클래스를 구현합니다. 이 간단한 예제에서 `UserRepository`는 현재 사용자를 가져오는 `getUser` 메서드 하나만 노출합니다. stub으로 처리됐지만 실제로는 백엔드에서 현재 사용자를 쿼리하는 곳입니다. `user_repository` 패키지가 거의 완료됐습니다. 남은 건 public exports를 정의하는 `packages/user_repository/lib`의 `user_repository.dart` 파일을 만드는 것뿐입니다: 이제 `authentication_repository`와 `user_repository` 패키지가 완료됐으니 Flutter 앱에 집중할 수 있습니다. ## 의존성 설치 프로젝트 루트에 생성된 `pubspec.yaml`을 업데이트합니다: 다음을 실행해서 의존성을 설치합니다: ## Authentication Bloc `AuthenticationBloc`은 (`AuthenticationRepository`가 노출하는) 인증 상태 변화에 반응하고 프레젠테이션 레이어에서 반응할 수 있는 상태를 emit합니다. `AuthenticationBloc` 구현은 `lib/authentication` 안에 있습니다. 인증을 앱 레이어의 기능으로 취급하기 때문입니다. ``` ├── lib │ ├── app.dart │ ├── authentication │ │ ├── authentication.dart │ │ └── bloc │ │ ├── authentication_bloc.dart │ │ ├── authentication_event.dart │ │ └── authentication_state.dart │ ├── main.dart ``` :::tip [VSCode Extension](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc)이나 [IntelliJ Plugin](https://plugins.jetbrains.com/plugin/12129-bloc)을 사용하면 bloc을 자동으로 생성할 수 있습니다. ::: ### authentication_event.dart `AuthenticationEvent` 인스턴스는 `AuthenticationBloc`의 입력이 되고, 처리되어 새로운 `AuthenticationState` 인스턴스를 emit하는 데 사용됩니다. 이 앱에서 `AuthenticationBloc`은 두 가지 이벤트에 반응합니다: - `AuthenticationSubscriptionRequested`: bloc에게 `AuthenticationStatus` 스트림을 구독하라고 알리는 초기 이벤트 - `AuthenticationLogoutPressed`: 사용자 로그아웃 액션을 bloc에 알림 다음으로 `AuthenticationState`를 살펴봅니다. ### authentication_state.dart `AuthenticationState` 인스턴스는 `AuthenticationBloc`의 출력이 되고 프레젠테이션 레이어에서 사용됩니다. `AuthenticationState` 클래스는 세 개의 named constructor가 있습니다: - `AuthenticationState.unknown()`: bloc이 현재 사용자가 인증됐는지 아닌지 아직 모르는 기본 상태. - `AuthenticationState.authenticated()`: 사용자가 현재 인증된 상태. - `AuthenticationState.unauthenticated()`: 사용자가 현재 인증되지 않은 상태. `AuthenticationEvent`와 `AuthenticationState` 구현을 봤으니 이제 `AuthenticationBloc`을 살펴봅니다. ### authentication_bloc.dart `AuthenticationBloc`은 사용자를 로그인 페이지에서 시작할지 홈 페이지에서 시작할지 같은 것들을 결정하는 데 사용되는 앱의 인증 상태를 관리합니다. `AuthenticationBloc`은 `AuthenticationRepository`와 `UserRepository` 모두에 의존하고 초기 상태를 `AuthenticationState.unknown()`으로 정의합니다. 생성자 본문에서 `AuthenticationEvent` 하위 클래스가 해당 이벤트 핸들러에 매핑됩니다. `_onSubscriptionRequested` 이벤트 핸들러에서 `AuthenticationBloc`은 `emit.onEach`를 사용해서 `AuthenticationRepository`의 `status` 스트림을 구독하고 각 `AuthenticationStatus`에 대한 응답으로 상태를 emit합니다. `emit.onEach`는 내부적으로 스트림 구독을 생성하고 `AuthenticationBloc`이나 `status` 스트림이 닫히면 취소를 처리합니다. `status` 스트림이 에러를 emit하면 `addError`가 에러와 stackTrace를 listen하고 있는 `BlocObserver`에 전달합니다. :::caution `onError`가 생략되면 `status` 스트림의 모든 에러는 처리되지 않은 것으로 간주되어 `onEach`에서 throw됩니다. 결과적으로 `status` 스트림에 대한 구독이 취소됩니다. ::: :::tip [`BlocObserver`](/ko/bloc-concepts/#blocobserver-1)는 특히 분석과 크래시 리포팅에서 Bloc 이벤트, 에러, 상태 변화를 로깅하는 데 좋습니다. ::: `status` 스트림이 `AuthenticationStatus.unknown`이나 `unauthenticated`를 emit하면 해당 `AuthenticationState`가 emit됩니다. `AuthenticationStatus.authenticated`가 emit되면 `AuthenticationBloc`이 `UserRepository`를 통해 사용자를 쿼리합니다. ## main.dart 기본 `main.dart`를 다음으로 교체합니다: ## App `app.dart`는 전체 앱의 루트 `App` 위젯을 포함합니다. :::note `app.dart`는 `App`과 `AppView` 두 부분으로 나뉩니다. `App`은 `AppView`가 사용할 `AuthenticationBloc`을 생성/제공하는 역할을 합니다. 이런 분리를 통해 나중에 `App`과 `AppView` 위젯 모두 쉽게 테스트할 수 있습니다. ::: :::note `RepositoryProvider`는 `AuthenticationRepository`의 단일 인스턴스를 전체 앱에 제공하는 데 사용되고, 나중에 유용하게 쓰입니다. ::: 기본적으로 `BlocProvider`는 lazy라서 Bloc이 처음 접근될 때까지 `create`를 호출하지 않습니다. `AuthenticationBloc`은 항상 (`AuthenticationSubscriptionRequested` 이벤트를 통해) `AuthenticationStatus` 스트림을 즉시 구독해야 하므로 `lazy: false`를 설정해서 이 동작을 명시적으로 opt out합니다. `AppView`는 `NavigatorState`에 접근하는 데 사용되는 `GlobalKey`를 유지하기 때문에 `StatefulWidget`입니다. 기본적으로 `AppView`는 `SplashPage`(나중에 살펴봄)를 렌더링하고 `BlocListener`를 사용해서 `AuthenticationState` 변화에 따라 다른 페이지로 이동합니다. ## Splash splash 기능은 앱이 시작될 때 사용자가 인증됐는지 판단하는 동안 렌더링될 간단한 뷰만 포함합니다. ``` lib └── splash ├── splash.dart └── view └── splash_page.dart ``` :::tip `SplashPage`는 static `Route`를 노출해서 `Navigator.of(context).push(SplashPage.route())`를 통해 쉽게 이동할 수 있습니다. ::: ## Login login 기능은 `LoginPage`, `LoginForm`, `LoginBloc`을 포함하고 사용자가 앱에 로그인하기 위해 username과 password를 입력할 수 있게 합니다. ``` ├── lib │ ├── login │ │ ├── bloc │ │ │ ├── login_bloc.dart │ │ │ ├── login_event.dart │ │ │ └── login_state.dart │ │ ├── login.dart │ │ ├── models │ │ │ ├── models.dart │ │ │ ├── password.dart │ │ │ └── username.dart │ │ └── view │ │ ├── login_form.dart │ │ ├── login_page.dart │ │ └── view.dart ``` ### Login Models [`package:formz`](https://pub.dev/packages/formz)를 사용해서 `username`과 `password`에 대한 재사용 가능하고 표준적인 모델을 만듭니다. #### Username 단순화를 위해 username이 비어있지 않은지만 검증하지만, 실제로는 특수 문자 사용, 길이 등을 적용할 수 있습니다. #### Password 마찬가지로 password가 비어있지 않은지 간단히 확인합니다. #### Models Barrel 이전처럼 단일 import로 `Username`과 `Password` 모델을 쉽게 가져올 수 있도록 `models.dart` barrel이 있습니다. ### Login Bloc `LoginBloc`은 `LoginForm`의 상태를 관리하고 username과 password 입력 유효성 검사와 폼 상태를 처리합니다. #### login_event.dart 이 앱에는 세 가지 `LoginEvent` 타입이 있습니다: - `LoginUsernameChanged`: username이 수정됐음을 bloc에 알림. - `LoginPasswordChanged`: password가 수정됐음을 bloc에 알림. - `LoginSubmitted`: 폼이 submit됐음을 bloc에 알림. #### login_state.dart `LoginState`는 폼의 상태와 username, password 입력 상태를 포함합니다. :::note `Username`과 `Password` 모델은 `LoginState`의 일부로 사용되고 status도 [package:formz](https://pub.dev/packages/formz)의 일부입니다. ::: #### login_bloc.dart `LoginBloc`은 `LoginForm`의 사용자 상호작용에 반응하고 폼의 유효성 검사와 submit을 처리합니다. `LoginBloc`은 폼이 submit될 때 `logIn`을 호출하므로 `AuthenticationRepository`에 의존합니다. bloc의 초기 상태는 `pure`로, 입력이나 폼이 아직 터치되거나 상호작용되지 않은 상태입니다. `username`이나 `password`가 바뀔 때마다 bloc은 `Username`/`Password` 모델의 dirty variant를 생성하고 `Formz.validate` API를 통해 폼 상태를 업데이트합니다. `LoginSubmitted` 이벤트가 추가되면 폼의 현재 상태가 valid인 경우 bloc이 `logIn`을 호출하고 요청 결과에 따라 상태를 업데이트합니다. 다음으로 `LoginPage`와 `LoginForm`을 살펴봅니다. ### Login Page `LoginPage`는 `Route`를 노출하고 `LoginBloc`을 생성해서 `LoginForm`에 제공하는 역할을 합니다. :::note `context.read()`를 사용해서 `BuildContext`를 통해 `AuthenticationRepository` 인스턴스를 찾습니다. ::: ### Login Form `LoginForm`은 사용자 이벤트를 `LoginBloc`에 알리고 `BlocBuilder`와 `BlocListener`를 사용해서 상태 변화에 반응합니다. `BlocListener`는 로그인 submit이 실패하면 `SnackBar`를 표시하는 데 사용됩니다. 또한 `context.select`를 사용해서 각 위젯이 `LoginState`의 특정 부분에 효율적으로 접근하여 불필요한 rebuild를 방지합니다. `onChanged` 콜백은 username/password 변경을 `LoginBloc`에 알리는 데 사용됩니다. `_LoginButton` 위젯은 폼의 상태가 valid인 경우에만 활성화되고, 폼이 submit되는 동안에는 `CircularProgressIndicator`가 대신 표시됩니다. ## Home 성공적인 `logIn` 요청 시 `AuthenticationBloc`의 상태가 `authenticated`로 바뀌고 사용자는 user의 `id`와 로그아웃 버튼이 표시되는 `HomePage`로 이동합니다. ``` ├── lib │ ├── home │ │ ├── home.dart │ │ └── view │ │ └── home_page.dart ``` ### Home Page `HomePage`는 `context.select((AuthenticationBloc bloc) => bloc.state.user.id)`를 통해 현재 user id에 접근하고 `Text` 위젯을 통해 표시합니다. 또한 logout 버튼이 탭되면 `AuthenticationBloc`에 `AuthenticationLogoutPressed` 이벤트가 추가됩니다. :::note `context.select((AuthenticationBloc bloc) => bloc.state.user.id)`는 user id가 바뀌면 업데이트를 트리거합니다. ::: 이 시점에서 꽤 괜찮은 로그인 구현이 있고, Bloc을 사용해서 프레젠테이션 레이어와 비즈니스 로직 레이어를 분리했습니다. 이 예제의 전체 소스 코드(단위 테스트와 위젯 테스트 포함)는 [여기](https://github.com/felangel/Bloc/tree/master/examples/flutter_login)에서 확인할 수 있습니다. ================================================ FILE: docs/src/content/docs/ko/tutorials/flutter-timer.mdx ================================================ --- title: Flutter Timer description: Bloc을 사용한 Flutter 타이머 앱 만들기 튜토리얼입니다. sidebar: order: 2 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-timer/FlutterCreateSnippet.astro'; import TimerBlocEmptySnippet from '~/components/tutorials/flutter-timer/TimerBlocEmptySnippet.astro'; import TimerBlocInitialStateSnippet from '~/components/tutorials/flutter-timer/TimerBlocInitialStateSnippet.astro'; import TimerBlocTickerSnippet from '~/components/tutorials/flutter-timer/TimerBlocTickerSnippet.astro'; import TimerBlocOnStartedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnStartedSnippet.astro'; import TimerBlocOnTickedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnTickedSnippet.astro'; import TimerBlocOnPausedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnPausedSnippet.astro'; import TimerBlocOnResumedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnResumedSnippet.astro'; import TimerPageSnippet from '~/components/tutorials/flutter-timer/TimerPageSnippet.astro'; import ActionsSnippet from '~/components/tutorials/flutter-timer/ActionsSnippet.astro'; import BackgroundSnippet from '~/components/tutorials/flutter-timer/BackgroundSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) 이 튜토리얼에서는 bloc 라이브러리를 사용해서 타이머 앱을 만들어 봅니다. 완성된 앱은 다음과 같습니다: ![demo](~/assets/tutorials/flutter-timer.gif) ## 핵심 주제 - [BlocObserver](/ko/bloc-concepts#blocobserver)로 상태 변화 관찰하기. - [BlocProvider](/ko/flutter-bloc-concepts#blocprovider)로 하위 위젯에 bloc 제공하기. - [BlocBuilder](/ko/flutter-bloc-concepts#blocbuilder)로 상태 변화에 따라 위젯 다시 그리기. - [Equatable](/ko/faqs#언제-equatable를-사용해야-하나요)로 불필요한 rebuild 방지하기. - Bloc에서 `StreamSubscription` 사용하기. - `buildWhen`으로 불필요한 rebuild 방지하기. ## 프로젝트 설정 새로운 Flutter 프로젝트를 생성합니다: pubspec.yaml 파일을 아래 내용으로 교체합니다: :::note 이 앱에서는 [flutter_bloc](https://pub.dev/packages/flutter_bloc)과 [equatable](https://pub.dev/packages/equatable) 패키지를 사용합니다. ::: `flutter pub get`을 실행해서 의존성을 설치합니다. ## 프로젝트 구조 ``` ├── lib | ├── timer │ │ ├── bloc │ │ │ └── timer_bloc.dart | | | └── timer_event.dart | | | └── timer_state.dart │ │ └── view │ │ | ├── timer_page.dart │ │ ├── timer.dart │ ├── app.dart │ ├── ticker.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` ## Ticker Ticker는 타이머 앱의 데이터 소스입니다. 구독하고 반응할 수 있는 tick 스트림을 제공합니다. `ticker.dart` 파일을 생성합니다. `Ticker` 클래스는 원하는 tick 수(초)를 받아서 매초마다 남은 시간을 emit하는 스트림을 반환하는 tick 함수를 제공합니다. 다음으로 `Ticker`를 사용하는 `TimerBloc`을 만들어야 합니다. ## Timer Bloc ### TimerState 먼저 `TimerBloc`이 가질 수 있는 `TimerState`를 정의합니다. `TimerBloc`의 상태는 다음 중 하나입니다: - `TimerInitial`: 지정된 시간부터 카운트다운을 시작할 준비가 된 상태. - `TimerRunInProgress`: 지정된 시간부터 카운트다운 중인 상태. - `TimerRunPause`: 남은 시간에서 일시 정지된 상태. - `TimerRunComplete`: 남은 시간이 0으로 완료된 상태. 각 상태는 UI와 사용자가 수행할 수 있는 액션에 영향을 줍니다. 예를 들어: - `TimerInitial` 상태면 사용자가 타이머를 시작할 수 있습니다. - `TimerRunInProgress` 상태면 사용자가 타이머를 일시 정지하고 리셋할 수 있으며, 남은 시간을 볼 수 있습니다. - `TimerRunPause` 상태면 사용자가 타이머를 재개하고 리셋할 수 있습니다. - `TimerRunComplete` 상태면 사용자가 타이머를 리셋할 수 있습니다. bloc 파일들을 한곳에 모아두기 위해 bloc 디렉토리에 `bloc/timer_state.dart`를 생성합니다. :::tip [IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc-code-generator)나 [VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) 확장을 사용하면 bloc 파일을 자동으로 생성할 수 있습니다. ::: 모든 `TimerState`는 duration 속성을 가진 추상 클래스 `TimerState`를 상속합니다. `TimerBloc`이 어떤 상태에 있든 남은 시간을 알아야 하기 때문입니다. 또한 `TimerState`는 `Equatable`을 상속해서 동일한 상태가 발생했을 때 불필요한 rebuild를 방지합니다. 다음으로 `TimerBloc`이 처리할 `TimerEvent`를 정의하고 구현합니다. ### TimerEvent `TimerBloc`은 다음 이벤트를 처리해야 합니다: - `TimerStarted`: 타이머를 시작해야 함을 알립니다. - `TimerPaused`: 타이머를 일시 정지해야 함을 알립니다. - `TimerResumed`: 타이머를 재개해야 함을 알립니다. - `TimerReset`: 타이머를 원래 상태로 리셋해야 함을 알립니다. - `_TimerTicked`: tick이 발생했고 그에 따라 상태를 업데이트해야 함을 알립니다. [IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc-code-generator)나 [VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) 확장을 사용하지 않았다면 `bloc/timer_event.dart`를 생성하고 이벤트를 구현합니다. 다음으로 `TimerBloc`을 구현합니다! ### TimerBloc 아직 하지 않았다면 `bloc/timer_bloc.dart`를 생성하고 빈 `TimerBloc`을 만듭니다. 먼저 `TimerBloc`의 초기 상태를 정의해야 합니다. 여기서는 `TimerBloc`이 1분(60초)의 기본 시간으로 `TimerInitial` 상태에서 시작하도록 합니다. 다음으로 `Ticker` 의존성을 정의합니다. `Ticker`에 대한 `StreamSubscription`도 정의합니다. 이건 잠시 후에 다룹니다. 이제 이벤트 핸들러만 구현하면 됩니다. 가독성을 위해 각 이벤트 핸들러를 별도의 헬퍼 함수로 분리합니다. `TimerStarted` 이벤트부터 시작합니다. `TimerBloc`이 `TimerStarted` 이벤트를 받으면 시작 시간과 함께 `TimerRunInProgress` 상태를 push합니다. 이미 열린 `_tickerSubscription`이 있다면 메모리 해제를 위해 취소해야 합니다. 또한 `TimerBloc`이 닫힐 때 `_tickerSubscription`을 취소하도록 `close` 메서드를 override해야 합니다. 마지막으로 `_ticker.tick` 스트림을 listen하고 매 tick마다 남은 시간과 함께 `_TimerTicked` 이벤트를 추가합니다. 다음으로 `_TimerTicked` 이벤트 핸들러를 구현합니다. `_TimerTicked` 이벤트를 받을 때마다 tick의 duration이 0보다 크면 새로운 duration과 함께 `TimerRunInProgress` 상태를 push합니다. 그렇지 않고 tick의 duration이 0이면 타이머가 끝난 것이므로 `TimerRunComplete` 상태를 push합니다. 이제 `TimerPaused` 이벤트 핸들러를 구현합니다. `_onPaused`에서 `TimerBloc`의 `state`가 `TimerRunInProgress`이면 `_tickerSubscription`을 일시 정지하고 현재 타이머 duration과 함께 `TimerRunPause` 상태를 push합니다. 다음으로 타이머를 재개할 수 있도록 `TimerResumed` 이벤트 핸들러를 구현합니다. `TimerResumed` 이벤트 핸들러는 `TimerPaused` 이벤트 핸들러와 매우 비슷합니다. `TimerBloc`의 `state`가 `TimerRunPause`이고 `TimerResumed` 이벤트를 받으면 `_tickerSubscription`을 재개하고 현재 duration과 함께 `TimerRunInProgress` 상태를 push합니다. 마지막으로 `TimerReset` 이벤트 핸들러를 구현합니다. `TimerBloc`이 `TimerReset` 이벤트를 받으면 추가 tick 알림을 받지 않도록 현재 `_tickerSubscription`을 취소하고 원래 duration과 함께 `TimerInitial` 상태를 push합니다. `TimerBloc`은 이게 전부입니다. 이제 타이머 앱의 UI만 구현하면 됩니다. ## 앱 UI ### MyApp `main.dart`의 내용을 삭제하고 다음으로 교체합니다. 다음으로 앱의 루트가 될 'App' 위젯을 `app.dart`에 생성합니다. 다음으로 `Timer` 위젯을 구현합니다. ### Timer `Timer` 위젯(`lib/timer/view/timer_page.dart`)은 남은 시간을 표시하고 사용자가 타이머를 시작, 일시 정지, 리셋할 수 있는 버튼을 제공합니다. 여기서는 `BlocProvider`를 사용해서 `TimerBloc` 인스턴스에 접근합니다. 다음으로 적절한 액션(시작, 일시 정지, 리셋)을 가진 `Actions` 위젯을 구현합니다. ### Barrel `Timer` 섹션의 import를 깔끔하게 정리하기 위해 barrel 파일 `timer/timer.dart`를 생성합니다. ### Actions `Actions` 위젯은 `BlocBuilder`를 사용해서 새로운 `TimerState`가 올 때마다 UI를 rebuild하는 `StatelessWidget`입니다. `Actions`는 `context.read()`을 사용해서 `TimerBloc` 인스턴스에 접근하고 `TimerBloc`의 현재 상태에 따라 다른 `FloatingActionButton`을 반환합니다. 각 `FloatingActionButton`은 `onPressed` 콜백에서 `TimerBloc`에 알리기 위해 이벤트를 추가합니다. `builder` 함수가 호출되는 시점을 세밀하게 제어하고 싶다면 `BlocBuilder`에 선택적으로 `buildWhen`을 제공할 수 있습니다. `buildWhen`은 이전 bloc 상태와 현재 bloc 상태를 받아서 `boolean`을 반환합니다. `buildWhen`이 `true`를 반환하면 `state`와 함께 `builder`가 호출되고 위젯이 rebuild됩니다. `buildWhen`이 `false`를 반환하면 `state`와 함께 `builder`가 호출되지 않고 rebuild도 일어나지 않습니다. 이 경우 매 tick마다 `Actions` 위젯이 rebuild되는 것은 비효율적이므로 원하지 않습니다. 대신 `TimerState`의 `runtimeType`이 바뀔 때만 `Actions`가 rebuild되길 원합니다 (TimerInitial => TimerRunInProgress, TimerRunInProgress => TimerRunPause 등). 결과적으로 매 rebuild마다 위젯에 랜덤 색상을 칠하면 다음과 같이 보입니다: ![BlocBuilder buildWhen demo](https://cdn-images-1.medium.com/max/1600/1*YyjpH1rcZlYWxCX308l_Ew.gif) :::note `Text` 위젯은 매 tick마다 rebuild되지만 `Actions`는 필요할 때만 rebuild됩니다. ::: ### Background 마지막으로 background 위젯을 추가합니다: ### 완성 이게 전부입니다! 이제 필요한 위젯만 효율적으로 rebuild하는 꽤 괜찮은 타이머 앱이 완성됐습니다. 이 예제의 전체 소스 코드는 [여기](https://github.com/felangel/Bloc/tree/master/examples/flutter_timer)에서 확인할 수 있습니다. ================================================ FILE: docs/src/content/docs/ko/tutorials/flutter-todos.mdx ================================================ --- title: Flutter Todos description: Flutter에서 bloc을 사용하여 todos 앱을 구현하는 심층 가이드. sidebar: order: 6 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-todos/FlutterCreateSnippet.astro'; import ActivateVeryGoodCLISnippet from '~/components/tutorials/flutter-todos/ActivateVeryGoodCLISnippet.astro'; import FlutterCreatePackagesSnippet from '~/components/tutorials/flutter-todos/FlutterCreatePackagesSnippet.astro'; import ProjectStructureSnippet from '~/components/tutorials/flutter-todos/ProjectStructureSnippet.astro'; import VeryGoodPackagesGetSnippet from '~/components/tutorials/flutter-todos/VeryGoodPackagesGetSnippet.astro'; import HomePageTreeSnippet from '~/components/tutorials/flutter-todos/HomePageTreeSnippet.astro'; import TodosOverviewPageTreeSnippet from '~/components/tutorials/flutter-todos/TodosOverviewPageTreeSnippet.astro'; import StatsPageTreeSnippet from '~/components/tutorials/flutter-todos/StatsPageTreeSnippet.astro'; import EditTodosPageTreeSnippet from '~/components/tutorials/flutter-todos/EditTodosPageTreeSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) 이번 튜토리얼에서는 Bloc 라이브러리를 사용하여 Flutter에서 todos 앱을 만들어 봅니다. ![demo](~/assets/tutorials/flutter-todos.gif) ## 주요 주제 - [Bloc과 Cubit](/ko/bloc-concepts#cubit-vs-bloc)을 사용한 다양한 기능 상태 관리. - 관심사 분리와 재사용성을 위한 [레이어드 아키텍처](/ko/architecture). - 상태 변화를 관찰하기 위한 [BlocObserver](/ko/bloc-concepts#blocobserver). - [BlocProvider](/ko/flutter-bloc-concepts#blocprovider), 자식에게 bloc을 제공하는 Flutter 위젯. - [BlocBuilder](/ko/flutter-bloc-concepts#blocbuilder), 새로운 상태에 따라 위젯을 빌드하는 Flutter 위젯. - [BlocListener](/ko/flutter-bloc-concepts#bloclistener), 상태 변화에 따라 사이드 이펙트를 수행하는 Flutter 위젯. - [RepositoryProvider](/ko/flutter-bloc-concepts#repositoryprovider), 자식에게 리포지토리를 제공하는 Flutter 위젯. - [Equatable](/ko/faqs#언제-equatable를-사용해야-하나요)을 사용한 불필요한 리빌드 방지. - [MultiBlocListener](/ko/flutter-bloc-concepts#multibloclistener), 여러 BlocListener 사용 시 중첩을 줄이는 Flutter 위젯. ## 설정 [very_good_cli](https://pub.dev/packages/very_good_cli)를 사용하여 새 Flutter 프로젝트를 생성합니다. :::note 다음 명령으로 `very_good_cli`를 설치할 수 있습니다 ::: 다음으로 `very_good_cli`를 사용하여 `todos_api`, `local_storage_todos_api`, `todos_repository` 패키지를 생성합니다: 그런 다음 `pubspec.yaml`의 내용을 다음으로 교체합니다: 마지막으로 모든 의존성을 설치합니다: ## 프로젝트 구조 애플리케이션의 프로젝트 구조는 다음과 같습니다: 프로젝트를 여러 패키지로 분리하여 각 패키지가 명확한 경계와 함께 명시적 의존성을 갖도록 합니다. 이는 [단일 책임 원칙](https://en.wikipedia.org/wiki/Single-responsibility_principle)을 따릅니다. 이렇게 프로젝트를 모듈화하면 다음과 같은 이점이 있습니다: - 여러 프로젝트에서 패키지를 쉽게 재사용 - CI/CD 효율성 향상 (변경된 코드에 대해서만 검사 실행) - 전용 테스트 스위트, 시맨틱 버저닝, 릴리스 주기로 패키지를 독립적으로 유지보수 가능 ## 아키텍처 ![Todos Architecture Diagram](~/assets/tutorials/todos-architecture.png) 코드를 레이어로 분리하는 것은 매우 중요하며 빠르고 자신감 있게 반복 작업을 할 수 있게 해줍니다. 각 레이어는 단일 책임을 가지며 독립적으로 사용하고 테스트할 수 있습니다. 이를 통해 변경 사항을 특정 레이어에 한정하여 전체 애플리케이션에 미치는 영향을 최소화할 수 있습니다. 또한 애플리케이션을 레이어로 분리하면 여러 프로젝트에서 라이브러리를 쉽게 재사용할 수 있습니다(특히 데이터 레이어). 애플리케이션은 세 가지 주요 레이어로 구성됩니다: - 데이터 레이어 - 도메인 레이어 - 기능 레이어 - 프레젠테이션/UI (위젯) - 비즈니스 로직 (bloc/cubit) **데이터 레이어** 이 레이어는 가장 낮은 레이어로 데이터베이스, API 등의 외부 소스에서 원시 데이터를 가져오는 역할을 합니다. 데이터 레이어의 패키지는 일반적으로 UI에 의존하지 않으며 [pub.dev](https://pub.dev)에 독립적인 패키지로 재사용하거나 게시할 수 있습니다. 이 예제에서 데이터 레이어는 `todos_api`와 `local_storage_todos_api` 패키지로 구성됩니다. **도메인 레이어** 이 레이어는 하나 이상의 데이터 프로바이더를 결합하고 데이터에 "비즈니스 규칙"을 적용합니다. 이 레이어의 각 컴포넌트는 리포지토리라고 하며, 각 리포지토리는 일반적으로 단일 도메인을 관리합니다. 리포지토리 레이어의 패키지는 일반적으로 데이터 레이어와만 상호작용해야 합니다. 이 예제에서 리포지토리 레이어는 `todos_repository` 패키지로 구성됩니다. **기능 레이어** 이 레이어는 애플리케이션별 기능과 유스케이스를 포함합니다. 각 기능은 일반적으로 UI와 비즈니스 로직으로 구성됩니다. 기능은 일반적으로 다른 기능과 독립적이어야 하며, 나머지 코드베이스에 영향을 주지 않고 쉽게 추가/제거할 수 있어야 합니다. 각 기능 내에서 기능의 상태와 비즈니스 로직은 bloc으로 관리됩니다. Bloc은 0개 이상의 리포지토리와 상호작용합니다. Bloc은 이벤트에 반응하고 UI 변경을 트리거하는 상태를 방출합니다. 각 기능 내의 위젯은 일반적으로 해당 bloc에만 의존하고 현재 상태를 기반으로 UI를 렌더링해야 합니다. UI는 이벤트를 통해 bloc에 사용자 입력을 알릴 수 있습니다. 이 예제에서 애플리케이션은 `home`, `todos_overview`, `stats`, `edit_todos` 기능으로 구성됩니다. 레이어에 대한 개요를 살펴봤으니 이제 데이터 레이어부터 시작하여 애플리케이션을 만들어 봅시다! ## 데이터 레이어 데이터 레이어는 애플리케이션의 가장 낮은 레이어로 원시 데이터 프로바이더로 구성됩니다. 이 레이어의 패키지는 주로 데이터가 어디서/어떻게 오는지에 관심을 둡니다. 이 경우 데이터 레이어는 인터페이스인 `TodosApi`와 `shared_preferences`를 기반으로 `TodosApi`를 구현한 `LocalStorageTodosApi`로 구성됩니다. ### TodosApi `todos_api` 패키지는 todos와 상호작용/관리하기 위한 제네릭 인터페이스를 내보냅니다. 나중에 `shared_preferences`를 사용하여 `TodosApi`를 구현할 것입니다. 추상화를 통해 애플리케이션의 다른 부분을 변경하지 않고도 다른 구현을 쉽게 지원할 수 있습니다. 예를 들어, 나중에 `shared_preferences` 대신 `cloud_firestore`를 사용하는 `FirestoreTodosApi`를 최소한의 코드 변경으로 추가할 수 있습니다. #### Todo 모델 다음으로 `Todo` 모델을 정의합니다. 첫 번째로 주목할 점은 `Todo` 모델이 앱에 있지 않다는 것입니다 — `todos_api` 패키지의 일부입니다. 이는 `TodosApi`가 `Todo` 객체를 반환/수락하는 API를 정의하기 때문입니다. 모델은 저장/검색될 원시 Todo 객체의 Dart 표현입니다. `Todo` 모델은 json (역)직렬화를 처리하기 위해 [json_serializable](https://pub.dev/packages/json_serializable)을 사용합니다. 따라 하는 경우 컴파일러 오류를 해결하려면 [코드 생성 단계](https://pub.dev/packages/json_serializable#running-the-code-generator)를 실행해야 합니다. `json_map.dart`는 코드 검사 및 린팅을 위한 `typedef`를 제공합니다. `Todo` 모델은 `todos_api/models/todo.dart`에 정의되어 있으며 `package:todos_api/todos_api.dart`에서 내보냅니다. #### Export 업데이트 `Todo` 모델과 `TodosApi`는 배럴 파일을 통해 내보냅니다. 모델을 직접 임포트하지 않고 `lib/src/todos_api.dart`에서 패키지 배럴 파일을 참조하여 임포트합니다: `import 'package:todos_api/todos_api.dart';`. 나머지 임포트 오류를 해결하기 위해 배럴 파일을 업데이트합니다: #### Stream vs Future 이 튜토리얼의 이전 버전에서는 `TodosApi`가 `Stream` 기반이 아닌 `Future` 기반이었습니다. `Future` 기반 API의 예시는 [Brian Egan의 Architecture Samples 구현](https://github.com/brianegan/flutter_architecture_samples/tree/master/todos_repository_core)을 참고하세요. `Future` 기반 구현은 두 개의 메서드로 구성될 수 있습니다: `loadTodos`와 `saveTodos`(복수형에 주목). 이는 매번 메서드에 전체 todos 목록을 제공해야 한다는 의미입니다. - 이 접근 방식의 한계 중 하나는 표준 CRUD(Create, Read, Update, Delete) 작업이 매번 전체 todos 목록을 보내야 한다는 것입니다. 예를 들어, Todo 추가 화면에서 추가된 todo 항목만 보낼 수 없습니다. 대신 전체 목록을 추적하고 업데이트된 목록을 저장할 때 전체 새 todos 목록을 제공해야 합니다. - 두 번째 한계는 `loadTodos`가 일회성 데이터 전달이라는 것입니다. 앱에는 주기적으로 업데이트를 요청하는 로직이 포함되어야 합니다. 현재 구현에서 `TodosApi`는 `getTodos()`를 통해 `Stream>`를 노출하며, todos 목록이 변경되면 모든 구독자에게 실시간 업데이트를 보고합니다. 또한 todos는 개별적으로 생성, 삭제, 업데이트할 수 있습니다. 예를 들어, todo 삭제와 저장 모두 `todo`만 인자로 사용합니다. 매번 새로 업데이트된 todos 목록을 제공할 필요가 없습니다. ### LocalStorageTodosApi 이 패키지는 [`shared_preferences`](https://pub.dev/packages/shared_preferences) 패키지를 사용하여 `todos_api`를 구현합니다. ## 리포지토리 레이어 [리포지토리](/ko/architecture#repository)는 비즈니스 레이어의 일부입니다. 리포지토리는 비즈니스 가치가 없는 하나 이상의 데이터 프로바이더에 의존하며, 그들의 공개 API를 비즈니스 가치를 제공하는 API로 결합합니다. 또한 리포지토리 레이어를 가지면 애플리케이션의 나머지 부분에서 데이터 획득을 추상화하여 앱의 다른 부분에 영향을 주지 않고 데이터가 어디서/어떻게 저장되는지 변경할 수 있습니다. ### TodosRepository 리포지토리를 인스턴스화하려면 이 튜토리얼 앞부분에서 논의한 `TodosApi`를 지정해야 하므로 `pubspec.yaml`에 의존성으로 추가했습니다: #### 라이브러리 Export `TodosRepository` 클래스 외에도 `todos_api` 패키지의 `Todo` 모델도 내보냅니다. 이 단계는 애플리케이션과 데이터 프로바이더 간의 긴밀한 결합을 방지합니다. 이 경우 데이터 모델을 완전히 제어할 수 있기 때문에 `todos_repository`에 별도의 모델을 재정의하지 않고 `todos_api`에서 동일한 `Todo` 모델을 재내보내기로 했습니다. 많은 경우 데이터 프로바이더는 제어할 수 없는 것일 수 있습니다. 그러한 경우에는 인터페이스와 API 계약을 완전히 제어하기 위해 리포지토리 레이어에서 자체 모델 정의를 유지하는 것이 더욱 중요해집니다. ## 기능 레이어 ### 진입점 앱의 진입점은 `main.dart`입니다. 이 경우 세 가지 버전이 있습니다: 가장 주목할 점은 `local_storage_todos_api`의 구체적인 구현이 각 진입점 내에서 인스턴스화된다는 것입니다. ### 부트스트래핑 `bootstrap.dart`는 `BlocObserver`를 로드하고 `TodosRepository` 인스턴스를 생성합니다. ### App `App`은 모든 자식에게 리포지토리를 제공하는 `RepositoryProvider` 위젯을 감쌉니다. `EditTodoPage`와 `HomePage` 서브트리 모두 자손이므로 모든 bloc과 cubit이 리포지토리에 접근할 수 있습니다. `AppView`는 `MaterialApp`을 생성하고 테마와 로컬라이제이션을 구성합니다. ### Theme 라이트 모드와 다크 모드에 대한 테마 정의를 제공합니다. ### Home home 기능은 현재 선택된 탭의 상태를 관리하고 올바른 서브트리를 표시하는 역할을 합니다. #### HomeState 두 화면인 `todos`와 `stats`에 연결된 두 가지 상태만 있습니다. :::note `EditTodo`는 별도의 라우트이므로 `HomeState`의 일부가 아닙니다. ::: #### HomeCubit 비즈니스 로직이 단순하기 때문에 이 경우 cubit이 적합합니다. 탭을 변경하는 `setTab` 메서드 하나가 있습니다. #### HomeView `view.dart`는 home 기능에 대한 모든 관련 UI 컴포넌트를 내보내는 배럴 파일입니다. `home_page.dart`는 앱이 시작될 때 사용자가 볼 루트 페이지의 UI를 포함합니다. `HomePage`의 위젯 트리를 간략하게 나타내면: `HomePage`는 `HomeView`에 `HomeCubit` 인스턴스를 제공합니다. `HomeView`는 탭이 변경될 때마다 선택적으로 리빌드하기 위해 `context.select`를 사용합니다. 이를 통해 mock `HomeCubit`을 제공하고 상태를 스텁하여 `HomeView`를 쉽게 위젯 테스트할 수 있습니다. `BottomAppBar`는 `HomeCubit`에서 `setTab`을 호출하는 `HomeTabButton` 위젯을 포함합니다. cubit 인스턴스는 `context.read`를 통해 조회하고 cubit 인스턴스에서 적절한 메서드가 호출됩니다. :::caution `context.read`는 변경사항을 리슨하지 않으며, `HomeCubit`에 접근하고 `setTab`을 호출하는 데만 사용됩니다. ::: ### TodosOverview todos overview 기능을 사용하면 사용자가 todos를 생성, 편집, 삭제 및 필터링하여 관리할 수 있습니다. #### TodosOverviewEvent `todos_overview/bloc/todos_overview_event.dart`를 만들고 이벤트를 정의합니다. - `TodosOverviewSubscriptionRequested`: 시작 이벤트입니다. 응답으로 bloc은 `TodosRepository`의 todos 스트림을 구독합니다. - `TodosOverviewTodoDeleted`: Todo를 삭제합니다. - `TodosOverviewTodoCompletionToggled`: todo의 완료 상태를 토글합니다. - `TodosOverviewToggleAllRequested`: 모든 todos의 완료를 토글합니다. - `TodosOverviewClearCompletedRequested`: 완료된 모든 todos를 삭제합니다. - `TodosOverviewUndoDeletionRequested`: 실수로 인한 삭제 등 todo 삭제를 실행 취소합니다. - `TodosOverviewFilterChanged`: `TodosViewFilter`를 인자로 받아 필터를 적용하여 뷰를 변경합니다. #### TodosOverviewState `todos_overview/bloc/todos_overview_state.dart`를 만들고 상태를 정의합니다. `TodosOverviewState`는 todos 목록, 활성 필터, `lastDeletedTodo`, 상태를 추적합니다. :::note 기본 getter와 setter 외에도 `filteredTodos`라는 커스텀 getter가 있습니다. UI는 `BlocBuilder`를 사용하여 `state.filteredTodos` 또는 `state.todos`에 접근합니다. ::: #### TodosOverviewBloc `todos_overview/bloc/todos_overview_bloc.dart`를 만듭니다. :::note bloc은 내부적으로 `TodosRepository` 인스턴스를 생성하지 않습니다. 대신 생성자를 통해 리포지토리 인스턴스가 주입되는 것에 의존합니다. ::: ##### onSubscriptionRequested `TodosOverviewSubscriptionRequested`가 추가되면 bloc은 먼저 `loading` 상태를 방출합니다. 응답으로 UI는 로딩 인디케이터를 렌더링할 수 있습니다. 다음으로 `emit.forEach>( ... )`를 사용하여 `TodosRepository`의 todos 스트림에 대한 구독을 생성합니다. :::caution `emit.forEach()`는 리스트에서 사용하는 `forEach()`와 다릅니다. 이 `forEach`는 bloc이 `Stream`을 구독하고 스트림의 각 업데이트마다 새 상태를 방출할 수 있게 합니다. ::: :::note 이 튜토리얼에서는 `stream.listen`을 직접 호출하지 않습니다. `await emit.forEach()`는 bloc이 내부적으로 구독을 관리할 수 있게 하는 새로운 패턴입니다. ::: 이제 구독이 처리되었으니 todos 추가, 수정, 삭제와 같은 다른 이벤트를 처리합니다. ##### onTodoSaved `_onTodoSaved`는 단순히 `_todosRepository.saveTodo(event.todo)`를 호출합니다. :::note `onTodoSaved`와 다른 많은 이벤트 핸들러 내에서 `emit`이 호출되지 않습니다. 대신 리포지토리에 알리고 리포지토리가 todos 스트림을 통해 업데이트된 목록을 방출합니다. 자세한 내용은 [데이터 흐름](#데이터-흐름) 섹션을 참조하세요. ::: ##### 실행 취소 실행 취소 기능을 통해 사용자는 마지막으로 삭제된 항목을 복원할 수 있습니다. `_onTodoDeleted`는 두 가지를 수행합니다. 먼저 삭제할 `Todo`와 함께 새 상태를 방출합니다. 그런 다음 리포지토리 호출을 통해 `Todo`를 삭제합니다. `_onUndoDeletionRequested`는 UI에서 삭제 취소 요청 이벤트가 올 때 실행됩니다. `_onUndoDeletionRequested`는 다음을 수행합니다: - 마지막으로 삭제된 todo의 복사본을 임시 저장합니다. - `lastDeletedTodo`를 제거하여 상태를 업데이트합니다. - 삭제를 되돌립니다. ##### 필터링 `_onFilterChanged`는 새 이벤트 필터와 함께 새 상태를 방출합니다. #### Models 뷰 필터링을 다루는 모델 파일이 하나 있습니다. `todos_view_filter.dart`는 세 가지 뷰 필터를 나타내는 enum과 필터를 적용하는 메서드입니다. `models.dart`는 내보내기를 위한 배럴 파일입니다. 다음으로 `TodosOverviewPage`를 살펴봅시다. #### TodosOverviewPage `TodosOverviewPage`의 위젯 트리를 간략하게 나타내면: `Home` 기능과 마찬가지로 `TodosOverviewPage`는 `BlocProvider`을 통해 서브트리에 `TodosOverviewBloc` 인스턴스를 제공합니다. 이는 `TodosOverviewBloc`의 범위를 `TodosOverviewPage` 아래의 위젯으로만 한정합니다. `TodosOverviewBloc`의 변경사항을 리슨하는 세 개의 위젯이 있습니다. 1. 첫 번째는 에러를 리슨하는 `BlocListener`입니다. `listener`는 `listenWhen`이 `true`를 반환할 때만 호출됩니다. 상태가 `TodosOverviewStatus.failure`이면 `SnackBar`가 표시됩니다. 2. 삭제를 리슨하는 두 번째 `BlocListener`를 만들었습니다. todo가 삭제되면 실행 취소 버튼이 있는 `SnackBar`가 표시됩니다. 사용자가 실행 취소를 탭하면 `TodosOverviewUndoDeletionRequested` 이벤트가 bloc에 추가됩니다. 3. 마지막으로 `BlocBuilder`를 사용하여 todos를 표시하는 ListView를 빌드합니다. `AppBar`에는 todos를 필터링하고 조작하기 위한 드롭다운인 두 가지 액션이 있습니다. :::note `TodosOverviewTodoCompletionToggled`와 `TodosOverviewTodoDeleted`는 `context.read`를 통해 bloc에 추가됩니다. ::: `view.dart`는 `todos_overview_page.dart`를 내보내는 배럴 파일입니다. #### Widgets `widgets.dart`는 `todos_overview` 기능 내에서 사용되는 모든 컴포넌트를 내보내는 또 다른 배럴 파일입니다. `todo_list_tile.dart`는 각 todo 항목의 `ListTile`입니다. `todos_overview_options_button.dart`는 todos를 조작하기 위한 두 가지 옵션을 노출합니다: - `toggleAll` - `clearCompleted` `todos_overview_filter_button.dart`는 세 가지 필터 옵션을 노출합니다: - `all` - `activeOnly` - `completedOnly` ### Stats stats 기능은 활성 및 완료된 todos에 대한 통계를 표시합니다. #### StatsState `StatsState`는 요약 정보와 현재 `StatsStatus`를 추적합니다. #### StatsEvent `StatsEvent`에는 `StatsSubscriptionRequested`라는 하나의 이벤트만 있습니다: #### StatsBloc `StatsBloc`은 `TodosOverviewBloc`과 마찬가지로 `TodosRepository`에 의존합니다. `_todosRepository.getTodos`를 통해 todos 스트림을 구독합니다. #### Stats View `view.dart`는 `stats_page`를 위한 배럴 파일입니다. `stats_page.dart`는 todos 통계를 표시하는 페이지의 UI를 포함합니다. `StatsPage`의 위젯 트리를 간략하게 나타내면: :::caution `TodosOverviewBloc`과 `StatsBloc`은 모두 `TodosRepository`와 통신하지만, bloc 간에 직접적인 통신은 없다는 점에 유의해야 합니다. 자세한 내용은 [데이터 흐름](#데이터-흐름) 섹션을 참조하세요. ::: ### EditTodo `EditTodo` 기능을 통해 사용자는 기존 todo 항목을 편집하고 변경사항을 저장할 수 있습니다. #### EditTodoState `EditTodoState`는 todo를 편집할 때 필요한 정보를 추적합니다. #### EditTodoEvent bloc이 반응할 다양한 이벤트는 다음과 같습니다: - `EditTodoTitleChanged` - `EditTodoDescriptionChanged` - `EditTodoSubmitted` #### EditTodoBloc `EditTodoBloc`은 `TodosOverviewBloc` 및 `StatsBloc`과 마찬가지로 `TodosRepository`에 의존합니다. :::caution 다른 Bloc과 달리 `EditTodoBloc`은 `_todosRepository.getTodos`를 구독하지 않습니다. 리포지토리에서 정보를 읽을 필요가 없는 "쓰기 전용" bloc입니다. ::: ##### 데이터 흐름 동일한 todos 목록에 의존하는 많은 기능이 있지만, bloc 간 통신은 없습니다. 대신 모든 기능은 서로 독립적이며 todos 목록의 변경사항을 리슨하고 목록을 업데이트하기 위해 `TodosRepository`에 의존합니다. 예를 들어, `EditTodos`는 `TodosOverview`나 `Stats` 기능에 대해 아무것도 모릅니다. UI가 `EditTodoSubmitted` 이벤트를 제출하면: - `EditTodoBloc`이 `TodosRepository`를 업데이트하는 비즈니스 로직을 처리합니다. - `TodosRepository`가 `TodosOverviewBloc`과 `StatsBloc`에 알립니다. - `TodosOverviewBloc`과 `StatsBloc`이 새 상태로 업데이트하는 UI에 알립니다. #### EditTodoPage 이전 기능과 마찬가지로 `EditTodosPage`는 `BlocProvider`를 통해 `EditTodosBloc` 인스턴스를 제공합니다. 다른 기능과 달리 `EditTodosPage`는 별도의 라우트이므로 `static` `route` 메서드를 노출합니다. 이를 통해 `Navigator.of(context).push(...)`를 통해 네비게이션 스택에 `EditTodosPage`를 쉽게 푸시할 수 있습니다. `EditTodosPage`의 위젯 트리를 간략하게 나타내면: ## 요약 튜토리얼을 완료했습니다! 🎉 유닛 테스트와 위젯 테스트를 포함한 전체 소스 코드는 [여기](https://github.com/felangel/bloc/tree/master/examples/flutter_todos)에서 확인할 수 있습니다. ================================================ FILE: docs/src/content/docs/ko/tutorials/flutter-weather.mdx ================================================ --- title: Flutter Weather description: Flutter에서 bloc을 사용하여 날씨 앱을 구현하는 심층 가이드. sidebar: order: 5 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-weather/FlutterCreateSnippet.astro'; import FeatureTreeSnippet from '~/components/tutorials/flutter-weather/FeatureTreeSnippet.astro'; import FlutterCreateApiClientSnippet from '~/components/tutorials/flutter-weather/FlutterCreateApiClientSnippet.astro'; import OpenMeteoModelsTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoModelsTreeSnippet.astro'; import LocationJsonSnippet from '~/components/tutorials/flutter-weather/LocationJsonSnippet.astro'; import LocationDartSnippet from '~/components/tutorials/flutter-weather/LocationDartSnippet.astro'; import WeatherJsonSnippet from '~/components/tutorials/flutter-weather/WeatherJsonSnippet.astro'; import WeatherDartSnippet from '~/components/tutorials/flutter-weather/WeatherDartSnippet.astro'; import OpenMeteoModelsBarrelTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoModelsBarrelTreeSnippet.astro'; import OpenMeteoLibrarySnippet from '~/components/tutorials/flutter-weather/OpenMeteoLibrarySnippet.astro'; import BuildRunnerBuildSnippet from '~/components/tutorials/flutter-weather/BuildRunnerBuildSnippet.astro'; import OpenMeteoApiClientTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoApiClientTreeSnippet.astro'; import LocationSearchMethodSnippet from '~/components/tutorials/flutter-weather/LocationSearchMethodSnippet.astro'; import GetWeatherMethodSnippet from '~/components/tutorials/flutter-weather/GetWeatherMethodSnippet.astro'; import FlutterTestCoverageSnippet from '~/components/tutorials/flutter-weather/FlutterTestCoverageSnippet.astro'; import FlutterCreateRepositorySnippet from '~/components/tutorials/flutter-weather/FlutterCreateRepositorySnippet.astro'; import RepositoryModelsBarrelTreeSnippet from '~/components/tutorials/flutter-weather/RepositoryModelsBarrelTreeSnippet.astro'; import WeatherRepositoryLibrarySnippet from '~/components/tutorials/flutter-weather/WeatherRepositoryLibrarySnippet.astro'; import WeatherCubitTreeSnippet from '~/components/tutorials/flutter-weather/WeatherCubitTreeSnippet.astro'; import WeatherBarrelDartSnippet from '~/components/tutorials/flutter-weather/WeatherBarrelDartSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) 이번 튜토리얼에서는 Flutter로 날씨 앱을 만들어 여러 cubit을 관리하여 동적 테마, 당겨서 새로고침 등을 구현하는 방법을 보여줍니다. 날씨 앱은 공개 OpenMeteo API에서 실시간 날씨 데이터를 가져오고 애플리케이션을 레이어(데이터, 리포지토리, 비즈니스 로직, 프레젠테이션)로 분리하는 방법을 보여줍니다. ![demo](~/assets/tutorials/flutter-weather.gif) ## 프로젝트 요구사항 앱에서 사용자가 할 수 있어야 하는 것: - 전용 검색 페이지에서 도시 검색 - [Open Meteo API](https://open-meteo.com)에서 반환된 날씨 데이터를 보기 좋게 표시 - 표시되는 단위 변경 (미터법 vs 야드파운드법) 추가로: - 애플리케이션의 테마는 선택한 도시의 날씨를 반영해야 함 - 애플리케이션 상태는 세션 간에 유지되어야 함: 즉, 앱을 닫았다가 다시 열어도 상태를 기억해야 함 ([HydratedBloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) 사용) ## 주요 개념 - [BlocObserver](/ko/bloc-concepts#blocobserver)로 상태 변화 관찰. - [BlocProvider](/ko/flutter-bloc-concepts#blocprovider), 자식에게 bloc을 제공하는 Flutter 위젯. - [BlocBuilder](/ko/flutter-bloc-concepts#blocbuilder), 새로운 상태에 따라 위젯을 빌드하는 Flutter 위젯. - [Equatable](/ko/faqs#언제-equatable를-사용해야-하나요)을 사용한 불필요한 리빌드 방지. - [RepositoryProvider](/ko/flutter-bloc-concepts#repositoryprovider), 자식에게 리포지토리를 제공하는 Flutter 위젯. - [BlocListener](/ko/flutter-bloc-concepts#bloclistener), bloc의 상태 변화에 따라 리스너 코드를 호출하는 Flutter 위젯. - [MultiBlocProvider](/ko/flutter-bloc-concepts#multiblocprovider), 여러 BlocProvider 위젯을 하나로 합치는 Flutter 위젯. - [BlocConsumer](/ko/flutter-bloc-concepts#blocconsumer), 새로운 상태에 반응하기 위해 builder와 listener를 노출하는 Flutter 위젯. - 상태를 관리하고 유지하기 위한 [HydratedBloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc). ## 설정 새 flutter 프로젝트를 생성합니다 ### 프로젝트 구조 앱은 해당 디렉토리에 분리된 기능으로 구성됩니다. 이를 통해 기능 수가 증가함에 따라 확장할 수 있으며 개발자가 서로 다른 기능을 병렬로 작업할 수 있습니다. 앱은 네 가지 주요 기능으로 나눌 수 있습니다: **search, settings, theme, weather**. 해당 디렉토리를 만들어 봅시다. ### 아키텍처 [bloc 아키텍처](/ko/architecture) 가이드라인에 따라 애플리케이션은 여러 레이어로 구성됩니다. 이 튜토리얼에서 각 레이어가 하는 일: - **데이터**: API에서 원시 날씨 데이터 검색 - **리포지토리**: 데이터 레이어를 추상화하고 애플리케이션이 사용할 도메인 모델 노출 - **비즈니스 로직**: 각 기능의 상태 관리 (단위 정보, 도시 정보, 테마 등) - **프레젠테이션**: 날씨 정보 표시 및 사용자 입력 수집 (설정 페이지, 검색 페이지 등) ## 데이터 레이어 이 애플리케이션에서는 [Open Meteo API](https://open-meteo.com)를 사용합니다. 두 가지 엔드포인트에 집중합니다: - `https://geocoding-api.open-meteo.com/v1/search?name=$city&count=1` - 주어진 도시 이름의 위치 가져오기 - `https://api.open-meteo.com/v1/forecast?latitude=$latitude&longitude=$longitude¤t_weather=true` - 주어진 위치의 날씨 가져오기 브라우저에서 [https://geocoding-api.open-meteo.com/v1/search?name=chicago&count=1](https://geocoding-api.open-meteo.com/v1/search?name=chicago&count=1)을 열어 시카고 도시의 응답을 확인하세요. 응답의 `latitude`와 `longitude`를 사용하여 날씨 엔드포인트를 호출합니다. 시카고의 `latitude`/`longitude`는 `41.85003`/`-87.65005`입니다. 브라우저에서 [https://api.open-meteo.com/v1/forecast?latitude=43.0389&longitude=-87.90647¤t_weather=true](https://api.open-meteo.com/v1/forecast?latitude=43.0389&longitude=-87.90647¤t_weather=true)로 이동하면 시카고의 날씨 응답을 볼 수 있으며, 앱에 필요한 모든 데이터가 포함되어 있습니다. ### OpenMeteo API 클라이언트 OpenMeteo API 클라이언트는 애플리케이션과 독립적입니다. 따라서 내부 패키지로 생성하고 ([pub.dev](https://pub.dev)에 게시할 수도 있음) 리포지토리 레이어의 `pubspec.yaml`에 추가하여 메인 날씨 애플리케이션의 데이터 요청을 처리합니다. 프로젝트 레벨에 `packages`라는 새 디렉토리를 만듭니다. 이 디렉토리에 모든 내부 패키지를 저장합니다. 이 디렉토리 내에서 내장된 `flutter create` 명령을 실행하여 API 클라이언트용 `open_meteo_api`라는 새 패키지를 만듭니다. ### 날씨 데이터 모델 다음으로 `location` 및 `weather` API 엔드포인트 응답에 대한 모델을 포함할 `location.dart`와 `weather.dart`를 만듭니다. #### Location 모델 `location.dart` 모델은 다음과 같은 location API가 반환하는 데이터를 저장해야 합니다: 위 응답을 저장하는 진행 중인 `location.dart` 파일입니다: #### Weather 모델 다음으로 `weather.dart`를 작업합니다. weather 모델은 다음과 같은 weather API가 반환하는 데이터를 저장해야 합니다: 위 응답을 저장하는 진행 중인 `weather.dart` 파일입니다: ### 배럴 파일 여기서 임포트를 정리하기 위해 [배럴 파일](https://adrianfaciu.dev/posts/barrel-files/)을 빠르게 만들어 봅시다. `models.dart` 배럴 파일을 만들고 두 모델을 내보냅니다: 패키지 레벨 배럴 파일 `open_meteo_api.dart`도 만듭니다 최상위 `open_meteo_api.dart`에서 모델을 내보냅니다: ### 설정 API 데이터로 작업하려면 모델을 [직렬화 및 역직렬화](https://en.wikipedia.org/wiki/Serialization)할 수 있어야 합니다. 이를 위해 모델에 `toJson` 및 `fromJson` 메서드를 추가합니다. 또한 API에서 데이터를 가져오기 위해 [HTTP 네트워크 요청을 만드는](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) 방법이 필요합니다. 다행히 이를 위한 인기 있는 패키지가 여러 개 있습니다. `toJson` 및 `fromJson` 구현을 생성하기 위해 [json_annotation](https://pub.dev/packages/json_annotation), [json_serializable](https://pub.dev/packages/json_serializable), [build_runner](https://pub.dev/packages/build_runner) 패키지를 사용합니다. 이후 단계에서는 [http](https://pub.dev/packages/http) 패키지를 사용하여 weather API에 네트워크 요청을 보내 애플리케이션이 현재 날씨 데이터를 표시할 수 있도록 합니다. 이러한 의존성을 `pubspec.yaml`에 추가합니다. :::note 의존성을 추가한 후 `flutter pub get`을 실행하는 것을 잊지 마세요. ::: ### (역)직렬화 코드 생성이 작동하려면 다음을 사용하여 코드에 어노테이션을 달아야 합니다: - `@JsonSerializable` - 직렬화할 수 있는 클래스에 레이블 지정 - `@JsonKey` - 필드 이름의 문자열 표현 제공 - `@JsonValue` - 필드 값의 문자열 표현 제공 - `JSONConverter` 구현 - 객체 표현을 JSON 표현으로 변환 각 파일에 대해 다음도 필요합니다: - `json_annotation` 임포트 - [part](https://dart.dev/tools/pub/create-packages#organizing-a-package) 키워드를 사용하여 생성된 코드 포함 - 역직렬화를 위한 `fromJson` 메서드 포함 #### Location 모델 완성된 `location.dart` 모델 파일입니다: #### Weather 모델 완성된 `weather.dart` 모델 파일입니다: #### Build 파일 생성 `open_meteo_api` 폴더에 `build.yaml` 파일을 만듭니다. 이 파일의 목적은 `json_serializable` 필드 이름의 명명 규칙 간 불일치를 처리하는 것입니다. #### 코드 생성 `build_runner`를 사용하여 코드를 생성합니다. `build_runner`가 `location.g.dart`와 `weather.g.dart` 파일을 생성해야 합니다. ### OpenMeteo API 클라이언트 `src` 디렉토리 내에 `open_meteo_api_client.dart`에 API 클라이언트를 만듭니다. 이제 프로젝트 구조는 다음과 같아야 합니다: 이전에 `pubspec.yaml` 파일에 추가한 [http](https://pub.dev/packages/http) 패키지를 사용하여 weather API에 HTTP 요청을 하고 이 정보를 애플리케이션에서 사용할 수 있습니다. API 클라이언트는 두 가지 메서드를 노출합니다: - `locationSearch` - `Future` 반환 - `getWeather` - `Future` 반환 #### Location 검색 `locationSearch` 메서드는 location API를 호출하고 해당되는 경우 `LocationRequestFailure` 에러를 throw합니다. 완성된 메서드는 다음과 같습니다: #### 날씨 가져오기 마찬가지로 `getWeather` 메서드는 weather API를 호출하고 해당되는 경우 `WeatherRequestFailure` 에러를 throw합니다. 완성된 메서드는 다음과 같습니다: 완성된 파일은 다음과 같습니다: #### 배럴 파일 업데이트 배럴 파일에 API 클라이언트를 추가하여 이 패키지를 마무리합니다. ### 유닛 테스트 데이터 레이어는 애플리케이션의 기반이므로 유닛 테스트를 작성하는 것이 특히 중요합니다. 유닛 테스트는 패키지가 예상대로 동작한다는 확신을 줍니다. #### 설정 이전에 pubspec.yaml에 [test](https://pub.dev/packages/test) 패키지를 추가하여 유닛 테스트를 쉽게 작성할 수 있습니다. API 클라이언트와 두 모델에 대한 테스트 파일을 만들 것입니다. #### Location 테스트 #### Weather 테스트 #### API 클라이언트 테스트 다음으로 API 클라이언트를 테스트합니다. API 클라이언트가 엣지 케이스를 포함하여 두 API 호출을 올바르게 처리하는지 테스트해야 합니다. :::note 우리의 목표는 API 자체가 아닌 API 클라이언트 로직(모든 엣지 케이스 포함)을 테스트하는 것이므로 테스트에서 실제 API 호출을 하지 않습니다. 일관되고 제어된 테스트 환경을 갖기 위해 이전에 pubspec.yaml 파일에 추가한 [mocktail](https://github.com/felangel/mocktail)을 사용하여 `http` 클라이언트를 모킹합니다. ::: #### 테스트 커버리지 마지막으로 각 코드 라인이 최소한 하나의 테스트 케이스로 커버되었는지 확인하기 위해 테스트 커버리지를 수집합니다. ## 리포지토리 레이어 리포지토리 레이어의 목표는 데이터 레이어를 추상화하고 bloc 레이어와의 통신을 용이하게 하는 것입니다. 이렇게 하면 코드베이스의 나머지 부분은 특정 데이터 프로바이더 구현이 아닌 리포지토리 레이어에서 노출하는 함수에만 의존합니다. 이를 통해 애플리케이션 레벨 코드를 방해하지 않고 데이터 프로바이더를 변경할 수 있습니다. 예를 들어, 이 특정 weather API에서 마이그레이션하기로 결정하면 리포지토리 또는 애플리케이션 레이어의 공개 API를 변경하지 않고도 새 API 클라이언트를 만들어 교체할 수 있어야 합니다. ### 설정 packages 디렉토리 내에서 다음 명령을 실행합니다: 마지막 단계의 `open_meteo_api` 패키지를 포함하여 `open_meteo_api` 패키지와 동일한 패키지를 사용합니다. `pubspec.yaml`을 업데이트하고 `flutter pub get`을 실행합니다. :::note `open_meteo_api`의 위치를 지정하기 위해 `path`를 사용하며, 이를 통해 `pub.dev`의 외부 패키지처럼 취급할 수 있습니다. ::: ### Weather Repository 모델 도메인별 날씨 모델을 노출하기 위해 새 `weather.dart` 파일을 만듭니다. 이 모델은 비즈니스 케이스와 관련된 데이터만 포함합니다 -- 즉, API 클라이언트 및 원시 데이터 형식과 완전히 분리되어야 합니다. 평소처럼 `models.dart` 배럴 파일도 만듭니다. 이번에는 weather 모델이 `location, temperature, condition` 프로퍼티만 저장합니다. 직렬화 및 역직렬화를 위해 코드에 어노테이션을 계속 달 것입니다. 이전에 만든 배럴 파일을 업데이트하여 모델을 포함합니다. #### Build 파일 생성 이전과 마찬가지로 다음 내용으로 `build.yaml` 파일을 만들어야 합니다: #### 코드 생성 이전에 했던 것처럼 다음 명령을 실행하여 (역)직렬화 구현을 생성합니다. #### 배럴 파일 모델을 내보내기 위해 `packages/weather_repository/lib/weather_repository.dart`라는 패키지 레벨 배럴 파일도 만듭니다: ### Weather Repository `WeatherRepository`의 주요 목표는 데이터 프로바이더를 추상화하는 인터페이스를 제공하는 것입니다. 이 경우 `WeatherRepository`는 `WeatherApiClient`에 의존하고 단일 공개 메서드 `getWeather(String city)`를 노출합니다. :::note `WeatherRepository`의 소비자는 날씨 API에 두 번의 네트워크 요청이 이루어진다는 사실과 같은 기저 구현 세부 사항을 알 수 없습니다. `WeatherRepository`의 목표는 "무엇"과 "어떻게"를 분리하는 것입니다 -- 즉, 주어진 도시의 날씨를 가져오는 방법을 갖고 싶지만, 데이터가 어디서 어떻게 오는지는 신경 쓰지 않습니다. ::: #### 설정 패키지의 `src` 디렉토리 내에 `weather_repository.dart` 파일을 만들고 리포지토리 구현을 작업합니다. 집중할 주요 메서드는 `getWeather(String city)`입니다. 다음과 같이 API 클라이언트에 대한 두 번의 호출을 사용하여 구현할 수 있습니다: #### 배럴 파일 이전에 만든 배럴 파일을 업데이트합니다. ### 유닛 테스트 데이터 레이어와 마찬가지로 도메인 레벨 로직이 올바른지 확인하기 위해 리포지토리 레이어를 테스트하는 것이 중요합니다. `WeatherRepository`를 테스트하기 위해 [mocktail](https://github.com/felangel/mocktail) 라이브러리를 사용합니다. 격리된, 제어된 환경에서 `WeatherRepository` 로직을 유닛 테스트하기 위해 기저 api 클라이언트를 모킹합니다. ## 비즈니스 로직 레이어 비즈니스 로직 레이어에서는 `WeatherRepository`의 날씨 도메인 모델을 사용하고 UI를 통해 사용자에게 표시될 기능 레벨 모델을 노출합니다. :::note 이것이 우리가 구현하는 세 번째 다른 유형의 날씨 모델입니다. API 클라이언트에서 weather 모델은 API가 반환한 모든 정보를 포함했습니다. 리포지토리 레이어에서 weather 모델은 비즈니스 케이스를 기반으로 추상화된 모델만 포함했습니다. 이 레이어에서 weather 모델은 현재 기능 세트에 특별히 필요한 관련 정보를 포함합니다. ::: ### 설정 비즈니스 로직 레이어가 메인 앱에 있으므로 전체 `flutter_weather` 프로젝트의 `pubspec.yaml`을 편집하고 사용할 모든 패키지를 포함해야 합니다. - [equatable](https://pub.dev/packages/equatable)을 사용하면 앱의 상태 클래스 인스턴스를 equals `==` 연산자를 사용하여 비교할 수 있습니다. 내부적으로 bloc은 상태가 같은지 비교하고, 같지 않으면 리빌드를 트리거합니다. 이는 위젯 트리가 필요할 때만 리빌드되도록 하여 성능을 빠르고 반응적으로 유지합니다. - [google_fonts](https://pub.dev/packages/google_fonts)로 사용자 인터페이스를 꾸밀 수 있습니다. - [HydratedBloc](https://pub.dev/packages/hydrated_bloc)을 사용하면 앱을 닫았다가 다시 열 때 애플리케이션 상태를 유지할 수 있습니다. - 방금 만든 `weather_repository` 패키지를 포함하여 현재 날씨 데이터를 가져올 수 있습니다! 테스트를 위해 일반적인 `test` 패키지와 의존성 모킹을 위한 `mocktail`, 비즈니스 로직 단위 또는 bloc의 쉬운 테스트를 가능하게 하는 [bloc_test](https://pub.dev/packages/bloc_test)를 포함합니다! 다음으로 `weather` 기능 디렉토리 내의 애플리케이션 레이어를 작업합니다. ### Weather 모델 weather 모델의 목표는 앱에 표시되는 날씨 데이터와 온도 설정(섭씨 또는 화씨)을 추적하는 것입니다. `flutter_weather/lib/weather/models/weather.dart`를 만듭니다: ### Build 파일 생성 비즈니스 로직 레이어를 위한 `build.yaml` 파일을 만듭니다. ### 코드 생성 `build_runner`를 실행하여 (역)직렬화 구현을 생성합니다. ### 배럴 파일 배럴 파일(`flutter_weather/lib/weather/models/models.dart`)에서 모델을 내보냅니다: 그런 다음 최상위 weather 배럴 파일(`flutter_weather/lib/weather/weather.dart`)을 만듭니다; ### Weather `HydratedCubit`을 사용하여 앱이 닫았다가 다시 열어도 애플리케이션 상태를 기억할 수 있게 합니다. :::note `HydratedCubit`은 세션 간 상태 유지 및 복원을 처리하는 `Cubit`의 확장입니다. ::: #### Weather 상태 [Bloc VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) 또는 [Bloc IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) 확장을 사용하여 `weather` 디렉토리를 우클릭하고 `Weather`라는 새 cubit을 만듭니다. 프로젝트 구조는 다음과 같아야 합니다: 날씨 앱이 있을 수 있는 네 가지 상태: - `initial` - 아무것도 로드되기 전 - `loading` - API 호출 중 - `success` - API 호출 성공 - `failure` - API 호출 실패 `WeatherStatus` enum이 위의 내용을 나타냅니다. 완성된 weather 상태는 다음과 같습니다: #### Weather Cubit `WeatherState`를 정의했으니 이제 다음 메서드를 노출하는 `WeatherCubit`을 작성합니다: - `fetchWeather(String? city)` - weather repository를 사용하여 주어진 도시의 weather 객체를 가져오려고 시도 - `refreshWeather()` - 현재 weather 상태를 기반으로 weather repository를 사용하여 새 weather 객체를 가져옴 - `toggleUnits()` - 섭씨와 화씨 사이에서 상태를 토글 - `fromJson(Map json)`, `toJson(WeatherState state)` - 지속성에 사용 :::note 다음을 통해 (역)직렬화 코드를 생성하는 것을 잊지 마세요: ::: ### 유닛 테스트 데이터 및 리포지토리 레이어와 마찬가지로 기능 레벨 로직이 예상대로 동작하는지 확인하기 위해 비즈니스 로직 레이어를 유닛 테스트하는 것이 중요합니다. `mocktail` 외에도 [bloc_test](https://pub.dev/packages/bloc_test)를 사용합니다. `test`, `bloc_test`, `mocktail` 패키지를 `dev_dependencies`에 추가합니다. :::note [bloc_test](https://pub.dev/packages/bloc_test) 패키지를 사용하면 bloc을 테스트용으로 쉽게 준비하고, 상태 변경을 처리하고, 일관된 방식으로 결과를 확인할 수 있습니다. ::: #### Weather Cubit 테스트 ## 프레젠테이션 레이어 ### Weather 페이지 `WeatherPage`는 `BlocProvider`를 사용하여 `WeatherCubit` 인스턴스를 위젯 트리에 제공합니다. 페이지가 `SettingsPage`와 `SearchPage` 위젯에 의존하는 것을 알 수 있으며, 다음에 만들 것입니다. ### SettingsPage 설정 페이지에서 사용자는 온도 단위에 대한 기본 설정을 업데이트할 수 있습니다. ### SearchPage 검색 페이지에서 사용자는 원하는 도시 이름을 입력할 수 있으며 `Navigator.of(context).pop`을 통해 이전 라우트에 검색 결과를 제공합니다. ### Weather 위젯 앱은 `WeatherCubit`의 네 가지 가능한 상태에 따라 다른 화면을 표시합니다. #### WeatherEmpty 이 화면은 사용자가 아직 도시를 선택하지 않아 표시할 데이터가 없을 때 표시됩니다. #### WeatherError 에러가 있을 때 이 화면이 표시됩니다. #### WeatherLoading 애플리케이션이 데이터를 가져오는 동안 이 화면이 표시됩니다. #### WeatherPopulated 사용자가 도시를 선택하고 데이터를 가져온 후 이 화면이 표시됩니다. ### 배럴 파일 임포트를 정리하기 위해 이러한 상태를 배럴 파일에 추가합니다. ### 진입점 `main.dart` 파일은 `WeatherApp`과 `BlocObserver`(디버깅 목적)를 초기화하고 세션 간 상태를 유지하기 위해 `HydratedStorage`를 설정해야 합니다. `app.dart` 위젯은 이전에 만든 `WeatherPage` 뷰를 빌드하고 `BlocProvider`를 사용하여 `WeatherCubit`을 주입합니다. ### 위젯 테스트 [`bloc_test`](https://pub.dev/packages/bloc_test) 라이브러리는 UI를 쉽게 테스트할 수 있는 `MockBlocs`와 `MockCubits`도 노출합니다. 다양한 cubit의 상태를 모킹하고 UI가 올바르게 반응하는지 확인할 수 있습니다. :::note 각 테스트 케이스에서 cubit의 상태를 스텁하기 위해 `mocktail`의 `when` API와 함께 `MockWeatherCubit`을 사용합니다. 이를 통해 모든 상태를 시뮬레이션하고 모든 상황에서 UI가 올바르게 동작하는지 확인할 수 있습니다. ::: ## 요약 튜토리얼을 완료했습니다! 🎉 `flutter run` 명령을 사용하여 최종 앱을 실행할 수 있습니다. 유닛 테스트와 위젯 테스트를 포함한 전체 소스 코드는 [여기](https://github.com/felangel/bloc/tree/master/examples/flutter_weather)에서 확인할 수 있습니다. ================================================ FILE: docs/src/content/docs/ko/tutorials/github-search.mdx ================================================ --- title: GitHub 검색 description: Flutter와 AngularDart에서 bloc을 사용하여 GitHub 검색 앱을 구현하는 심층 가이드. sidebar: order: 9 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import SetupSnippet from '~/components/tutorials/github-search/SetupSnippet.astro'; import DartPubGetSnippet from '~/components/tutorials/github-search/DartPubGetSnippet.astro'; import FlutterCreateSnippet from '~/components/tutorials/github-search/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; import StagehandSnippet from '~/components/tutorials/github-search/StagehandSnippet.astro'; import ActivateStagehandSnippet from '~/components/tutorials/github-search/ActivateStagehandSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) 이번 튜토리얼에서는 Flutter와 AngularDart에서 GitHub 검색 앱을 만들어 두 프로젝트 간에 데이터 레이어와 비즈니스 로직 레이어를 어떻게 공유할 수 있는지 보여줍니다. ![demo](~/assets/tutorials/flutter-github-search.gif) ![demo](~/assets/tutorials/ngdart-github-search.gif) ## 주요 주제 - [BlocProvider](/ko/flutter-bloc-concepts#blocprovider), 자식 위젯에 bloc을 제공하는 Flutter 위젯. - [BlocBuilder](/ko/flutter-bloc-concepts#blocbuilder), 새로운 상태에 따라 위젯을 빌드하는 Flutter 위젯. - Bloc 대신 Cubit 사용하기. [차이점은 무엇인가요?](/ko/bloc-concepts#cubit-vs-bloc) - [Equatable](/ko/faqs#언제-equatable를-사용해야-하나요)을 사용하여 불필요한 리빌드 방지. - [`bloc_concurrency`](https://pub.dev/packages/bloc_concurrency)와 함께 커스텀 `EventTransformer` 사용. - `http` 패키지를 사용한 네트워크 요청. ## Common GitHub Search 라이브러리 Common GitHub Search 라이브러리는 AngularDart와 Flutter 간에 공유될 모델, 데이터 프로바이더, 리포지토리, 그리고 bloc을 포함합니다. ### 설정 먼저 애플리케이션을 위한 새 디렉토리를 만듭니다. :::note `common_github_search` 디렉토리가 공유 라이브러리를 포함할 것입니다. ::: 필요한 의존성이 담긴 `pubspec.yaml`을 생성해야 합니다. 마지막으로 의존성을 설치합니다. 프로젝트 설정이 완료되었습니다! 이제 `common_github_search` 패키지를 만들어 봅시다. ### Github 클라이언트 `GithubClient`는 [GitHub API](https://developer.github.com/v3/)에서 원시 데이터를 제공합니다. :::note 반환되는 데이터의 샘플은 [여기](https://api.github.com/search/repositories?q=dartlang)에서 확인할 수 있습니다. ::: `github_client.dart`를 생성합니다. :::note `GithubClient`는 단순히 Github의 Repository Search API에 네트워크 요청을 하고 결과를 `SearchResult` 또는 `SearchResultError`로 변환하여 `Future`로 반환합니다. ::: :::note `GithubClient` 구현은 아직 구현하지 않은 `SearchResult.fromJson`에 의존합니다. ::: 다음으로 `SearchResult`와 `SearchResultError` 모델을 정의해야 합니다. #### Search Result 모델 사용자의 쿼리를 기반으로 한 `SearchResultItems` 리스트를 나타내는 `search_result.dart`를 생성합니다: :::note `SearchResult` 구현은 아직 구현하지 않은 `SearchResultItem.fromJson`에 의존합니다. ::: :::note 모델에서 사용하지 않을 프로퍼티는 포함하지 않습니다. ::: #### Search Result Item 모델 다음으로 `search_result_item.dart`를 생성합니다. :::note 마찬가지로 `SearchResultItem` 구현은 아직 구현하지 않은 `GithubUser.fromJson`에 의존합니다. ::: #### GitHub User 모델 다음으로 `github_user.dart`를 생성합니다. 여기까지 `SearchResult`와 그 의존성 구현을 완료했습니다. 이제 `SearchResultError`로 넘어갑니다. #### Search Result Error 모델 `search_result_error.dart`를 생성합니다. `GithubClient`가 완성되었으니 다음으로 성능 최적화를 위해 [메모이제이션](https://en.wikipedia.org/wiki/Memoization)을 담당할 `GithubCache`로 넘어갑니다. ### GitHub Cache `GithubCache`는 모든 이전 쿼리를 기억하여 GitHub API에 불필요한 네트워크 요청을 피하는 역할을 합니다. 이를 통해 애플리케이션의 성능도 향상됩니다. `github_cache.dart`를 생성합니다. 이제 `GithubRepository`를 만들 준비가 되었습니다! ### GitHub Repository Github Repository는 데이터 레이어(`GithubClient`)와 비즈니스 로직 레이어(`Bloc`) 사이에 추상화를 만드는 역할을 합니다. 여기서 `GithubCache`도 사용하게 됩니다. `github_repository.dart`를 생성합니다. :::note `GithubRepository`는 `GithubCache`와 `GithubClient`에 의존하며 기저 구현을 추상화합니다. 애플리케이션은 데이터가 어디서 어떻게 가져와지는지 알 필요가 없습니다. 인터페이스를 변경하지 않는 한 클라이언트 코드를 변경하지 않고도 리포지토리의 동작 방식을 언제든지 바꿀 수 있습니다. ::: 여기까지 데이터 프로바이더 레이어와 리포지토리 레이어를 완성했으니 이제 비즈니스 로직 레이어로 넘어갑니다. ### GitHub Search 이벤트 Bloc은 사용자가 리포지토리 이름을 입력하면 알림을 받게 되며, 이를 `TextChanged` `GithubSearchEvent`로 나타냅니다. `github_search_event.dart`를 생성합니다. :::note `GithubSearchEvent`의 인스턴스를 비교할 수 있도록 [`Equatable`](https://pub.dev/packages/equatable)을 상속합니다. 기본적으로 동등 연산자는 this와 other가 동일한 인스턴스인 경우에만 true를 반환합니다. ::: ### Github Search 상태 프레젠테이션 레이어가 적절하게 표시되려면 여러 정보가 필요합니다: - `SearchStateEmpty` - 사용자가 아무 입력도 하지 않았음을 프레젠테이션 레이어에 알립니다. - `SearchStateLoading` - 로딩 인디케이터를 표시해야 함을 프레젠테이션 레이어에 알립니다. - `SearchStateSuccess` - 표시할 데이터가 있음을 프레젠테이션 레이어에 알립니다. - `items` - 표시될 `List`입니다. - `SearchStateError` - 리포지토리를 가져오는 중 에러가 발생했음을 프레젠테이션 레이어에 알립니다. - `error` - 발생한 정확한 에러입니다. 이제 `github_search_state.dart`를 생성하고 다음과 같이 구현합니다. :::note `GithubSearchState`의 인스턴스를 비교할 수 있도록 [`Equatable`](https://pub.dev/packages/equatable)을 상속합니다. 기본적으로 동등 연산자는 this와 other가 동일한 인스턴스인 경우에만 true를 반환합니다. ::: 이벤트와 상태가 구현되었으니 이제 `GithubSearchBloc`을 만들 수 있습니다. ### GitHub Search Bloc `github_search_bloc.dart`를 생성합니다: :::note `GithubSearchBloc`은 `GithubSearchEvent`를 `GithubSearchState`로 변환하며 `GithubRepository`에 의존합니다. ::: :::note `GithubSearchEvents`를 [디바운스](https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/debounce.html)하기 위해 커스텀 `EventTransformer`를 생성합니다. `Cubit` 대신 `Bloc`을 만든 이유 중 하나가 바로 스트림 트랜스포머를 활용하기 위해서입니다. ::: `common_github_search` 패키지가 완성되었습니다. 최종 결과물은 [여기](https://github.com/felangel/bloc/tree/master/examples/github_search/common_github_search)에서 확인할 수 있습니다. 다음으로 Flutter 구현을 진행합니다. ## Flutter GitHub Search Flutter Github Search는 `common_github_search`의 모델, 데이터 프로바이더, 리포지토리, bloc을 재사용하여 Github Search를 구현하는 Flutter 애플리케이션입니다. ### 설정 `github_search` 디렉토리 내에서 `common_github_search`와 같은 레벨에 새 Flutter 프로젝트를 생성합니다. 다음으로 필요한 모든 의존성을 포함하도록 `pubspec.yaml`을 업데이트합니다. :::note 새로 만든 `common_github_search` 라이브러리를 의존성으로 포함하고 있습니다. ::: 이제 의존성을 설치합니다. 프로젝트 설정이 완료되었습니다. `common_github_search` 패키지가 데이터 레이어와 비즈니스 로직 레이어를 포함하고 있으므로 프레젠테이션 레이어만 구현하면 됩니다. ### Search Form `_SearchBar`와 `_SearchBody` 위젯이 있는 폼을 만들어야 합니다. - `_SearchBar`는 사용자 입력을 받는 역할을 합니다. - `_SearchBody`는 검색 결과, 로딩 인디케이터, 에러를 표시하는 역할을 합니다. `search_form.dart`를 생성합니다. `SearchForm`은 `_SearchBar`와 `_SearchBody` 위젯을 렌더링하는 `StatelessWidget`입니다. `_SearchBar`도 `StatefulWidget`인데, 사용자가 입력한 내용을 추적하기 위해 자체 `TextEditingController`를 유지해야 하기 때문입니다. `_SearchBody`는 검색 결과, 에러, 로딩 인디케이터를 표시하는 `StatelessWidget`입니다. `GithubSearchBloc`의 소비자가 됩니다. 상태가 `SearchStateSuccess`이면 다음에 구현할 `_SearchResults`를 렌더링합니다. `_SearchResults`는 `List`을 받아 `_SearchResultItems` 리스트로 표시하는 `StatelessWidget`입니다. `_SearchResultItem`은 단일 검색 결과의 정보를 렌더링하는 `StatelessWidget`입니다. 사용자 상호작용을 처리하고 탭 시 리포지토리 URL로 이동하는 역할도 합니다. :::note `_SearchBar`는 `context.read()`을 통해 `GitHubSearchBloc`에 접근하고 bloc에 `TextChanged` 이벤트를 알립니다. ::: :::note `_SearchBody`는 상태 변화에 따라 리빌드하기 위해 `BlocBuilder`를 사용합니다. `BlocBuilder` 객체의 bloc 파라미터가 생략되었으므로 `BlocBuilder`는 `BlocProvider`와 현재 `BuildContext`를 사용하여 자동으로 조회합니다. 더 자세한 내용은 [여기](/ko/flutter-bloc-concepts#blocbuilder)를 참고하세요. ::: :::note 스크롤 가능한 `_SearchResultItem` 리스트를 구성하기 위해 `ListView.builder`를 사용합니다. ::: :::note 외부 URL을 열기 위해 [url_launcher](https://pub.dev/packages/url_launcher) 패키지를 사용합니다. ::: ### 통합하기 이제 `main.dart`에서 메인 앱을 구현하기만 하면 됩니다. :::note `GithubRepository`는 `main`에서 생성되어 `App`에 주입됩니다. `SearchForm`은 `GithubSearchBloc`의 인스턴스를 초기화, 종료하고 `SearchForm` 위젯과 그 자식들이 사용할 수 있도록 하는 `BlocProvider`로 감싸져 있습니다. ::: 이것이 전부입니다! [bloc](https://pub.dev/packages/bloc)과 [flutter_bloc](https://pub.dev/packages/flutter_bloc) 패키지를 사용하여 Flutter에서 GitHub 검색 앱을 성공적으로 구현했고, 프레젠테이션 레이어와 비즈니스 로직을 성공적으로 분리했습니다. 전체 소스 코드는 [여기](https://github.com/felangel/bloc/tree/master/examples/github_search/flutter_github_search)에서 확인할 수 있습니다. 마지막으로 AngularDart GitHub Search 앱을 만들어 봅시다. ## AngularDart GitHub Search AngularDart GitHub Search는 `common_github_search`의 모델, 데이터 프로바이더, 리포지토리, bloc을 재사용하여 Github Search를 구현하는 AngularDart 애플리케이션입니다. ### 설정 `common_github_search`와 같은 레벨의 github_search 디렉토리에 새 AngularDart 프로젝트를 생성합니다. :::note `stagehand`는 다음 명령으로 설치할 수 있습니다: ::: 그런 다음 `pubspec.yaml`의 내용을 다음으로 교체합니다: ### Search Form Flutter 앱과 마찬가지로 `SearchBar`와 `SearchBody` 컴포넌트가 있는 `SearchForm`을 만들어야 합니다. `SearchForm` 컴포넌트는 `GithubSearchBloc`을 생성하고 닫아야 하므로 `OnInit`과 `OnDestroy`를 구현합니다. - `SearchBar`는 사용자 입력을 받는 역할을 합니다. - `SearchBody`는 검색 결과, 로딩 인디케이터, 에러를 표시하는 역할을 합니다. `search_form_component.dart`를 생성합니다. :::note `GithubRepository`가 `SearchFormComponent`에 주입됩니다. ::: :::note `GithubSearchBloc`은 `SearchFormComponent`에서 생성되고 닫힙니다. ::: 템플릿(`search_form_component.html`)은 다음과 같습니다: 다음으로 `SearchBar` 컴포넌트를 구현합니다. ### Search Bar `SearchBar`는 사용자 입력을 받고 `GithubSearchBloc`에 텍스트 변경을 알리는 컴포넌트입니다. `search_bar_component.dart`를 생성합니다. :::note `SearchBarComponent`는 bloc에 `TextChanged` 이벤트를 알리는 역할을 하므로 `GitHubSearchBloc`에 의존합니다. ::: 다음으로 `search_bar_component.html`을 생성합니다. `SearchBar`가 완성되었습니다. 이제 `SearchBody`로 넘어갑니다. ### Search Body `SearchBody`는 검색 결과, 에러, 로딩 인디케이터를 표시하는 컴포넌트입니다. `GithubSearchBloc`의 소비자가 됩니다. `search_body_component.dart`를 생성합니다. :::note `SearchBodyComponent`는 `angular_bloc` bloc 파이프를 통해 `GithubSearchBloc`이 제공하는 `GithubSearchState`에 의존합니다. ::: `search_body_component.html`을 생성합니다. 상태가 `isSuccess`이면 `SearchResults`를 렌더링합니다. 다음에 구현해 봅시다. ### Search Results `SearchResults`는 `List`을 받아 `SearchResultItems` 리스트로 표시하는 컴포넌트입니다. `search_results_component.dart`를 생성합니다. 다음으로 `search_results_component.html`을 생성합니다. :::note `SearchResultItem` 컴포넌트 리스트를 구성하기 위해 `ngFor`를 사용합니다. ::: 이제 `SearchResultItem`을 구현할 차례입니다. ### Search Result Item `SearchResultItem`은 단일 검색 결과의 정보를 렌더링하는 컴포넌트입니다. 사용자 상호작용을 처리하고 탭 시 리포지토리 URL로 이동하는 역할도 합니다. `search_result_item_component.dart`를 생성합니다. 그리고 `search_result_item_component.html`에 해당 템플릿을 작성합니다. ### 통합하기 모든 컴포넌트가 준비되었으니 이제 `app_component.dart`에서 통합합니다. :::note `AppComponent`에서 `GithubRepository`를 생성하고 `SearchForm` 컴포넌트에 주입합니다. ::: 이것이 전부입니다! `bloc`과 `angular_bloc` 패키지를 사용하여 AngularDart에서 GitHub 검색 앱을 성공적으로 구현했고, 프레젠테이션 레이어와 비즈니스 로직을 성공적으로 분리했습니다. 전체 소스 코드는 [여기](https://github.com/felangel/bloc/tree/master/examples/github_search/angular_github_search)에서 확인할 수 있습니다. ## 요약 이번 튜토리얼에서는 Flutter와 AngularDart 앱을 만들면서 두 앱 간에 모든 모델, 데이터 프로바이더, bloc을 공유했습니다. 실제로 두 번 작성해야 했던 것은 프레젠테이션 레이어(UI)뿐이며, 이는 효율성과 개발 속도 면에서 큰 장점입니다. 또한 웹 앱과 모바일 앱이 서로 다른 사용자 경험과 스타일을 갖는 것은 꽤 흔한 일인데, 이 접근 방식은 완전히 다르게 보이는 두 앱이 동일한 데이터와 비즈니스 로직 레이어를 공유하는 것이 얼마나 쉬운지 보여줍니다. 전체 소스 코드는 [여기](https://github.com/felangel/bloc/tree/master/examples/github_search)에서 확인할 수 있습니다. ================================================ FILE: docs/src/content/docs/ko/why-bloc.mdx ================================================ --- title: 왜 Bloc인가? description: 어떤 요소가 Bloc을 견고한 상태 관리 솔루션으로 만드는 지에 대한 개요입니다. sidebar: order: 1 --- Bloc을 사용하면 Business Logic에서 Presentation을 쉽게 분리하여 코드를 _빠르게_, _테스트하기 쉽게_, *재사용 가능*하게 만들 수 있습니다. 프로덕션 품질의 애플리케이션을 구축할 때 상태 관리가 중요해집니다. 개발자로서 우리는 다음과 같은 요구사항을 가질 수 있습니다: - 언제든지 애플리케이션의 상태를 알 수 있어야 합니다. - 모든 케이스를 쉽게 테스트하여 앱이 적절하게 응답하는지 확인해야 합니다. - 데이터 기반 결정을 내릴 수 있도록 애플리케이션의 모든 단일 사용자 상호 작용을 기록해야 합니다. - 애플리케이션 내부와 다른 애플리케이션 전반에서 최대한 효율적으로 작업하고, 구성 요소를 재사용해야 합니다. - 많은 개발자가 동일한 패턴과 규칙에 따라 단일 코드 기반 내에서 원활하게 작업할 수 있어야 합니다. - 빠르고 반응성이 뛰어난 앱을 개발해야 합니다. Bloc은 이러한 모든 요구 사항과 그 이상을 충족하도록 설계되었습니다. 상태 관리 솔루션은 다양하며 어떤 솔루션을 사용할지 결정하는 것은 어려운 작업일 수 있습니다. 완벽한 상태 관리 솔루션은 없습니다! 하지만 중요한 것은 팀과 프로젝트에 가장 적합한 것을 선택하는 것입니다. Bloc은 다음 세 가지 핵심 가치를 염두에 두고 설계되었습니다: - **간단함:** 이해하기 쉽고 다양한 스킬 수준을 가진 개발자가 사용할 수 있습니다. - **강력함:** 더 작은 구성 요소로 구성하여 놀랍고 복잡한 애플리케이션을 만드는 데 도움을 줍니다. - **테스트 가능:** 애플리케이션의 모든 측면을 쉽게 테스트할 수 있으므로 자신있게 테스트를 반복할 수 있습니다. 전반적으로, Bloc은 상태 변경이 발생할 수 있는 시기를 규제하고 애플리케이션 전체에 걸쳐 단일한 상태를 변경하는 방법을 시행함으로써 상태 변경을 예측 가능하게 만들려고 시도합니다. ================================================ FILE: docs/src/content/docs/lint/configuration.mdx ================================================ --- title: Linter Configuration description: Configuring the bloc linter. sidebar: order: 3 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import BlocLintBasicAnalysisOptionsSnippet from '~/components/lint/BlocLintBasicAnalysisOptionsSnippet.astro'; import RunBlocLintInCurrentDirectorySnippet from '~/components/lint/RunBlocLintInCurrentDirectorySnippet.astro'; import RunBlocLintInSrcTestSnippet from '~/components/lint/RunBlocLintInSrcTestSnippet.astro'; import AvoidFlutterImportsWarningSnippet from '~/components/lint/ImportFlutterWarningSnippet.mdx'; import RunBlocLintCounterCubitSnippet from '~/components/lint/RunBlocLintCounterCubitSnippet.astro'; import AvoidFlutterImportsWarningOutputSnippet from '~/components/lint/ImportFlutterWarningOutputSnippet.astro'; By default, the bloc linter will not report any diagnostics unless you have explicitly configured a project's analysis options. To get started, create or modify the existing `analysis_options.yaml` at the root of your project to include a list of rules under the top-level bloc key: Run the linter using the following command in your terminal: The above command will analyze all files in the current directory and its subdirectories, but you can also lint specific files and directories by passing them as command-line arguments: The above command will analyze all code in the `src` and `test` directories. If the `avoid_flutter_imports` rule is enabled, any bloc or cubit file that contains a flutter import will be reported as a warning: You can see the warning by running the `bloc lint` command: The output should look like: :::note Here are all of the supported lint rules: ::: ================================================ FILE: docs/src/content/docs/lint/customizing-rules.mdx ================================================ --- title: Customizing Lint Rules description: Customizing bloc lint rules sidebar: order: 4 --- import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import BlocLintEnablingRulesSnippet from '~/components/lint/BlocLintEnablingRulesSnippet.astro'; import BlocLintDisablingRulesSnippet from '~/components/lint/BlocLintDisablingRulesSnippet.astro'; import BlocLintChangingSeveritySnippet from '~/components/lint/BlocLintChangingSeveritySnippet.astro'; import ImportFlutterInfoSnippet from '~/components/lint/ImportFlutterInfoSnippet.mdx'; import ImportFlutterInfoOutputSnippet from '~/components/lint/ImportFlutterInfoOutputSnippet.astro'; import BlocLintExcludingFilesSnippet from '~/components/lint/BlocLintExcludingFilesSnippet.astro'; import BlocLintIgnoreForLineSnippet from '~/components/lint/BlocLintIgnoreForLineSnippet.astro'; import BlocLintIgnoreForFileSnippet from '~/components/lint/BlocLintIgnoreForFileSnippet.astro'; You can customize the behavior of the bloc linter by changing the severity of individual rules, individually enabling or disabling rules, and excluding files from static analysis. ## Enabling and Disabling Rules The bloc linter supports a growing list of lint rules. Note that lint rules don't have to agree with each other. For example, some developers might prefer to use blocs (`prefer_bloc`) while others might prefer to use cubits (`prefer_cubit`). :::note Unlike static analysis, lint rules might contain false positives. Feel free to report any false positives or other issues by [filing an issue](https://github.com/felangel/bloc/issues/new/choose). ::: ### Enabling Recommended Rules The bloc library provides a set of recommended lint rules as part of the [`bloc_lint`](https://pub.dev/packages/bloc_lint) package. To enable the recommended set of lints add the `bloc_lint` package as a dev dependency: Then edit your `analysis_options.yaml` to include the rule set: :::note When a new version of `bloc_lint` is published, code that previously passed static analysis might start failing. We recommend updating your code to work with the new rules, or you can also optionally enable or disable individual rules. ::: ### Enabling Individual Rules To enable individual rules, add `bloc:` to the `analysis_options.yaml` file as a top-level key and `rules:` as a second-level key. On subsequent lines, specify the rules you want as a YAML list (prefixed with dashes). For example: ### Disabling Individual Rules If you include an existing rule set such as the `recommended` set, you may want to disable one or more included lint rules. Disabling rules is similar to enabling them, but requires the use of a YAML map rather than a list. For example, the following includes the recommended set of lint rules except for `avoid_public_bloc_methods` and additionally enables the `prefer_bloc` rule: ## Customizing Rule Severity You can adjust the severity of any rule like so: Now the same lint rule will be reported with a severity of `info` instead of `warning`: The output of the `bloc lint` command should look like: The supported severity options are: | Severity | Description | | --------- | -------------------------------------------------- | | `error` | Indicates the pattern is not allowed. | | `warning` | Indicates the pattern is suspicious but allowed. | | `info` | Provides information to users but is not a problem | | `hint` | Proposes a better way of achieving a result. | ## Excluding Files Sometimes it's okay for static analysis to fail. For example, you might want to ignore warnings or errors reported in generated code that wasn't written by you and your team. Just like with official Dart lint rules, you can use the `exclude:` analyzer option to exclude files from static analysis. You can either list individual files or use [`glob`](https://pub.dev/packages/glob) patterns. :::note All usage of glob patterns should be relative to the directory containing the corresponding `analysis_options.yaml` file. ::: For example, we can exclude all generated Dart code via the following analysis options: ## Ignoring Rules Just like with official Dart lint rules, you can ignore bloc lint rules for a given file or line of code using `// ignore_for_file` and `// ignore` respectively. :::note To ignore multiple rules for a given line or file, supply a comma-separated list. ::: ### Ignoring Lines We can ignore specific occurrences of rule violations by adding an `ignore` comment either right above the offending line or by appending it to the offending line. For example, we can ignore specific occurrences of `prefer_file_naming_conventions` in a given file: ### Ignoring Files We can ignore all occurrences of rule violations within a file by adding an `ignore_for_file` comment anywhere in the file. For example, we can ignore all occurrences of `prefer_file_naming_conventions` in a given file: ================================================ FILE: docs/src/content/docs/lint/index.mdx ================================================ --- title: Linter Overview description: An introduction to the bloc linter. sidebar: order: 1 --- import AvoidFlutterImportsWarningSnippet from '~/components/lint/ImportFlutterWarningSnippet.mdx'; import AvoidFlutterImportsWarningOutputSnippet from '~/components/lint/ImportFlutterWarningOutputSnippet.astro'; import InstallBlocToolsSnippet from '~/components/lint/InstallBlocToolsSnippet.astro'; import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import RunBlocLintInCurrentDirectorySnippet from '~/components/lint/RunBlocLintInCurrentDirectorySnippet.astro'; Linting is the process of statically analyzing code for potential bugs in addition to programmatic and stylistic errors. Bloc has a built-in linter, which can be used through your IDE or the [`bloc command-line tools`](https://pub.dev/packages/bloc_tools) with the `bloc lint` command. With the help of the bloc linter, you can improve the quality of your codebase and enforce consistency without executing a single line of code. For example, perhaps you accidentally imported a Flutter dependency into your cubit: If properly configured, the bloc linter will point to the import and produce the following warning: In the following sections, we'll cover how to install, configure, and customize the bloc linter so that you can take advantage of static analysis in your codebase. ## Quick Start Get started using the bloc linter in just a few quick and easy steps. :::note In order to start using bloc you must have the [Dart SDK](https://dart.dev/get-dart) installed on your machine. ::: 1. Install the [bloc command-line tools](https://pub.dev/packages/bloc_tools) 1. Install the [bloc_lint](https://pub.dev/packages/bloc_lint) package 1. Add an `analysis_options.yaml` to the root of your project with the recommended rules 1. Run the linter That's all there is to it 🎉 Continue reading for a more in-depth look at configuring and customizing the bloc linter. ================================================ FILE: docs/src/content/docs/lint/installation.mdx ================================================ --- title: Linter Installation description: Installing the bloc linter. sidebar: order: 2 --- import { CardGrid } from '@astrojs/starlight/components'; import Card from '~/components/landing/Card.astro'; import InstallBlocToolsSnippet from '~/components/lint/InstallBlocToolsSnippet.astro'; import BlocToolsLintHelpOutputSnippet from '~/components/lint/BlocToolsLintHelpOutputSnippet.astro'; import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import BlocLintMultipleRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintMultipleRecommendedAnalysisOptionsSnippet.astro'; ## Command-Line Tools To use the linter from the command line, install [`package:bloc_tools`](https://pub.dev/packages/bloc_tools) via the following command: Once the bloc command-line tools have been installed, you can run the bloc linter via the `bloc lint` command: ## Recommended Rule Set To install the recommended lint rule set, install [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) as a dev dependency via the following command: Then, add an `analysis_options.yaml` to the root of your project with the recommended rule set: If needed, you can include multiple rule sets by defining them as a list: ## IDE Integrations The following IDEs officially support the bloc linter and language server to provide instant diagnostics directly within your IDE. Support for the [Bloc VSCode Extension](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) is available in v6.8.0. Support for the [Bloc IntelliJ Plugin](https://plugins.jetbrains.com/plugin/12129-bloc) is available in v4.1.0. ================================================ FILE: docs/src/content/docs/lint-rules/avoid_build_context_extensions.mdx ================================================ --- title: Avoid BuildContext Extensions description: The avoid_build_context_extensions rule. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_build_context_extensions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_build_context_extensions/GoodSnippet.astro';
Avoid using `BuildContext` extensions to access `Bloc` or `Cubit` instances. :::note This lint rule was introduced in version `0.3.0` of [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Rationale For consistency and for the sake of being explicit, prefer directly using the underlying methods instead of `BuildContext` extensions. This is also beneficial for testing because it is not possible to mock an extension method. | extension | explicit method | | ---------------- | ------------------------------------------------------------------- | | `context.read` | `BlocProvider.of(context, listen: false)` | | `context.watch` | `BlocBuilder(...)` or `BlocProvider.of(context)` | | `context.select` | `BlocSelector(...)` | ## Examples **Avoid** using `BuildContext` extensions to interact with `Bloc` or `Cubit` instances. **BAD**: **GOOD**: ## Enable To enable the `avoid_build_context_extensions` rule, add it to your `analysis_options.yaml` under `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/lint-rules/avoid_flutter_imports.mdx ================================================ --- title: Avoid Flutter Imports description: The avoid_flutter_imports bloc lint rule. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_flutter_imports/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_flutter_imports/GoodSnippet.astro';
Avoid introducing dependencies on Flutter within business logic components (`Bloc` or `Cubit` instances). ## Rationale Layering an application is a key part of building a maintainable codebase and helps developers iterate quickly and with confidence. Each layer should have a single responsibility and be able to function and tested in isolation. This allows you to contain changes to specific layers, minimizing the impact on the entire application. As a result, business logic components should generally manage feature state and be decoupled from the UI layer. Events should flow into business logic components from the UI layer and state should flow out of the business logic layer into the UI layer. Keeping business logic components decoupled from Flutter provides the ability to reuse business logic across multiple platforms/frameworks (e.g. Flutter, AngularDart, Jaspr, etc.). ## Examples **DO NOT** import Flutter within your business logic components. **BAD**: **GOOD**: ## Enable To enable the `avoid_flutter_imports` rule, add it to your `analysis_options.yaml` under `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/lint-rules/avoid_public_bloc_methods.mdx ================================================ --- title: Avoid Public Bloc Methods description: The avoid_public_bloc_methods rule. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_public_bloc_methods/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_public_bloc_methods/GoodSnippet.astro';
Avoid exposing public methods on `Bloc` instances. ## Rationale Blocs react to incoming events and emit outgoing states. As a result, the recommended way of communicating with a bloc instance is via the `add` method. In most cases, there's no need to create additional abstractions on top of the `add` API. ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) ## Examples **DO NOT** expose public methods on bloc instances. **BAD**: **GOOD**: ## Enable To enable the `avoid_public_bloc_methods` rule, add it to your `analysis_options.yaml` under `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/lint-rules/avoid_public_fields.mdx ================================================ --- title: Avoid Public Fields description: The avoid_public_fields rule. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_public_fields/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_public_fields/GoodSnippet.astro';
Avoid exposing public fields on `Bloc` and `Cubit` instances. ## Rationale Business logic components maintain their own `state` and emit state changes via the `emit` API. As a result, all public facing state should be exposed via the `state` object. ## Examples **DO NOT** expose public fields on bloc and cubit instances. **BAD**: **GOOD**: ## Enable To enable the `avoid_public_fields` rule, add it to your `analysis_options.yaml` under `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/lint-rules/prefer_bloc.mdx ================================================ --- title: Prefer Bloc description: The prefer_bloc rule. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_bloc/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_bloc/GoodSnippet.astro';
Prefer using `Bloc` instances of `Cubit` instances. ## Rationale This rule is purely a stylistic rule. In some cases, teams may prefer to standardize on just using `Bloc` instances throughout their entire application for consistency. :::tip Learn more about the benefits of `Bloc` in [Core Concepts](/bloc-concepts/#bloc-advantages). ::: ## Examples **Avoid** using `Cubit` instances. **BAD**: **GOOD**: ## Enable To enable the `prefer_bloc` rule, add it to your `analysis_options.yaml` under `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/lint-rules/prefer_build_context_extensions.mdx ================================================ --- title: Prefer BuildContext Extensions description: The prefer_build_context_extensions rule. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_build_context_extensions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_build_context_extensions/GoodSnippet.astro';
Prefer using `BuildContext` extensions to access a `Bloc` or `Repository` instance. :::note This lint rule was introduced in version `0.3.2` of [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Rationale For consistency, prefer using `BuildContext` extensions like `context.read`, `context.watch`, and `context.select` instead of `BlocProvider.of`, `RepositoryProvider.of`, `BlocBuilder` or `BlocSelector`. | explicit method | extension | | ------------------------------------------------------------------- | --------------------- | | `BlocProvider.of(context, listen: false)` | `context.read` | | `BlocBuilder(...)` or `BlocProvider.of(context)` | `context.watch` | | `BlocSelector(...)` | `context.select` | ## Examples **Avoid** using `BlocProvider.of(context)` to access a `Bloc` instance. **BAD**: **GOOD**: ## Enable To enable the `prefer_build_context_extensions` rule, add it to your `analysis_options.yaml` under `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/lint-rules/prefer_cubit.mdx ================================================ --- title: Prefer Cubit description: The prefer_cubit rule. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_cubit/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_cubit/GoodSnippet.astro';
Prefer using `Cubit` instances of `Bloc` instances. ## Rationale This rule is purely a stylistic rule. In some cases, teams may prefer to standardize on just using `Cubit` instances throughout their entire application for consistency. :::tip Learn more about the benefits of `Cubit` in [Core Concepts](/bloc-concepts/#cubit-advantages). ::: ## Examples **Avoid** using `Bloc` instances. **BAD**: **GOOD**: ## Enable To enable the `prefer_cubit` rule, add it to your `analysis_options.yaml` under `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/lint-rules/prefer_file_naming_conventions.mdx ================================================ --- title: Prefer File Naming Conventions description: The prefer_file_naming_conventions rule. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_file_naming_conventions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_file_naming_conventions/GoodSnippet.astro';
Prefer following file naming conventions. :::note This lint rule was introduced in version `0.3.0` of [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Rationale For consistency, ease of maintenance, and separation of concerns prefer to define bloc and cubit instances in their respective Dart files instead of inlining them. :::tip Consider using the `bloc new ` command from [package:bloc_tools](https://pub.dev/packages/bloc_tools) to quickly and consistently generate new bloc/cubit instances. ::: ## Examples **Prefer** declaring bloc/cubit instances in their own respective files. **GOOD**: **BAD**: ## Enable To enable the `prefer_file_naming_conventions` rule, add it to your `analysis_options.yaml` under `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/lint-rules/prefer_void_public_cubit_methods.mdx ================================================ --- title: Prefer Void Public Cubit Methods description: The prefer_void_public_cubit_methods rule. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_void_public_cubit_methods/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_void_public_cubit_methods/GoodSnippet.astro';
Prefer void public methods on `Cubit` instances. :::note This lint rule was introduced in version `0.2.0-dev.2` of [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Rationale Public methods on `Cubit` instances should be used to notify the `Cubit` and initiate state changes via the `emit` method. If the caller needs access to any state information, they should access it from the `state` instead. :::note The following rules are related and are usually enabled in combination with `prefer_void_public_cubit_methods`. - [`avoid_public_bloc_methods`](/lint-rules/avoid_public_bloc_methods) - [`avoid_public_fields`](/lint-rules/avoid_public_fields) ::: ## Examples **Avoid** non-void public methods on `Cubit` instances. **BAD**: **GOOD**: ## Enable To enable the `prefer_void_public_cubit_methods` rule, add it to your `analysis_options.yaml` under `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/migration.mdx ================================================ --- title: Migration Guide description: Migrate to the latest stable version of Bloc. --- import { Code, Tabs, TabItem } from '@astrojs/starlight/components'; :::tip Please refer to the [release log](https://github.com/felangel/bloc/releases) for more information regarding what changed in each release. ::: ## v10.0.0 ### `package:bloc_test` #### ❗✨ Decouple `blocTest` from `BlocBase` :::note[What Changed?] In bloc_test v10.0.0, the `blocTest` API is no longer tightly coupled to `BlocBase`. ::: ##### Rationale `blocTest` should use the core bloc interfaces when possible for increased flexibility and reusability. Previously this wasn't possible because `BlocBase` implemented `StateStreamableSource` which was not enough for `blocTest` due to the internal dependency on the `emit` API. ### `package:hydrated_bloc` #### ❗✨ Support WebAssembly :::note[What Changed?] In hydrated_bloc v10.0.0, support for compiling to WebAssembly (wasm) was added. ::: ##### Rationale It was previously not possible to compile apps to wasm when using `hydrated_bloc`. In v10.0.0, the package was refactored to allow compiling to wasm. **v9.x.x** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` **v10.x.x** ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorageDirectory.web : HydratedStorageDirectory((await getTemporaryDirectory()).path), ); runApp(const App()); } ``` ## v9.0.0 ### `package:bloc` #### ❗🧹 Remove Deprecated APIs :::note[What Changed?] In bloc v9.0.0, all previously deprecated APIs were removed. ::: ##### Summary - `BlocOverrides` removed in favor of `Bloc.observer` and `Bloc.transformer` #### ❗✨ Introduce new `EmittableStateStreamableSource` Interface :::note[What Changed?] In bloc v9.0.0, a new core interface `EmittableStateStreamableSource` was introduced. ::: ##### Rationale `package:bloc_test` was previously tightly coupled to `BlocBase`. The `EmittableStateStreamableSource` interface was introduced in order to allow `blocTest` to be decoupled from the `BlocBase` concrete implementation. ### `package:hydrated_bloc` #### ✨ Reintroduce `HydratedBloc.storage` API :::note[What Changed?] In hydrated_bloc v9.0.0, `HydratedBlocOverrides` was removed in favor of the `HydratedBloc.storage` API.\*\* ::: ##### Rationale Refer to the [rationale for reintroducing the Bloc.observer and Bloc.transformer overrides](/migration#-reintroduce-blocobserver-and-bloctransformer-apis). **v8.x.x** ```dart Future main() async { final storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); HydratedBlocOverrides.runZoned( () => runApp(App()), storage: storage, ); } ``` **v9.0.0** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` ## v8.1.0 ### `package:bloc` #### ✨ Reintroduce `Bloc.observer` and `Bloc.transformer` APIs :::note[What Changed?] In bloc v8.1.0, `BlocOverrides` was deprecated in favor of the `Bloc.observer` and `Bloc.transformer` APIs. ::: ##### Rationale The `BlocOverrides` API was introduced in v8.0.0 in an attempt to support scoping bloc-specific configurations such as `BlocObserver`, `EventTransformer`, and `HydratedStorage`. In pure Dart applications, the changes worked well; however, in Flutter applications the new API caused more problems than it solved. The `BlocOverrides` API was inspired by similar APIs in Flutter/Dart: - [HttpOverrides](https://api.flutter.dev/flutter/dart-io/HttpOverrides-class.html) - [IOOverrides](https://api.flutter.dev/flutter/dart-io/IOOverrides-class.html) **Problems** While it wasn't the primary reason for these changes, the `BlocOverrides` API introduced additional complexity for developers. In addition to increasing the amount of nesting and lines of code needed to achieve the same effect, the `BlocOverrides` API required developers to have a solid understanding of [Zones](https://api.dart.dev/stable/2.17.6/dart-async/Zone-class.html) in Dart. `Zones` are not a beginner-friendly concept and failure to understand how Zones work could lead to the introduction of bugs (such as uninitialized observers, transformers, storage instances). For example, many developers would have something like: ```dart void main() { WidgetsFlutterBinding.ensureInitialized(); BlocOverrides.runZoned(...); } ``` The above code, while appearing harmless, can actually lead to many difficult to track bugs. Whatever zone `WidgetsFlutterBinding.ensureInitialized` is initially called from will be the zone in which gesture events are handled (e.g. `onTap`, `onPressed` callbacks) due to `GestureBinding.initInstances`. This is just one of many issues caused by using `zoneValues`. In addition, Flutter does many things behind the scenes which involve forking/manipulating Zones (especially when running tests) which can lead to unexpected behaviors (and in many cases behaviors that are outside the developer's control -- see issues below). Due to the use of the [runZoned](https://api.flutter.dev/flutter/dart-async/runZoned.html), the transition to the `BlocOverrides` API led to the discovery of several bugs/limitations in Flutter (specifically around Widget and Integration Tests): - https://github.com/flutter/flutter/issues/96939 - https://github.com/flutter/flutter/issues/94123 - https://github.com/flutter/flutter/issues/93676 which affected many developers using the bloc library: - https://github.com/felangel/bloc/issues/3394 - https://github.com/felangel/bloc/issues/3350 - https://github.com/felangel/bloc/issues/3319 **v8.0.x** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` **v8.1.0** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` ## v8.0.0 ### `package:bloc` #### ❗✨ Introduce new `BlocOverrides` API :::note[What Changed?] In bloc v8.0.0, `Bloc.observer` and `Bloc.transformer` were removed in favor of the `BlocOverrides` API. ::: ##### Rationale The previous API used to override the default `BlocObserver` and `EventTransformer` relied on a global singleton for both the `BlocObserver` and `EventTransformer`. As a result, it was not possible to: - Have multiple `BlocObserver` or `EventTransformer` implementations scoped to different parts of the application - Have `BlocObserver` or `EventTransformer` overrides be scoped to a package - If a package were to depend on `package:bloc` and registered its own `BlocObserver`, any consumer of the package would either have to overwrite the package's `BlocObserver` or report to the package's `BlocObserver`. It was also more difficult to test because of the shared global state across tests. Bloc v8.0.0 introduces a `BlocOverrides` class which allows developers to override `BlocObserver` and/or `EventTransformer` for a specific `Zone` rather than relying on a global mutable singleton. **v7.x.x** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` **v8.0.0** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` `Bloc` instances will use the `BlocObserver` and/or `EventTransformer` for the current `Zone` via `BlocOverrides.current`. If there are no `BlocOverrides` for the zone, they will use the existing internal defaults (no change in behavior/functionality). This allows allow each `Zone` to function independently with its own `BlocOverrides`. ```dart BlocOverrides.runZoned( () { // BlocObserverA and eventTransformerA final overrides = BlocOverrides.current; // Blocs in this zone report to BlocObserverA // and use eventTransformerA as the default transformer. // ... // Later... BlocOverrides.runZoned( () { // BlocObserverB and eventTransformerB final overrides = BlocOverrides.current; // Blocs in this zone report to BlocObserverB // and use eventTransformerB as the default transformer. // ... }, blocObserver: BlocObserverB(), eventTransformer: eventTransformerB(), ); }, blocObserver: BlocObserverA(), eventTransformer: eventTransformerA(), ); ``` #### ❗✨ Improve Error Handling and Reporting :::note[What Changed?] In bloc v8.0.0, `BlocUnhandledErrorException` is removed. In addition, any uncaught exceptions are always reported to `onError` and rethrown (regardless of debug or release mode). The `addError` API reports errors to `onError`, but does not treat reported errors as uncaught exceptions. ::: ##### Rationale The goal of these changes is: - make internal unhandled exceptions extremely obvious while still preserving bloc functionality - support `addError` without disrupting control flow Previously, error handling and reporting varied depending on whether the application was running in debug or release mode. In addition, errors reported via `addError` were treated as uncaught exceptions in debug mode which led to a poor developer experience when using the `addError` API (specifically when writing unit tests). In v8.0.0, `addError` can be safely used to report errors and `blocTest` can be used to verify that errors are reported. All errors are still reported to `onError`, however, only uncaught exceptions are rethrown (regardless of debug or release mode). #### ❗🧹 Make `BlocObserver` abstract :::note[What Changed?] In bloc v8.0.0, `BlocObserver` was converted into an `abstract` class which means an instance of `BlocObserver` cannot be instantiated. ::: ##### Rationale `BlocObserver` was intended to be an interface. Since the default API implementation are no-ops, `BlocObserver` is now an `abstract` class to clearly communicate that the class is meant to be extended and not directly instantiated. **v7.x.x** ```dart void main() { // It was possible to create an instance of the base class. final observer = BlocObserver(); } ``` **v8.0.0** ```dart class MyBlocObserver extends BlocObserver {...} void main() { // Cannot instantiate the base class. final observer = BlocObserver(); // ERROR // Extend `BlocObserver` instead. final observer = MyBlocObserver(); // OK } ``` #### ❗✨ `add` throws `StateError` if Bloc is closed :::note[What Changed?] In bloc v8.0.0, calling `add` on a closed bloc will result in a `StateError`. ::: ##### Rationale Previously, it was possible to call `add` on a closed bloc and the internal error would get swallowed, making it difficult to debug why the added event was not being processed. In order to make this scenario more visible, in v8.0.0, calling `add` on a closed bloc will throw a `StateError` which will be reported as an uncaught exception and propagated to `onError`. #### ❗✨ `emit` throws `StateError` if Bloc is closed :::note[What Changed?] In bloc v8.0.0, calling `emit` within a closed bloc will result in a `StateError`. ::: ##### Rationale Previously, it was possible to call `emit` within a closed bloc and no state change would occur but there would also be no indication of what went wrong, making it difficult to debug. In order to make this scenario more visible, in v8.0.0, calling `emit` within a closed bloc will throw a `StateError` which will be reported as an uncaught exception and propagated to `onError`. #### ❗🧹 Remove Deprecated APIs :::note[What Changed?] In bloc v8.0.0, all previously deprecated APIs were removed. ::: ##### Summary - `mapEventToState` removed in favor of `on` - `transformEvents` removed in favor of `EventTransformer` API - `TransitionFunction` typedef removed in favor of `EventTransformer` API - `listen` removed in favor of `stream.listen` ### `package:bloc_test` #### ✨ `MockBloc` and `MockCubit` no longer require `registerFallbackValue` :::note[What Changed?] In bloc_test v9.0.0, developers no longer need to explicitly call `registerFallbackValue` when using `MockBloc` or `MockCubit`. ::: ##### Summary `registerFallbackValue` is only needed when using the `any()` matcher from `package:mocktail` for a custom type. Previously, `registerFallbackValue` was needed for every `Event` and `State` when using `MockBloc` or `MockCubit`. **v8.x.x** ```dart class FakeMyEvent extends Fake implements MyEvent {} class FakeMyState extends Fake implements MyState {} class MyMockBloc extends MockBloc implements MyBloc {} void main() { setUpAll(() { registerFallbackValue(FakeMyEvent()); registerFallbackValue(FakeMyState()); }); // Tests... } ``` **v9.0.0** ```dart class MyMockBloc extends MockBloc implements MyBloc {} void main() { // Tests... } ``` ### `package:hydrated_bloc` #### ❗✨ Introduce new `HydratedBlocOverrides` API :::note[What Changed?] In hydrated_bloc v8.0.0, `HydratedBloc.storage` was removed in favor of the `HydratedBlocOverrides` API. ::: ##### Rationale Previously, a global singleton was used to override the `Storage` implementation. As a result, it was not possible to have multiple `Storage` implementations scoped to different parts of the application. It was also more difficult to test because of the shared global state across tests. `HydratedBloc` v8.0.0 introduces a `HydratedBlocOverrides` class which allows developers to override `Storage` for a specific `Zone` rather than relying on a global mutable singleton. **v7.x.x** ```dart void main() async { HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); // ... } ``` **v8.0.0** ```dart void main() { final storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); HydratedBlocOverrides.runZoned( () { // ... }, storage: storage, ); } ``` `HydratedBloc` instances will use the `Storage` for the current `Zone` via `HydratedBlocOverrides.current`. This allows allow each `Zone` to function independently with its own `BlocOverrides`. ## v7.2.0 ### `package:bloc` #### ✨ Introduce new `on` API :::note[What Changed?] In bloc v7.2.0, `mapEventToState` was deprecated in favor of `on`. `mapEventToState` will be removed in bloc v8.0.0. ::: ##### Rationale The `on` API was introduced as part of [[Proposal] Replace mapEventToState with on\ in Bloc](https://github.com/felangel/bloc/issues/2526). Due to [an issue in Dart](https://github.com/dart-lang/sdk/issues/44616) it's not always obvious what the value of `state` will be when dealing with nested async generators (`async*`). Even though there are ways to work around the issue, one of the core principles of the bloc library is to be predictable. The `on` API was created to make the library as safe as possible to use and to eliminate any uncertainty when it comes to state changes. :::tip For more information, [read the full proposal](https://github.com/felangel/bloc/issues/2526). ::: **Summary** `on` allows you to register an event handler for all events of type `E`. By default, events will be processed concurrently when using `on` as opposed to `mapEventToState` which processes events `sequentially`. **v7.1.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0); @override Stream mapEventToState(CounterEvent event) async* { if (event is Increment) { yield state + 1; } } } ``` **v7.2.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } } ``` :::note Each registered `EventHandler` functions independently so it's important to register event handlers based on the type of transformer you'd like applied. ::: If you want to retain the exact same behavior as in v7.1.0 you can register a single event handler for all events and apply a `sequential` transformer: ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; class MyBloc extends Bloc { MyBloc() : super(MyState()) { on(_onEvent, transformer: sequential()) } FutureOr _onEvent(MyEvent event, Emitter emit) async { // TODO: logic goes here... } } ``` You can also override the default `EventTransformer` for all blocs in your application: ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; void main() { Bloc.transformer = sequential(); ... } ``` #### ✨ Introduce new `EventTransformer` API :::note[What Changed?] In bloc v7.2.0, `transformEvents` was deprecated in favor of the `EventTransformer` API. `transformEvents` will be removed in bloc v8.0.0. ::: ##### Rationale The `on` API opened the door to being able to provide a custom event transformer per event handler. A new `EventTransformer` typedef was introduced which enables developers to transform the incoming event stream for each event handler rather than having to specify a single event transformer for all events. **Summary** An `EventTransformer` is responsible for taking the incoming stream of events along with an `EventMapper` (your event handler) and returning a new stream of events. ```dart typedef EventTransformer = Stream Function(Stream events, EventMapper mapper) ``` The default `EventTransformer` processes all events concurrently and looks something like: ```dart EventTransformer concurrent() { return (events, mapper) => events.flatMap(mapper); } ``` :::tip Check out [package:bloc_concurrency](https://pub.dev/packages/bloc_concurrency) for an opinionated set of custom event transformers ::: **v7.1.0** ```dart @override Stream> transformEvents(events, transitionFn) { return events .debounceTime(const Duration(milliseconds: 300)) .flatMap(transitionFn); } ``` **v7.2.0** ```dart /// Define a custom `EventTransformer` EventTransformer debounce(Duration duration) { return (events, mapper) => events.debounceTime(duration).flatMap(mapper); } MyBloc() : super(MyState()) { /// Apply the custom `EventTransformer` to the `EventHandler` on(_onEvent, transformer: debounce(const Duration(milliseconds: 300))) } ``` #### ⚠️ Deprecate `transformTransitions` API :::note[What Changed?] In bloc v7.2.0, `transformTransitions` was deprecated in favor of overriding the `stream` API. `transformTransitions` will be removed in bloc v8.0.0. ::: ##### Rationale The `stream` getter on `Bloc` makes it easy to override the outbound stream of states therefore it's no longer valuable to maintain a separate `transformTransitions` API. **Summary** **v7.1.0** ```dart @override Stream> transformTransitions( Stream> transitions, ) { return transitions.debounceTime(const Duration(milliseconds: 42)); } ``` **v7.2.0** ```dart @override Stream get stream => super.stream.debounceTime(const Duration(milliseconds: 42)); ``` ## v7.0.0 ### `package:bloc` #### ❗ Bloc and Cubit extend BlocBase ##### Rationale As a developer, the relationship between blocs and cubits was a bit awkward. When cubit was first introduced it began as the base class for blocs which made sense because it had a subset of the functionality and blocs would just extend Cubit and define additional APIs. This came with a few drawbacks: - All APIs would either have to be renamed to accept a cubit for accuracy or they would need to be kept as bloc for consistency even though hierarchically it is inaccurate ([#1708](https://github.com/felangel/bloc/issues/1708), [#1560](https://github.com/felangel/bloc/issues/1560)). - Cubit would need to extend Stream and implement EventSink in order to have a common base which widgets like BlocBuilder, BlocListener, etc. can be implemented against ([#1429](https://github.com/felangel/bloc/issues/1429)). Later, we experimented with inverting the relationship and making bloc the base class which partially resolved the first bullet above but introduced other issues: - The cubit API is bloated due to the underlying bloc APIs like mapEventToState, add, etc. ([#2228](https://github.com/felangel/bloc/issues/2228)) - Developers can technically invoke these APIs and break things - We still have the same issue of cubit exposing the entire stream API as before ([#1429](https://github.com/felangel/bloc/issues/1429)) To address these issues we introduced a base class for both `Bloc` and `Cubit` called `BlocBase` so that upstream components can still interoperate with both bloc and cubit instances but without exposing the entire `Stream` and `EventSink` API directly. **Summary** **BlocObserver** **v6.1.x** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(Cubit cubit) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(Cubit cubit, Object event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(Cubit cubit, Object error, StackTrace stackTrace) {...} @override void onClose(Cubit cubit) {...} } ``` **v7.0.0** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(BlocBase bloc) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(BlocBase bloc, Object? event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) {...} @override void onClose(BlocBase bloc) {...} } ``` **Bloc/Cubit** **v6.1.x** ```dart final bloc = MyBloc(); bloc.listen((state) {...}); final cubit = MyCubit(); cubit.listen((state) {...}); ``` **v7.0.0** ```dart final bloc = MyBloc(); bloc.stream.listen((state) {...}); final cubit = MyCubit(); cubit.stream.listen((state) {...}); ``` ### `package:bloc_test` #### ❗seed returns a function to support dynamic values ##### Rationale In order to support having a mutable seed value which can be updated dynamically in `setUp`, `seed` returns a function. **Summary** **v7.x.x** ```dart blocTest( '...', seed: MyState(), ... ); ``` **v8.0.0** ```dart blocTest( '...', seed: () => MyState(), ... ); ``` #### ❗expect returns a function to support dynamic values and includes matcher support ##### Rationale In order to support having a mutable expectation which can be updated dynamically in `setUp`, `expect` returns a function. `expect` also supports `Matchers`. **Summary** **v7.x.x** ```dart blocTest( '...', expect: [MyStateA(), MyStateB()], ... ); ``` **v8.0.0** ```dart blocTest( '...', expect: () => [MyStateA(), MyStateB()], ... ); // It can also be a `Matcher` blocTest( '...', expect: () => contains(MyStateA()), ... ); ``` #### ❗errors returns a function to support dynamic values and includes matcher support ##### Rationale In order to support having a mutable errors which can be updated dynamically in `setUp`, `errors` returns a function. `errors` also supports `Matchers`. **Summary** **v7.x.x** ```dart blocTest( '...', errors: [MyError()], ... ); ``` **v8.0.0** ```dart blocTest( '...', errors: () => [MyError()], ... ); // It can also be a `Matcher` blocTest( '...', errors: () => contains(MyError()), ... ); ``` #### ❗MockBloc and MockCubit ##### Rationale To support stubbing of various core APIs, `MockBloc` and `MockCubit` are exported as part of the `bloc_test` package. Previously, `MockBloc` had to be used for both `Bloc` and `Cubit` instances which was not intuitive. **Summary** **v7.x.x** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockBloc implements MyBloc {} ``` **v8.0.0** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockCubit implements MyCubit {} ``` #### ❗Mocktail Integration ##### Rationale Due to various limitations of the null-safe [package:mockito](https://pub.dev/packages/mockito) described [here](https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#problems-with-typical-mocking-and-stubbing), [package:mocktail](https://pub.dev/packages/mocktail) is used by `MockBloc` and `MockCubit`. This allows developers to continue using a familiar mocking API without the need to manually write stubs or rely on code generation. **Summary** **v7.x.x** ```dart import 'package:mockito/mockito.dart'; ... when(bloc.state).thenReturn(MyState()); verify(bloc.add(any)).called(1); ``` **v8.0.0** ```dart import 'package:mocktail/mocktail.dart'; ... when(() => bloc.state).thenReturn(MyState()); verify(() => bloc.add(any())).called(1); ``` > Please refer to [#347](https://github.com/dart-lang/mockito/issues/347) as > well as the > [mocktail documentation](https://github.com/felangel/mocktail/tree/main/packages/mocktail) > for more information. ### `package:flutter_bloc` #### ❗ rename `cubit` parameter to `bloc` ##### Rationale As a result of the refactor in `package:bloc` to introduce `BlocBase` which `Bloc` and `Cubit` extend, the parameters of `BlocBuilder`, `BlocConsumer`, and `BlocListener` were renamed from `cubit` to `bloc` because the widgets operate on the `BlocBase` type. This also further aligns with the library name and hopefully improves readability. **Summary** **v6.1.x** ```dart BlocBuilder( cubit: myBloc, ... ) BlocListener( cubit: myBloc, ... ) BlocConsumer( cubit: myBloc, ... ) ``` **v7.0.0** ```dart BlocBuilder( bloc: myBloc, ... ) BlocListener( bloc: myBloc, ... ) BlocConsumer( bloc: myBloc, ... ) ``` ### `package:hydrated_bloc` #### ❗storageDirectory is required when calling HydratedStorage.build ##### Rationale In order to make `package:hydrated_bloc` a pure Dart package, the dependency on [package:path_provider](https://pub.dev/packages/path_provider) was removed and the `storageDirectory` parameter when calling `HydratedStorage.build` is required and no longer defaults to `getTemporaryDirectory`. **Summary** **v6.x.x** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` **v7.0.0** ```dart import 'package:path_provider/path_provider.dart'; ... HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getTemporaryDirectory(), ); ``` ## v6.1.0 ### `package:flutter_bloc` #### ❗context.bloc and context.repository are deprecated in favor of context.read and context.watch ##### Rationale `context.read`, `context.watch`, and `context.select` were added to align with the existing [provider](https://pub.dev/packages/provider) API which many developers are familiar and to address issues that have been raised by the community. To improve the safety of the code and maintain consistency, `context.bloc` was deprecated because it can be replaced with either `context.read` or `context.watch` dependending on if it's used directly within `build`. **context.watch** `context.watch` addresses the request to have a [MultiBlocBuilder](https://github.com/felangel/bloc/issues/538) because we can watch several blocs within a single `Builder` in order to render UI based on multiple states: ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // return a Widget which depends on the state of BlocA, BlocB, and BlocC } ); ``` **context.select** `context.select` allows developers to render/update UI based on a part of a bloc state and addresses the request to have a [simpler buildWhen](https://github.com/felangel/bloc/issues/1521). ```dart final name = context.select((UserBloc bloc) => bloc.state.user.name); ``` The above snippet allows us to access and rebuild the widget only when the current user's name changes. **context.read** Even though it looks like `context.read` is identical to `context.bloc` there are some subtle but significant differences. Both allow you to access a bloc with a `BuildContext` and do not result in rebuilds; however, `context.read` cannot be called directly within a `build` method. There are two main reasons to use `context.bloc` within `build`: 1. **To access the bloc's state** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` The above usage is error prone because the `Text` widget will not be rebuilt if the state of the bloc changes. In this scenario, either a `BlocBuilder` or `context.watch` should be used. ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` or ```dart @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) => Text('$state'), ); } ``` :::note Using `context.watch` at the root of the `build` method will result in the entire widget being rebuilt when the bloc state changes. If the entire widget does not need to be rebuilt, either use `BlocBuilder` to wrap the parts that should rebuild, use a `Builder` with `context.watch` to scope the rebuilds, or decompose the widget into smaller widgets. ::: 2. **To access the bloc so that an event can be added** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` The above usage is inefficient because it results in a bloc lookup on each rebuild when the bloc is only needed when the user taps the `ElevatedButton`. In this scenario, prefer to use `context.read` to access the bloc directly where it is needed (in this case, in the `onPressed` callback). ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` **Summary** **v6.0.x** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` ?> If accessing a bloc to add an event, perform the bloc access using `context.read` in the callback where it is needed. **v6.0.x** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` ?> Use `context.watch` when accessing the state of the bloc in order to ensure the widget is rebuilt when the state changes. ## v6.0.0 ### `package:bloc` #### ❗BlocObserver onError takes Cubit ##### Rationale Due to the integration of `Cubit`, `onError` is now shared between both `Bloc` and `Cubit` instances. Since `Cubit` is the base, `BlocObserver` will accept a `Cubit` type rather than a `Bloc` type in the `onError` override. **v5.x.x** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Bloc bloc, Object error, StackTrace stackTrace) { super.onError(bloc, error, stackTrace); } } ``` **v6.0.0** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Cubit cubit, Object error, StackTrace stackTrace) { super.onError(cubit, error, stackTrace); } } ``` #### ❗Bloc does not emit last state on subscription ##### Rationale This change was made to align `Bloc` and `Cubit` with the built-in `Stream` behavior in `Dart`. In addition, conforming this the old behavior in the context of `Cubit` led to many unintended side-effects and overall complicated the internal implementations of other packages such as `flutter_bloc` and `bloc_test` unnecessarily (requiring `skip(1)`, etc...). **v5.x.x** ```dart final bloc = MyBloc(); bloc.listen(print); ``` Previously, the above snippet would output the initial state of the bloc followed by subsequent state changes. **v6.x.x** In v6.0.0, the above snippet does not output the initial state and only outputs subsequent state changes. The previous behavior can be achieved with the following: ```dart final bloc = MyBloc(); print(bloc.state); bloc.listen(print); ``` ?> **Note**: This change will only affect code that relies on direct bloc subscriptions. When using `BlocBuilder`, `BlocListener`, or `BlocConsumer` there will be no noticeable change in behavior. ### `package:bloc_test` #### ❗MockBloc only requires State type ##### Rationale It is not necessary and eliminates extra code while also making `MockBloc` compatible with `Cubit`. **v5.x.x** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` **v6.0.0** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` #### ❗whenListen only requires State type ##### Rationale It is not necessary and eliminates extra code while also making `whenListen` compatible with `Cubit`. **v5.x.x** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` **v6.0.0** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` #### ❗blocTest does not require Event type ##### Rationale It is not necessary and eliminates extra code while also making `blocTest` compatible with `Cubit`. **v5.x.x** ```dart blocTest( 'emits [1] when increment is called', build: () async => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` **v6.0.0** ```dart blocTest( 'emits [1] when increment is called', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` #### ❗blocTest skip defaults to 0 ##### Rationale Since `bloc` and `cubit` instances will no longer emit the latest state for new subscriptions, it was no longer necessary to default `skip` to `1`. **v5.x.x** ```dart blocTest( 'emits [0] when skip is 0', build: () async => CounterBloc(), skip: 0, expect: const [0], ); ``` **v6.0.0** ```dart blocTest( 'emits [] when skip is 0', build: () => CounterBloc(), skip: 0, expect: const [], ); ``` The initial state of a bloc or cubit can be tested with the following: ```dart test('initial state is correct', () { expect(MyBloc().state, InitialState()); }); ``` #### ❗blocTest make build synchronous ##### Rationale Previously, `build` was made `async` so that various preparation could be done to put the bloc under test in a specific state. It is no longer necessary and also resolves several issues due to the added latency between the build and the subscription internally. Instead of doing async prep to get a bloc in a desired state we can now set the bloc state by chaining `emit` with the desired state. **v5.x.x** ```dart blocTest( 'emits [2] when increment is added', build: () async { final bloc = CounterBloc(); bloc.add(CounterEvent.increment); await bloc.take(2); return bloc; } act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` **v6.0.0** ```dart blocTest( 'emits [2] when increment is added', build: () => CounterBloc()..emit(1), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` :::note `emit` is only visible for testing and should never be used outside of tests. ::: ### `package:flutter_bloc` #### ❗BlocBuilder bloc parameter renamed to cubit ##### Rationale In order to make `BlocBuilder` interoperate with `bloc` and `cubit` instances the `bloc` parameter was renamed to `cubit` (since `Cubit` is the base class). **v5.x.x** ```dart BlocBuilder( bloc: myBloc, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocBuilder( cubit: myBloc, builder: (context, state) {...} ) ``` #### ❗BlocListener bloc parameter renamed to cubit ##### Rationale In order to make `BlocListener` interoperate with `bloc` and `cubit` instances the `bloc` parameter was renamed to `cubit` (since `Cubit` is the base class). **v5.x.x** ```dart BlocListener( bloc: myBloc, listener: (context, state) {...} ) ``` **v6.0.0** ```dart BlocListener( cubit: myBloc, listener: (context, state) {...} ) ``` #### ❗BlocConsumer bloc parameter renamed to cubit ##### Rationale In order to make `BlocConsumer` interoperate with `bloc` and `cubit` instances the `bloc` parameter was renamed to `cubit` (since `Cubit` is the base class). **v5.x.x** ```dart BlocConsumer( bloc: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocConsumer( cubit: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` --- ## v5.0.0 ### `package:bloc` #### ❗initialState has been removed ##### Rationale As a developer, having to override `initialState` when creating a bloc presents two main issues: - The `initialState` of the bloc can be dynamic and can also be referenced at a later point in time (even outside of the bloc itself). In some ways, this can be viewed as leaking internal bloc information to the UI layer. - It's verbose. **v4.x.x** ```dart class CounterBloc extends Bloc { @override int get initialState => 0; ... } ``` **v5.0.0** ```dart class CounterBloc extends Bloc { CounterBloc() : super(0); ... } ``` ?> For more information check out [#1304](https://github.com/felangel/bloc/issues/1304) #### ❗BlocDelegate renamed to BlocObserver ##### Rationale The name `BlocDelegate` was not an accurate description of the role that the class played. `BlocDelegate` suggests that the class plays an active role whereas in reality the intended role of the `BlocDelegate` was for it to be a passive component which simply observes all blocs in an application. :::note There should ideally be no user-facing functionality or features handled within `BlocObserver`. ::: **v4.x.x** ```dart class MyBlocDelegate extends BlocDelegate { ... } ``` **v5.0.0** ```dart class MyBlocObserver extends BlocObserver { ... } ``` #### ❗BlocSupervisor has been removed ##### Rationale `BlocSupervisor` was yet another component that developers had to know about and interact with for the sole purpose of specifying a custom `BlocDelegate`. With the change to `BlocObserver` we felt it improved the developer experience to set the observer directly on the bloc itself. ?> This changed also enabled us to decouple other bloc add-ons like `HydratedStorage` from the `BlocObserver`. **v4.x.x** ```dart BlocSupervisor.delegate = MyBlocDelegate(); ``` **v5.0.0** ```dart Bloc.observer = MyBlocObserver(); ``` ### `package:flutter_bloc` #### ❗BlocBuilder condition renamed to buildWhen ##### Rationale When using `BlocBuilder`, we previously could specify a `condition` to determine whether the `builder` should rebuild. ```dart BlocBuilder( condition: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...} ) ``` The name `condition` is not very self-explanatory or obvious and more importantly, when interacting with a `BlocConsumer` the API became inconsistent because developers can provide two conditions (one for `builder` and one for `listener`). As a result, the `BlocConsumer` API exposed a `buildWhen` and `listenWhen` ```dart BlocConsumer( listenWhen: (previous, current) { // return true/false to determine whether to call listener }, listener: (context, state) {...}, buildWhen: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...}, ) ``` In order to align the API and provide a more consistent developer experience, `condition` was renamed to `buildWhen`. **v4.x.x** ```dart BlocBuilder( condition: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...} ) ``` **v5.0.0** ```dart BlocBuilder( buildWhen: (previous, current) { // return true/false to determine whether to call builder }, builder: (context, state) {...} ) ``` #### ❗BlocListener condition renamed to listenWhen ##### Rationale For the same reasons as described above, the `BlocListener` condition was also renamed. **v4.x.x** ```dart BlocListener( condition: (previous, current) { // return true/false to determine whether to call listener }, listener: (context, state) {...} ) ``` **v5.0.0** ```dart BlocListener( listenWhen: (previous, current) { // return true/false to determine whether to call listener }, listener: (context, state) {...} ) ``` ### `package:hydrated_bloc` #### ❗HydratedStorage and HydratedBlocStorage renamed ##### Rationale In order to improve code reuse between [hydrated_bloc](https://pub.dev/packages/hydrated_bloc) and [hydrated_cubit](https://pub.dev/packages/hydrated_cubit), the concrete default storage implementation was renamed from `HydratedBlocStorage` to `HydratedStorage`. In addition, the `HydratedStorage` interface was renamed from `HydratedStorage` to `Storage`. **v4.0.0** ```dart class MyHydratedStorage implements HydratedStorage { ... } ``` **v5.0.0** ```dart class MyHydratedStorage implements Storage { ... } ``` #### ❗HydratedStorage decoupled from BlocDelegate ##### Rationale As mentioned earlier, `BlocDelegate` was renamed to `BlocObserver` and was set directly as part of the `bloc` via: ```dart Bloc.observer = MyBlocObserver(); ``` The following change was made to: - Stay consistent with the new bloc observer API - Keep the storage scoped to just `HydratedBloc` - Decouple the `BlocObserver` from `Storage` **v4.0.0** ```dart BlocSupervisor.delegate = await HydratedBlocDelegate.build(); ``` **v5.0.0** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` #### ❗Simplified Initialization ##### Rationale Previously, developers had to manually call `super.initialState ?? DefaultInitialState()` in order to setup their `HydratedBloc` instances. This is clunky and verbose and also incompatible with the breaking changes to `initialState` in `bloc`. As a result, in v5.0.0 `HydratedBloc` initialization is identical to normal `Bloc` initialization. **v4.0.0** ```dart class CounterBloc extends HydratedBloc { @override int get initialState => super.initialState ?? 0; } ``` **v5.0.0** ```dart class CounterBloc extends HydratedBloc { CounterBloc() : super(0); ... } ``` ================================================ FILE: docs/src/content/docs/modeling-state.mdx ================================================ --- title: Modeling State description: An overview of several ways to model states when using package:bloc. --- import ConcreteClassAndStatusEnumSnippet from '~/components/modeling-state/ConcreteClassAndStatusEnumSnippet.astro'; import SealedClassAndSubclassesSnippet from '~/components/modeling-state/SealedClassAndSubclassesSnippet.astro'; There are many different approaches when it comes to structuring application state. Each has its own advantages and drawbacks. In this section, we'll take a look at several approaches, their pros and cons, and when to use each one. The following approaches are simply recommendations and are completely optional. Feel free to use whatever approach you prefer. You may find some of the examples/documentation do not follow the approaches mainly for simplicity/conciseness. :::tip The following code snippets are focused on the state structure. In practice, you may also want to: - Extend `Equatable` from [`package:equatable`](https://pub.dev/packages/equatable) - Annotate the class with `@Data()` from [`package:data_class`](https://pub.dev/packages/data_class) - Annotate the class with **@immutable** from [`package:meta`](https://pub.dev/packages/meta) - Implement a `copyWith` method - Use the `const` keyword for constructors ::: ## Concrete Class and Status Enum This approach consists of a **single concrete class** for all states along with an `enum` representing different statuses. Properties are made nullable and are handled based on the current status. This approach works best for states which are not strictly exclusive and/or contain lots of shared properties. #### Pros - **Simple**: Easy to manage a single class and a status enum and all properties are readily accessible. - **Concise**: Generally requires fewer lines of code as compared to other approaches. #### Cons - **Not Type Safe**: Requires checking the `status` before accessing properties. It's possible to `emit` a malformed state which can lead to bugs. Properties for specific states are nullable, which can be cumbersome to manage and requires either force unwrapping or performing null checks. Some of these cons can be mitigated by writing unit tests and writing specialized, named constructors. - **Bloated**: Results in a single state that can become bloated with many properties over time. #### Verdict This approach works best for simple states or when the requirements call for states that aren't exclusive (e.g. showing a snackbar when an error occurs while still showing old data from the last success state). This approach provides flexibility and conciseness at the cost of type safety. ## Sealed Class and Subclasses This approach consists of a **sealed class** that holds any shared properties and multiple subclasses for the separate states. This approach is great for separate, exclusive states. #### Pros - **Type Safe**: The code is compile-safe and it's not possible to accidentally access an invalid property. Each subclass holds its own properties, making it clear which properties belong to which state. - **Explicit:** Separates shared properties from state-specific properties. - **Exhaustive**: Using a `switch` statement for exhaustiveness checks to ensure that each state is explicitly handled. - If you don't want [exhaustive switching](https://dart.dev/language/branches#exhaustiveness-checking) or want to be able to add subtypes later without breaking the API, use the [final](https://dart.dev/language/class-modifiers#final) modifier. - See the [sealed class documentation](https://dart.dev/language/class-modifiers#sealed) for more details. #### Cons - **Verbose**: Requires more code (one base class and a subclass per state). Also may require duplicate code for shared properties across subclasses. - **Complex**: Adding new properties requires updating each subclass and the base class, which can be cumbersome and lead to increases in complexity of the state. In addition, may require unnecessary/excessive type checking to access properties. #### Verdict This approach works best for well-defined, exclusive states with unique properties. This approach provides type safety & exhaustiveness checks and emphasizes safety over conciseness and simplicity. ================================================ FILE: docs/src/content/docs/naming-conventions.mdx ================================================ --- title: Naming Conventions description: Overview of the recommended naming conventions when using bloc. --- import EventExamplesGood1 from '~/components/naming-conventions/EventExamplesGood1Snippet.astro'; import EventExamplesBad1 from '~/components/naming-conventions/EventExamplesBad1Snippet.astro'; import StateExamplesGood1Snippet from '~/components/naming-conventions/StateExamplesGood1Snippet.astro'; import SingleStateExamplesGood1Snippet from '~/components/naming-conventions/SingleStateExamplesGood1Snippet.astro'; import StateExamplesBad1Snippet from '~/components/naming-conventions/StateExamplesBad1Snippet.astro'; The following naming conventions are simply recommendations and are completely optional. Feel free to use whatever naming conventions you prefer. You may find some of the examples/documentation do not follow the naming conventions mainly for simplicity/conciseness. These conventions are strongly recommended for large projects with multiple developers. ## Event Conventions Events should be named in the **past tense** because events are things that have already occurred from the bloc's perspective. ### Anatomy `BlocSubject` + `Noun (optional)` + `Verb (event)` Initial load events should follow the convention: `BlocSubject` + `Started` :::note The base event class should be name: `BlocSubject` + `Event`. ::: ### Examples ✅ **Good** ❌ **Bad** ## State Conventions States should be nouns because a state is just a snapshot at a particular point in time. There are two common ways to represent state: using subclasses or using a single class. ### Anatomy #### Subclasses `BlocSubject` + `Verb (action)` + `State` When representing the state as multiple subclasses `State` should be one of the following: `Initial` | `Success` | `Failure` | `InProgress` :::note Initial states should follow the convention: `BlocSubject` + `Initial`. ::: #### Single Class `BlocSubject` + `State` When representing the state as a single base class an enum named `BlocSubject` + `Status` should be used to represent the status of the state: `initial` | `success` | `failure` | `loading`. :::note The base state class should always be named: `BlocSubject` + `State`. ::: ### Examples ✅ **Good** ##### Subclasses ##### Single Class ❌ **Bad** ================================================ FILE: docs/src/content/docs/pt-br/architecture.mdx ================================================ --- title: Arquitetura description: Visão geral dos padrões de arquitetura recomendados ao usar bloc. --- import DataProviderSnippet from '~/components/architecture/DataProviderSnippet.astro'; import RepositorySnippet from '~/components/architecture/RepositorySnippet.astro'; import BusinessLogicComponentSnippet from '~/components/architecture/BusinessLogicComponentSnippet.astro'; import BlocTightCouplingSnippet from '~/components/architecture/BlocTightCouplingSnippet.astro'; import BlocLooseCouplingPresentationSnippet from '~/components/architecture/BlocLooseCouplingPresentationSnippet.astro'; import AppIdeasRepositorySnippet from '~/components/architecture/AppIdeasRepositorySnippet.astro'; import AppIdeaRankingBlocSnippet from '~/components/architecture/AppIdeaRankingBlocSnippet.astro'; import PresentationComponentSnippet from '~/components/architecture/PresentationComponentSnippet.astro'; ![Arquitetura Bloc](~/assets/concepts/bloc_architecture_full.png) Usar a biblioteca bloc nos permite separar nossa aplicação em três camadas: - Apresentação - Lógica de Negócios - Dados - Repositório - Provedor de Dados Começaremos na camada de nível mais baixo (mais distante da interface do usuário) e avançaremos até a camada de apresentação. ## Camada de Dados A responsabilidade da camada de dados é recuperar/manipular dados de uma ou mais fontes. A camada de dados pode ser dividida em duas partes: - Repositório - Provedor de Dados Esta camada é o nível mais baixo da aplicação e interage com bancos de dados, requisições de rede e outras fontes de dados assíncronas. ### Provedor de Dados A responsabilidade do provedor de dados é fornecer dados brutos. O provedor de dados deve ser genérico e versátil. O provedor de dados geralmente expõe APIs simples para executar operações [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete). Podemos ter os métodos `createData`, `readData`, `updateData`, e `deleteData` como parte da nossa camada de dados. ### Repositório A camada de repositório é um wrapper em torno de um ou mais provedores de dados com os quais a camada Bloc se comunica. Como pode ver, nossa camada de repositório pode interagir com vários provedores de dados e executar transformações nos dados antes de entregar o resultado para a camada de lógica de negócios. ## Camada de Lógica de Negócios A responsabilidade da camada de lógica de negócios é responder às entradas da camada de apresentação com novos estados. Esta camada pode depender de um ou mais repositórios para recuperar os dados necessários para construir o estado do aplicativo. Pense na camada de lógica de negócios como a ponte entre a interface do usuário (camada de apresentação) e a camada de dados. A camada de lógica de negócios é notificada por eventos/ações da camada de apresentação e então se comunica com o repositório para criar um novo estado para a camada de apresentação consumir. ### Comunicação Bloc-to-Bloc Como os blocs expõem streams, pode ser tentador criar um bloc que ouça outro bloc. Você **não** deve fazer isso. Existem alternativas melhores do que recorrer ao código abaixo: Embora o código acima esteja livre de erros (e até mesmo se limpe depois de executado), ele tem um grande problema: ele cria uma dependência entre dois blocs. Em geral, dependências entre duas entidades na mesma camada arquitetural devem ser evitadas a todo custo, pois criam um alto acoplamento que é difícil de manter. Como os blocs ficam na camada arquitetural da lógica de negócios, nenhum bloc deve saber sobre qualquer outro bloc. ![Camadas de Arquitetura da Aplicação](~/assets/architecture/architecture.png) Um bloc só deve receber informações por meio de eventos e de repositórios injetados (ou seja, repositórios fornecidos ao bloc em seu construtor). Se você estiver em uma situação em que um bloc precisa responder a outro bloc, você tem duas outras opções. Você pode enviar o problema para uma camada acima (a camada de apresentação) ou para uma camada abaixo (a camada de domínio). #### Conectando Blocs com Apresentação Você pode usar um `BlocListener` para escutar um bloc e adicionar um evento a outro bloc sempre que o primeiro bloc for alterado. O código acima impede que o `SecondBloc` precise saber algo sobre o `FirstBloc`, encorajando o baixo acoplamento. O aplicativo [flutter_weather](/pt-br/tutorials/flutter-weather) usa [essa técnica](https://github.com/felangel/bloc/blob/b4c8db938ad71a6b60d4a641ec357905095c3965/examples/flutter_weather/lib/weather/view/weather_page.dart#L38-L42) para alterar o tema do aplicativo com base nas informações meteorológicas recebidas. Em algumas situações, você pode não querer acoplar dois blocs na camada de apresentação. Em vez disso, pode fazer sentido que dois blocs compartilhem a mesma fonte de dados e atualizem sempre que os dados mudarem. #### Conectando Blocs com Domínio Dois blocs podem escutar um stream de um repositório e atualizar seus estados, independente um do outro, sempre que os dados do repositório mudarem. Usar repositórios reativos para manter o estado sincronizado é comum em aplicativos empresariais de larga escala. Primeiro, crie ou use um repositório que forneça um `Stream` de dados. Por exemplo, o seguinte repositório expõe um stream interminável das mesmas ideias de aplicativos: O mesmo repositório pode ser injetado em cada bloc que precisa reagir a novas ideias de aplicativo. Abaixo está um `AppIdeaRankingBloc` que produz um estado para cada ideia de aplicativo recebida do repositório acima: Para saber mais sobre como usar streams com Bloc, consulte [Como usar o Bloc com streams e concorrência](https://verygood.ventures/blog/how-to-use-bloc-with-streams-and-concurrency). ## Camada de Apresentação A responsabilidade da camada de apresentação é resolver como se renderizar com base em um ou mais estados do bloc. Além disso, ela deve manipular as entradas do usuário e os eventos do ciclo de vida do aplicativo. A maioria dos fluxos de aplicativos começa com um evento `AppStart` que aciona a aplicação para buscar alguns dados para apresentar ao usuário. Nesse cenário, a camada de apresentação adicionaria um evento `AppStart`. Além disso, a camada de apresentação terá que descobrir o que renderizar na tela com base no estado da camada do bloc. Até agora, embora tenhamos alguns trechos de código, tudo isso tem sido muito de alto nível. Na seção tutorial, vamos juntar tudo isso enquanto construímos vários aplicativos de exemplo diferentes. ================================================ FILE: docs/src/content/docs/pt-br/bloc-concepts.mdx ================================================ --- title: Conceitos do Bloc description: Uma visão geral dos conceitos básicos do package:bloc. sidebar: order: 1 --- import CountStreamSnippet from '~/components/concepts/bloc/CountStreamSnippet.astro'; import SumStreamSnippet from '~/components/concepts/bloc/SumStreamSnippet.astro'; import StreamsMainSnippet from '~/components/concepts/bloc/StreamsMainSnippet.astro'; import CounterCubitSnippet from '~/components/concepts/bloc/CounterCubitSnippet.astro'; import CounterCubitInitialStateSnippet from '~/components/concepts/bloc/CounterCubitInitialStateSnippet.astro'; import CounterCubitInstantiationSnippet from '~/components/concepts/bloc/CounterCubitInstantiationSnippet.astro'; import CounterCubitIncrementSnippet from '~/components/concepts/bloc/CounterCubitIncrementSnippet.astro'; import CounterCubitBasicUsageSnippet from '~/components/concepts/bloc/CounterCubitBasicUsageSnippet.astro'; import CounterCubitStreamUsageSnippet from '~/components/concepts/bloc/CounterCubitStreamUsageSnippet.astro'; import CounterCubitOnChangeSnippet from '~/components/concepts/bloc/CounterCubitOnChangeSnippet.astro'; import CounterCubitOnChangeUsageSnippet from '~/components/concepts/bloc/CounterCubitOnChangeUsageSnippet.astro'; import CounterCubitOnChangeOutputSnippet from '~/components/concepts/bloc/CounterCubitOnChangeOutputSnippet.astro'; import SimpleBlocObserverOnChangeSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeSnippet.astro'; import SimpleBlocObserverOnChangeUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeUsageSnippet.astro'; import SimpleBlocObserverOnChangeOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeOutputSnippet.astro'; import CounterCubitOnErrorSnippet from '~/components/concepts/bloc/CounterCubitOnErrorSnippet.astro'; import SimpleBlocObserverOnErrorSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnErrorSnippet.astro'; import CounterCubitOnErrorOutputSnippet from '~/components/concepts/bloc/CounterCubitOnErrorOutputSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/bloc/CounterBlocSnippet.astro'; import CounterBlocEventHandlerSnippet from '~/components/concepts/bloc/CounterBlocEventHandlerSnippet.astro'; import CounterBlocIncrementSnippet from '~/components/concepts/bloc/CounterBlocIncrementSnippet.astro'; import CounterBlocUsageSnippet from '~/components/concepts/bloc/CounterBlocUsageSnippet.astro'; import CounterBlocStreamUsageSnippet from '~/components/concepts/bloc/CounterBlocStreamUsageSnippet.astro'; import CounterBlocOnChangeSnippet from '~/components/concepts/bloc/CounterBlocOnChangeSnippet.astro'; import CounterBlocOnChangeUsageSnippet from '~/components/concepts/bloc/CounterBlocOnChangeUsageSnippet.astro'; import CounterBlocOnChangeOutputSnippet from '~/components/concepts/bloc/CounterBlocOnChangeOutputSnippet.astro'; import CounterBlocOnTransitionSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionSnippet.astro'; import CounterBlocOnTransitionOutputSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionOutputSnippet.astro'; import SimpleBlocObserverOnTransitionSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionSnippet.astro'; import SimpleBlocObserverOnTransitionUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionUsageSnippet.astro'; import SimpleBlocObserverOnTransitionOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionOutputSnippet.astro'; import CounterBlocOnEventSnippet from '~/components/concepts/bloc/CounterBlocOnEventSnippet.astro'; import SimpleBlocObserverOnEventSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventSnippet.astro'; import SimpleBlocObserverOnEventOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventOutputSnippet.astro'; import CounterBlocOnErrorSnippet from '~/components/concepts/bloc/CounterBlocOnErrorSnippet.astro'; import CounterBlocOnErrorOutputSnippet from '~/components/concepts/bloc/CounterBlocOnErrorOutputSnippet.astro'; import CounterCubitFullSnippet from '~/components/concepts/bloc/CounterCubitFullSnippet.astro'; import CounterBlocFullSnippet from '~/components/concepts/bloc/CounterBlocFullSnippet.astro'; import AuthenticationStateSnippet from '~/components/concepts/bloc/AuthenticationStateSnippet.astro'; import AuthenticationTransitionSnippet from '~/components/concepts/bloc/AuthenticationTransitionSnippet.astro'; import AuthenticationChangeSnippet from '~/components/concepts/bloc/AuthenticationChangeSnippet.astro'; import DebounceEventTransformerSnippet from '~/components/concepts/bloc/DebounceEventTransformerSnippet.astro'; :::note Por favor, certifique-se de ler atentamente as seções a seguir antes de trabalhar com [`package:bloc`](https://pub.dev/packages/bloc). ::: Existem vários conceitos básicos que são essenciais para entender como usar o pacote bloc. Nas próximas seções, discutiremos cada um deles em detalhes e veremos como eles se aplicariam a um aplicativo de contador. ## Streams :::note Confira a [Documentação oficial do Dart](https://dart.dev/tutorials/language/streams) para mais informações sobre `Streams`. ::: Um stream é uma sequência de dados asincronos. Para usar a biblioteca bloc, é fundamental ter um compreensão básica de `Streams` e como eles funcionam. Se você não está familiarizado com `Streams`, basta pensar em um tubo com água fluindo por dele. O tubo é o `Stream` e a água são os dados assíncronos. Nós podemos criar um `Stream` em Dart escrevendo uma função `async*` (gerador de async). Marcando a função como `async*` podemos usar a palavra-chave `yield` para retornar um `Stream` de dados. No exemplo acima, retornamos um `Stream` de inteiros até o parâmetro inteiro `max`. Toda vez que colocamos um `yield` em uma função `async*` estamos inserindo esse pedaço de dados no `Stream`. Podemos consumir o `Stream` acima em várias formas. Se quisermos escrever uma função para retornar a soma de um `Stream` de inteiros, poderia escrever algo como: Marcando a função acima como `async` podemos usar a palavra-chave `await` para retornar um `Future` de inteiros. Neste exemplo, esperamos cada valor do stream e retornamos a soma de todos os inteiros no stream. Podemos juntar todo isso assim: Agora que temos uma compreensão básica de como `Streams` funcionam em Dart, estamos prontos para aprender sobre o componente central do pacote bloc: o `Cubit`. ## Cubit Um `Cubit` é uma classe que estende `BlocBase` e pode ser estendida para gerenciar qualquer tipo de estado. ![Cubit Architecture](~/assets/concepts/cubit_architecture_full.png) Um `Cubit` pode expor funções que podem ser chamadas para disparar mudanças de estado. Estados são a saida de um `Cubit` e representam uma parte do estado da sua aplicação. Os componentes de IU podem ser notificados pelos estados e se redesenharem com base no estado atual. :::note Para mais informações sobre as origens do `Cubit`, consulte o seguinte questão [aqui](https://github.com/felangel/cubit/issues/69). ::: ### Criando um Cubit Podemos criar um `CounterCubit` como: Ao criar um `Cubit`, precisamos definir o tipo de estado que o `Cubit` vai gerenciar. No caso do `CounterCubit` acima, o estado é representado por um `int` mas em casos mais complexos, pode ser necessário usar uma `class` ao invés de um tipo primitivo. A segunda coisa que precisamos fazer ao criar um `Cubit` é especificar o estado inicial. Podemos fazer isso chamando `super` com o valor do estado inicial. No exemplo acima, definimos o estado inicial para `0` internamente, mas também podemos permitir que o `Cubit` seja mais flexível aceitando um valor externo: Isto nos permitiria criar instâncias de `CounterCubit` com diferentes estados iniciais, como: ### Mudanças de estado do Cubit Cada `Cubit` tem a capacidade de emitir um novo estado via `emit`. No trecho acima, o `CouterCubit` está expondo um método publico chamado `increment` que pode ser chamado externamente para notificar o `CounterCubit` para incrementar seu estado. Quando `increment` é chamado, podemos acessar o estado atual do `Cubit` através do getter `state` e `emit` um novo estado adicionando 1 ao estado atual. :::caution O método `emit` é protegido, o que significa que deve ser usado apenas dentro de um `Cubit`. ::: ### Usando um Cubit Agora podemos pegar o `CounterCubit` implementado e coloca-lo em uso! #### Uso Básico No trecho acima, começamos criando uma instância de um `CounterCubit`. Em seguida, imprimimos o estado atual do cubit, que é o estado inicial (já que nenhum estado novo foi emitido ainda). Em seguida, chamamos a função `increment` para disparar uma mudança de estado. Por fim, imprimimos o estado do `Cubit` novamente que passou de `0` para `1` e chamamos `close` no `Cubit` para fechar o fluxo de dados interno. #### Uso de Stream `Cubit` expõe um `Stream` que nos permite receber atualizações de estado em tempo real: No trecho acima, estamos assinando o `CounterCubit` e chamando print a cada mudança de estado. Em seguida, invocamos a função `increment` que emitirá um novo estado. Por fim, estamos chamando `cancel` na `subscription` quando não queremos mais receber atualizações e fechando o `Cubit`. :::note `await Future.delayed(Duration.zero)` é adicionado a este exemplo para evitar cancelar a assinatura imediatamente. ::: :::caution Somente as mudanças de estado subsequentes serão recebidas quando chamamos `listen` em um `Cubit`. ::: ### Observando um Cubit Quando um `Cubit` emite um novo estado, ocorre uma `Change`. Podemos observar todas as mudanças em um `Cubit` substituindo `onChange`. Podemos então interagir com o `Cubit` e observar todas as alterações geradas no console. O exemplo acima produziria: :::note Uma `Change` ocorre logo antes do estado do `Cubit` ser atualizado. Uma `Change` consiste em no `currentState` e no `nextState`. ::: #### BlocObserver Um bônus adicional de usar a biblioteca bloc é que podemos ter acesso a todas as `Changes` em um lugar. Embora nessa aplicação tenhamos apenas um `Cubit`, é muito comum em aplicações maiores ter muitos `Cubits` gerenciando diferentes partes do estado da aplicação. Se quisermos fazer algo em resposta a todas as `Changes`, podemos simplesmente criar nosso próprio `BlocObserver`. :::note Tudo que precisamos fazer é extender `BlocObserver` e substituir o método `onChange`. ::: Para usar o `SimpleBlocObserver`, precisamos apenas ajustar a função `main`: O trecho acima produziria então: :::note A sobrescrita interna do `onChange` é chamada primeiro, que chama `super.onChange` notificando o `onChange` no `BlocObserver`. ::: :::tip No `BlocObserver` temos acesso à instância do `Cubit` além da própria `Change`. ::: ### Tratamento de Erros do Cubit Todo `Cubit` tem um método `addError` que pode ser usado para indicar que ocorreu um erro. :::note `onError` pode ser sobrescrito dentro do `Cubit` para manipular todos os erros de um `Cubit` específico. ::: `onError` também pode ser sobrescrito no `BlocObserver` para manipular todos os erros relatados globalmente. Se rodarmos o mesmo programa novamente, devemos ver o seguinte output: ## Bloc Um `Bloc` é uma classe mais avançada que depende de `eventos` para acionar mudanças de `estado` em vez de funções. `Bloc` também estende `BlocBase`, o que significa que ele tem uma API pública semelhante ao `Cubit`. No entanto, em vez de chamar uma `função` em um `Bloc` e emitir diretamente um novo `estado`, os `Blocs` recebem `eventos` e convertem os `eventos` de entrada em `estados` de saída. ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) ### Criando um Bloc Criar um `Bloc` é semelhante a criar um `Cubit`, exceto que além de definir o estado que iremos gerenciar, também devemos definir o evento que o `Bloc` poderá processar. Eventos são a entrada para um Bloc. Eles normalmente são adicionados em resposta a interações do usuário, como botões pressionados ou eventos do ciclo de vida, como carregamentos de página. Assim como ao criar o `CounterCubit`, devemos especificar um estado inicial passando-o para a superclasse via `super`. ### Mundanças de Estado do Bloc `Bloc` exige que registremos manipuladores de eventos via a API `on`, em vez de funções no `Cubit`. Um manipulador de eventos é responsável por converter quaisquer eventos de entrada em zero ou mais estados de saída. :::tip Um `EventHandler` tem acesso ao evento adicionado, bem como a um `Emitter`, que pode ser usado para emitir zero ou mais estados em resposta ao evento recebido. ::: Podemos então atualizar o `EventHandler` para lidar com o evento `CounterIncrementPressed`: No trecho acima, registramos um `EventHandler` para gerenciar todos os eventos `CounterIncrementPressed`. Para cada evento `CounterIncrementPressed` recebido, podemos acessar o estado atual do bloc via o getter `state` e `emit(state + 1)`. :::note Como a classe `Bloc` estende `BlocBase`, temos acesso ao estado atual do bloc em qualquer momento via o getter `state`, assim como no `Cubit`. ::: :::caution Blocs nunca devem `emitir` novos estados diretamente. Em vez disso, toda mudança de estado deve ser emitida em resposta a um evento de entrada dentro de um `EventHandler`. ::: :::caution Ambos blocs e cubits irão ignorar estados duplicados. Se emitirmos `State nextState` onde `state == nextState`, então nenhuma mudança de estado irá ocorrer. ::: ### Usando um Bloc Neste ponto, podemos criar uma instância do nosso `CounterBloc` e colocá-lo em uso! #### Uso Básico No trecho acima, começamos criando uma instância do nosso `CounterBloc`. Em seguida, imprimimos o estado atual do `Bloc` que é o estado inicial (já que nenhum estado novo foi emitido ainda). Em seguida, adicionamos o evento `CounterIncrementPressed` para disparar uma mudança de estado. Por fim, imprimimos o estado do `Bloc` novamente que foi de `0` para `1` e chamamos `close` no `Bloc` para fechar o fluxo de dados interno. :::note `await Future.delayed(Duration.zero)` é adicionado para garantir que aguardamos a próxima iteração do event-loop (permitindo que o `EventHandler` processe o evento). ::: #### Uso de Stream Assim como com `Cubit`, um `Bloc` é um tipo especial de `Stream`, o que significa que também podemos assinar um `Bloc` para atualizações em tempo real de seu estado: No trecho acima, estamos assinando o `CounterBloc` e chamando print a cada mudança de estado. Em seguida, adicionamos o evento `CounterIncrementPressed`, que aciona o `EventHandler` `on` e emite um novo estado. Por fim, estamos chamando o `cancel` da assinatura quando não queremos mais receber atualizações e fechando o `Bloc`. :::note `await Future.delayed(Duration.zero)` é adicionado neste exemplo para evitar o cancelamento imediato da assinatura. ::: ### Observando um Bloc Como o `Bloc` estende `BlocBase`, podemos observar todas as mudanças de estado de um `Bloc` usando `onChange`. Podemos então atualizar `main.dart` para: Agora, se executarmos o trecho acima, a saída será: Um fator-chave de diferenciação entre `Bloc` e `Cubit` é que, como o `Bloc` é orientado a eventos, nós também podemos capturar informações sobre o que disparou a mudança de estado. Podemos fazer isso substituindo `onTransition`. A mudança de um estado para outro é chamada de `Transition`. Uma `Transition` consiste no estado atual, no evento e no próximo estado. Se executarmos novamente o mesmo trecho `main.dart` de antes, veremos a seguinte saída: :::note `onTransition` é invocado antes de `onChange` e contém o evento que acionou a mudança de `currentState` para `nextState`. ::: #### BlocObserver Assim como antes, podemos substituir o `onTransition` em um `BlocObserver` personalizado para observar todas as transições que ocorrem em um único lugar. Podemos inicializar o `SimpleBlocObserver` como antes: Agora, se executarmos o trecho acima, a saída será semelhante a: :::note `onTransition` é invocado primeiro (local antes de global), seguido por `onChange`. ::: Outro recurso exclusivo das instâncias do `Bloc` é que elas nos permitem sobrescrever `onEvent`, que é chamado sempre que um novo evento é adicionado ao `Bloc`. Assim como com `onChange` e `onTransition`, `onEvent` pode ser sobrescrito localmente e também globalmente. Podemos executar o mesmo `main.dart` de antes e devemos ver a seguinte saída: :::note `onEvent` é chamado assim que o evento é adicionado. O `onEvent` local é invocado antes do `onEvent` global no `BlocObserver`. ::: ### Tratamento de Erros no Bloc Assim como no `Cubit`, cada `Bloc` tem um método `addError` e `onError`. Podemos indicar que ocorreu um erro chamando `addError` de qualquer lugar dentro do nosso `Bloc`. Podemos então reagir a todos os erros sobrescrevendo `onError` assim como no `Cubit`. Se executarmos novamente o mesmo `main.dart` de antes, podemos ver como fica quando um erro é reportado: :::note O `onError` local é invocado primeiro, seguido pelo `onError` global no `BlocObserver`. ::: :::note `onError` e `onChange` funcionam exatamente da mesma maneira para instâncias `Bloc` e `Cubit`. ::: :::caution Quaisquer exceções não tratadas que ocorram em um `EventHandler` também são reportadas no `onError`. ::: ## Cubit vs. Bloc Agora que cobrimos os conceitos básicos das classes `Cubit` e `Bloc`, você pode estar se perguntando quando deve usar `Cubit` e quando deve usar `Bloc`. ### Vantagens do Cubit #### Simplicidade Uma das maiores vantagens de usar o `Cubit` é a simplicidade. Ao criar um `Cubit`, precisamos apenas definir o estado, bem como as funções que queremos expor para alterar o estado. Em comparação, ao criar um `Bloc`, temos de definir os estados, os eventos e a implementação do `EventHandler`. Isso torna o Cubit mais fácil de entender e há menos código envolvido. Agora vamos dar uma olhada nas duas implementações do contador: ##### CounterCubit ##### CounterBloc A implementação do `Cubit` é mais concisa e, em vez de definir eventos separadamente, as funções agem como eventos. Além disso, ao usar um `Cubit`, podemos simplesmente chamar `emit` de qualquer lugar para disparar uma mudança de estado. ### Vantagens do Bloc #### Rastreabilidade Uma das maiores vantagens de usar o `Bloc` é conhecer a sequência de alterações de estado, bem como o que exatamente desencadeou essas alterações. Para o estado que é essencial para a funcionalidade de um aplicativo, pode ser muito vantajoso usar uma abordagem mais orientada a eventos para capturar todos os eventos, além das mudanças de estado. Um caso de uso comum pode ser gerenciar `AuthenticationState`. Para simplificar, digamos que podemos representar `AuthenticationState` por meio de um `enum`: Pode haver muitos motivos pelos quais o estado do aplicativo pode mudar de `authenticated` para não `unauthenticated`. Por exemplo, o usuário pode ter tocado no botão de logout e solicitado que fosse desconectado do aplicativo. Por outro lado, talvez o token de acesso do usuário tenha sido revogado e ele foi desconectado à força. Ao usar o `Bloc`, podemos rastrear claramente como o estado do aplicativo chegou a um determinado valor. A `Transition` acima nos dá todas as informações que precisamos para entender por que o estado mudou. Se tivéssemos usado um `Cubit` para gerenciar o `AuthenticationState`, nossos logs ficariam assim: Isso nos diz que o usuário foi desconectado, mas não explica o motivo, o que pode ser crítico para a depuração e compreensão de como o estado do aplicativo está mudando ao longo do tempo. #### Transformações Avançadas de Eventos Outra área em que o `Bloc` se destaca sobre o `Cubit` é quando precisamos tirar vantagem de operadores reativos, como `buffer`, `debounceTime`, `throttle`, etc. O `Bloc` tem um coletor de eventos que nos permite controlar e transformar o fluxo de entrada de eventos. Por exemplo, se estivéssemos criando uma pesquisa em tempo real, provavelmente desejaríamos reduzir as solicitações para o backend para evitar limitações de taxa e também para reduzir custos/carga no backend. Com o `Bloc`, podemos fornecer um `EventTransformer` personalizado para alterar a maneira como os eventos recebidos são processados pelo `Bloc`. Com o código acima, podemos facilmente reduzir o retorno de eventos recebidos com muito pouco código adicional. :::tip Confira o [`package:bloc_concurrency`](https://pub.dev/packages/bloc_concurrency) para um conjunto opinativo de transformadores de eventos. ::: Se não tiver certeza sobre qual usar, comece com o `Cubit` e depois refatore ou expanda para um `Bloc`, conforme necessário. ================================================ FILE: docs/src/content/docs/pt-br/flutter-bloc-concepts.mdx ================================================ --- title: Conceitos do Flutter Bloc description: Uma visão geral dos conceitos principais do package:flutter_bloc. sidebar: order: 2 --- import BlocBuilderSnippet from '~/components/concepts/flutter-bloc/BlocBuilderSnippet.astro'; import BlocBuilderExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocBuilderExplicitBlocSnippet.astro'; import BlocBuilderConditionSnippet from '~/components/concepts/flutter-bloc/BlocBuilderConditionSnippet.astro'; import BlocSelectorSnippet from '~/components/concepts/flutter-bloc/BlocSelectorSnippet.astro'; import BlocProviderSnippet from '~/components/concepts/flutter-bloc/BlocProviderSnippet.astro'; import BlocProviderEagerSnippet from '~/components/concepts/flutter-bloc/BlocProviderEagerSnippet.astro'; import BlocProviderValueSnippet from '~/components/concepts/flutter-bloc/BlocProviderValueSnippet.astro'; import BlocProviderLookupSnippet from '~/components/concepts/flutter-bloc/BlocProviderLookupSnippet.astro'; import NestedBlocProviderSnippet from '~/components/concepts/flutter-bloc/NestedBlocProviderSnippet.astro'; import MultiBlocProviderSnippet from '~/components/concepts/flutter-bloc/MultiBlocProviderSnippet.astro'; import BlocListenerSnippet from '~/components/concepts/flutter-bloc/BlocListenerSnippet.astro'; import BlocListenerExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocListenerExplicitBlocSnippet.astro'; import BlocListenerConditionSnippet from '~/components/concepts/flutter-bloc/BlocListenerConditionSnippet.astro'; import NestedBlocListenerSnippet from '~/components/concepts/flutter-bloc/NestedBlocListenerSnippet.astro'; import MultiBlocListenerSnippet from '~/components/concepts/flutter-bloc/MultiBlocListenerSnippet.astro'; import BlocConsumerSnippet from '~/components/concepts/flutter-bloc/BlocConsumerSnippet.astro'; import BlocConsumerConditionSnippet from '~/components/concepts/flutter-bloc/BlocConsumerConditionSnippet.astro'; import RepositoryProviderSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderSnippet.astro'; import RepositoryProviderLookupSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderLookupSnippet.astro'; import RepositoryProviderDisposeSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderDisposeSnippet.astro'; import NestedRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/NestedRepositoryProviderSnippet.astro'; import MultiRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/MultiRepositoryProviderSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/flutter-bloc/CounterBlocSnippet.astro'; import CounterMainSnippet from '~/components/concepts/flutter-bloc/CounterMainSnippet.astro'; import CounterPageSnippet from '~/components/concepts/flutter-bloc/CounterPageSnippet.astro'; import WeatherRepositorySnippet from '~/components/concepts/flutter-bloc/WeatherRepositorySnippet.astro'; import WeatherMainSnippet from '~/components/concepts/flutter-bloc/WeatherMainSnippet.astro'; import WeatherAppSnippet from '~/components/concepts/flutter-bloc/WeatherAppSnippet.astro'; import WeatherPageSnippet from '~/components/concepts/flutter-bloc/WeatherPageSnippet.astro'; :::note Leia atentamente as seções a seguir antes de trabalhar com [`package:flutter_bloc`](https://pub.dev/packages/flutter_bloc). ::: :::note Todos os widgets exportados pelo pacote `flutter_bloc` integram-se com instâncias do `Cubit` e do `Bloc`. ::: ## Widgets do Bloc ### BlocBuilder **BlocBuilder** é um widget do Flutter que requer uma função `Bloc` e uma função `builder`. `BlocBuilder` lida com a construção do widget em resposta a novos estados. `BlocBuilder` é muito semelhante a `StreamBuilder`, mas possui uma API mais simples para reduzir a quantidade de código boilerplate necessária. A função `builder` será potencialmente chamada muitas vezes e deve ser uma [função pura](https://en.wikipedia.org/wiki/Pure_function) que retorna um widget em resposta ao estado. Veja `BlocListener` se quiser "fazer" algo em resposta a mudanças de estado, como navegação, exibição de uma caixa de diálogo, etc... Se o parâmetro `bloc` for omitido, o `BlocBuilder` executará automaticamente uma pesquisa usando `BlocProvider` e o `BuildContext` atual. Especifique o bloc somente se desejar fornecer um bloc que terá como escopo um único widget e não seja acessível por meio de um `BlocProvider` pai e do `BuildContext` atual. Para um controle mais preciso sobre quando a função `builder` é chamada, um `buildWhen` opcional pode ser fornecido. O `buildWhen` pega o estado do bloc anterior e o estado do bloc atual e retorna um booleano. Se `buildWhen` retornar true, o `builder` será chamado com `state` e o widget será reconstruído. Se `buildWhen` retornar false, `builder` não será chamado com `state` e nenhuma reconstrução ocorrerá. ### BlocSelector **BlocSelector** é um widget do Flutter análogo ao `BlocBuilder`, mas permite que os desenvolvedores filtrem atualizações selecionando um novo valor com base no estado atual do bloc. Builds desnecessários são evitados se o valor selecionado não mudar. O valor selecionado deve ser imutável para que `BlocSelector` determine com precisão se `builder` deve ser chamado novamente. Se o parâmetro `bloc` for omitido, `BlocSelector` executará automaticamente uma pesquisa usando `BlocProvider` e o `BuildContext` atual. ### BlocProvider **BlocProvider** é um widget do Flutter que fornece um bloc para seus filhos por meio de `BlocProvider.of(context)`. Ele é usado como um widget de injeção de dependência (DI) para que uma única instância de um bloc possa ser fornecida a vários widgets dentro de uma subárvore. Na maioria dos casos, `BlocProvider` deve ser usado para criar novos blocs que serão disponibilizados para o restante da subárvore. Nesse caso, como `BlocProvider` é responsável pela criação do bloc, ele tratará automaticamente do fechamento do bloc. Por padrão, `BlocProvider` criará o bloc preguiçosamente, o que significa que `create` será executado quando o bloc for consultado via `BlocProvider.of(context)`. Para substituir esse comportamento e forçar a execução imediata de `create`, `lazy` pode ser definido como `false`. Em alguns casos, `BlocProvider` pode ser usado para fornecer um bloc existente a uma nova parte da árvore de widgets. Isso será mais comumente usado quando um bloc existente precisa ser disponibilizado para uma nova rota. Nesse caso, `BlocProvider` não fechará o bloc automaticamente, pois não o criou. então, de `ChildA` ou `ScreenA`, podemos recuperar `BlocA` com: ### MultiBlocProvider **MultiBlocProvider** é um widget do Flutter que mescla vários widgets `BlocProvider` em um. `MultiBlocProvider` melhora a legibilidade e elimina a necessidade de aninhar vários `BlocProviders`. Usando `MultiBlocProvider`, podemos ir de: para: :::caution Quando um `BlocProvider` é definido dentro do contexto de um `MultiBlocProvider`, qualquer `child` será ignorado. ::: ### BlocListener **BlocListener** é um widget Flutter que recebe um `BlocWidgetListener` e um `bloc` opcional e invoca o `listener` em resposta a mudanças de estado no bloc. Ele deve ser usado para funcionalidades que precisam ocorrer uma vez a cada mudança de estado, como navegação, exibição de um `SnackBar`, exibição de um `Dialog`, etc... `listener` é chamado apenas uma vez para cada mudança de estado (**NÃO** incluindo o estado inicial), ao contrário de `builder` em `BlocBuilder`, e é uma função `void`. Se o parâmetro `bloc` for omitido, `BlocListener` executará automaticamente uma pesquisa usando `BlocProvider` e o `BuildContext` atual. Especifique o bloc somente se desejar fornecer um bloc que não seja acessível via `BlocProvider` e o `BuildContext` atual. Para um controle mais preciso sobre quando a função `listener` é chamada, um `listenWhen` opcional pode ser fornecido. `listenWhen` recebe o estado do bloc anterior e o estado do bloc atual e retorna um booleano. Se `listenWhen` retornar verdadeiro, `listener` será chamado com `state`. Se `listenWhen` retornar falso, `listener` não será chamado com `state`. ### MultiBlocListener **MultiBlocListener** é um widget do Flutter que mescla vários widgets `BlocListener` em um. `MultiBlocListener` melhora a legibilidade e elimina a necessidade de aninhar vários `BlocListeners`. Usando `MultiBlocListener`, podemos ir de: para: :::caution Quando um `BlocListener` é definido dentro do contexto de um `MultiBlocListener`, qualquer `child` será ignorado. ::: ### BlocConsumer **BlocConsumer** expõe um `builder` e um `listener` para reagir a novos estados. `BlocConsumer` é análogo a um `BlocListener` e `BlocBuilder` aninhados, mas reduz a quantidade de boilerplate necessária. `BlocConsumer` deve ser usado apenas quando for necessário reconstruir a UI e executar outras reações a mudanças de estado no `bloc`. `BlocConsumer` recebe um `BlocWidgetBuilder` e um `BlocWidgetListener` obrigatórios e um `bloc`, `BlocBuilderCondition` e `BlocListenerCondition` opcionais. Se o parâmetro `bloc` for omitido, `BlocConsumer` executará automaticamente uma pesquisa usando `BlocProvider` e o `BuildContext` atual. Os métodos opcionais `listenWhen` e `buildWhen` podem ser implementados para um controle mais granular sobre quando `listener` e `builder` são chamados. `listenWhen` e `buildWhen` serão invocados a cada alteração de `state` do `bloc`. Cada um deles assume o `state` anterior e o `state` atual e deve retornar um `bool` que determina se a função `builder` e/ou `listener` será invocada. O `state` anterior será inicializado com o `state` do `bloc` quando o `BlocConsumer` for inicializado. `listenWhen` e `buildWhen` são opcionais e, se não forem implementados, o valor padrão será `true`. ### RepositoryProvider **RepositoryProvider** é um widget do Flutter que fornece um repositório para seus filhos por meio de `RepositoryProvider.of(context)`. Ele é usado como um widget de injeção de dependência (DI) para que uma única instância de um repositório possa ser fornecida a vários widgets dentro de uma subárvore. `BlocProvider` deve ser usado para fornecer blocs, enquanto `RepositoryProvider` deve ser usado apenas para repositórios. então de `ChildA` podemos recuperar a instância `Repository` com: Repositórios que gerenciam recursos que devem ser descartados podem fazê-lo por meio do callback `dispose`: ### MultiRepositoryProvider **MultiRepositoryProvider** é um widget Flutter que mescla vários widgets `RepositoryProvider` em um. `MultiRepositoryProvider` melhora a legibilidade e elimina a necessidade de aninhar vários `RepositoryProvider`. Usando `MultiRepositoryProvider``, podemos ir de: para: :::caution Quando um `RepositoryProvider` é definido dentro do contexto de um `MultiRepositoryProvider`, qualquer `child` será ignorado. ::: ## Uso do BlocProvider Vamos dar uma olhada em como usar `BlocProvider` para fornecer um `CounterBloc` para uma `CounterPage` e reagir a mudanças de estado com `BlocBuilder`. Neste ponto, separamos com sucesso nossa camada de apresentação da nossa camada de lógica de negócios. Observe que o widget `CounterPage` não sabe nada sobre o que acontece quando um usuário toca nos botões. O widget simplesmente informa ao `CounterBloc` que o usuário pressionou o botão de incremento ou decremento. ## Uso do RepositoryProvider Vamos dar uma olhada em como usar `RepositoryProvider` dentro do contexto do exemplo [`flutter_weather`][flutter_weather_link]. Em nosso `main.dart`, chamamos `runApp` com nosso widget `WeatherApp`. Injetaremos nossa instância `WeatherRepository` em nossa árvore de widgets via `RepositoryProvider`. Ao instanciar um bloc, podemos acessar a instância de um repositório via `context.read` e injetar o repositório no bloc via construtor. :::tip Se você tiver mais de um repositório, poderá usar `MultiRepositoryProvider` para fornecer múltiplas instâncias de repositório para a subárvore. ::: :::note Use o retorno de chamada `dispose` para lidar com a liberação de quaisquer recursos quando o `RepositoryProvider` for desmontado. ::: [flutter_weather_link]: https://github.com/felangel/bloc/blob/master/examples/flutter_weather ## Extension Methods [Métodos de extensão](https://dart.dev/guides/language/extension-methods), introduzidos no Dart 2.7, são uma forma de adicionar funcionalidades a bibliotecas existentes. Nesta seção, veremos os métodos de extensão incluídos em `package:flutter_bloc` e como eles podem ser usados. `flutter_bloc` tem uma dependência de [package:provider](https://pub.dev/packages/provider) que simplifica o uso de [`InheritedWidget`](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html). Internamente, `package:flutter_bloc` usa `package:provider` para implementar: os widgets `BlocProvider`, `MultiBlocProvider`, `RepositoryProvider` e `MultiRepositoryProvider`. `package:flutter_bloc` exporta as extensões `ReadContext`, `WatchContext` e `SelectContext` do `package:provider`. :::note Saiba mais sobre [`package:provider`](https://pub.dev/packages/provider). ::: ### context.read `context.read()` procura a instância ancestral mais próxima do tipo `T` e é funcionalmente equivalente a `BlocProvider.of(context)`. `context.read` é mais comumente usado para recuperar uma instância de bloc para adicionar um evento dentro de callbacks `onPressed`. :::note `context.read()` não escuta `T` -- se o `Object` fornecido do tipo `T` mudar, `context.read` não acionará uma reconstrução do widget. ::: #### Uso ✅ **USE** `context.read` para adicionar eventos em retornos de chamada. ```dart onPressed() { context.read().add(CounterIncrementPressed()), } ``` ❌ **EVITE** usar `context.read` para recuperar o estado dentro de um método `build`. ```dart @override Widget build(BuildContext context) { final state = context.read().state; return Text('$state'); } ``` O uso acima é propenso a erros porque o widget `Text` não será reconstruído se o estado do bloc mudar. :::caution Use `BlocBuilder` ou `context.watch` para reconstruir em resposta a mudanças de estado. ::: ### context.watch Assim como `context.read()`, `context.watch()` fornece a instância ancestral mais próxima do tipo `T`, porém também monitora alterações na instância. É funcionalmente equivalente a `BlocProvider.of(context, listen: true)`. Se o `Object` fornecido do tipo `T` for alterado, `context.watch` acionará uma reconstrução. :::caution `context.watch` só é acessível dentro do método `build` de uma classe `StatelessWidget` ou `State`. ::: #### Uso ✅ **USE** `BlocBuilder` em vez de `context.watch` para delimitar explicitamente reconstruções. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (context, state) { // Sempre que o estado muda, somente o Text é reconstruído. return Text(state.value); }, ), ), ); } ``` Como alternativa, use um `Builder` para definir o escopo das reconstruções. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Sempre que o estado muda, somente o Text é reconstruído. final state = context.watch().state; return Text(state.value); }, ), ), ); } ``` ✅ **USE** `Builder` e `context.watch` como `MultiBlocBuilder`. ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // retorna um Widget que depende do estado de BlocA, BlocB e BlocC } ); ``` ❌ **EVITE** usar `context.watch` quando o widget pai no método `build` não depende do estado. ```dart @override Widget build(BuildContext context) { // Sempre que o estado muda, o MaterialApp é reconstruído // mesmo que seja usado apenas no widget Text. final state = context.watch().state; return MaterialApp( home: Scaffold( body: Text(state.value), ), ); } ``` :::caution Usar `context.watch` na raiz do método `build` resultará na reconstrução de todo o widget quando o estado do bloc mudar. ::: ### context.select Assim como `context.watch()`, `context.select(R function(T value))` fornece a instância ancestral mais próxima do tipo `T` e monitora as alterações em `T`. Ao contrário de `context.watch`, `context.select` permite que você monitore as alterações em uma parte menor de um estado. ```dart Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); } ``` O procedimento acima só reconstruirá o widget quando a propriedade `name` do estado do `ProfileBloc` mudar. #### Uso ✅ **USE** `BlocSelector` em vez de `context.select` para delimitar explicitamente reconstruções. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector( selector: (state) => state.name, builder: (context, name) { // Sempre que o state.name muda, somente o Text é reconstruído. return Text(name); }, ), ), ); } ``` Como alternativa, use um `Builder` para definir o escopo das reconstruções. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Sempre que state.name muda, somente o Text é reconstruído. final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), ); } ``` ❌ **EVITE** usar `context.select` quando o widget pai em um método de construção não depende do estado. ```dart @override Widget build(BuildContext context) { // Sempre que o state.value muda, o MaterialApp é reconstruído // mesmo que seja usado apenas no widget Text. final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), ); } ``` :::caution Usar `context.select` na raiz do método `build` resultará na reconstrução de todo o widget quando a seleção for alterada. ::: ================================================ FILE: docs/src/content/docs/pt-br/getting-started.mdx ================================================ --- title: Começando description: Tudo o que você precisa para começar a construir com o Bloc. --- import InstallationTabs from '~/components/getting-started/InstallationTabs.astro'; import ImportTabs from '~/components/getting-started/ImportTabs.astro'; ## Pacotes O ecossistema do bloc consiste nos vários pacotes listados abaixo: | Pacote | Descrição | Link | | ------------------------------------------------------------------------------------------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | Componentes AngularDart | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | APIs Centrais do Dart | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | Transformadores de Eventos | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | Custom Linter | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | APIs de Teste | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | Command-line Tools | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | Widgets do Flutter | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | Suporte a Cache/Persistência | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | Suporte a Desfazer/Refazer | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | ## Instalação :::note Para começar a usar o bloc, é necessário ter o [Dart SDK](https://dart.dev/get-dart) instalado em seu computador. ::: ## Importes Agora que instalamos o bloc com sucesso, podemos criar nosso `main.dart` e importar o respectivo pacote `bloc`. ================================================ FILE: docs/src/content/docs/pt-br/index.mdx ================================================ --- template: splash title: Biblioteca de Gerenciamento de Estado Bloc description: Documentação oficial da biblioteca de gerenciamento de estado bloc. Suporte para Dart, Flutter e AngularDart. Inclui exemplos e tutoriais. banner: content: | ✨ Visite a loja Bloc ✨ editUrl: false lastUpdated: false hero: title: Bloc v8.1.3 tagline: Uma biblioteca de gerenciamento de estado previsível para Dart. image: alt: Bloc logo file: ~/assets/bloc.svg actions: - text: Começar link: /pt-br/getting-started/ variant: primary icon: rocket - text: Ver no GitHub link: https://github.com/felangel/bloc icon: github variant: secondary --- import { CardGrid } from '@astrojs/starlight/components'; import SponsorsGrid from '~/components/landing/SponsorsGrid.astro'; import Card from '~/components/landing/Card.astro'; import ListCard from '~/components/landing/ListCard.astro'; import SplitCard from '~/components/landing/SplitCard.astro'; import Discord from '~/components/landing/Discord.astro';
```sh # Adicione o bloc ao seu projeto. dart pub add bloc ``` Nosso [guia de iniciação](/pt-br/getting-started) contém instruções passo a passo sobre como começar a utilizar o Bloc em apenas alguns minutos. Complete [os tutoriais oficiais](/pt-br/tutorials/flutter-counter) para aprender as práticas recomendadas e criar uma variedade de aplicativos diferentes desenvolvidos pelo Bloc. Explore [amostras de aplicações](https://github.com/felangel/bloc/tree/master/examples) de alta qualidade e totalmente testadas, como contador, temporizador, lista infinita, previsão do tempo, lista de tarefas e muito mais! - [Por que Bloc?](/pt-br/why-bloc) - [Conceitos Básicos](/pt-br/bloc-concepts) - [Arquitetura](/pt-br/architecture) - [Testes](/pt-br/testing) - [Convenções de Nomenclatura](/pt-br/naming-conventions) - [FAQs](/pt-br/faqs) - [Integração com VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) - [Integração com IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) - [Integração com Neovim](https://github.com/wa11breaker/flutter-bloc.nvim) - [Integração com Mason CLI](https://github.com/felangel/bloc/blob/master/bricks/README.md) - [Templates Customizados](https://brickhub.dev/search?q=bloc) - [Ferramentas de Desenvolvedor](https://github.com/felangel/bloc/blob/master/packages/bloc_tools/README.md) ================================================ FILE: docs/src/content/docs/pt-br/modeling-state.mdx ================================================ --- title: Modelagem de Estado description: Uma visão geral das várias formas de modelar estados quando se utiliza package:bloc. --- import ConcreteClassAndStatusEnumSnippet from '~/components/modeling-state/ConcreteClassAndStatusEnumSnippet.astro'; import SealedClassAndSubclassesSnippet from '~/components/modeling-state/SealedClassAndSubclassesSnippet.astro'; Há muitas abordagens diferentes quando se trata de estruturar o estado do aplicativo. Cada uma tem suas próprias vantagens e desvantagens. Nesta seção, daremos uma olhada em várias abordagens, seus prós e contras, e quando usar cada uma delas. As abordagens a seguir são simplesmente recomendações e são completamente opcionais. Sinta-se à vontade para usar qualquer abordagem que preferir. Você pode descobrir que alguns dos exemplos/documentação não seguem estas abordagens principalmente por simplicidade/concisão. :::tip Os trechos de código a seguir são focados na estrutura de estado. Na prática, você também pode querer: - Estender `Equatable` do [`package:equatable`](https://pub.dev/packages/equatable) - Anotar a classe com `@Data()` do [`package:data_class`](https://pub.dev/packages/data_class) - Anotar a classe com **@immutable** do [`package:meta`](https://pub.dev/packages/meta) - Implementar um método `copyWith` - Usar a palavra-chave `const` nos construtores ::: ## Classe Concreta e Enum de Status Esta abordagem consiste em uma **única classe concreta** para todos os estados junto com um `enum` representando diferentes status. Propriedades são tornadas anuláveis e são manipuladas com base no status atual. Esta abordagem funciona melhor para estados que não são estritamente exclusivos e/ou contêm muitas propriedades compartilhadas. #### Prós - **Simples**: Fácil de gerenciar uma única classe e um status enum e todas as propriedades são prontamente acessíveis. - **Conciso**: Geralmente requer menos linhas de código em comparação a outras abordagens. #### Contras - **Não é Type Safe**: Requer a verificação do `status` antes de acessar as propriedades. É possível fazer `emit` de um estado malformado que pode levar a bugs. Propriedades para estados específicos são anuláveis, o que pode ser trabalhoso de gerenciar e requer desempacotamento forçado ou execução de verificações de nulos. Alguns desses contras podem ser atenuados escrevendo testes unitários e escrevendo construtores especializados e nomeados. - **Inchado**: Resulta em um único estado que pode ficar inchado com muitas propriedades ao longo do tempo. #### Veredito Essa abordagem funciona melhor para estados simples ou quando os requisitos exigem estados que não são exclusivos (por exemplo, mostrar uma snackbar quando ocorre um erro enquanto ainda mostra dados antigos do último estado de sucesso). Essa abordagem fornece flexibilidade e concisão ao custo da segurança de tipo. ## Classe Selada e Subclasses Essa abordagem consiste em uma **classe selada** que contém as propriedades compartilhadas e múltiplas subclasses para os estados separados. Essa abordagem é ótima para estados separados e exclusivos. #### Prós - **Tipagem segura**: O código é seguro para compilação e não é possível acessar acidentalmente uma propriedade inválida. Cada subclasse mantém suas próprias propriedades, deixando claro quais propriedades pertencem a qual estado. - **Explícito:** Separa propriedades compartilhadas de propriedades específicas do estado. - **Exaustivo**: Usa uma declaração `switch` para verificações de exaustividade para garantir que cada estado seja explicitamente manipulado. - Se você não quer usar o [switch exaustivo](https://dart.dev/language/branches#exhaustiveness-checking) ou pretende adicionar subtipos mais tarde sem quebrar a API, use o modificador [final](https://dart.dev/language/class-modifiers#final). - Veja a [documentação da classe selada](https://dart.dev/language/class-modifiers#sealed) para mais detalhes. #### Contras - **Verboso**: Requer mais código (uma classe base e uma subclasse por estado). Também pode exigir código duplicado para propriedades compartilhadas entre subclasses. - **Complexo**: Adicionar novas propriedades requer a atualização de cada subclasse e da classe base, o que pode ser trabalhoso e levar ao aumento da complexidade do estado. Além disso, pode exigir verificação de tipo desnecessária/excessiva para acessar propriedades. #### Veredito Esta abordagem funciona melhor para estados bem definidos, exclusivos e com propriedades únicas. Esta abordagem fornece verificações de segurança de tipo e exaustividade e enfatiza a segurança em vez da concisão e simplicidade. ================================================ FILE: docs/src/content/docs/pt-br/why-bloc.mdx ================================================ --- title: Por que Bloc? description: Uma visão geral do que torna o Bloc uma solução de gerenciamento de estado sólido. sidebar: order: 1 --- O Bloc facilita separar a apresentação da lógica de negócios, tornando seu código _rápido_, _fácil de testar_ e _reutilizável_. Ao construir aplicativos de qualidade em produção, o gerenciamento do estado torna-se crítico. Como desenvolvedores, queremos: - saber em que estado o nosso aplicativo está em qualquer momento. - testar facilmente todos os casos de uso para garantir que nosso aplicativo esteja respondendo adequadamente. - registrar cada interação do usuário em nosso aplicativo para que possamos tomar decisões baseadas em dados. - trabalhar da forma mais eficiente possível e reutilizar componentes tanto em nosso aplicativo quanto em outros aplicativos. - ter muitos desenvolvedores trabalhando facilmente em uma única base de código, seguindo os mesmos padrões e convenções. - desenvolver aplicativos rápidos e reativos. O Bloc foi projetado para atender a todas essas necessidades e muito mais. Existem muitas soluções de gerenciamento de estado e decidir qual delas usar pode ser uma tarefa difícil. Não existe uma solução de gerenciamento de estado perfeita! O importante é que você escolha a que funcione melhor para a sua equipe e o seu projeto. O Bloc foi projetado com três valores fundamentais em mente: - **Simples:** Fácil de entender e pode ser usado por desenvolvedores com diferentes níveis de habilidade. - **Poderoso:** Ajuda a criar aplicativos incríveis e complexos, compondo-os com componentes menores. - **Testável:** Testa facilmente todos os aspectos de um aplicativo para que possamos iterar com confiança. No geral, o Bloc tenta tornar as mudanças de estado previsíveis, regulando quando uma alteração de estado pode ocorrer e impondo uma única maneira de alterar o estado em todo o aplicativo. ================================================ FILE: docs/src/content/docs/ru/architecture.mdx ================================================ --- title: Архитектура description: Обзор рекомендуемых архитектурных шаблонов при использовании bloc. --- import DataProviderSnippet from '~/components/architecture/DataProviderSnippet.astro'; import RepositorySnippet from '~/components/architecture/RepositorySnippet.astro'; import BusinessLogicComponentSnippet from '~/components/architecture/BusinessLogicComponentSnippet.astro'; import BlocTightCouplingSnippet from '~/components/architecture/BlocTightCouplingSnippet.astro'; import BlocLooseCouplingPresentationSnippet from '~/components/architecture/BlocLooseCouplingPresentationSnippet.astro'; import AppIdeasRepositorySnippet from '~/components/architecture/AppIdeasRepositorySnippet.astro'; import AppIdeaRankingBlocSnippet from '~/components/architecture/AppIdeaRankingBlocSnippet.astro'; import PresentationComponentSnippet from '~/components/architecture/PresentationComponentSnippet.astro'; ![Архитектура Bloc](~/assets/concepts/bloc_architecture_full.png) Использование библиотеки bloc позволяет нам разделить наше приложение на три слоя: - Представление - Бизнес-логика - Данные - Репозиторий - Поставщик данных Мы начнем с самого нижнего слоя (наиболее удаленного от пользовательского интерфейса) и будем двигаться вверх к слою представления. ## Слой данных Ответственность слоя данных заключается в получении/манипулировании данными из одного или нескольких источников. Слой данных можно разделить на две части: - Репозиторий - Поставщик данных Этот слой является самым низким уровнем приложения и взаимодействует с базами данных, сетевыми запросами и другими асинхронными источниками данных. ### Поставщик данных Ответственность поставщика данных заключается в предоставлении необработанных данных. Поставщик данных должен быть универсальным и многофункциональным. Поставщик данных обычно предоставляет простые API для выполнения [CRUD](https://ru.wikipedia.org/wiki/CRUD) операций. У нас могут быть методы `createData`, `readData`, `updateData` и `deleteData` как часть нашего слоя данных. ### Репозиторий Слой репозитория — это обертка вокруг одного или нескольких поставщиков данных, с которыми общается слой Bloc. Как вы можете видеть, наш слой репозитория может взаимодействовать с несколькими поставщиками данных и выполнять преобразования данных перед передачей результата на слой бизнес-логики. ## Слой бизнес-логики Ответственность слоя бизнес-логики заключается в ответе на ввод из слоя представления новыми состояниями. Этот слой может зависеть от одного или нескольких репозиториев для получения данных, необходимых для построения состояния приложения. Думайте о слое бизнес-логики как о мосте между пользовательским интерфейсом (слой представления) и слоем данных. Слой бизнес-логики уведомляется о событиях/действиях из слоя представления, а затем взаимодействует с репозиторием, чтобы построить новое состояние для использования слоем представления. ### Взаимодействие между блоками Поскольку блоки предоставляют потоки, может возникнуть соблазн создать блок, который прослушивает другой блок. Вы **не должны** делать этого. Существуют лучшие альтернативы, чем прибегание к коду ниже: Хотя приведенный выше код не содержит ошибок (и даже очищается за собой), у него есть более серьезная проблема: он создает зависимость между двумя блоками. Как правило, зависимостей между двумя сущностями на одном архитектурном слое следует избегать любой ценой, так как это создает жесткую связь, которую трудно поддерживать. Поскольку блоки находятся на архитектурном слое бизнес-логики, ни один блок не должен знать о каком-либо другом блоке. ![Слои архитектуры приложения](~/assets/architecture/architecture.png) Блок должен получать информацию только через события и из внедренных репозиториев (то есть репозиториев, переданных блоку в его конструкторе). Если вы находитесь в ситуации, когда блок должен реагировать на другой блок, у вас есть два других варианта. Вы можете переместить проблему на слой выше (в слой представления) или на слой ниже (в слой домена). #### Соединение блоков через представление Вы можете использовать `BlocListener` для прослушивания одного блока и добавления события в другой блок всякий раз, когда первый блок изменяется. Приведенный выше код предотвращает необходимость `SecondBloc` знать о `FirstBloc`, поощряя слабую связь. Приложение [flutter_weather](/ru/tutorials/flutter-weather) [использует эту технику](https://github.com/felangel/bloc/blob/b4c8db938ad71a6b60d4a641ec357905095c3965/examples/flutter_weather/lib/weather/view/weather_page.dart#L38-L42) для изменения темы приложения на основе полученной информации о погоде. В некоторых ситуациях вы можете не захотеть связывать два блока в слое представления. Вместо этого часто имеет смысл, чтобы два блока использовали один и тот же источник данных и обновлялись при изменении данных. #### Соединение блоков через домен Два блока могут прослушивать поток из репозитория и обновлять свои состояния независимо друг от друга всякий раз, когда изменяются данные репозитория. Использование реактивных репозиториев для синхронизации состояния является обычным делом в крупномасштабных корпоративных приложениях. Сначала создайте или используйте репозиторий, который предоставляет `Stream` данных. Например, следующий репозиторий предоставляет бесконечный поток одних и тех же нескольких идей приложений: Один и тот же репозиторий может быть внедрен в каждый блок, который должен реагировать на новые идеи приложений. Ниже приведен `AppIdeaRankingBloc`, который выдает состояние для каждой входящей идеи приложения из репозитория выше: Подробнее об использовании потоков с Bloc см. в статье [Как использовать Bloc с потоками и параллелизмом](https://verygood.ventures/blog/how-to-use-bloc-with-streams-and-concurrency). ## Слой представления Ответственность слоя представления заключается в определении того, как отрисовать себя на основе одного или нескольких состояний блоков. Кроме того, он должен обрабатывать ввод пользователя и события жизненного цикла приложения. Большинство потоков приложений начинаются с события `AppStart`, которое запускает приложение для получения некоторых данных для представления пользователю. В этом сценарии слой представления добавит событие `AppStart`. Кроме того, слой представления должен будет выяснить, что отрисовать на экране на основе состояния из слоя bloc. До сих пор, хотя у нас были некоторые фрагменты кода, все это было довольно высокоуровневым. В разделе руководств мы объединим все это вместе, когда будем создавать несколько различных примеров приложений. ================================================ FILE: docs/src/content/docs/ru/bloc-concepts.mdx ================================================ --- title: Концепции Bloc description: Обзор основных концепций для package:bloc. sidebar: order: 1 --- import CountStreamSnippet from '~/components/concepts/bloc/CountStreamSnippet.astro'; import SumStreamSnippet from '~/components/concepts/bloc/SumStreamSnippet.astro'; import StreamsMainSnippet from '~/components/concepts/bloc/StreamsMainSnippet.astro'; import CounterCubitSnippet from '~/components/concepts/bloc/CounterCubitSnippet.astro'; import CounterCubitInitialStateSnippet from '~/components/concepts/bloc/CounterCubitInitialStateSnippet.astro'; import CounterCubitInstantiationSnippet from '~/components/concepts/bloc/CounterCubitInstantiationSnippet.astro'; import CounterCubitIncrementSnippet from '~/components/concepts/bloc/CounterCubitIncrementSnippet.astro'; import CounterCubitBasicUsageSnippet from '~/components/concepts/bloc/CounterCubitBasicUsageSnippet.astro'; import CounterCubitStreamUsageSnippet from '~/components/concepts/bloc/CounterCubitStreamUsageSnippet.astro'; import CounterCubitOnChangeSnippet from '~/components/concepts/bloc/CounterCubitOnChangeSnippet.astro'; import CounterCubitOnChangeUsageSnippet from '~/components/concepts/bloc/CounterCubitOnChangeUsageSnippet.astro'; import CounterCubitOnChangeOutputSnippet from '~/components/concepts/bloc/CounterCubitOnChangeOutputSnippet.astro'; import SimpleBlocObserverOnChangeSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeSnippet.astro'; import SimpleBlocObserverOnChangeUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeUsageSnippet.astro'; import SimpleBlocObserverOnChangeOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeOutputSnippet.astro'; import CounterCubitOnErrorSnippet from '~/components/concepts/bloc/CounterCubitOnErrorSnippet.astro'; import SimpleBlocObserverOnErrorSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnErrorSnippet.astro'; import CounterCubitOnErrorOutputSnippet from '~/components/concepts/bloc/CounterCubitOnErrorOutputSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/bloc/CounterBlocSnippet.astro'; import CounterBlocEventHandlerSnippet from '~/components/concepts/bloc/CounterBlocEventHandlerSnippet.astro'; import CounterBlocIncrementSnippet from '~/components/concepts/bloc/CounterBlocIncrementSnippet.astro'; import CounterBlocUsageSnippet from '~/components/concepts/bloc/CounterBlocUsageSnippet.astro'; import CounterBlocStreamUsageSnippet from '~/components/concepts/bloc/CounterBlocStreamUsageSnippet.astro'; import CounterBlocOnChangeSnippet from '~/components/concepts/bloc/CounterBlocOnChangeSnippet.astro'; import CounterBlocOnChangeUsageSnippet from '~/components/concepts/bloc/CounterBlocOnChangeUsageSnippet.astro'; import CounterBlocOnChangeOutputSnippet from '~/components/concepts/bloc/CounterBlocOnChangeOutputSnippet.astro'; import CounterBlocOnTransitionSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionSnippet.astro'; import CounterBlocOnTransitionOutputSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionOutputSnippet.astro'; import SimpleBlocObserverOnTransitionSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionSnippet.astro'; import SimpleBlocObserverOnTransitionUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionUsageSnippet.astro'; import SimpleBlocObserverOnTransitionOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionOutputSnippet.astro'; import CounterBlocOnEventSnippet from '~/components/concepts/bloc/CounterBlocOnEventSnippet.astro'; import SimpleBlocObserverOnEventSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventSnippet.astro'; import SimpleBlocObserverOnEventOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventOutputSnippet.astro'; import CounterBlocOnErrorSnippet from '~/components/concepts/bloc/CounterBlocOnErrorSnippet.astro'; import CounterBlocOnErrorOutputSnippet from '~/components/concepts/bloc/CounterBlocOnErrorOutputSnippet.astro'; import CounterCubitFullSnippet from '~/components/concepts/bloc/CounterCubitFullSnippet.astro'; import CounterBlocFullSnippet from '~/components/concepts/bloc/CounterBlocFullSnippet.astro'; import AuthenticationStateSnippet from '~/components/concepts/bloc/AuthenticationStateSnippet.astro'; import AuthenticationTransitionSnippet from '~/components/concepts/bloc/AuthenticationTransitionSnippet.astro'; import AuthenticationChangeSnippet from '~/components/concepts/bloc/AuthenticationChangeSnippet.astro'; import DebounceEventTransformerSnippet from '~/components/concepts/bloc/DebounceEventTransformerSnippet.astro'; :::note Пожалуйста, внимательно прочитайте следующие разделы перед началом работы с [`package:bloc`](https://pub.dev/packages/bloc). ::: Существует несколько ключевых концепций, которые критически важны для понимания того, как использовать пакет bloc. В предстоящих разделах мы подробно обсудим каждую из них, а также рассмотрим, как они применяются на примере приложения-счетчика. ## Потоки (Streams) :::note Ознакомьтесь с официальной [Документацией Dart](https://dart.dev/tutorials/language/streams) для получения дополнительной информации о `Streams`. ::: Поток (stream) — это последовательность асинхронных данных. Для использования библиотеки bloc критически важно иметь базовое понимание `Streams` и того, как они работают. Если вы не знакомы с `Streams`, просто представьте трубу с водой, текущей через неё. Труба — это `Stream`, а вода — это асинхронные данные. Мы можем создать `Stream` в Dart, написав функцию `async*` (асинхронный генератор). Помечая функцию как `async*`, мы получаем возможность использовать ключевое слово `yield` и возвращать `Stream` данных. В приведенном выше примере мы возвращаем `Stream` целых чисел до значения параметра `max`. Каждый раз, когда мы используем `yield` в функции `async*`, мы проталкиваем этот фрагмент данных через `Stream`. Мы можем использовать вышеуказанный `Stream` несколькими способами. Если бы мы хотели написать функцию для возврата суммы `Stream` целых чисел, она могла бы выглядеть так: Помечая вышеуказанную функцию как `async`, мы получаем возможность использовать ключевое слово `await` и возвращать `Future` целых чисел. В этом примере мы ожидаем каждое значение в потоке и возвращаем сумму всех целых чисел в потоке. Мы можем собрать все это вместе следующим образом: Теперь, когда у нас есть базовое понимание того, как работают `Streams` в Dart, мы готовы узнать об основном компоненте пакета bloc: `Cubit`. ## Cubit `Cubit` — это класс, который расширяет `BlocBase` и может быть расширен для управления любым типом состояния. ![Cubit Architecture](~/assets/concepts/cubit_architecture_full.png) `Cubit` может предоставлять функции, которые можно вызывать для запуска изменений состояния. Состояния — это выходные данные `Cubit` и представляют часть состояния вашего приложения. Компоненты UI могут быть уведомлены о состояниях и перерисовывать части себя на основе текущего состояния. :::note Для получения дополнительной информации о происхождении `Cubit` ознакомьтесь с [этим issue](https://github.com/felangel/cubit/issues/69). ::: ### Создание Cubit Мы можем создать `CounterCubit` следующим образом: При создании `Cubit` нам необходимо определить тип состояния, которым будет управлять `Cubit`. В случае `CounterCubit` выше состояние может быть представлено через `int`, но в более сложных случаях может быть необходимо использовать `class` вместо примитивного типа. Второе, что нам нужно сделать при создании `Cubit`, — это указать начальное состояние. Мы можем сделать это, вызвав `super` со значением начального состояния. В приведенном выше фрагменте мы устанавливаем начальное состояние в `0` внутренне, но мы также можем позволить `Cubit` быть более гибким, принимая внешнее значение: Это позволило бы нам создавать экземпляры `CounterCubit` с различными начальными состояниями, например: ### Изменения состояния Cubit Каждый `Cubit` имеет возможность выдавать новое состояние через `emit`. В приведенном выше фрагменте `CounterCubit` предоставляет публичный метод `increment`, который может быть вызван извне для уведомления `CounterCubit` об увеличении его состояния. Когда вызывается `increment`, мы можем получить доступ к текущему состоянию `Cubit` через геттер `state` и вызвать `emit` нового состояния, добавив 1 к текущему состоянию. :::caution Метод `emit` защищен, что означает, что он должен использоваться только внутри `Cubit`. ::: ### Использование Cubit Теперь мы можем взять реализованный `CounterCubit` и использовать его! #### Базовое использование В приведенном выше фрагменте мы начинаем с создания экземпляра `CounterCubit`. Затем мы выводим текущее состояние cubit, которое является начальным состоянием (поскольку новые состояния еще не были выпущены). Далее мы вызываем функцию `increment` для запуска изменения состояния. Наконец, мы снова выводим состояние `Cubit`, которое изменилось с `0` на `1`, и вызываем `close` на `Cubit` для закрытия внутреннего потока состояний. #### Использование Stream `Cubit` предоставляет `Stream`, который позволяет нам получать обновления состояния в реальном времени: В приведенном выше фрагменте мы подписываемся на `CounterCubit` и вызываем print при каждом изменении состояния. Затем мы вызываем функцию `increment`, которая выдаст новое состояние. Наконец, мы вызываем `cancel` на `subscription`, когда больше не хотим получать обновления, и закрываем `Cubit`. :::note `await Future.delayed(Duration.zero)` добавлен для этого примера, чтобы избежать немедленной отмены подписки. ::: :::caution Только последующие изменения состояния будут получены при вызове `listen` на `Cubit`. ::: ### Наблюдение за Cubit Когда `Cubit` выдает новое состояние, происходит `Change`. Мы можем наблюдать все изменения для данного `Cubit`, переопределив `onChange`. Затем мы можем взаимодействовать с `Cubit` и наблюдать все изменения, выводимые в консоль. Приведенный выше пример выведет: :::note `Change` происходит непосредственно перед обновлением состояния `Cubit`. `Change` состоит из `currentState` и `nextState`. ::: #### BlocObserver Одним из дополнительных преимуществ использования библиотеки bloc является то, что мы можем иметь доступ ко всем `Changes` в одном месте. Хотя в этом приложении у нас есть только один `Cubit`, в больших приложениях довольно часто встречается много `Cubits`, управляющих различными частями состояния приложения. Если мы хотим иметь возможность что-то делать в ответ на все `Changes`, мы можем просто создать собственный `BlocObserver`. :::note Все, что нам нужно сделать, — это расширить `BlocObserver` и переопределить метод `onChange`. ::: Чтобы использовать `SimpleBlocObserver`, нам просто нужно изменить функцию `main`: Приведенный выше фрагмент затем выведет: :::note Внутреннее переопределение `onChange` вызывается первым, которое вызывает `super.onChange`, уведомляя `onChange` в `BlocObserver`. ::: :::tip В `BlocObserver` мы имеем доступ к экземпляру `Cubit` в дополнение к самому `Change`. ::: ### Обработка ошибок в Cubit Каждый `Cubit` имеет метод `addError`, который можно использовать для указания на то, что произошла ошибка. :::note `onError` может быть переопределен внутри `Cubit` для обработки всех ошибок для конкретного `Cubit`. ::: `onError` также может быть переопределен в `BlocObserver` для глобальной обработки всех сообщаемых ошибок. Если мы снова запустим ту же программу, мы должны увидеть следующий вывод: ## Bloc `Bloc` — это более продвинутый класс, который полагается на `события` для запуска изменений `состояния`, а не на функции. `Bloc` также расширяет `BlocBase`, что означает, что он имеет аналогичный публичный API, как `Cubit`. Однако вместо вызова `функции` на `Bloc` и прямого выпуска нового `состояния`, `Bloc`-и получают `события` и преобразуют входящие `события` в исходящие `состояния`. ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) ### Создание Bloc Создание `Bloc` аналогично созданию `Cubit`, за исключением того, что в дополнение к определению состояния, которым мы будем управлять, мы также должны определить событие, которое `Bloc` сможет обрабатывать. События — это входные данные для Bloc. Они обычно добавляются в ответ на пользовательские взаимодействия, такие как нажатия кнопок, или события жизненного цикла, такие как загрузка страницы. Так же, как при создании `CounterCubit`, мы должны указать начальное состояние, передав его в суперкласс через `super`. ### Изменения состояния Bloc `Bloc` требует, чтобы мы регистрировали обработчики событий через API `on`, в отличие от функций в `Cubit`. Обработчик событий отвечает за преобразование любых входящих событий в ноль или более исходящих состояний. :::tip `EventHandler` имеет доступ к добавленному событию, а также к `Emitter`, который может использоваться для выдачи нуля или более состояний в ответ на входящее событие. ::: Затем мы можем обновить `EventHandler` для обработки события `CounterIncrementPressed`: В приведенном выше фрагменте мы зарегистрировали `EventHandler` для управления всеми событиями `CounterIncrementPressed`. Для каждого входящего события `CounterIncrementPressed` мы можем получить доступ к текущему состоянию bloc через геттер `state` и вызвать `emit(state + 1)`. :::note Поскольку класс `Bloc` расширяет `BlocBase`, мы имеем доступ к текущему состоянию bloc в любой момент времени через геттер `state`, так же как в `Cubit`. ::: :::caution Bloc-и никогда не должны напрямую вызывать `emit` для новых состояний. Вместо этого каждое изменение состояния должно быть выведено в ответ на входящее событие внутри `EventHandler`. ::: :::caution И bloc-и, и cubit-ы будут игнорировать дублирующиеся состояния. Если мы выдадим `State nextState`, где `state == nextState`, то изменение состояния не произойдет. ::: ### Использование Bloc На этом этапе мы можем создать экземпляр нашего `CounterBloc` и использовать его! #### Базовое использование В приведенном выше фрагменте мы начинаем с создания экземпляра `CounterBloc`. Затем мы выводим текущее состояние `Bloc`, которое является начальным состоянием (поскольку новые состояния еще не были выпущены). Далее мы добавляем событие `CounterIncrementPressed` для запуска изменения состояния. Наконец, мы снова выводим состояние `Bloc`, которое изменилось с `0` на `1`, и вызываем `close` на `Bloc` для закрытия внутреннего потока состояний. :::note `await Future.delayed(Duration.zero)` добавлен, чтобы убедиться, что мы ждем следующей итерации цикла событий (позволяя `EventHandler` обработать событие). ::: #### Использование Stream Так же, как с `Cubit`, `Bloc` — это специальный тип `Stream`, что означает, что мы также можем подписаться на `Bloc` для получения обновлений его состояния в реальном времени: В приведенном выше фрагменте мы подписываемся на `CounterBloc` и вызываем print при каждом изменении состояния. Затем мы добавляем событие `CounterIncrementPressed`, которое запускает `EventHandler` `on` и выдает новое состояние. Наконец, мы вызываем `cancel` на подписке, когда больше не хотим получать обновления, и закрываем `Bloc`. :::note `await Future.delayed(Duration.zero)` добавлен для этого примера, чтобы избежать немедленной отмены подписки. ::: ### Наблюдение за Bloc Поскольку `Bloc` расширяет `BlocBase`, мы можем наблюдать все изменения состояния для `Bloc` с помощью `onChange`. Затем мы можем обновить `main.dart` до: Теперь, если мы запустим приведенный выше фрагмент, вывод будет: Одним из ключевых отличительных факторов между `Bloc` и `Cubit` является то, что поскольку `Bloc` управляется событиями, мы также можем захватить информацию о том, что вызвало изменение состояния. Мы можем сделать это, переопределив `onTransition`. Изменение от одного состояния к другому называется `Transition`. `Transition` состоит из текущего состояния, события и следующего состояния. Если мы затем повторно запустим тот же фрагмент `main.dart` как раньше, мы должны увидеть следующий вывод: :::note `onTransition` вызывается перед `onChange` и содержит событие, которое вызвало изменение от `currentState` к `nextState`. ::: #### BlocObserver Так же, как и раньше, мы можем переопределить `onTransition` в пользовательском `BlocObserver` для наблюдения за всеми переходами, которые происходят из одного места. Мы можем инициализировать `SimpleBlocObserver` так же, как и раньше: Теперь, если мы запустим приведенный выше фрагмент, вывод должен выглядеть так: :::note `onTransition` вызывается первым (локальный перед глобальным), за которым следует `onChange`. ::: Еще одной уникальной особенностью экземпляров `Bloc` является то, что они позволяют нам переопределить `onEvent`, который вызывается всякий раз, когда новое событие добавляется в `Bloc`. Так же, как с `onChange` и `onTransition`, `onEvent` может быть переопределен локально, а также глобально. Мы можем запустить тот же `main.dart`, как и раньше, и должны увидеть следующий вывод: :::note `onEvent` вызывается, как только событие добавлено. Локальный `onEvent` вызывается перед глобальным `onEvent` в `BlocObserver`. ::: ### Обработка ошибок в Bloc Так же, как с `Cubit`, каждый `Bloc` имеет методы `addError` и `onError`. Мы можем указать, что произошла ошибка, вызвав `addError` из любого места внутри нашего `Bloc`. Затем мы можем реагировать на все ошибки, переопределив `onError`, так же как с `Cubit`. Если мы повторно запустим тот же `main.dart`, как раньше, мы можем увидеть, как это выглядит, когда об ошибке сообщается: :::note Локальный `onError` вызывается первым, за которым следует глобальный `onError` в `BlocObserver`. ::: :::note `onError` и `onChange` работают точно так же для экземпляров как `Bloc`, так и `Cubit`. ::: :::caution Любые необработанные исключения, которые возникают внутри `EventHandler`, также сообщаются в `onError`. ::: ## Cubit против Bloc Теперь, когда мы рассмотрели основы классов `Cubit` и `Bloc`, вам может быть интересно, когда следует использовать `Cubit`, а когда — `Bloc`. ### Преимущества Cubit #### Простота Одним из самых больших преимуществ использования `Cubit` является простота. При создании `Cubit` нам нужно определить только состояние, а также функции, которые мы хотим предоставить для изменения состояния. Для сравнения, при создании `Bloc` мы должны определить состояния, события и реализацию `EventHandler`. Это делает `Cubit` более понятным и требует меньше кода. Теперь давайте рассмотрим две реализации счетчика: ##### CounterCubit ##### CounterBloc Реализация `Cubit` более лаконична, и вместо отдельного определения событий функции действуют как события. Кроме того, при использовании `Cubit` мы можем просто вызвать `emit` из любого места, чтобы вызвать изменение состояния. ### Преимущества Bloc #### Отслеживаемость Одним из самых больших преимуществ использования `Bloc` является знание последовательности изменений состояния, а также того, что именно вызвало эти изменения. Для состояния, которое критически важно для функциональности приложения, может быть очень полезно использовать более управляемый событиями подход, чтобы захватить все события в дополнение к изменениям состояния. Распространенным случаем использования может быть управление `AuthenticationState`. Для простоты предположим, что мы можем представить `AuthenticationState` через `enum`: Может быть много причин, по которым состояние приложения могло измениться с `authenticated` на `unauthenticated`. Например, пользователь мог нажать кнопку выхода и запросить выход из приложения. С другой стороны, возможно, токен доступа пользователя был отозван, и он был принудительно разлогинен. При использовании `Bloc` мы можем четко отследить, как состояние приложения попало в определенное состояние. Приведенный выше `Transition` дает нам всю информацию, необходимую для понимания того, почему изменилось состояние. Если бы мы использовали `Cubit` для управления `AuthenticationState`, наши логи выглядели бы так: Это говорит нам, что пользователь был разлогинен, но не объясняет почему, что может быть критически важно для отладки и понимания того, как состояние приложения меняется со временем. #### Расширенные преобразования событий Еще одна область, в которой `Bloc` превосходит `Cubit`, — это когда нам нужно воспользоваться реактивными операторами, такими как `buffer`, `debounceTime`, `throttle` и т.д. :::tip См. [`package:stream_transform`](https://pub.dev/packages/stream_transform) и [`package:rxdart`](https://pub.dev/packages/rxdart) для преобразователей потоков. ::: `Bloc` имеет приемник событий, который позволяет нам контролировать и преобразовывать входящий поток событий. Например, если бы мы создавали поиск в реальном времени, мы, вероятно, хотели бы отложить запросы к бэкенду, чтобы избежать ограничения скорости, а также сократить затраты/нагрузку на бэкенд. С `Bloc` мы можем предоставить пользовательский `EventTransformer` для изменения способа обработки входящих событий `Bloc`. С приведенным выше кодом мы можем легко задержать входящие события с очень небольшим количеством дополнительного кода. :::tip Ознакомьтесь с [`package:bloc_concurrency`](https://pub.dev/packages/bloc_concurrency) для набора преобразователей событий с определенным мнением. ::: Если вы не уверены, что использовать, начните с `Cubit`, и вы сможете позже отрефакторить или масштабироваться до `Bloc` по мере необходимости. ================================================ FILE: docs/src/content/docs/ru/faqs.mdx ================================================ --- title: Часто задаваемые вопросы description: Ответы на часто задаваемые вопросы о библиотеке bloc. --- import StateNotUpdatingGood1Snippet from '~/components/faqs/StateNotUpdatingGood1Snippet.astro'; import StateNotUpdatingGood2Snippet from '~/components/faqs/StateNotUpdatingGood2Snippet.astro'; import StateNotUpdatingGood3Snippet from '~/components/faqs/StateNotUpdatingGood3Snippet.astro'; import StateNotUpdatingBad1Snippet from '~/components/faqs/StateNotUpdatingBad1Snippet.astro'; import StateNotUpdatingBad2Snippet from '~/components/faqs/StateNotUpdatingBad2Snippet.astro'; import StateNotUpdatingBad3Snippet from '~/components/faqs/StateNotUpdatingBad3Snippet.astro'; import EquatableEmitSnippet from '~/components/faqs/EquatableEmitSnippet.astro'; import EquatableBlocTestSnippet from '~/components/faqs/EquatableBlocTestSnippet.astro'; import NoEquatableBlocTestSnippet from '~/components/faqs/NoEquatableBlocTestSnippet.astro'; import SingleStateSnippet from '~/components/faqs/SingleStateSnippet.astro'; import SingleStateUsageSnippet from '~/components/faqs/SingleStateUsageSnippet.astro'; import BlocProviderGood1Snippet from '~/components/faqs/BlocProviderGood1Snippet.astro'; import BlocProviderGood2Snippet from '~/components/faqs/BlocProviderGood2Snippet.astro'; import BlocProviderBad1Snippet from '~/components/faqs/BlocProviderBad1Snippet.astro'; import BlocInternalAddEventSnippet from '~/components/faqs/BlocInternalAddEventSnippet.astro'; import BlocInternalEventSnippet from '~/components/faqs/BlocInternalEventSnippet.astro'; import BlocExternalForEachSnippet from '~/components/faqs/BlocExternalForEachSnippet.astro'; ## Состояние не обновляется ❔ **Вопрос**: Я испускаю состояние в моем блоке, но пользовательский интерфейс не обновляется. Что я делаю неправильно? 💡 **Ответ**: Если вы используете Equatable, убедитесь, что вы передаете все свойства в геттер props. ✅ **ХОРОШО** ❌ **ПЛОХО** Кроме того, убедитесь, что вы испускаете новый экземпляр состояния в вашем блоке. ✅ **ХОРОШО** ❌ **ПЛОХО** :::caution Свойства `Equatable` всегда должны копироваться, а не изменяться. Если класс `Equatable` содержит `List` или `Map` в качестве свойств, обязательно используйте `List.of` или `Map.of` соответственно, чтобы гарантировать, что равенство оценивается на основе значений свойств, а не ссылки. ::: ## Когда использовать Equatable ❔**Вопрос**: Когда мне следует использовать Equatable? 💡**Ответ**: В приведенном выше сценарии, если `StateA` расширяет `Equatable`, произойдет только одно изменение состояния (второе испускание будет проигнорировано). В общем, вы должны использовать `Equatable`, если вы хотите оптимизировать свой код, чтобы уменьшить количество перестроек. Вы не должны использовать `Equatable`, если хотите, чтобы одно и то же состояние подряд вызывало несколько переходов. Кроме того, использование `Equatable` значительно упрощает тестирование блоков, поскольку мы можем ожидать конкретные экземпляры состояний блока, а не использовать `Matchers` или `Predicates`. Без `Equatable` приведенный выше тест не пройдет, и его нужно будет переписать следующим образом: ## Обработка ошибок ❔ **Вопрос**: Как я могу обработать ошибку, сохраняя при этом предыдущие данные? 💡 **Ответ**: Это во многом зависит от того, как было смоделировано состояние блока. В случаях, когда данные должны сохраняться даже при наличии ошибки, рассмотрите использование одного класса состояния. Это позволит виджетам иметь доступ к свойствам `data` и `error` одновременно, и блок может использовать `state.copyWith` для сохранения старых данных даже когда произошла ошибка. ## Bloc vs. Redux ❔ **Вопрос**: В чем разница между Bloc и Redux? 💡 **Ответ**: BLoC — это шаблон проектирования, определяемый следующими правилами: 1. Вход и выход BLoC — это простые потоки и приемники. 2. Зависимости должны быть внедряемыми и независимыми от платформы. 3. Разветвление платформы не допускается. 4. Реализация может быть любой, если вы следуете приведенным выше правилам. Рекомендации по пользовательскому интерфейсу: 1. Каждый "достаточно сложный" компонент имеет соответствующий BLoC. 2. Компоненты должны отправлять входы "как есть". 3. Компоненты должны показывать выходы как можно ближе к "как есть". 4. Все разветвления должны основываться на простых логических выходах BLoC. Библиотека Bloc реализует шаблон проектирования BLoC и нацелена на абстрагирование RxDart для упрощения опыта разработчика. Три принципа Redux: 1. Единый источник истины 2. Состояние доступно только для чтения 3. Изменения производятся чистыми функциями Библиотека bloc нарушает первый принцип; с bloc состояние распределено по нескольким блокам. Кроме того, в bloc нет концепции middleware, и bloc предназначен для упрощения асинхронных изменений состояния, позволяя вам испускать несколько состояний для одного события. ## Bloc vs. Provider ❔ **Вопрос**: В чем разница между Bloc и Provider? 💡 **Ответ**: `provider` предназначен для внедрения зависимостей (он оборачивает `InheritedWidget`). Вам все равно нужно выяснить, как управлять вашим состоянием (через `ChangeNotifier`, `Bloc`, `Mobx` и т.д...). Библиотека Bloc использует `provider` внутренне, чтобы упростить предоставление и доступ к блокам по всему дереву виджетов. ## BlocProvider.of() не может найти Bloc ❔ **Вопрос**: При использовании `BlocProvider.of(context)` он не может найти блок. Как это исправить? 💡 **Ответ**: Вы не можете получить доступ к блоку из того же контекста, в котором он был предоставлен, поэтому вы должны убедиться, что `BlocProvider.of()` вызывается внутри дочернего `BuildContext`. ✅ **ХОРОШО** ❌ **ПЛОХО** ## Структура проекта ❔ **Вопрос**: Как мне структурировать мой проект? 💡 **Ответ**: Хотя на этот вопрос действительно нет правильного/неправильного ответа, некоторые рекомендуемые ссылки: - [I/O Photobooth](https://github.com/flutter/photobooth) - [I/O Pinball](https://github.com/flutter/pinball) - [Flutter News Toolkit](https://github.com/flutter/news_toolkit) Самое важное — иметь **последовательную** и **продуманную** структуру проекта. ## Добавление событий внутри блока ❔ **Вопрос**: Можно ли добавлять события внутри блока? 💡 **Ответ**: В большинстве случаев события должны добавляться извне, но в некоторых избранных случаях может иметь смысл добавлять события внутренне. Наиболее распространенная ситуация, в которой используются внутренние события, — это когда изменения состояния должны происходить в ответ на обновления в реальном времени из репозитория. В этих ситуациях репозиторий является стимулом для изменения состояния вместо внешнего события, такого как нажатие кнопки. В следующем примере состояние `MyBloc` зависит от текущего пользователя, который предоставляется через `Stream` из `UserRepository`. `MyBloc` прослушивает изменения текущего пользователя и добавляет внутреннее событие `_UserChanged` всякий раз, когда пользователь испускается из потока пользователей. Добавляя внутреннее событие, мы также можем указать пользовательский `transformer` для события, чтобы определить, как будут обрабатываться несколько событий `_UserChanged` — по умолчанию они будут обрабатываться одновременно. Настоятельно рекомендуется, чтобы внутренние события были приватными. Это явный способ сигнализировать, что конкретное событие используется только внутри самого блока и предотвращает знание внешних компонентов о событии. В качестве альтернативы мы можем определить внешнее событие `Started` и использовать API `emit.forEach` для обработки реагирования на обновления пользователей в реальном времени: Преимущества приведенного выше подхода: - Нам не нужно внутреннее событие `_UserChanged` - Нам не нужно управлять `StreamSubscription` вручную - У нас есть полный контроль над тем, когда блок подписывается на поток обновлений пользователей Недостатки приведенного выше подхода: - Мы не можем легко приостановить `pause` или возобновить `resume` подписку - Нам нужно предоставить публичное событие `Started`, которое должно быть добавлено извне - Мы не можем использовать пользовательский `transformer` для настройки того, как мы реагируем на обновления пользователей ## Предоставление публичных методов ❔ **Вопрос**: Можно ли предоставлять публичные методы в моих экземплярах bloc и cubit? 💡 **Ответ** При создании cubit рекомендуется предоставлять только публичные методы для целей запуска изменений состояния. В результате, как правило, все публичные методы в экземпляре cubit должны возвращать `void` или `Future`. При создании bloc рекомендуется избегать предоставления каких-либо пользовательских публичных методов и вместо этого уведомлять блок о событиях, вызывая `add`. ================================================ FILE: docs/src/content/docs/ru/flutter-bloc-concepts.mdx ================================================ --- title: Концепции Flutter Bloc description: Обзор основных концепций для package:flutter_bloc. sidebar: order: 2 --- import BlocBuilderSnippet from '~/components/concepts/flutter-bloc/BlocBuilderSnippet.astro'; import BlocBuilderExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocBuilderExplicitBlocSnippet.astro'; import BlocBuilderConditionSnippet from '~/components/concepts/flutter-bloc/BlocBuilderConditionSnippet.astro'; import BlocSelectorSnippet from '~/components/concepts/flutter-bloc/BlocSelectorSnippet.astro'; import BlocProviderSnippet from '~/components/concepts/flutter-bloc/BlocProviderSnippet.astro'; import BlocProviderEagerSnippet from '~/components/concepts/flutter-bloc/BlocProviderEagerSnippet.astro'; import BlocProviderValueSnippet from '~/components/concepts/flutter-bloc/BlocProviderValueSnippet.astro'; import BlocProviderLookupSnippet from '~/components/concepts/flutter-bloc/BlocProviderLookupSnippet.astro'; import NestedBlocProviderSnippet from '~/components/concepts/flutter-bloc/NestedBlocProviderSnippet.astro'; import MultiBlocProviderSnippet from '~/components/concepts/flutter-bloc/MultiBlocProviderSnippet.astro'; import BlocListenerSnippet from '~/components/concepts/flutter-bloc/BlocListenerSnippet.astro'; import BlocListenerExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocListenerExplicitBlocSnippet.astro'; import BlocListenerConditionSnippet from '~/components/concepts/flutter-bloc/BlocListenerConditionSnippet.astro'; import NestedBlocListenerSnippet from '~/components/concepts/flutter-bloc/NestedBlocListenerSnippet.astro'; import MultiBlocListenerSnippet from '~/components/concepts/flutter-bloc/MultiBlocListenerSnippet.astro'; import BlocConsumerSnippet from '~/components/concepts/flutter-bloc/BlocConsumerSnippet.astro'; import BlocConsumerConditionSnippet from '~/components/concepts/flutter-bloc/BlocConsumerConditionSnippet.astro'; import RepositoryProviderSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderSnippet.astro'; import RepositoryProviderLookupSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderLookupSnippet.astro'; import RepositoryProviderDisposeSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderDisposeSnippet.astro'; import NestedRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/NestedRepositoryProviderSnippet.astro'; import MultiRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/MultiRepositoryProviderSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/flutter-bloc/CounterBlocSnippet.astro'; import CounterMainSnippet from '~/components/concepts/flutter-bloc/CounterMainSnippet.astro'; import CounterPageSnippet from '~/components/concepts/flutter-bloc/CounterPageSnippet.astro'; import WeatherRepositorySnippet from '~/components/concepts/flutter-bloc/WeatherRepositorySnippet.astro'; import WeatherMainSnippet from '~/components/concepts/flutter-bloc/WeatherMainSnippet.astro'; import WeatherAppSnippet from '~/components/concepts/flutter-bloc/WeatherAppSnippet.astro'; import WeatherPageSnippet from '~/components/concepts/flutter-bloc/WeatherPageSnippet.astro'; :::note Пожалуйста, внимательно прочитайте следующие разделы перед работой с [`package:flutter_bloc`](https://pub.dev/packages/flutter_bloc). ::: :::note Все виджеты, экспортируемые пакетом `flutter_bloc`, интегрируются как с экземплярами `Cubit`, так и с экземплярами `Bloc`. ::: ## Bloc Виджеты ### BlocBuilder **BlocBuilder** — это Flutter виджет, которому требуется `Bloc` и функция `builder`. `BlocBuilder` обрабатывает построение виджета в ответ на новые состояния. `BlocBuilder` очень похож на `StreamBuilder`, но имеет более простой API для уменьшения количества шаблонного кода. Функция `builder` может потенциально вызываться много раз и должна быть [чистой функцией](https://en.wikipedia.org/wiki/Pure_function), которая возвращает виджет в ответ на состояние. Смотрите `BlocListener`, если вы хотите "делать" что-либо в ответ на изменения состояния, такие как навигация, показ диалога и т.д. Если параметр `bloc` опущен, `BlocBuilder` автоматически выполнит поиск, используя `BlocProvider` и текущий `BuildContext`. Указывайте bloc только в том случае, если вы хотите предоставить bloc, который будет ограничен одним виджетом и не доступен через родительский `BlocProvider` и текущий `BuildContext`. Для точного контроля над тем, когда вызывается функция `builder`, можно предоставить опциональный параметр `buildWhen`. `buildWhen` принимает предыдущее состояние bloc и текущее состояние bloc и возвращает логическое значение. Если `buildWhen` возвращает true, `builder` будет вызван с `state` и виджет будет перестроен. Если `buildWhen` возвращает false, `builder` не будет вызван с `state` и перестроение не произойдет. ### BlocSelector **BlocSelector** — это Flutter виджет, который аналогичен `BlocBuilder`, но позволяет разработчикам фильтровать обновления, выбирая новое значение на основе текущего состояния bloc. Ненужные построения предотвращаются, если выбранное значение не изменяется. Выбранное значение должно быть неизменяемым, чтобы `BlocSelector` мог точно определить, должен ли `builder` быть вызван снова. Если параметр `bloc` опущен, `BlocSelector` автоматически выполнит поиск, используя `BlocProvider` и текущий `BuildContext`. ### BlocProvider **BlocProvider** — это Flutter виджет, который предоставляет bloc своим дочерним элементам через `BlocProvider.of(context)`. Он используется как виджет внедрения зависимостей (DI), чтобы один экземпляр bloc мог быть предоставлен нескольким виджетам в поддереве. В большинстве случаев `BlocProvider` должен использоваться для создания новых bloc, которые будут доступны остальной части поддерева. В этом случае, поскольку `BlocProvider` отвечает за создание bloc, он автоматически обработает закрытие bloc. По умолчанию `BlocProvider` создаст bloc лениво, что означает, что `create` будет выполнен, когда bloc будет найден через `BlocProvider.of(context)`. Чтобы переопределить это поведение и принудительно запустить `create` немедленно, `lazy` можно установить в `false`. В некоторых случаях `BlocProvider` может использоваться для предоставления существующего bloc новой части дерева виджетов. Это чаще всего используется, когда существующий bloc необходимо сделать доступным для нового маршрута. В этом случае `BlocProvider` не будет автоматически закрывать bloc, поскольку он не создавал его. затем из `ChildA` или `ScreenA` мы можем получить `BlocA` с помощью: ### MultiBlocProvider **MultiBlocProvider** — это Flutter виджет, который объединяет несколько виджетов `BlocProvider` в один. `MultiBlocProvider` улучшает читаемость и устраняет необходимость вкладывать несколько `BlocProviders`. Используя `MultiBlocProvider`, мы можем перейти от: к: :::caution Когда `BlocProvider` определен в контексте `MultiBlocProvider`, любой `child` будет игнорироваться. ::: ### BlocListener **BlocListener** — это Flutter виджет, который принимает `BlocWidgetListener` и опциональный `Bloc` и вызывает `listener` в ответ на изменения состояния в bloc. Он должен использоваться для функциональности, которая должна выполняться один раз на каждое изменение состояния, такой как навигация, показ `SnackBar`, показ `Dialog` и т.д. `listener` вызывается только один раз для каждого изменения состояния (**НЕ** включая начальное состояние), в отличие от `builder` в `BlocBuilder`, и является функцией `void`. Если параметр `bloc` опущен, `BlocListener` автоматически выполнит поиск, используя `BlocProvider` и текущий `BuildContext`. Указывайте bloc только в том случае, если вы хотите предоставить bloc, который иначе не доступен через `BlocProvider` и текущий `BuildContext`. Для точного контроля над тем, когда вызывается функция `listener`, можно предоставить опциональный параметр `listenWhen`. `listenWhen` принимает предыдущее состояние bloc и текущее состояние bloc и возвращает логическое значение. Если `listenWhen` возвращает true, `listener` будет вызван с `state`. Если `listenWhen` возвращает false, `listener` не будет вызван с `state`. ### MultiBlocListener **MultiBlocListener** — это Flutter виджет, который объединяет несколько виджетов `BlocListener` в один. `MultiBlocListener` улучшает читаемость и устраняет необходимость вкладывать несколько `BlocListeners`. Используя `MultiBlocListener`, мы можем перейти от: к: :::caution Когда `BlocListener` определен в контексте `MultiBlocListener`, любой `child` будет игнорироваться. ::: ### BlocConsumer **BlocConsumer** предоставляет `builder` и `listener` для реагирования на новые состояния. `BlocConsumer` аналогичен вложенным `BlocListener` и `BlocBuilder`, но уменьшает количество необходимого шаблонного кода. `BlocConsumer` должен использоваться только когда необходимо как перестроить UI, так и выполнить другие реакции на изменения состояния в `bloc`. `BlocConsumer` принимает обязательные `BlocWidgetBuilder` и `BlocWidgetListener` и опциональные `bloc`, `BlocBuilderCondition` и `BlocListenerCondition`. Если параметр `bloc` опущен, `BlocConsumer` автоматически выполнит поиск, используя `BlocProvider` и текущий `BuildContext`. Опциональные `listenWhen` и `buildWhen` могут быть реализованы для более детального контроля над тем, когда вызываются `listener` и `builder`. `listenWhen` и `buildWhen` будут вызваны при каждом изменении `state` в `bloc`. Каждый принимает предыдущее `state` и текущее `state` и должен вернуть `bool`, который определяет, будет ли вызвана функция `builder` и/или `listener`. Предыдущее `state` будет инициализировано состоянием `state` блока `bloc` при инициализации `BlocConsumer`. `listenWhen` и `buildWhen` являются опциональными, и если они не реализованы, по умолчанию будет `true`. ### RepositoryProvider **RepositoryProvider** — это Flutter виджет, который предоставляет репозиторий своим дочерним элементам через `RepositoryProvider.of(context)`. Он используется как виджет внедрения зависимостей (DI), чтобы один экземпляр репозитория мог быть предоставлен нескольким виджетам в поддереве. `BlocProvider` должен использоваться для предоставления bloc, в то время как `RepositoryProvider` должен использоваться только для репозиториев. затем из `ChildA` мы можем получить экземпляр `Repository` с помощью: Репозитории, которые управляют ресурсами, которые должны быть освобождены, могут сделать это через колбэк `dispose`: ### MultiRepositoryProvider **MultiRepositoryProvider** — это Flutter виджет, который объединяет несколько виджетов `RepositoryProvider` в один. `MultiRepositoryProvider` улучшает читаемость и устраняет необходимость вкладывать несколько `RepositoryProvider`. Используя `MultiRepositoryProvider`, мы можем перейти от: к: :::caution Когда `RepositoryProvider` определен в контексте `MultiRepositoryProvider`, любой `child` будет игнорироваться. ::: ## Использование BlocProvider Давайте рассмотрим, как использовать `BlocProvider` для предоставления `CounterBloc` в `CounterPage` и реагировать на изменения состояния с помощью `BlocBuilder`. На этом этапе мы успешно отделили наш слой представления от слоя бизнес-логики. Обратите внимание, что виджет `CounterPage` ничего не знает о том, что происходит, когда пользователь нажимает на кнопки. Виджет просто сообщает `CounterBloc`, что пользователь нажал либо кнопку увеличения, либо уменьшения. ## Использование RepositoryProvider Мы рассмотрим, как использовать `RepositoryProvider` в контексте примера [`flutter_weather`][flutter_weather_link]. В нашем `main.dart` мы вызываем `runApp` с нашим виджетом `WeatherApp`. Мы внедрим наш экземпляр `WeatherRepository` в дерево виджетов через `RepositoryProvider`. При создании экземпляра bloc мы можем получить доступ к экземпляру репозитория через `context.read` и внедрить репозиторий в bloc через конструктор. :::tip Если у вас более одного репозитория, вы можете использовать `MultiRepositoryProvider` для предоставления нескольких экземпляров репозиториев поддереву. ::: :::note Используйте колбэк `dispose` для освобождения любых ресурсов, когда `RepositoryProvider` размонтируется. ::: [flutter_weather_link]: https://github.com/felangel/bloc/blob/master/examples/flutter_weather ## Методы расширения [Методы расширения](https://dart.dev/guides/language/extension-methods), представленные в Dart 2.7, — это способ добавить функциональность к существующим библиотекам. В этом разделе мы рассмотрим методы расширения, включенные в `package:flutter_bloc`, и как их можно использовать. `flutter_bloc` имеет зависимость от [package:provider](https://pub.dev/packages/provider), которая упрощает использование [`InheritedWidget`](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html). Внутренне `package:flutter_bloc` использует `package:provider` для реализации: `BlocProvider`, `MultiBlocProvider`, `RepositoryProvider` и виджетов `MultiRepositoryProvider`. `package:flutter_bloc` экспортирует расширения `ReadContext`, `WatchContext` и `SelectContext` из `package:provider`. :::note Узнайте больше о [`package:provider`](https://pub.dev/packages/provider). ::: ### context.read `context.read()` ищет ближайший экземпляр предка типа `T` и функционально эквивалентен `BlocProvider.of(context)`. `context.read` чаще всего используется для получения экземпляра bloc, чтобы добавить событие в колбэках `onPressed`. :::note `context.read()` не прослушивает `T` — если предоставленный `Object` типа `T` изменяется, `context.read` не вызовет перестроение виджета. ::: #### Использование ✅ **ИСПОЛЬЗУЙТЕ** `context.read` для добавления событий в колбэках. ```dart onPressed() { context.read().add(CounterIncrementPressed()), } ``` ❌ **ИЗБЕГАЙТЕ** использования `context.read` для получения состояния в методе `build`. ```dart @override Widget build(BuildContext context) { final state = context.read().state; return Text('$state'); } ``` Вышеуказанное использование подвержено ошибкам, потому что виджет `Text` не будет перестроен, если состояние bloc изменится. :::caution Используйте `BlocBuilder` или `context.watch` вместо этого, чтобы перестраивать в ответ на изменения состояния. ::: ### context.watch Как и `context.read()`, `context.watch()` предоставляет ближайший экземпляр предка типа `T`, однако он также прослушивает изменения экземпляра. Это функционально эквивалентно `BlocProvider.of(context, listen: true)`. Если предоставленный `Object` типа `T` изменяется, `context.watch` вызовет перестроение. :::caution `context.watch` доступен только в методе `build` класса `StatelessWidget` или `State`. ::: #### Использование ✅ **ИСПОЛЬЗУЙТЕ** `BlocBuilder` вместо `context.watch` для явного ограничения перестроений. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (context, state) { // Когда состояние изменяется, перестраивается только Text. return Text(state.value); }, ), ), ); } ``` Альтернативно, используйте `Builder` для ограничения перестроений. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Когда состояние изменяется, перестраивается только Text. final state = context.watch().state; return Text(state.value); }, ), ), ); } ``` ✅ **ИСПОЛЬЗУЙТЕ** `Builder` и `context.watch` как `MultiBlocBuilder`. ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // возвращает виджет, который зависит от состояния BlocA, BlocB и BlocC } ); ``` ❌ **ИЗБЕГАЙТЕ** использования `context.watch`, когда родительский виджет в методе `build` не зависит от состояния. ```dart @override Widget build(BuildContext context) { // Когда состояние изменяется, MaterialApp перестраивается // даже если оно используется только в виджете Text. final state = context.watch().state; return MaterialApp( home: Scaffold( body: Text(state.value), ), ); } ``` :::caution Использование `context.watch` в корне метода `build` приведет к перестроению всего виджета при изменении состояния bloc. ::: ### context.select Как и `context.watch()`, `context.select(R function(T value))` предоставляет ближайший экземпляр предка типа `T` и прослушивает изменения `T`. В отличие от `context.watch`, `context.select` позволяет прослушивать изменения в меньшей части состояния. ```dart Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); } ``` Вышеуказанное будет перестраивать виджет только когда свойство `name` состояния `ProfileBloc` изменится. #### Использование ✅ **ИСПОЛЬЗУЙТЕ** `BlocSelector` вместо `context.select` для явного ограничения перестроений. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector( selector: (state) => state.name, builder: (context, name) { // Когда state.name изменяется, перестраивается только Text. return Text(name); }, ), ), ); } ``` Альтернативно, используйте `Builder` для ограничения перестроений. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Когда state.name изменяется, перестраивается только Text. final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), ); } ``` ❌ **ИЗБЕГАЙТЕ** использования `context.select`, когда родительский виджет в методе build не зависит от состояния. ```dart @override Widget build(BuildContext context) { // Когда state.value изменяется, MaterialApp перестраивается // даже если оно используется только в виджете Text. final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), ); } ``` :::caution Использование `context.select` в корне метода `build` приведет к перестроению всего виджета при изменении выбранного значения. ::: ================================================ FILE: docs/src/content/docs/ru/getting-started.mdx ================================================ --- title: Начало работы description: Всё, что нужно для начала работы с Bloc. --- import InstallationTabs from '~/components/getting-started/InstallationTabs.astro'; import ImportTabs from '~/components/getting-started/ImportTabs.astro'; ## Пакеты Экосистема bloc состоит из нескольких пакетов, перечисленных ниже: | Пакет | Описание | Ссылка | | ------------------------------------------------------------------------------------------ | --------------------------------- | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | Компоненты AngularDart | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | Основные API Dart | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | Преобразователи событий | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | Пользовательский линтер | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | API для тестирования | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | Инструменты командной строки | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | Виджеты Flutter | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | Поддержка кэширования/сохранения | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | Поддержка отмены/повтора действий | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | ## Установка :::note Для начала использования bloc у вас должен быть установлен [Dart SDK](https://dart.dev/get-dart) на вашем компьютере. ::: ## Импорты Теперь, когда мы успешно установили bloc, мы можем создать наш `main.dart` и импортировать соответствующий пакет `bloc`. ================================================ FILE: docs/src/content/docs/ru/index.mdx ================================================ --- template: splash title: Bloc State Management Library description: Официальная документация библиотеки управления состоянием bloc. Поддержка Dart, Flutter и AngularDart. Включает примеры и руководства. banner: content: | ✨ Посетите Магазин Bloc ✨ editUrl: false lastUpdated: false hero: title: Bloc v9.2.0 tagline: Предсказуемая библиотека управления состоянием для Dart. image: alt: Логотип Bloc file: ~/assets/bloc.svg actions: - text: Начать link: /ru/getting-started/ variant: primary icon: rocket - text: Посмотреть на GitHub link: https://github.com/felangel/bloc icon: github variant: secondary --- import { CardGrid } from '@astrojs/starlight/components'; import SponsorsGrid from '~/components/landing/SponsorsGrid.astro'; import Card from '~/components/landing/Card.astro'; import ListCard from '~/components/landing/ListCard.astro'; import SplitCard from '~/components/landing/SplitCard.astro'; import Discord from '~/components/landing/Discord.astro';
```sh # Добавьте bloc в ваш проект. dart pub add bloc ``` Наше [руководство по началу работы](/ru/getting-started) содержит пошаговые инструкции о том, как начать использовать Bloc всего за несколько минут. Пройдите [официальные руководства](/ru/tutorials/flutter-counter), чтобы изучить лучшие практики и создать различные приложения на основе Bloc. Изучайте высококачественные, полностью протестированные [примеры приложений](https://github.com/felangel/bloc/tree/master/examples), такие как счётчик, таймер, бесконечный список, погода, задачи и многое другое! - [Почему Bloc?](/ru/why-bloc) - [Основные концепции](/ru/bloc-concepts) - [Архитектура](/ru/architecture) - [Тестирование](/ru/testing) - [Соглашения об именовании](/ru/naming-conventions) - [Часто задаваемые вопросы](/ru/faqs) - [Интеграция с VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) - [Интеграция с IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) - [Интеграция с Neovim](https://github.com/wa11breaker/flutter-bloc.nvim) - [Интеграция с Mason CLI](https://github.com/felangel/bloc/blob/master/bricks/README.md) - [Пользовательские шаблоны](https://brickhub.dev/search?q=bloc) - [Инструменты разработчика](https://github.com/felangel/bloc/blob/master/packages/bloc_tools/README.md) ================================================ FILE: docs/src/content/docs/ru/lint/configuration.mdx ================================================ --- title: Конфигурация линтера description: Настройка линтера bloc. sidebar: order: 3 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import BlocLintBasicAnalysisOptionsSnippet from '~/components/lint/BlocLintBasicAnalysisOptionsSnippet.astro'; import RunBlocLintInCurrentDirectorySnippet from '~/components/lint/RunBlocLintInCurrentDirectorySnippet.astro'; import RunBlocLintInSrcTestSnippet from '~/components/lint/RunBlocLintInSrcTestSnippet.astro'; import AvoidFlutterImportsWarningSnippet from '~/components/lint/ImportFlutterWarningSnippet.mdx'; import RunBlocLintCounterCubitSnippet from '~/components/lint/RunBlocLintCounterCubitSnippet.astro'; import AvoidFlutterImportsWarningOutputSnippet from '~/components/lint/ImportFlutterWarningOutputSnippet.astro'; По умолчанию линтер bloc не будет выдавать никакой диагностики, если вы явно не настроили параметры анализа проекта. Для начала создайте или измените существующий файл `analysis_options.yaml` в корне вашего проекта, чтобы включить список правил под ключом верхнего уровня bloc: Запустите линтер с помощью следующей команды в терминале: Приведённая выше команда проанализирует все файлы в текущем каталоге и его подкаталогах, но вы также можете проверить конкретные файлы и каталоги, передав их в качестве аргументов командной строки: Приведённая выше команда проанализирует весь код в каталогах `src` и `test`. Если правило `avoid_flutter_imports` включено, любой файл bloc или cubit, который содержит импорт flutter, будет помечен как предупреждение: Вы можете увидеть предупреждение, запустив команду `bloc lint`: Вывод должен выглядеть так: :::note Вот все поддерживаемые правила линтера: ::: ================================================ FILE: docs/src/content/docs/ru/lint/customizing-rules.mdx ================================================ --- title: Настройка правил линтера description: Настройка правил линтера bloc sidebar: order: 4 --- import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import BlocLintEnablingRulesSnippet from '~/components/lint/BlocLintEnablingRulesSnippet.astro'; import BlocLintDisablingRulesSnippet from '~/components/lint/BlocLintDisablingRulesSnippet.astro'; import BlocLintChangingSeveritySnippet from '~/components/lint/BlocLintChangingSeveritySnippet.astro'; import ImportFlutterInfoSnippet from '~/components/lint/ImportFlutterInfoSnippet.mdx'; import ImportFlutterInfoOutputSnippet from '~/components/lint/ImportFlutterInfoOutputSnippet.astro'; import BlocLintExcludingFilesSnippet from '~/components/lint/BlocLintExcludingFilesSnippet.astro'; import BlocLintIgnoreForLineSnippet from '~/components/lint/BlocLintIgnoreForLineSnippet.astro'; import BlocLintIgnoreForFileSnippet from '~/components/lint/BlocLintIgnoreForFileSnippet.astro'; Вы можете настроить поведение линтера bloc, изменив серьёзность отдельных правил, индивидуально включая или отключая правила, а также исключая файлы из статического анализа. ## Включение и отключение правил Линтер bloc поддерживает растущий список правил линтера. Обратите внимание, что правила линтера не обязательно должны соответствовать друг другу. Например, некоторые разработчики могут предпочитать использовать блоки (`prefer_bloc`), в то время как другие могут предпочитать использовать кубиты (`prefer_cubit`). :::note В отличие от статического анализа, правила линтера могут содержать ложные срабатывания. Не стесняйтесь сообщать о любых ложных срабатываниях или других проблемах, [создав issue](https://github.com/felangel/bloc/issues/new/choose). ::: ### Включение рекомендуемых правил Библиотека bloc предоставляет набор рекомендуемых правил линтера в составе пакета [`bloc_lint`](https://pub.dev/packages/bloc_lint). Для включения рекомендуемого набора линтов добавьте пакет `bloc_lint` как dev-зависимость: Затем отредактируйте ваш файл `analysis_options.yaml`, чтобы включить набор правил: :::note Когда публикуется новая версия `bloc_lint`, код, который ранее проходил статический анализ, может начать проваливаться. Мы рекомендуем обновить ваш код для работы с новыми правилами, или вы также можете опционально включить или отключить отдельные правила. ::: ### Включение отдельных правил Для включения отдельных правил добавьте `bloc:` в файл `analysis_options.yaml` в качестве ключа верхнего уровня и `rules:` в качестве ключа второго уровня. В последующих строках укажите правила, которые вы хотите, в виде списка YAML (с префиксом в виде дефисов). Например: ### Отключение отдельных правил Если вы включаете существующий набор правил, такой как набор `recommended`, вы можете захотеть отключить одно или несколько включённых правил линтера. Отключение правил аналогично их включению, но требует использования YAML-карты, а не списка. Например, следующее включает рекомендуемый набор правил линтера за исключением `avoid_public_bloc_methods` и дополнительно включает правило `prefer_bloc`: ## Настройка серьёзности правил Вы можете настроить серьёзность любого правила следующим образом: Теперь то же самое правило линтера будет отображаться с серьёзностью `info` вместо `warning`: Вывод команды `bloc lint` должен выглядеть так: Поддерживаемые варианты серьёзности: | Серьёзность | Описание | | ----------- | ---------------------------------------------------------------- | | `error` | Указывает, что шаблон не разрешён. | | `warning` | Указывает, что шаблон подозрителен, но разрешён. | | `info` | Предоставляет информацию пользователям, но не является проблемой | | `hint` | Предлагает лучший способ достижения результата. | ## Исключение файлов Иногда нормально, что статический анализ проваливается. Например, вы можете захотеть игнорировать предупреждения или ошибки, отображаемые в сгенерированном коде, который не был написан вами и вашей командой. Так же, как и с официальными правилами линтера Dart, вы можете использовать опцию анализатора `exclude:`, чтобы исключить файлы из статического анализа. Вы можете либо перечислить отдельные файлы, либо использовать паттерны [`glob`](https://pub.dev/packages/glob). :::note Все использования паттернов glob должны быть относительно каталога, содержащего соответствующий файл `analysis_options.yaml`. ::: Например, мы можем исключить весь сгенерированный код Dart с помощью следующих параметров анализа: ## Игнорирование правил Так же, как и с официальными правилами линтера Dart, вы можете игнорировать правила линтера bloc для данного файла или строки кода, используя `// ignore_for_file` и `// ignore` соответственно. :::note Чтобы игнорировать несколько правил для данной строки или файла, укажите список, разделённый запятыми. ::: ### Игнорирование строк Мы можем игнорировать конкретные случаи нарушений правил, добавив комментарий `ignore` либо прямо над проблемной строкой, либо добавив его в конец проблемной строки. Например, мы можем игнорировать конкретные случаи `prefer_file_naming_conventions` в данном файле: ### Игнорирование файлов Мы можем игнорировать все случаи нарушений правил в файле, добавив комментарий `ignore_for_file` в любом месте файла. Например, мы можем игнорировать все случаи `prefer_file_naming_conventions` в данном файле: ================================================ FILE: docs/src/content/docs/ru/lint/index.mdx ================================================ --- title: Обзор линтера description: Введение в линтер bloc. sidebar: order: 1 --- import AvoidFlutterImportsWarningSnippet from '~/components/lint/ImportFlutterWarningSnippet.mdx'; import AvoidFlutterImportsWarningOutputSnippet from '~/components/lint/ImportFlutterWarningOutputSnippet.astro'; import InstallBlocToolsSnippet from '~/components/lint/InstallBlocToolsSnippet.astro'; import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import RunBlocLintInCurrentDirectorySnippet from '~/components/lint/RunBlocLintInCurrentDirectorySnippet.astro'; Линтинг — это процесс статического анализа кода для выявления потенциальных ошибок, а также программных и стилистических проблем. Bloc имеет встроенный линтер, который можно использовать через вашу IDE или [`инструменты командной строки bloc`](https://pub.dev/packages/bloc_tools) с помощью команды `bloc lint`. С помощью линтера bloc вы можете повысить качество вашей кодовой базы и обеспечить единообразие без выполнения ни одной строки кода. Например, возможно, вы случайно импортировали зависимость Flutter в ваш cubit: При правильной настройке линтер bloc укажет на импорт и выдаст следующее предупреждение: В следующих разделах мы рассмотрим, как установить, настроить и кастомизировать линтер bloc, чтобы вы могли воспользоваться преимуществами статического анализа в вашей кодовой базе. ## Быстрый старт Начните использовать линтер bloc всего за несколько быстрых и простых шагов. :::note Для начала использования bloc у вас должен быть установлен [Dart SDK](https://dart.dev/get-dart) на вашей машине. ::: 1. Установите [инструменты командной строки bloc](https://pub.dev/packages/bloc_tools) 1. Установите пакет [bloc_lint](https://pub.dev/packages/bloc_lint) 1. Добавьте файл `analysis_options.yaml` в корень вашего проекта с рекомендуемыми правилами 1. Запустите линтер Вот и всё 🎉 Продолжайте чтение для более подробного изучения настройки и кастомизации линтера bloc. ================================================ FILE: docs/src/content/docs/ru/lint/installation.mdx ================================================ --- title: Установка линтера description: Установка линтера bloc. sidebar: order: 2 --- import { CardGrid } from '@astrojs/starlight/components'; import Card from '~/components/landing/Card.astro'; import InstallBlocToolsSnippet from '~/components/lint/InstallBlocToolsSnippet.astro'; import BlocToolsLintHelpOutputSnippet from '~/components/lint/BlocToolsLintHelpOutputSnippet.astro'; import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import BlocLintMultipleRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintMultipleRecommendedAnalysisOptionsSnippet.astro'; ## Инструменты командной строки Для использования линтера из командной строки установите [`package:bloc_tools`](https://pub.dev/packages/bloc_tools) с помощью следующей команды: После установки инструментов командной строки bloc вы можете запустить линтер bloc с помощью команды `bloc lint`: ## Рекомендуемый набор правил Для установки рекомендуемого набора правил линтера установите [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) как dev-зависимость с помощью следующей команды: Затем добавьте файл `analysis_options.yaml` в корень вашего проекта с рекомендуемым набором правил: При необходимости вы можете включить несколько наборов правил, определив их в виде списка: ## Интеграции с IDE Следующие IDE официально поддерживают линтер bloc и языковой сервер для предоставления мгновенной диагностики непосредственно в вашей IDE. Поддержка в [Bloc VSCode Extension](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) доступна начиная с версии v6.8.0. Поддержка в [Bloc IntelliJ Plugin](https://plugins.jetbrains.com/plugin/12129-bloc) доступна начиная с версии v4.1.0. ================================================ FILE: docs/src/content/docs/ru/lint-rules/avoid_build_context_extensions.mdx ================================================ --- title: Избегайте расширений BuildContext description: Правило avoid_build_context_extensions. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_build_context_extensions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_build_context_extensions/GoodSnippet.astro';
Избегайте использования расширений `BuildContext` для доступа к экземплярам `Bloc` или `Cubit`. :::note Это правило линтера было введено в версии `0.3.0` пакета [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Обоснование Для согласованности и ради явности предпочтительно использовать напрямую базовые методы вместо расширений `BuildContext`. Это также полезно для тестирования, поскольку невозможно замокать метод расширения. | расширение | явный метод | | ---------------- | -------------------------------------------------------------------- | | `context.read` | `BlocProvider.of(context, listen: false)` | | `context.watch` | `BlocBuilder(...)` или `BlocProvider.of(context)` | | `context.select` | `BlocSelector(...)` | ## Примеры **Избегайте** использования расширений `BuildContext` для взаимодействия с экземплярами `Bloc` или `Cubit`. **ПЛОХО**: **ХОРОШО**: ## Включение Чтобы включить правило `avoid_build_context_extensions`, добавьте его в `analysis_options.yaml` в разделе `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ru/lint-rules/avoid_flutter_imports.mdx ================================================ --- title: Избегайте импортов Flutter description: Правило линтера bloc avoid_flutter_imports. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_flutter_imports/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_flutter_imports/GoodSnippet.astro';
Избегайте введения зависимостей от Flutter в компонентах бизнес-логики (экземплярах `Bloc` или `Cubit`). ## Обоснование Разделение приложения на слои является ключевой частью построения поддерживаемой кодовой базы и помогает разработчикам итерировать быстро и уверенно. Каждый слой должен иметь единственную ответственность и быть способным функционировать и тестироваться изолированно. Это позволяет вам ограничивать изменения конкретными слоями, минимизируя влияние на все приложение. В результате компоненты бизнес-логики обычно должны управлять состоянием функций и быть отделены от слоя пользовательского интерфейса. События должны поступать в компоненты бизнес-логики из слоя UI, а состояние должно вытекать из слоя бизнес-логики в слой UI. Сохранение компонентов бизнес-логики отделенными от Flutter дает возможность повторно использовать бизнес-логику на нескольких платформах/фреймворках (например, Flutter, AngularDart, Jaspr и т.д.). ## Примеры **НЕ ИМПОРТИРУЙТЕ** Flutter в ваших компонентах бизнес-логики. **ПЛОХО**: **ХОРОШО**: ## Включение Чтобы включить правило `avoid_flutter_imports`, добавьте его в `analysis_options.yaml` в разделе `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ru/lint-rules/avoid_public_bloc_methods.mdx ================================================ --- title: Избегайте публичных методов Bloc description: Правило avoid_public_bloc_methods. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_public_bloc_methods/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_public_bloc_methods/GoodSnippet.astro';
Избегайте предоставления публичных методов в экземплярах `Bloc`. ## Обоснование Блоки реагируют на входящие события и выдают исходящие состояния. В результате рекомендуемый способ взаимодействия с экземпляром bloc — через метод `add`. В большинстве случаев нет необходимости создавать дополнительные абстракции поверх API `add`. ![Архитектура Bloc](~/assets/concepts/bloc_architecture_full.png) ## Примеры **НЕ ПРЕДОСТАВЛЯЙТЕ** публичные методы в экземплярах bloc. **ПЛОХО**: **ХОРОШО**: ## Включение Чтобы включить правило `avoid_public_bloc_methods`, добавьте его в `analysis_options.yaml` в разделе `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ru/lint-rules/avoid_public_fields.mdx ================================================ --- title: Избегайте публичных полей description: Правило avoid_public_fields. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_public_fields/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_public_fields/GoodSnippet.astro';
Избегайте предоставления публичных полей в экземплярах `Bloc` и `Cubit`. ## Обоснование Компоненты бизнес-логики поддерживают свое собственное `state` и выдают изменения состояния через API `emit`. В результате все публично доступное состояние должно быть предоставлено через объект `state`. ## Примеры **НЕ ПРЕДОСТАВЛЯЙТЕ** публичные поля в экземплярах bloc и cubit. **ПЛОХО**: **ХОРОШО**: ## Включение Чтобы включить правило `avoid_public_fields`, добавьте его в `analysis_options.yaml` в разделе `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ru/lint-rules/prefer_bloc.mdx ================================================ --- title: Предпочитайте Bloc description: Правило prefer_bloc. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_bloc/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_bloc/GoodSnippet.astro';
Предпочитайте использование экземпляров `Bloc` вместо экземпляров `Cubit`. ## Обоснование Это правило является чисто стилистическим. В некоторых случаях команды могут предпочесть стандартизировать использование только экземпляров `Bloc` во всем приложении для согласованности. :::tip Узнайте больше о преимуществах `Bloc` в разделе [Основные концепции](/ru/bloc-concepts/#преимущества-bloc). ::: ## Примеры **Избегайте** использования экземпляров `Cubit`. **ПЛОХО**: **ХОРОШО**: ## Включение Чтобы включить правило `prefer_bloc`, добавьте его в `analysis_options.yaml` в разделе `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ru/lint-rules/prefer_build_context_extensions.mdx ================================================ --- title: Предпочитайте расширения BuildContext description: Правило prefer_build_context_extensions. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_build_context_extensions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_build_context_extensions/GoodSnippet.astro';
Предпочитайте использование расширений `BuildContext` для доступа к экземпляру `Bloc` или `Repository`. :::note Это правило линтера было введено в версии `0.3.2` пакета [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Обоснование Для согласованности предпочитайте использовать расширения `BuildContext` такие как `context.read`, `context.watch` и `context.select` вместо `BlocProvider.of`, `RepositoryProvider.of`, `BlocBuilder` или `BlocSelector`. | явный метод | расширение | | -------------------------------------------------------------------- | --------------------- | | `BlocProvider.of(context, listen: false)` | `context.read` | | `BlocBuilder(...)` или `BlocProvider.of(context)` | `context.watch` | | `BlocSelector(...)` | `context.select` | ## Примеры **Избегайте** использования `BlocProvider.of(context)` для доступа к экземпляру `Bloc`. **ПЛОХО**: **ХОРОШО**: ## Включение Чтобы включить правило `prefer_build_context_extensions`, добавьте его в `analysis_options.yaml` в разделе `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ru/lint-rules/prefer_cubit.mdx ================================================ --- title: Предпочитайте Cubit description: Правило prefer_cubit. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_cubit/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_cubit/GoodSnippet.astro';
Предпочитайте использование экземпляров `Cubit` вместо экземпляров `Bloc`. ## Обоснование Это правило является чисто стилистическим. В некоторых случаях команды могут предпочесть стандартизировать использование только экземпляров `Cubit` во всем приложении для согласованности. :::tip Узнайте больше о преимуществах `Cubit` в разделе [Основные концепции](/ru/bloc-concepts/#преимущества-cubit). ::: ## Примеры **Избегайте** использования экземпляров `Bloc`. **ПЛОХО**: **ХОРОШО**: ## Включение Чтобы включить правило `prefer_cubit`, добавьте его в `analysis_options.yaml` в разделе `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ru/lint-rules/prefer_file_naming_conventions.mdx ================================================ --- title: Предпочитайте соглашения об именовании файлов description: Правило prefer_file_naming_conventions. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_file_naming_conventions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_file_naming_conventions/GoodSnippet.astro';
Предпочитайте следовать соглашениям об именовании файлов. :::note Это правило линтера было введено в версии `0.3.0` пакета [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Обоснование Для согласованности, простоты обслуживания и разделения ответственности предпочтительно определять экземпляры bloc и cubit в их соответствующих файлах Dart вместо встраивания их напрямую. :::tip Рассмотрите использование команды `bloc new ` из пакета [package:bloc_tools](https://pub.dev/packages/bloc_tools) для быстрого и последовательного генерирования новых экземпляров bloc/cubit. ::: ## Примеры **Предпочитайте** объявлять экземпляры bloc/cubit в их собственных соответствующих файлах. **ХОРОШО**: **ПЛОХО**: ## Включение Чтобы включить правило `prefer_file_naming_conventions`, добавьте его в `analysis_options.yaml` в разделе `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ru/lint-rules/prefer_void_public_cubit_methods.mdx ================================================ --- title: Предпочитайте void публичные методы Cubit description: Правило prefer_void_public_cubit_methods. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_void_public_cubit_methods/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_void_public_cubit_methods/GoodSnippet.astro';
Предпочитайте void публичные методы в экземплярах `Cubit`. :::note Это правило линтера было введено в версии `0.2.0-dev.2` пакета [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Обоснование Публичные методы в экземплярах `Cubit` должны использоваться для уведомления `Cubit` и инициации изменений состояния через метод `emit`. Если вызывающей стороне нужен доступ к какой-либо информации о состоянии, она должна получать её из `state`. :::note Следующие правила связаны и обычно включаются в комбинации с `prefer_void_public_cubit_methods`. - [`avoid_public_bloc_methods`](/ru/lint-rules/avoid_public_bloc_methods) - [`avoid_public_fields`](/ru/lint-rules/avoid_public_fields) ::: ## Примеры **Избегайте** невозвратных (non-void) публичных методов в экземплярах `Cubit`. **ПЛОХО**: **ХОРОШО**: ## Включение Чтобы включить правило `prefer_void_public_cubit_methods`, добавьте его в `analysis_options.yaml` в разделе `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/ru/migration.mdx ================================================ --- title: Руководство по миграции description: Мигрируйте на последнюю стабильную версию Bloc. --- import { Code, Tabs, TabItem } from '@astrojs/starlight/components'; :::tip Пожалуйста, обратитесь к [журналу релизов](https://github.com/felangel/bloc/releases) для получения дополнительной информации о том, что изменилось в каждом релизе. ::: ## v10.0.0 ### `package:bloc_test` #### ❗✨ Отделение `blocTest` от `BlocBase` :::note[Что изменилось?] В bloc_test v10.0.0 API `blocTest` больше не тесно связан с `BlocBase`. ::: ##### Обоснование `blocTest` должен использовать основные интерфейсы bloc, когда это возможно, для повышения гибкости и возможности повторного использования. Ранее это было невозможно, поскольку `BlocBase` реализовывал `StateStreamableSource`, чего было недостаточно для `blocTest` из-за внутренней зависимости от API `emit`. ### `package:hydrated_bloc` #### ❗✨ Поддержка WebAssembly :::note[Что изменилось?] В hydrated_bloc v10.0.0 была добавлена поддержка компиляции в WebAssembly (wasm). ::: ##### Обоснование Ранее было невозможно компилировать приложения в wasm при использовании `hydrated_bloc`. В v10.0.0 пакет был переработан для поддержки компиляции в wasm. **v9.x.x** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` **v10.x.x** ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorageDirectory.web : HydratedStorageDirectory((await getTemporaryDirectory()).path), ); runApp(const App()); } ``` ## v9.0.0 ### `package:bloc` #### ❗🧹 Удаление устаревших API :::note[Что изменилось?] В bloc v9.0.0 все ранее устаревшие API были удалены. ::: ##### Краткое изложение - `BlocOverrides` удален в пользу `Bloc.observer` и `Bloc.transformer` #### ❗✨ Введение нового интерфейса `EmittableStateStreamableSource` :::note[Что изменилось?] В bloc v9.0.0 был введен новый основной интерфейс `EmittableStateStreamableSource`. ::: ##### Обоснование `package:bloc_test` ранее был тесно связан с `BlocBase`. Интерфейс `EmittableStateStreamableSource` был введен для того, чтобы позволить `blocTest` отделиться от конкретной реализации `BlocBase`. ### `package:hydrated_bloc` #### ✨ Возврат API `HydratedBloc.storage` :::note[Что изменилось?] В hydrated_bloc v9.0.0 `HydratedBlocOverrides` был удален в пользу API `HydratedBloc.storage`.\*\* ::: ##### Обоснование Обратитесь к [обоснованию возврата переопределений Bloc.observer и Bloc.transformer](/ru/migration#-возврат-api-blocobserver-и-bloctransformer). **v8.x.x** ```dart Future main() async { final storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); HydratedBlocOverrides.runZoned( () => runApp(App()), storage: storage, ); } ``` **v9.0.0** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` ## v8.1.0 ### `package:bloc` #### ✨ Возврат API `Bloc.observer` и `Bloc.transformer` :::note[Что изменилось?] В bloc v8.1.0 `BlocOverrides` был объявлен устаревшим в пользу API `Bloc.observer` и `Bloc.transformer`. ::: ##### Обоснование API `BlocOverrides` был введен в v8.0.0 в попытке поддержать ограничение области действия конфигураций, специфичных для bloc, таких как `BlocObserver`, `EventTransformer`, и `HydratedStorage`. В чистых приложениях Dart изменения работали хорошо; однако в приложениях Flutter новый API вызвал больше проблем, чем решил. API `BlocOverrides` был вдохновлен похожими API во Flutter/Dart: - [HttpOverrides](https://api.flutter.dev/flutter/dart-io/HttpOverrides-class.html) - [IOOverrides](https://api.flutter.dev/flutter/dart-io/IOOverrides-class.html) **Проблемы** Хотя это не было основной причиной этих изменений, API `BlocOverrides` привнес дополнительную сложность для разработчиков. Помимо увеличения количества вложенности и строк кода, необходимых для достижения того же эффекта, API `BlocOverrides` требовал от разработчиков глубокого понимания [Zones](https://api.dart.dev/stable/2.17.6/dart-async/Zone-class.html) в Dart. `Zones` — это не удобная для начинающих концепция, и непонимание того, как работают Zones, может привести к появлению ошибок (таких как неинициализированные наблюдатели, трансформеры, экземпляры хранилища). Например, многие разработчики имели бы что-то вроде: ```dart void main() { WidgetsFlutterBinding.ensureInitialized(); BlocOverrides.runZoned(...); } ``` Приведенный выше код, хотя и выглядит безобидным, на самом деле может привести ко многим трудно отслеживаемым ошибкам. Какая бы зона ни вызвала первоначально `WidgetsFlutterBinding.ensureInitialized`, будет зоной, в которой обрабатываются события жестов (например, колбэки `onTap`, `onPressed`) из-за `GestureBinding.initInstances`. Это всего лишь одна из многих проблем, вызванных использованием `zoneValues`. Кроме того, Flutter делает много вещей за кулисами, которые включают разветвление/манипуляцию Zones (особенно при запуске тестов), что может привести к неожиданному поведению (и во многих случаях к поведению, которое находится вне контроля разработчика — см. проблемы ниже). Из-за использования [runZoned](https://api.flutter.dev/flutter/dart-async/runZoned.html), переход на API `BlocOverrides` привел к обнаружению нескольких ошибок/ограничений во Flutter (особенно в виджетных и интеграционных тестах): - https://github.com/flutter/flutter/issues/96939 - https://github.com/flutter/flutter/issues/94123 - https://github.com/flutter/flutter/issues/93676 которые затронули многих разработчиков, использующих библиотеку bloc: - https://github.com/felangel/bloc/issues/3394 - https://github.com/felangel/bloc/issues/3350 - https://github.com/felangel/bloc/issues/3319 **v8.0.x** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` **v8.1.0** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` ## v8.0.0 ### `package:bloc` #### ❗✨ Введение нового API `BlocOverrides` :::note[Что изменилось?] В bloc v8.0.0 `Bloc.observer` и `Bloc.transformer` были удалены в пользу API `BlocOverrides`. ::: ##### Обоснование Предыдущий API, используемый для переопределения `BlocObserver` и `EventTransformer` по умолчанию, полагался на глобальный синглтон как для `BlocObserver`, так и для `EventTransformer`. В результате было невозможно: - Иметь несколько реализаций `BlocObserver` или `EventTransformer`, ограниченных различными частями приложения - Иметь переопределения `BlocObserver` или `EventTransformer`, ограниченные пакетом - Если пакет зависел от `package:bloc` и регистрировал свой собственный `BlocObserver`, любой потребитель пакета должен был бы либо перезаписать `BlocObserver` пакета, либо отчитываться перед `BlocObserver` пакета. Также было сложнее тестировать из-за общего глобального состояния в тестах. Bloc v8.0.0 вводит класс `BlocOverrides`, который позволяет разработчикам переопределять `BlocObserver` и/или `EventTransformer` для конкретной `Zone`, а не полагаться на глобальный изменяемый синглтон. **v7.x.x** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` **v8.0.0** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` Экземпляры `Bloc` будут использовать `BlocObserver` и/или `EventTransformer` для текущей `Zone` через `BlocOverrides.current`. Если для зоны нет `BlocOverrides`, они будут использовать существующие внутренние значения по умолчанию (никаких изменений в поведении/функциональности). Это позволяет каждой `Zone` функционировать независимо со своими `BlocOverrides`. ```dart BlocOverrides.runZoned( () { // BlocObserverA и eventTransformerA final overrides = BlocOverrides.current; // Bloc'и в этой зоне отчитываются перед BlocObserverA // и используют eventTransformerA как трансформер по умолчанию. // ... // Позже... BlocOverrides.runZoned( () { // BlocObserverB и eventTransformerB final overrides = BlocOverrides.current; // Bloc'и в этой зоне отчитываются перед BlocObserverB // и используют eventTransformerB как трансформер по умолчанию. // ... }, blocObserver: BlocObserverB(), eventTransformer: eventTransformerB(), ); }, blocObserver: BlocObserverA(), eventTransformer: eventTransformerA(), ); ``` #### ❗✨ Улучшение обработки и отчетности об ошибках :::note[Что изменилось?] В bloc v8.0.0 `BlocUnhandledErrorException` удален. Кроме того, любые неперехваченные исключения всегда сообщаются в `onError` и пробрасываются повторно (независимо от режима отладки или релиза). API `addError` сообщает об ошибках в `onError`, но не рассматривает сообщенные ошибки как неперехваченные исключения. ::: ##### Обоснование Цель этих изменений: - сделать внутренние необработанные исключения чрезвычайно очевидными, сохраняя при этом функциональность bloc - поддерживать `addError` без нарушения потока управления Ранее обработка и отчетность об ошибках различались в зависимости от того, работало ли приложение в режиме отладки или релиза. Кроме того, ошибки, сообщенные через `addError`, рассматривались как неперехваченные исключения в режиме отладки, что приводило к плохому опыту разработчика при использовании API `addError` (особенно при написании модульных тестов). В v8.0.0 `addError` можно безопасно использовать для сообщения об ошибках, а `blocTest` можно использовать для проверки того, что ошибки сообщаются. Все ошибки по-прежнему сообщаются в `onError`, однако повторно пробрасываются только неперехваченные исключения (независимо от режима отладки или релиза). #### ❗🧹 Сделать `BlocObserver` абстрактным :::note[Что изменилось?] В bloc v8.0.0 `BlocObserver` был преобразован в `abstract` класс, что означает, что экземпляр `BlocObserver` не может быть создан. ::: ##### Обоснование `BlocObserver` предназначался для использования в качестве интерфейса. Поскольку реализация API по умолчанию — это no-ops, `BlocObserver` теперь является `abstract` классом, чтобы четко показать, что класс предназначен для расширения, а не для прямого создания экземпляра. **v7.x.x** ```dart void main() { // Было возможно создать экземпляр базового класса. final observer = BlocObserver(); } ``` **v8.0.0** ```dart class MyBlocObserver extends BlocObserver {...} void main() { // Невозможно создать экземпляр базового класса. final observer = BlocObserver(); // ОШИБКА // Вместо этого расширьте `BlocObserver`. final observer = MyBlocObserver(); // OK } ``` #### ❗✨ `add` выбрасывает `StateError`, если Bloc закрыт :::note[Что изменилось?] В bloc v8.0.0 вызов `add` на закрытом bloc приведет к `StateError`. ::: ##### Обоснование Ранее было возможно вызвать `add` на закрытом bloc, и внутренняя ошибка проглатывалась, что затрудняло отладку того, почему добавленное событие не обрабатывалось. Чтобы сделать этот сценарий более видимым, в v8.0.0 вызов `add` на закрытом bloc выбросит `StateError`, который будет сообщен как неперехваченное исключение и передан в `onError`. #### ❗✨ `emit` выбрасывает `StateError`, если Bloc закрыт :::note[Что изменилось?] В bloc v8.0.0 вызов `emit` внутри закрытого bloc приведет к `StateError`. ::: ##### Обоснование Ранее было возможно вызвать `emit` внутри закрытого bloc, и никакого изменения состояния не происходило, но также не было никакого указания на то, что пошло не так, что затрудняло отладку. Чтобы сделать этот сценарий более видимым, в v8.0.0 вызов `emit` внутри закрытого bloc выбросит `StateError`, который будет сообщен как неперехваченное исключение и передан в `onError`. #### ❗🧹 Удаление устаревших API :::note[Что изменилось?] В bloc v8.0.0 все ранее устаревшие API были удалены. ::: ##### Краткое изложение - `mapEventToState` удален в пользу `on` - `transformEvents` удален в пользу API `EventTransformer` - typedef `TransitionFunction` удален в пользу API `EventTransformer` - `listen` удален в пользу `stream.listen` ### `package:bloc_test` #### ✨ `MockBloc` и `MockCubit` больше не требуют `registerFallbackValue` :::note[Что изменилось?] В bloc_test v9.0.0 разработчикам больше не нужно явно вызывать `registerFallbackValue` при использовании `MockBloc` или `MockCubit`. ::: ##### Краткое изложение `registerFallbackValue` требуется только при использовании матчера `any()` из `package:mocktail` для пользовательского типа. Ранее `registerFallbackValue` был необходим для каждого `Event` и `State` при использовании `MockBloc` или `MockCubit`. **v8.x.x** ```dart class FakeMyEvent extends Fake implements MyEvent {} class FakeMyState extends Fake implements MyState {} class MyMockBloc extends MockBloc implements MyBloc {} void main() { setUpAll(() { registerFallbackValue(FakeMyEvent()); registerFallbackValue(FakeMyState()); }); // Тесты... } ``` **v9.0.0** ```dart class MyMockBloc extends MockBloc implements MyBloc {} void main() { // Тесты... } ``` ### `package:hydrated_bloc` #### ❗✨ Введение нового API `HydratedBlocOverrides` :::note[Что изменилось?] В hydrated_bloc v8.0.0 `HydratedBloc.storage` был удален в пользу API `HydratedBlocOverrides`. ::: ##### Обоснование Ранее для переопределения реализации `Storage` использовался глобальный синглтон. В результате было невозможно иметь несколько реализаций `Storage`, ограниченных различными частями приложения. Также было сложнее тестировать из-за общего глобального состояния в тестах. `HydratedBloc` v8.0.0 вводит класс `HydratedBlocOverrides`, который позволяет разработчикам переопределять `Storage` для конкретной `Zone`, а не полагаться на глобальный изменяемый синглтон. **v7.x.x** ```dart void main() async { HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); // ... } ``` **v8.0.0** ```dart void main() { final storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); HydratedBlocOverrides.runZoned( () { // ... }, storage: storage, ); } ``` Экземпляры `HydratedBloc` будут использовать `Storage` для текущей `Zone` через `HydratedBlocOverrides.current`. Это позволяет каждой `Zone` функционировать независимо со своими `BlocOverrides`. ## v7.2.0 ### `package:bloc` #### ✨ Введение нового API `on` :::note[Что изменилось?] В bloc v7.2.0 `mapEventToState` был объявлен устаревшим в пользу `on`. `mapEventToState` будет удален в bloc v8.0.0. ::: ##### Обоснование API `on` был введен в рамках [[Предложение] Заменить mapEventToState на on\ в Bloc](https://github.com/felangel/bloc/issues/2526). Из-за [проблемы в Dart](https://github.com/dart-lang/sdk/issues/44616) не всегда очевидно, каким будет значение `state` при работе с вложенными асинхронными генераторами (`async*`). Несмотря на то, что существуют способы обойти эту проблему, одним из основных принципов библиотеки bloc является предсказуемость. API `on` был создан, чтобы сделать библиотеку максимально безопасной для использования и устранить любую неопределенность в отношении изменений состояния. :::tip Для получения дополнительной информации [прочитайте полное предложение](https://github.com/felangel/bloc/issues/2526). ::: **Краткое изложение** `on` позволяет зарегистрировать обработчик события для всех событий типа `E`. По умолчанию события будут обрабатываться параллельно при использовании `on`, в отличие от `mapEventToState`, который обрабатывает события `последовательно`. **v7.1.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0); @override Stream mapEventToState(CounterEvent event) async* { if (event is Increment) { yield state + 1; } } } ``` **v7.2.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } } ``` :::note Каждая зарегистрированная функция `EventHandler` работает независимо, поэтому важно регистрировать обработчики событий на основе типа трансформера, который вы хотите применить. ::: Если вы хотите сохранить точно такое же поведение, как в v7.1.0, вы можете зарегистрировать один обработчик событий для всех событий и применить трансформер `sequential`: ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; class MyBloc extends Bloc { MyBloc() : super(MyState()) { on(_onEvent, transformer: sequential()) } FutureOr _onEvent(MyEvent event, Emitter emit) async { // TODO: логика идет сюда... } } ``` Вы также можете переопределить `EventTransformer` по умолчанию для всех bloc'ов в вашем приложении: ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; void main() { Bloc.transformer = sequential(); ... } ``` #### ✨ Введение нового API `EventTransformer` :::note[Что изменилось?] В bloc v7.2.0 `transformEvents` был объявлен устаревшим в пользу API `EventTransformer`. `transformEvents` будет удален в bloc v8.0.0. ::: ##### Обоснование API `on` открыл возможность предоставления пользовательского трансформера событий для каждого обработчика событий. Был введен новый typedef `EventTransformer`, который позволяет разработчикам трансформировать входящий поток событий для каждого обработчика событий, а не указывать один трансформер событий для всех событий. **Краткое изложение** `EventTransformer` отвечает за прием входящего потока событий вместе с `EventMapper` (ваш обработчик события) и возврат нового потока событий. ```dart typedef EventTransformer = Stream Function(Stream events, EventMapper mapper) ``` `EventTransformer` по умолчанию обрабатывает все события параллельно и выглядит примерно так: ```dart EventTransformer concurrent() { return (events, mapper) => events.flatMap(mapper); } ``` :::tip Ознакомьтесь с [package:bloc_concurrency](https://pub.dev/packages/bloc_concurrency) для получения предвзятого набора пользовательских трансформеров событий ::: **v7.1.0** ```dart @override Stream> transformEvents(events, transitionFn) { return events .debounceTime(const Duration(milliseconds: 300)) .flatMap(transitionFn); } ``` **v7.2.0** ```dart /// Определите пользовательский `EventTransformer` EventTransformer debounce(Duration duration) { return (events, mapper) => events.debounceTime(duration).flatMap(mapper); } MyBloc() : super(MyState()) { /// Примените пользовательский `EventTransformer` к `EventHandler` on(_onEvent, transformer: debounce(const Duration(milliseconds: 300))) } ``` #### ⚠️ Устаревание API `transformTransitions` :::note[Что изменилось?] В bloc v7.2.0 `transformTransitions` был объявлен устаревшим в пользу переопределения API `stream`. `transformTransitions` будет удален в bloc v8.0.0. ::: ##### Обоснование Геттер `stream` в `Bloc` упрощает переопределение исходящего потока состояний, поэтому больше нет необходимости поддерживать отдельный API `transformTransitions`. **Краткое изложение** **v7.1.0** ```dart @override Stream> transformTransitions( Stream> transitions, ) { return transitions.debounceTime(const Duration(milliseconds: 42)); } ``` **v7.2.0** ```dart @override Stream get stream => super.stream.debounceTime(const Duration(milliseconds: 42)); ``` ## v7.0.0 ### `package:bloc` #### ❗ Bloc и Cubit расширяют BlocBase ##### Обоснование Как разработчик, отношения между bloc'ами и cubit'ами были немного неудобными. Когда cubit был впервые представлен, он начинался как базовый класс для bloc'ов, что имело смысл, потому что он имел подмножество функциональности, а bloc'и просто расширяли Cubit и определяли дополнительные API. Это привело к нескольким недостаткам: - Все API должны были бы либо быть переименованы, чтобы принимать cubit для точности, либо они должны были бы быть оставлены как bloc для последовательности, даже несмотря на то, что иерархически это неточно ([#1708](https://github.com/felangel/bloc/issues/1708), [#1560](https://github.com/felangel/bloc/issues/1560)). - Cubit должен был расширять Stream и реализовывать EventSink, чтобы иметь общую базу, на основе которой можно реализовать виджеты, такие как BlocBuilder, BlocListener и т. д. ([#1429](https://github.com/felangel/bloc/issues/1429)). Позже мы экспериментировали с инверсией отношений и сделали bloc базовым классом, что частично решило первый пункт выше, но привело к другим проблемам: - API cubit раздут из-за базовых API bloc, таких как mapEventToState, add и т. д. ([#2228](https://github.com/felangel/bloc/issues/2228)) - Разработчики технически могут вызывать эти API и ломать вещи - У нас все еще есть та же проблема с cubit, раскрывающим весь API stream, как и раньше ([#1429](https://github.com/felangel/bloc/issues/1429)) Чтобы решить эти проблемы, мы ввели базовый класс как для `Bloc`, так и для `Cubit` под названием `BlocBase`, чтобы вышестоящие компоненты по-прежнему могли взаимодействовать как с экземплярами bloc, так и с cubit, но без раскрытия всего API `Stream` и `EventSink` напрямую. **Краткое изложение** **BlocObserver** **v6.1.x** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(Cubit cubit) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(Cubit cubit, Object event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(Cubit cubit, Object error, StackTrace stackTrace) {...} @override void onClose(Cubit cubit) {...} } ``` **v7.0.0** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(BlocBase bloc) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(BlocBase bloc, Object? event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) {...} @override void onClose(BlocBase bloc) {...} } ``` **Bloc/Cubit** **v6.1.x** ```dart final bloc = MyBloc(); bloc.listen((state) {...}); final cubit = MyCubit(); cubit.listen((state) {...}); ``` **v7.0.0** ```dart final bloc = MyBloc(); bloc.stream.listen((state) {...}); final cubit = MyCubit(); cubit.stream.listen((state) {...}); ``` ### `package:bloc_test` #### ❗seed возвращает функцию для поддержки динамических значений ##### Обоснование Чтобы поддерживать изменяемое начальное значение, которое можно динамически обновлять в `setUp`, `seed` возвращает функцию. **Краткое изложение** **v7.x.x** ```dart blocTest( '...', seed: MyState(), ... ); ``` **v8.0.0** ```dart blocTest( '...', seed: () => MyState(), ... ); ``` #### ❗expect возвращает функцию для поддержки динамических значений и включает поддержку матчеров ##### Обоснование Чтобы поддерживать изменяемое ожидание, которое можно динамически обновлять в `setUp`, `expect` возвращает функцию. `expect` также поддерживает `Matchers`. **Краткое изложение** **v7.x.x** ```dart blocTest( '...', expect: [MyStateA(), MyStateB()], ... ); ``` **v8.0.0** ```dart blocTest( '...', expect: () => [MyStateA(), MyStateB()], ... ); // Это также может быть `Matcher` blocTest( '...', expect: () => contains(MyStateA()), ... ); ``` #### ❗errors возвращает функцию для поддержки динамических значений и включает поддержку матчеров ##### Обоснование Чтобы поддерживать изменяемые ошибки, которые можно динамически обновлять в `setUp`, `errors` возвращает функцию. `errors` также поддерживает `Matchers`. **Краткое изложение** **v7.x.x** ```dart blocTest( '...', errors: [MyError()], ... ); ``` **v8.0.0** ```dart blocTest( '...', errors: () => [MyError()], ... ); // Это также может быть `Matcher` blocTest( '...', errors: () => contains(MyError()), ... ); ``` #### ❗MockBloc и MockCubit ##### Обоснование Для поддержки заглушек различных основных API `MockBloc` и `MockCubit` экспортируются как часть пакета `bloc_test`. Ранее `MockBloc` приходилось использовать как для экземпляров `Bloc`, так и для `Cubit`, что было неинтуитивно. **Краткое изложение** **v7.x.x** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockBloc implements MyBloc {} ``` **v8.0.0** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockCubit implements MyCubit {} ``` #### ❗Интеграция с Mocktail ##### Обоснование Из-за различных ограничений null-safe [package:mockito](https://pub.dev/packages/mockito), описанных [здесь](https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#problems-with-typical-mocking-and-stubbing), [package:mocktail](https://pub.dev/packages/mocktail) используется `MockBloc` и `MockCubit`. Это позволяет разработчикам продолжать использовать знакомый API для моков без необходимости вручную писать заглушки или полагаться на генерацию кода. **Краткое изложение** **v7.x.x** ```dart import 'package:mockito/mockito.dart'; ... when(bloc.state).thenReturn(MyState()); verify(bloc.add(any)).called(1); ``` **v8.0.0** ```dart import 'package:mocktail/mocktail.dart'; ... when(() => bloc.state).thenReturn(MyState()); verify(() => bloc.add(any())).called(1); ``` > Пожалуйста, обратитесь к > [#347](https://github.com/dart-lang/mockito/issues/347), а также к > [документации mocktail](https://github.com/felangel/mocktail/tree/main/packages/mocktail) > для получения дополнительной информации. ### `package:flutter_bloc` #### ❗ переименование параметра `cubit` в `bloc` ##### Обоснование В результате рефакторинга в `package:bloc` для введения `BlocBase`, который расширяют `Bloc` и `Cubit`, параметры `BlocBuilder`, `BlocConsumer` и `BlocListener` были переименованы из `cubit` в `bloc`, потому что виджеты работают с типом `BlocBase`. Это также дополнительно согласуется с названием библиотеки и, надеюсь, улучшает читаемость. **Краткое изложение** **v6.1.x** ```dart BlocBuilder( cubit: myBloc, ... ) BlocListener( cubit: myBloc, ... ) BlocConsumer( cubit: myBloc, ... ) ``` **v7.0.0** ```dart BlocBuilder( bloc: myBloc, ... ) BlocListener( bloc: myBloc, ... ) BlocConsumer( bloc: myBloc, ... ) ``` ### `package:hydrated_bloc` #### ❗storageDirectory является обязательным при вызове HydratedStorage.build ##### Обоснование Чтобы сделать `package:hydrated_bloc` чистым пакетом Dart, зависимость от [package:path_provider](https://pub.dev/packages/path_provider) была удалена, и параметр `storageDirectory` при вызове `HydratedStorage.build` является обязательным и больше не использует `getTemporaryDirectory` по умолчанию. **Краткое изложение** **v6.x.x** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` **v7.0.0** ```dart import 'package:path_provider/path_provider.dart'; ... HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getTemporaryDirectory(), ); ``` ## v6.1.0 ### `package:flutter_bloc` #### ❗context.bloc и context.repository устарели в пользу context.read и context.watch ##### Обоснование `context.read`, `context.watch` и `context.select` были добавлены для согласования с существующим API [provider](https://pub.dev/packages/provider), с которым знакомы многие разработчики, и для решения проблем, поднятых сообществом. Чтобы повысить безопасность кода и поддерживать согласованность, `context.bloc` был объявлен устаревшим, потому что его можно заменить либо `context.read`, либо `context.watch` в зависимости от того, используется ли он непосредственно внутри `build`. **context.watch** `context.watch` решает запрос на наличие [MultiBlocBuilder](https://github.com/felangel/bloc/issues/538), потому что мы можем наблюдать за несколькими bloc'ами в одном `Builder`, чтобы отрендерить UI на основе нескольких состояний: ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // вернуть Widget, который зависит от состояния BlocA, BlocB и BlocC } ); ``` **context.select** `context.select` позволяет разработчикам рендерить/обновлять UI на основе части состояния bloc и решает запрос на наличие [более простого buildWhen](https://github.com/felangel/bloc/issues/1521). ```dart final name = context.select((UserBloc bloc) => bloc.state.user.name); ``` Приведенный выше фрагмент позволяет нам получить доступ и перестроить виджет только тогда, когда имя текущего пользователя изменяется. **context.read** Хотя кажется, что `context.read` идентичен `context.bloc`, есть некоторые тонкие, но значительные различия. Оба позволяют получить доступ к bloc с помощью `BuildContext` и не приводят к перестроениям; однако `context.read` не может быть вызван непосредственно внутри метода `build`. Есть две основные причины использовать `context.bloc` внутри `build`: 1. **Чтобы получить доступ к состоянию bloc** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` Приведенное выше использование подвержено ошибкам, потому что виджет `Text` не будет перестроен, если состояние bloc изменится. В этом сценарии следует использовать либо `BlocBuilder`, либо `context.watch`. ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` или ```dart @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) => Text('$state'), ); } ``` :::note Использование `context.watch` в корне метода `build` приведет к тому, что весь виджет будет перестроен при изменении состояния bloc. Если весь виджет не нужно перестраивать, либо используйте `BlocBuilder`, чтобы обернуть части, которые должны перестраиваться, используйте `Builder` с `context.watch` для ограничения перестроений, либо разбейте виджет на более мелкие виджеты. ::: 2. **Чтобы получить доступ к bloc для добавления события** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` Приведенное выше использование неэффективно, потому что оно приводит к поиску bloc на каждом перестроении, когда bloc нужен только тогда, когда пользователь нажимает `ElevatedButton`. В этом сценарии предпочтительнее использовать `context.read` для доступа к bloc непосредственно там, где он нужен (в данном случае в колбэке `onPressed`). ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` **Краткое изложение** **v6.0.x** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` ?> Если доступ к bloc для добавления события, выполняйте доступ к bloc, используя `context.read` в колбэке, где это необходимо. **v6.0.x** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` ?> Используйте `context.watch` при доступе к состоянию bloc, чтобы убедиться, что виджет перестраивается при изменении состояния. ## v6.0.0 ### `package:bloc` #### ❗BlocObserver onError принимает Cubit ##### Обоснование Из-за интеграции `Cubit`, `onError` теперь является общим как для экземпляров `Bloc`, так и для `Cubit`. Поскольку `Cubit` является базовым, `BlocObserver` будет принимать тип `Cubit`, а не тип `Bloc` в переопределении `onError`. **v5.x.x** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Bloc bloc, Object error, StackTrace stackTrace) { super.onError(bloc, error, stackTrace); } } ``` **v6.0.0** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Cubit cubit, Object error, StackTrace stackTrace) { super.onError(cubit, error, stackTrace); } } ``` #### ❗Bloc не испускает последнее состояние при подписке ##### Обоснование Это изменение было внесено для согласования `Bloc` и `Cubit` с встроенным поведением `Stream` в `Dart`. Кроме того, соответствие старому поведению в контексте `Cubit` привело ко многим непредвиденным побочным эффектам и в целом усложнило внутренние реализации других пакетов, таких как `flutter_bloc` и `bloc_test`, без необходимости (требующие `skip(1)` и т. д.). **v5.x.x** ```dart final bloc = MyBloc(); bloc.listen(print); ``` Ранее приведенный выше фрагмент выводил начальное состояние bloc, за которым следовали последующие изменения состояния. **v6.x.x** В v6.0.0 приведенный выше фрагмент не выводит начальное состояние и выводит только последующие изменения состояния. Предыдущее поведение может быть достигнуто следующим образом: ```dart final bloc = MyBloc(); print(bloc.state); bloc.listen(print); ``` ?> **Примечание**: Это изменение повлияет только на код, который зависит от прямых подписок на bloc. При использовании `BlocBuilder`, `BlocListener` или `BlocConsumer` не будет заметного изменения в поведении. ### `package:bloc_test` #### ❗MockBloc требует только тип State ##### Обоснование Это не обязательно и устраняет лишний код, а также делает `MockBloc` совместимым с `Cubit`. **v5.x.x** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` **v6.0.0** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` #### ❗whenListen требует только тип State ##### Обоснование Это не обязательно и устраняет лишний код, а также делает `whenListen` совместимым с `Cubit`. **v5.x.x** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` **v6.0.0** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` #### ❗blocTest не требует тип Event ##### Обоснование Это не обязательно и устраняет лишний код, а также делает `blocTest` совместимым с `Cubit`. **v5.x.x** ```dart blocTest( 'emits [1] when increment is called', build: () async => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` **v6.0.0** ```dart blocTest( 'emits [1] when increment is called', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` #### ❗blocTest skip по умолчанию равен 0 ##### Обоснование Поскольку экземпляры `bloc` и `cubit` больше не будут испускать последнее состояние для новых подписок, больше не было необходимости устанавливать `skip` по умолчанию равным `1`. **v5.x.x** ```dart blocTest( 'emits [0] when skip is 0', build: () async => CounterBloc(), skip: 0, expect: const [0], ); ``` **v6.0.0** ```dart blocTest( 'emits [] when skip is 0', build: () => CounterBloc(), skip: 0, expect: const [], ); ``` Начальное состояние bloc или cubit можно протестировать следующим образом: ```dart test('initial state is correct', () { expect(MyBloc().state, InitialState()); }); ``` #### ❗blocTest делает build синхронным ##### Обоснование Ранее `build` был сделан `async`, чтобы можно было выполнить различные подготовительные действия для приведения тестируемого bloc в определенное состояние. Это больше не нужно и также решает несколько проблем из-за дополнительной задержки между build и подпиской внутренне. Вместо того чтобы выполнять асинхронную подготовку для приведения bloc в желаемое состояние, мы теперь можем установить состояние bloc, связав `emit` с желаемым состоянием. **v5.x.x** ```dart blocTest( 'emits [2] when increment is added', build: () async { final bloc = CounterBloc(); bloc.add(CounterEvent.increment); await bloc.take(2); return bloc; } act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` **v6.0.0** ```dart blocTest( 'emits [2] when increment is added', build: () => CounterBloc()..emit(1), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` :::note `emit` виден только для тестирования и никогда не должен использоваться вне тестов. ::: ### `package:flutter_bloc` #### ❗параметр bloc в BlocBuilder переименован в cubit ##### Обоснование Чтобы сделать `BlocBuilder` совместимым с экземплярами `bloc` и `cubit`, параметр `bloc` был переименован в `cubit` (поскольку `Cubit` является базовым классом). **v5.x.x** ```dart BlocBuilder( bloc: myBloc, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocBuilder( cubit: myBloc, builder: (context, state) {...} ) ``` #### ❗параметр bloc в BlocListener переименован в cubit ##### Обоснование Чтобы сделать `BlocListener` совместимым с экземплярами `bloc` и `cubit`, параметр `bloc` был переименован в `cubit` (поскольку `Cubit` является базовым классом). **v5.x.x** ```dart BlocListener( bloc: myBloc, listener: (context, state) {...} ) ``` **v6.0.0** ```dart BlocListener( cubit: myBloc, listener: (context, state) {...} ) ``` #### ❗параметр bloc в BlocConsumer переименован в cubit ##### Обоснование Чтобы сделать `BlocConsumer` совместимым с экземплярами `bloc` и `cubit`, параметр `bloc` был переименован в `cubit` (поскольку `Cubit` является базовым классом). **v5.x.x** ```dart BlocConsumer( bloc: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocConsumer( cubit: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` --- ## v5.0.0 ### `package:bloc` #### ❗initialState был удален ##### Обоснование Как разработчик, необходимость переопределять `initialState` при создании bloc представляет две основные проблемы: - `initialState` bloc может быть динамическим и также может быть указан позже (даже вне самого bloc). В некотором смысле это можно рассматривать как утечку внутренней информации bloc на уровень UI. - Это многословно. **v4.x.x** ```dart class CounterBloc extends Bloc { @override int get initialState => 0; ... } ``` **v5.0.0** ```dart class CounterBloc extends Bloc { CounterBloc() : super(0); ... } ``` ?> Для получения дополнительной информации ознакомьтесь с [#1304](https://github.com/felangel/bloc/issues/1304) #### ❗BlocDelegate переименован в BlocObserver ##### Обоснование Название `BlocDelegate` не было точным описанием роли, которую играл класс. `BlocDelegate` предполагает, что класс играет активную роль, тогда как в действительности предполагаемая роль `BlocDelegate` заключалась в том, чтобы быть пассивным компонентом, который просто наблюдает за всеми bloc'ами в приложении. :::note В идеале не должно быть никаких пользовательских функций или возможностей, обрабатываемых внутри `BlocObserver`. ::: **v4.x.x** ```dart class MyBlocDelegate extends BlocDelegate { ... } ``` **v5.0.0** ```dart class MyBlocObserver extends BlocObserver { ... } ``` #### ❗BlocSupervisor был удален ##### Обоснование `BlocSupervisor` был еще одним компонентом, о котором разработчикам нужно было знать и взаимодействовать с ним исключительно для указания пользовательского `BlocDelegate`. С изменением на `BlocObserver` мы почувствовали, что это улучшило опыт разработчика, установив наблюдателя непосредственно на самом bloc. ?> Это изменение также позволило нам отделить другие дополнения к bloc, такие как `HydratedStorage`, от `BlocObserver`. **v4.x.x** ```dart BlocSupervisor.delegate = MyBlocDelegate(); ``` **v5.0.0** ```dart Bloc.observer = MyBlocObserver(); ``` ### `package:flutter_bloc` #### ❗condition в BlocBuilder переименовано в buildWhen ##### Обоснование При использовании `BlocBuilder` мы ранее могли указать `condition`, чтобы определить, должен ли `builder` перестраиваться. ```dart BlocBuilder( condition: (previous, current) { // вернуть true/false, чтобы определить, нужно ли вызывать builder }, builder: (context, state) {...} ) ``` Название `condition` не очень самообъяснительно или очевидно, и, что более важно, при взаимодействии с `BlocConsumer` API стал несогласованным, потому что разработчики могут предоставить два условия (одно для `builder` и одно для `listener`). В результате API `BlocConsumer` раскрыл `buildWhen` и `listenWhen` ```dart BlocConsumer( listenWhen: (previous, current) { // вернуть true/false, чтобы определить, нужно ли вызывать listener }, listener: (context, state) {...}, buildWhen: (previous, current) { // вернуть true/false, чтобы определить, нужно ли вызывать builder }, builder: (context, state) {...}, ) ``` Чтобы согласовать API и обеспечить более последовательный опыт разработчика, `condition` был переименован в `buildWhen`. **v4.x.x** ```dart BlocBuilder( condition: (previous, current) { // вернуть true/false, чтобы определить, нужно ли вызывать builder }, builder: (context, state) {...} ) ``` **v5.0.0** ```dart BlocBuilder( buildWhen: (previous, current) { // вернуть true/false, чтобы определить, нужно ли вызывать builder }, builder: (context, state) {...} ) ``` #### ❗condition в BlocListener переименовано в listenWhen ##### Обоснование По тем же причинам, что описаны выше, условие `BlocListener` также было переименовано. **v4.x.x** ```dart BlocListener( condition: (previous, current) { // вернуть true/false, чтобы определить, нужно ли вызывать listener }, listener: (context, state) {...} ) ``` **v5.0.0** ```dart BlocListener( listenWhen: (previous, current) { // вернуть true/false, чтобы определить, нужно ли вызывать listener }, listener: (context, state) {...} ) ``` ### `package:hydrated_bloc` #### ❗HydratedStorage и HydratedBlocStorage переименованы ##### Обоснование Чтобы улучшить повторное использование кода между [hydrated_bloc](https://pub.dev/packages/hydrated_bloc) и [hydrated_cubit](https://pub.dev/packages/hydrated_cubit), конкретная реализация хранилища по умолчанию была переименована из `HydratedBlocStorage` в `HydratedStorage`. Кроме того, интерфейс `HydratedStorage` был переименован из `HydratedStorage` в `Storage`. **v4.0.0** ```dart class MyHydratedStorage implements HydratedStorage { ... } ``` **v5.0.0** ```dart class MyHydratedStorage implements Storage { ... } ``` #### ❗HydratedStorage отделен от BlocDelegate ##### Обоснование Как упоминалось ранее, `BlocDelegate` был переименован в `BlocObserver` и был установлен непосредственно как часть `bloc` через: ```dart Bloc.observer = MyBlocObserver(); ``` Следующее изменение было внесено для: - Оставаться согласованным с новым API наблюдателя bloc - Ограничить область действия хранилища только `HydratedBloc` - Отделить `BlocObserver` от `Storage` **v4.0.0** ```dart BlocSupervisor.delegate = await HydratedBlocDelegate.build(); ``` **v5.0.0** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` #### ❗Упрощенная инициализация ##### Обоснование Ранее разработчикам приходилось вручную вызывать `super.initialState ?? DefaultInitialState()` для настройки своих экземпляров `HydratedBloc`. Это неудобно и многословно, а также несовместимо с критическими изменениями в `initialState` в `bloc`. В результате в v5.0.0 инициализация `HydratedBloc` идентична обычной инициализации `Bloc`. **v4.0.0** ```dart class CounterBloc extends HydratedBloc { @override int get initialState => super.initialState ?? 0; } ``` **v5.0.0** ```dart class CounterBloc extends HydratedBloc { CounterBloc() : super(0); ... } ``` ================================================ FILE: docs/src/content/docs/ru/modeling-state.mdx ================================================ --- title: Моделирование состояния description: Обзор нескольких способов моделирования состояний при использовании package:bloc. --- import ConcreteClassAndStatusEnumSnippet from '~/components/modeling-state/ConcreteClassAndStatusEnumSnippet.astro'; import SealedClassAndSubclassesSnippet from '~/components/modeling-state/SealedClassAndSubclassesSnippet.astro'; Существует множество различных подходов к структурированию состояния приложения. Каждый из них имеет свои преимущества и недостатки. В этом разделе мы рассмотрим несколько подходов, их плюсы и минусы, а также когда использовать каждый из них. Следующие подходы являются просто рекомендациями и являются полностью необязательными. Не стесняйтесь использовать любой подход, который вы предпочитаете. Вы можете обнаружить, что некоторые примеры/документация не следуют подходам в основном для простоты/краткости. :::tip Следующие фрагменты кода сосредоточены на структуре состояния. На практике вы также можете захотеть: - Расширить `Equatable` из [`package:equatable`](https://pub.dev/packages/equatable) - Аннотировать класс с помощью `@Data()` из [`package:data_class`](https://pub.dev/packages/data_class) - Аннотировать класс с помощью **@immutable** из [`package:meta`](https://pub.dev/packages/meta) - Реализовать метод `copyWith` - Использовать ключевое слово `const` для конструкторов ::: ## Конкретный класс и enum статуса Этот подход состоит из **одного конкретного класса** для всех состояний вместе с `enum`, представляющим различные статусы. Свойства делаются nullable и обрабатываются на основе текущего статуса. Этот подход лучше всего работает для состояний, которые не являются строго эксклюзивными и/или содержат много общих свойств. #### Плюсы - **Просто**: Легко управлять одним классом и enum статуса, и все свойства легко доступны. - **Кратко**: Обычно требует меньше строк кода по сравнению с другими подходами. #### Минусы - **Не типобезопасно**: Требует проверки `status` перед доступом к свойствам. Возможно `emit` неправильно сформированное состояние, что может привести к ошибкам. Свойства для конкретных состояний являются nullable, что может быть обременительным для управления и требует либо принудительного извлечения, либо выполнения проверок на null. Некоторые из этих минусов могут быть смягчены написанием модульных тестов и написанием специализированных именованных конструкторов. - **Раздутый**: Приводит к одному состоянию, которое может стать раздутым с множеством свойств со временем. #### Вердикт Этот подход лучше всего работает для простых состояний или когда требования требуют состояний, которые не являются эксклюзивными (например, показ snackbar при возникновении ошибки при сохранении старых данных из последнего успешного состояния). Этот подход обеспечивает гибкость и краткость за счет типобезопасности. ## Запечатанный класс и подклассы Этот подход состоит из **запечатанного класса**, который содержит любые общие свойства, и нескольких подклассов для отдельных состояний. Этот подход отлично подходит для раздельных эксклюзивных состояний. #### Плюсы - **Типобезопасно**: Код безопасен на этапе компиляции, и невозможно случайно получить доступ к недопустимому свойству. Каждый подкласс содержит свои собственные свойства, что делает ясным, какие свойства принадлежат какому состоянию. - **Явно:** Разделяет общие свойства от специфичных для состояния свойств. - **Исчерпывающе**: Использование оператора `switch` для проверки исчерпывающести, чтобы гарантировать, что каждое состояние явно обработано. - Если вы не хотите [исчерпывающего переключения](https://dart.dev/language/branches#exhaustiveness-checking) или хотите иметь возможность добавлять подтипы позже без нарушения API, используйте модификатор [final](https://dart.dev/language/class-modifiers#final). - См. [документацию по запечатанным классам](https://dart.dev/language/class-modifiers#sealed) для получения более подробной информации. #### Минусы - **Многословно**: Требует больше кода (один базовый класс и подкласс для каждого состояния). Также может потребоваться дублирование кода для общих свойств в подклассах. - **Сложно**: Добавление новых свойств требует обновления каждого подкласса и базового класса, что может быть обременительным и привести к увеличению сложности состояния. Кроме того, может потребоваться ненужная/избыточная проверка типов для доступа к свойствам. #### Вердикт Этот подход лучше всего работает для хорошо определенных эксклюзивных состояний с уникальными свойствами. Этот подход обеспечивает типобезопасность и исчерпывающие проверки и подчеркивает безопасность над краткостью и простотой. ================================================ FILE: docs/src/content/docs/ru/naming-conventions.mdx ================================================ --- title: Соглашения об именовании description: Обзор рекомендуемых соглашений об именовании при использовании bloc. --- import EventExamplesGood1 from '~/components/naming-conventions/EventExamplesGood1Snippet.astro'; import EventExamplesBad1 from '~/components/naming-conventions/EventExamplesBad1Snippet.astro'; import StateExamplesGood1Snippet from '~/components/naming-conventions/StateExamplesGood1Snippet.astro'; import SingleStateExamplesGood1Snippet from '~/components/naming-conventions/SingleStateExamplesGood1Snippet.astro'; import StateExamplesBad1Snippet from '~/components/naming-conventions/StateExamplesBad1Snippet.astro'; Следующие соглашения об именовании являются просто рекомендациями и являются полностью необязательными. Не стесняйтесь использовать любые соглашения об именовании, которые вы предпочитаете. Вы можете обнаружить, что некоторые примеры/документация не следуют соглашениям об именовании в основном для простоты/краткости. Эти соглашения настоятельно рекомендуются для больших проектов с несколькими разработчиками. ## Соглашения о событиях События должны быть названы в **прошедшем времени**, потому что события — это то, что уже произошло с точки зрения блока. ### Анатомия `BlocSubject` + `Существительное (необязательно)` + `Глагол (событие)` События начальной загрузки должны следовать соглашению: `BlocSubject` + `Started` :::note Базовый класс события должен быть назван: `BlocSubject` + `Event`. ::: ### Примеры ✅ **Хорошо** ❌ **Плохо** ## Соглашения о состояниях Состояния должны быть существительными, потому что состояние — это просто снимок в определенный момент времени. Существует два распространенных способа представления состояния: использование подклассов или использование одного класса. ### Анатомия #### Подклассы `BlocSubject` + `Глагол (действие)` + `State` При представлении состояния в виде нескольких подклассов `State` должно быть одним из следующих: `Initial` | `Success` | `Failure` | `InProgress` :::note Начальные состояния должны следовать соглашению: `BlocSubject` + `Initial`. ::: #### Один класс `BlocSubject` + `State` При представлении состояния в виде одного базового класса должен использоваться enum с именем `BlocSubject` + `Status` для представления статуса состояния: `initial` | `success` | `failure` | `loading`. :::note Базовый класс состояния всегда должен быть назван: `BlocSubject` + `State`. ::: ### Примеры ✅ **Хорошо** ##### Подклассы ##### Один класс ❌ **Плохо** ================================================ FILE: docs/src/content/docs/ru/testing.mdx ================================================ --- title: Тестирование description: Основы написания тестов для ваших блоков. --- import CounterBlocSnippet from '~/components/testing/CounterBlocSnippet.astro'; import AddDevDependenciesSnippet from '~/components/testing/AddDevDependenciesSnippet.astro'; import CounterBlocTestImportsSnippet from '~/components/testing/CounterBlocTestImportsSnippet.astro'; import CounterBlocTestMainSnippet from '~/components/testing/CounterBlocTestMainSnippet.astro'; import CounterBlocTestSetupSnippet from '~/components/testing/CounterBlocTestSetupSnippet.astro'; import CounterBlocTestInitialStateSnippet from '~/components/testing/CounterBlocTestInitialStateSnippet.astro'; import CounterBlocTestBlocTestSnippet from '~/components/testing/CounterBlocTestBlocTestSnippet.astro'; Bloc был разработан так, чтобы его было чрезвычайно легко тестировать. В этом разделе мы рассмотрим, как провести модульное тестирование блока. Для простоты давайте напишем тесты для `CounterBloc`, который мы создали в разделе [Основные концепции](/ru/bloc-concepts). Напомним, что реализация `CounterBloc` выглядит так: ## Настройка Прежде чем начать писать наши тесты, нам нужно добавить фреймворк для тестирования в наши зависимости. Нам нужно добавить [test](https://pub.dev/packages/test) и [bloc_test](https://pub.dev/packages/bloc_test) в наш проект. ## Тестирование Давайте начнем с создания файла для наших тестов `CounterBloc`, `counter_bloc_test.dart`, и импортируем пакет test. Далее нам нужно создать нашу функцию `main`, а также нашу группу тестов. :::note Группы используются для организации отдельных тестов, а также для создания контекста, в котором вы можете совместно использовать общие `setUp` и `tearDown` для всех отдельных тестов. ::: Давайте начнем с создания экземпляра нашего `CounterBloc`, который будет использоваться во всех наших тестах. Теперь мы можем начать писать наши индивидуальные тесты. :::note Мы можем запустить все наши тесты с помощью команды `dart test`. ::: На этом этапе у нас должен быть первый пройденный тест! Теперь давайте напишем более сложный тест, используя пакет [bloc_test](https://pub.dev/packages/bloc_test). Мы должны иметь возможность запустить тесты и увидеть, что все они проходят. Вот и все, тестирование должно быть легким, и мы должны чувствовать уверенность при внесении изменений и рефакторинге нашего кода. Вы можете обратиться к [приложению Weather](https://github.com/felangel/bloc/tree/master/examples/flutter_weather) в качестве примера полностью протестированного приложения. ================================================ FILE: docs/src/content/docs/ru/tutorials/flutter-counter.mdx ================================================ --- title: Счетчик Flutter description: Подробное руководство по созданию приложения-счетчика Flutter с использованием bloc. sidebar: order: 1 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-counter/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) В следующем руководстве мы собираемся создать счетчик на Flutter используя библиотеку Bloc. ![demo](~/assets/tutorials/flutter-counter.gif) ## Ключевые темы - Наблюдение за изменениями состояния с помощью [BlocObserver](/ru/bloc-concepts#blocobserver). - [BlocProvider](/ru/flutter-bloc-concepts#blocprovider), виджет Flutter который предоставляет bloc своим дочерним элементам. - [BlocBuilder](/ru/flutter-bloc-concepts#blocbuilder), виджет Flutter который обрабатывает построение виджета в ответ на новые состояния. - Использование Cubit вместо Bloc. [В чем разница?](/ru/bloc-concepts/#cubit-против-bloc) - Добавление событий с помощью [context.read](/ru/flutter-bloc-concepts#contextread). ## Настройка Начнем с создания нового Flutter проекта Затем можем заменить содержимое `pubspec.yaml` на и затем установить все наши зависимости ## Структура проекта ``` ├── lib │ ├── app.dart │ ├── counter │ │ ├── counter.dart │ │ ├── cubit │ │ │ └── counter_cubit.dart │ │ └── view │ │ ├── counter_page.dart │ │ ├── counter_view.dart │ │ └── view.dart │ ├── counter_observer.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` Приложение использует структуру каталогов, ориентированную на функции. Эта структура проекта позволяет нам масштабировать проект, имея автономные функции. В этом примере у нас будет только одна функция (сам счетчик), но в более сложных приложениях у нас может быть сотни различных функций. ## BlocObserver Первое, на что мы обратим внимание, это как создать `BlocObserver` который поможет нам наблюдать все изменения состояния в приложении. Давайте создадим `lib/counter_observer.dart`: В данном случае мы переопределяем только `onChange` чтобы увидеть все изменения состояния, которые происходят. :::note `onChange` работает одинаково для экземпляров `Bloc` и `Cubit`. ::: ## main.dart Далее, заменим содержимое `lib/main.dart` на: Мы инициализируем только что созданный `CounterObserver` и вызываем `runApp` с виджетом `CounterApp`, который мы рассмотрим далее. ## Counter App Создадим `lib/app.dart`: `CounterApp` будет `MaterialApp` и указывает `home` как `CounterPage`. :::note Мы расширяем `MaterialApp` потому что `CounterApp` _является_ `MaterialApp`. В большинстве случаев мы будем создавать экземпляры `StatelessWidget` или `StatefulWidget` и компоновать виджеты в `build`, но в данном случае нет виджетов для компоновки, поэтому проще просто расширить `MaterialApp`. ::: Давайте рассмотрим `CounterPage` далее! ## Counter Page Создадим `lib/counter/view/counter_page.dart`: Виджет `CounterPage` отвечает за создание `CounterCubit` (который мы рассмотрим далее) и предоставление его `CounterView`. :::note Важно отделить или разъединить создание `Cubit` от использования `Cubit`, чтобы иметь код, который намного более тестируем и пригоден для повторного использования. ::: ## Counter Cubit Создадим `lib/counter/cubit/counter_cubit.dart`: Класс `CounterCubit` предоставит два метода: - `increment`: добавляет 1 к текущему состоянию - `decrement`: вычитает 1 из текущего состояния Тип состояния, которым управляет `CounterCubit`, это просто `int` и начальное состояние равно `0`. :::tip Используйте [VSCode расширение](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) или [IntelliJ плагин](https://plugins.jetbrains.com/plugin/12129-bloc) для автоматического создания новых cubits. ::: Далее, давайте рассмотрим `CounterView`, который будет отвечать за использование состояния и взаимодействие с `CounterCubit`. ## Counter View Создадим `lib/counter/view/counter_view.dart`: `CounterView` отвечает за отображение текущего счета и отображение двух FloatingActionButtons для увеличения/уменьшения счетчика. `BlocBuilder` используется для обертывания виджета `Text` чтобы обновлять текст каждый раз когда изменяется состояние `CounterCubit`. Кроме того, `context.read()` используется для поиска ближайшего экземпляра `CounterCubit`. :::note Только виджет `Text` обернут в `BlocBuilder` потому что это единственный виджет, который необходимо перестроить в ответ на изменения состояния в `CounterCubit`. Избегайте ненужного оборачивания виджетов, которые не нужно перестраивать при изменении состояния. ::: ## Barrel Создадим `lib/counter/view/view.dart`: Добавим `view.dart` для экспорта всех публичных частей counter view. Создадим `lib/counter/counter.dart`: Добавим `counter.dart` для экспорта всех публичных частей функции counter. Вот и все! Мы отделили слой представления от слоя бизнес-логики. `CounterView` не знает, что происходит, когда пользователь нажимает кнопку; он просто уведомляет `CounterCubit`. Кроме того, `CounterCubit` не знает, что происходит со состоянием (значением счетчика); он просто выдает новые состояния в ответ на вызываемые методы. Мы можем запустить наше приложение с помощью `flutter run` и можем просмотреть его на нашем устройстве или симуляторе/эмуляторе. Полный исходный код (включая unit и widget тесты) этого примера можно найти [здесь](https://github.com/felangel/Bloc/tree/master/examples/flutter_counter). ================================================ FILE: docs/src/content/docs/ru/tutorials/flutter-firebase-login.mdx ================================================ --- title: Вход Flutter Firebase description: Подробное руководство по созданию потока входа Flutter с использованием bloc и Firebase. sidebar: order: 7 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-firebase-login/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) In the following tutorial, we're going to build a Firebase Login Flow in Flutter using the Bloc library. ![demo](~/assets/tutorials/flutter-firebase-login.gif) ## Ключевые темы - [BlocProvider](/ru/flutter-bloc-concepts#blocprovider), a Flutter widget which provides a bloc to its children. - Using Cubit instead of Bloc. [What's the difference?](/ru/bloc-concepts/#cubit-против-bloc) - Adding events with [context.read](/ru/flutter-bloc-concepts#contextread). - Prevent unnecessary rebuilds with [Equatable](/ru/faqs/#когда-использовать-equatable). - [RepositoryProvider](/ru/flutter-bloc-concepts#repositoryprovider), a Flutter widget which provides a repository to its children. - [BlocListener](/ru/flutter-bloc-concepts#bloclistener), a Flutter widget which invokes the listener code in response to state changes в каталоге bloc. - Adding events with [context.read](/ru/flutter-bloc-concepts#contextselect). ## Настройка Начнем с создания нового Flutter проекта. Так же, как в [руководстве по входу](/ru/tutorials/flutter-login), we're going to create internal packages to better layer our application architecture and maintain clear boundaries and to maximize both reusability as well as improve testability. In this case, the [firebase_auth](https://pub.dev/packages/firebase_auth) and [google_sign_in](https://pub.dev/packages/google_sign_in) packages are going to be our data layer so we're only going to be creating an `AuthenticationRepository` to compose data from the two API clients. ## Authentication Repository The `AuthenticationRepository` will be responsible for abstracting the internal implementation details of how we authenticate and fetch user information. In this case, it will be integrating with Firebase but we can always change the internal implementation later on and our application will be unaffected. ### Настройка We'll start by creating `packages/authentication_repository` and a `pubspec.yaml` at the root of the project. Далее, мы можем установить зависимости, запустив: в каталоге `authentication_repository` . Just like most packages, the `authentication_repository` will define it's API surface via `packages/authentication_repository/lib/authentication_repository.dart` :::note The `authentication_repository` package will be exposing an `AuthenticationRepository` as well as models. ::: Далее, рассмотрим модели. ### User The `User` model will describe a user в каталоге context of the authentication domain. For the purposes of this example, a user will consist of an `email`, `id`, `name`, and `photo`. :::note It's completely up to you to define what a user needs to look like в каталоге context of your domain. ::: [user.dart](https://raw.githubusercontent.com/felangel/bloc/master/examples/flutter_firebase_login/packages/authentication_repository/lib/src/models/user.dart ':include') :::note The `User` class is extending [equatable](https://pub.dev/packages/equatable) in order to override equality comparisons so that we can compare different instances of `User` by value. ::: :::tip It's useful to define a `static` empty `User` so that we don't have to handle `null` Users and can always work with a concrete `User` object. ::: ### Repository The `AuthenticationRepository` is responsible for abstracting the underlying implementation of how a user is authenticated, as well as how a user is fetched. The `AuthenticationRepository` exposes a `Stream` which we can subscribe to in order to be notified of when a `User` changes. In addition, it exposes methods to `signUp`, `logInWithGoogle`, `logInWithEmailAndPassword`, and `logOut`. :::note The `AuthenticationRepository` is also responsible for handling low-level errors that can occur в каталоге data layer and exposes a clean, simple set of errors that align with the domain. ::: That's it for the `AuthenticationRepository`. Next, let's take a look at how to integrate it into the Flutter project we created. ## Настройка Firebase We need to follow the [firebase_auth usage instructions](https://pub.dev/packages/firebase_auth#usage) in order to hook up our application to Firebase and enable [google_sign_in](https://pub.dev/packages/google_sign_in). :::caution Remember to update the `google-services.json` on Android and the `GoogleService-Info.plist` & `Info.plist` on iOS, otherwise the application will crash. ::: ## Зависимости проекта We can replace the generated `pubspec.yaml` at the root of the project with the following: Notice that we are specifying an assets directory for all of our applications local assets. Create an `assets` directory в каталоге root of your project and add the [bloc logo](https://github.com/felangel/bloc/blob/master/examples/flutter_firebase_login/assets/bloc_logo_small.png) asset (which we'll use later). Then install all of the dependencies: :::note We are depending on the `authentication_repository` package via path which will allow us to iterate quickly while still maintaining a clear separation. ::: ## main.dart The `main.dart` file can be replaced with the following: It's simply setting up some global configuration for the application and calling `runApp` with an instance of `App`. :::note We're injecting a single instance of `AuthenticationRepository` into the `App` and it is an explicit constructor dependency. ::: ## App Так же, как в [руководстве по входу](/ru/tutorials/flutter-login), our `app.dart` will provide an instance of the `AuthenticationRepository` to the application via `RepositoryProvider` and also creates and provides an instance of `AuthenticationBloc`. Then `AppView` consumes the `AuthenticationBloc` and handles updating the current route based on the `AuthenticationState`. ## App Bloc The `AppBloc` is responsible for managing the global state of the application. It has a dependency on the `AuthenticationRepository` and subscribes to the `user` Stream in order to emit new states in response to changes в каталоге current user. ### State The `AppState` consists of an `AppStatus` and a `User`. The default constructor accepts an optional `User` and redirects to the private constructor with the appropriate authentication status. ### Event The `AppEvent` has two subclasses: - `AppUserSubscriptionRequested` which notifies the bloc to subscribe to the user stream. - `AppLogoutPressed` which notifies the bloc of a user logout action. ### Bloc In the constructor body, `AppEvent` subclasses are mapped to their corresponding event handlers. In the `_onUserSubscriptionRequested` event handler, the `AppBloc` uses `emit.onEach` to subscribe to the user stream of the `AuthenticationRepository` and emit a state in response to each `User`. `emit.onEach` creates a stream subscription internally and takes care of canceling it when either `AppBloc` or the user stream is closed. If the user stream emits an error, `addError` forwards the error and stack trace to any `BlocObserver` listening. :::caution If `onError` is omitted, any errors on the user stream are considered unhandled, and will be thrown by `onEach`. As a result, the subscription to the user stream will be canceled. ::: :::tip A [`BlocObserver`](/ru/bloc-concepts/#blocobserver-1) is great for logging Bloc events, errors, and state changes especially в каталоге context analytics and crash reporting. ::: ## Модели An `Email` and `Password` input model are useful for encapsulating the validation logic and will be used in both the `LoginForm` and `SignUpForm` (later в каталоге tutorial). Both input models are made using the [formz](https://pub.dev/packages/formz) package and allow us to work with a validated object rather than a primitive type like a `String`. ### Email ### Password ## Страница входа The `LoginPage` is responsible for creating and providing an instance of `LoginCubit` to the `LoginForm`. :::tip It's very important to keep the creation of blocs/cubits separate from where they are consumed. This will allow you to easily inject mock instances and test your view in isolation. ::: ## Login Cubit The `LoginCubit` is responsible for managing the `LoginState` of the form. It exposes APIs to `logInWithCredentials`, `logInWithGoogle`, as well as gets notified when the email/password are updated. ### State The `LoginState` consists of an `Email`, `Password`, and `FormzStatus`. The `Email` and `Password` models extend `FormzInput` from the [formz](https://pub.dev/packages/formz) package. ### Cubit The `LoginCubit` has a dependency on the `AuthenticationRepository` in order to sign the user in either via credentials or via google sign in. :::note We used a `Cubit` instead of a `Bloc` here because the `LoginState` is fairly simple and localized. Even without events, we can still have a fairly good sense of what happened just by looking at the changes from one state to another and our code is a lot simpler and more concise. ::: ## Форма входа The `LoginForm` is responsible for rendering the form in response to the `LoginState` and invokes methods on the `LoginCubit` in response to user interactions. The `LoginForm` also renders a "Create Account" button which navigates to the `SignUpPage` where a user can create a brand new account. ## Страница регистрации The `SignUp` structure mirrors the `Login` structure and consists of a `SignUpPage`, `SignUpView`, and `SignUpCubit`. The `SignUpPage` is just responsible for creating and providing an instance of the `SignUpCubit` to the `SignUpForm` (exactly like in `LoginPage`). :::note Just as в каталоге `LoginCubit`, the `SignUpCubit` has a dependency on the `AuthenticationRepository` in order to create new user accounts. ::: ## Sign Up Cubit The `SignUpCubit` manages the state of the `SignUpForm` and communicates with the `AuthenticationRepository` in order to create new user accounts. ### State The `SignUpState` reuses the same `Email` and `Password` form input models because the validation logic is the same. ### Cubit The `SignUpCubit` is extremely similar to the `LoginCubit` with the main exception being it exposes an API to submit the form as opposed to login. ## Форма регистрации The `SignUpForm` is responsible for rendering the form in response to the `SignUpState` and invokes methods on the `SignUpCubit` in response to user interactions. ## Домашняя страница After a user either successfully logs in or signs up, the `user` stream will be updated which will trigger a state change в каталоге `AuthenticationBloc` and will result в каталоге `AppView` pushing the `HomePage` route onto the navigation stack. From the `HomePage`, the user can view their profile information and log out by tapping the exit icon в каталоге `AppBar`. :::note A `widgets` directory was created alongside the `view` directory withв каталоге `home` feature for reusable components that are specific to that particular feature. In this case a simple `Avatar` widget is exported and used withв каталоге `HomePage`. ::: :::note When the logout `IconButton` is tapped, an `AuthenticationLogoutRequested` event is added to the `AuthenticationBloc` which signs the user out and navigates them back to the `LoginPage`. ::: At this point we have a pretty solid login implementation using Firebase and we have decoupled our presentation layer from the business logic layer by using the Bloc Library. Полный исходный код этого примера можно найти [here](https://github.com/felangel/bloc/tree/master/examples/flutter_firebase_login). ================================================ FILE: docs/src/content/docs/ru/tutorials/flutter-infinite-list.mdx ================================================ --- title: Бесконечный список Flutter description: Подробное руководство по созданию бесконечного списка Flutter с использованием bloc. sidebar: order: 3 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-infinite-list/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/flutter-infinite-list/FlutterPubGetSnippet.astro'; import PostsJsonSnippet from '~/components/tutorials/flutter-infinite-list/PostsJsonSnippet.astro'; import PostBlocInitialStateSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocInitialStateSnippet.astro'; import PostBlocOnPostFetchedSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocOnPostFetchedSnippet.astro'; import PostBlocTransformerSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocTransformerSnippet.astro'; ![intermediate](https://img.shields.io/badge/level-intermediate-orange.svg) В этом руководстве мы создадим приложение, которое извлекает данные по сети и загружает их по мере прокрутки пользователем, используя Flutter и библиотеку bloc. ![demo](~/assets/tutorials/flutter-infinite-list.gif) ## Ключевые темы - Наблюдение за изменениями состояния с помощью [BlocObserver](/ru/bloc-concepts#blocobserver). - [BlocProvider](/ru/flutter-bloc-concepts#blocprovider), виджет Flutter который предоставляет bloc своим дочерним элементам. - [BlocBuilder](/ru/flutter-bloc-concepts#blocbuilder), виджет Flutter который обрабатывает построение виджета в ответ на новые состояния. - Добавление событий с помощью [context.read](/ru/flutter-bloc-concepts#contextread). - Предотвращение ненужных перестроек с помощью [Equatable](/ru/faqs/#когда-использовать-equatable). - Использование метода `transformEvents` с Rx. ## Настройка Начнем с создания нового Flutter проекта Затем можем заменить содержимое pubspec.yaml на и затем установить все наши зависимости ## Структура проекта ``` ├── lib | ├── posts │ │ ├── bloc │ │ │ └── post_bloc.dart | | | └── post_event.dart | | | └── post_state.dart | | └── models | | | └── models.dart* | | | └── post.dart │ │ └── view │ │ | ├── posts_page.dart │ │ | └── posts_list.dart | | | └── view.dart* | | └── widgets | | | └── bottom_loader.dart | | | └── post_list_item.dart | | | └── widgets.dart* │ │ ├── posts.dart* │ ├── app.dart │ ├── simple_bloc_observer.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` Приложение использует структуру каталогов, ориентированную на функции. Эта структура проекта позволяет нам масштабировать проект, имея автономные функции. В этом примере у нас будет только одна функция (функция post), и она разделена на соответствующие папки с barrel файлами, обозначенными звездочкой (\*). ## REST API Для этого демонстрационного приложения мы будем использовать [jsonplaceholder](http://jsonplaceholder.typicode.com) в качестве нашего источника данных. :::note jsonplaceholder - это онлайн REST API, который предоставляет фальшивые данные; он очень полезен для создания прототипов. ::: Откройте новую вкладку в браузере и посетите https://jsonplaceholder.typicode.com/posts?_start=0&_limit=2, чтобы увидеть, что возвращает API. :::note В нашем url мы указали start и limit в качестве параметров запроса для GET запроса. ::: Отлично, теперь, когда мы знаем, как будут выглядеть наши данные, создадим модель. ## Модель данных Создайте `post.dart` и приступим к работе над созданием модели нашего объекта Post. `Post` - это просто класс с `id`, `title` и `body`. :::note Мы расширяем [`Equatable`](https://pub.dev/packages/equatable), чтобы мы могли сравнивать `Posts`. Без этого нам пришлось бы вручную изменить наш класс, чтобы переопределить equality и hashCode, чтобы мы могли определить разницу между двумя объектами `Posts`. См. [пакет](https://pub.dev/packages/equatable) для более подробной информации. ::: Теперь, когда у нас есть наша модель объекта `Post`, начнем работать над компонентом бизнес-логики (bloc). ## Post Events Прежде чем погрузиться в реализацию, нам нужно определить, что будет делать наш `PostBloc`. На высоком уровне он будет реагировать на пользовательский ввод (прокрутку) и извлекать больше постов, чтобы слой представления мог их отобразить. Начнем с создания нашего `Event`. Наш `PostBloc` будет реагировать только на одно событие; `PostFetched`, которое будет добавлено слоем представления всякий раз, когда ему нужно больше Posts для представления. Поскольку наше событие `PostFetched` является типом `PostEvent`, мы можем создать `bloc/post_event.dart` и реализовать событие следующим образом. Подводя итог, наш `PostBloc` будет получать `PostEvents` и преобразовывать их в `PostStates`. Мы определили все наши `PostEvents` (PostFetched), поэтому далее определим наш `PostState`. ## Post States Нашему слою представления понадобится несколько частей информации, чтобы правильно расположить себя: - `PostInitial`- сообщит слою представления, что ему нужно отобразить индикатор загрузки пока загружается начальная партия постов - `PostSuccess`- сообщит слою представления, что у него есть контент для отображения - `posts`- будет `List`, который будет отображаться - `hasReachedMax`- сообщит слою представления, достигнуто ли максимальное количество постов - `PostFailure`- сообщит слою представления, что произошла ошибка при извлечении постов Теперь мы можем создать `bloc/post_state.dart` и реализовать его следующим образом. :::note Мы реализовали `copyWith`, чтобы мы могли скопировать экземпляр `PostSuccess` и обновить ноль или более свойств удобным образом (это пригодится позже). ::: Теперь, когда у нас реализованы наши `Events` и `States`, мы можем создать наш `PostBloc`. ## Post Bloc Для простоты наш `PostBloc` будет иметь прямую зависимость от `http client`; однако в производственном приложении мы предлагаем вместо этого внедрить api client и использовать паттерн repository [docs](/ru/architecture). Создадим `post_bloc.dart` и создадим наш пустой `PostBloc`. :::note Просто из объявления класса мы можем сказать, что наш PostBloc будет принимать PostEvents в качестве входных данных и выводить PostStates. ::: Далее, нам нужно зарегистрировать обработчик событий для обработки входящих событий `PostFetched`. В ответ на событие `PostFetched` мы вызовем `_fetchPosts` для извлечения постов из API. Наш `PostBloc` будет `emit` новые состояния через `Emitter`, предоставленный в обработчике событий. Смотрите [основные концепции](/ru/bloc-concepts/#потоки-streams) для более подробной информации. Теперь каждый раз, когда добавляется `PostEvent`, если это событие `PostFetched` и есть больше постов для извлечения, наш `PostBloc` извлечет следующие 20 постов. API вернет пустой массив, если мы попытаемся извлечь за пределами максимального количества постов (100), поэтому если мы получим пустой массив, наш bloc `emit` currentState, за исключением того, что мы установим `hasReachedMax` в true. Если мы не можем извлечь посты, мы выдаем `PostStatus.failure`. Если мы можем извлечь посты, мы выдаем `PostStatus.success` и весь список постов. Одна оптимизация, которую мы можем сделать, это `throttle` событие `PostFetched`, чтобы предотвратить ненужную рассылку спама нашему API. Мы можем сделать это, используя параметр `transform`, когда регистрируем обработчик события `_onFetched`. :::note Передача `transformer` в `on` позволяет нам настроить, как обрабатываются события. ::: :::note Убедитесь, что импортировали [`package:stream_transform`](https://pub.dev/packages/stream_transform), чтобы использовать api `throttle`. ::: Наш законченный `PostBloc` теперь должен выглядеть так: Отлично! Теперь, когда мы закончили реализацию бизнес-логики, все, что осталось сделать, это реализовать слой представления. ## Слой представления В нашем `main.dart` мы можем начать с реализации нашей главной функции и вызова `runApp` для отображения нашего корневого виджета. Здесь мы также можем включить наш bloc observer для логирования переходов и любых ошибок. В нашем виджете `App`, корне нашего проекта, мы можем затем установить home в `PostsPage` В нашем виджете `PostsPage` мы используем `BlocProvider` для создания и предоставления экземпляра `PostBloc` поддереву. Также мы добавляем событие `PostFetched`, чтобы когда приложение загружается, оно запрашивало начальную партию Posts. Далее, нам нужно реализовать наше представление `PostsList`, которое будет представлять наши посты и подключиться к нашему `PostBloc`. :::note `PostsList` - это `StatefulWidget`, потому что ему нужно поддерживать `ScrollController`. В `initState` мы добавляем слушателя к нашему `ScrollController`, чтобы мы могли реагировать на события прокрутки. Мы также получаем доступ к нашему экземпляру `PostBloc` через `context.read()`. ::: Двигаясь дальше, наш метод build возвращает `BlocBuilder`. `BlocBuilder` - это виджет Flutter из пакета [flutter_bloc](https://pub.dev/packages/flutter_bloc), который обрабатывает построение виджета в ответ на новые состояния bloc. Каждый раз, когда изменяется состояние нашего `PostBloc`, наша функция builder будет вызвана с новым `PostState`. :::caution Нам нужно не забыть очистить за собой и удалить наш `ScrollController`, когда StatefulWidget удаляется. ::: Всякий раз, когда пользователь прокручивает, мы вычисляем, как далеко вы прокрутили вниз по странице, и если наше расстояние ≥ 90% нашего `maxScrollextent`, мы добавляем событие `PostFetched`, чтобы загрузить больше постов. Далее, нам нужно реализовать наш виджет `BottomLoader`, который будет указывать пользователю, что мы загружаем больше постов. Наконец, нам нужно реализовать наш `PostListItem`, который будет отображать отдельный `Post`. На этом этапе мы должны быть в состоянии запустить наше приложение, и все должно работать; однако, есть еще одна вещь, которую мы можем сделать. Один дополнительный бонус использования библиотеки bloc заключается в том, что мы можем иметь доступ ко всем `Transitions` в одном месте. Изменение от одного состояния к другому называется `Transition`. :::note `Transition` состоит из текущего состояния, события и следующего состояния. ::: Даже несмотря на то, что в этом приложении у нас только один bloc, довольно часто в больших приложениях иметь много blocs, управляющих различными частями состояния приложения. Если мы хотим иметь возможность что-то делать в ответ на все `Transitions`, мы можем просто создать свой собственный `BlocObserver`. :::note Все, что нам нужно сделать, это расширить `BlocObserver` и переопределить метод `onTransition`. ::: Теперь каждый раз, когда происходит `Transition` Bloc, мы можем видеть переход, напечатанный в консоли. :::note На практике вы можете создавать различные `BlocObservers`, и поскольку каждое изменение состояния записывается, мы можем очень легко инструментировать наши приложения и отслеживать все взаимодействия пользователей и изменения состояния в одном месте! ::: Вот и все! Мы теперь успешно реализовали бесконечный список во flutter, используя пакеты [bloc](https://pub.dev/packages/bloc) и [flutter_bloc](https://pub.dev/packages/flutter_bloc), и мы успешно отделили наш слой представления от нашей бизнес-логики. Наш `PostsPage` не знает, откуда берутся `Posts` или как они извлекаются. И наоборот, наш `PostBloc` не знает, как `State` отображается, он просто преобразует события в состояния. Полный исходный код этого примера можно найти [здесь](https://github.com/felangel/Bloc/tree/master/examples/flutter_infinite_list). ================================================ FILE: docs/src/content/docs/ru/tutorials/flutter-login.mdx ================================================ --- title: Вход Flutter description: Подробное руководство по созданию потока входа Flutter с использованием bloc. sidebar: order: 4 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-login/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![intermediate](https://img.shields.io/badge/level-intermediate-orange.svg) В следующем руководстве мы собираемся создать поток входа в Flutter используя библиотеку Bloc. ![demo](~/assets/tutorials/flutter-login.gif) ## Ключевые темы - [BlocProvider](/ru/flutter-bloc-concepts#blocprovider), виджет Flutter который предоставляет bloc своим дочерним элементам. - Добавление событий с помощью [context.read](/ru/flutter-bloc-concepts#contextread). - Предотвращение ненужных перестроек с помощью [Equatable](/ru/faqs/#когда-использовать-equatable). - [RepositoryProvider](/ru/flutter-bloc-concepts#repositoryprovider), виджет Flutter который предоставляет repository своим дочерним элементам. - [BlocListener](/ru/flutter-bloc-concepts#bloclistener), виджет Flutter который вызывает код слушателя в ответ на изменения состояния в bloc. - Обновление UI на основе части состояния bloc с помощью [context.select](/ru/flutter-bloc-concepts#contextselect). ## Настройка проекта Начнем с создания нового Flutter проекта Далее, мы можем установить все наши зависимости ## Authentication Repository Первое, что мы собираемся сделать, это создать пакет `authentication_repository`, который будет отвечать за управление доменом аутентификации. Начнем с создания каталога `packages/authentication_repository` в корне проекта, который будет содержать все внутренние пакеты. На высоком уровне структура каталогов должна выглядеть так: ``` ├── android ├── ios ├── lib ├── packages │ └── authentication_repository └── test ``` Далее, мы можем создать `pubspec.yaml` для пакета `authentication_repository`: :::note `package:authentication_repository` будет чистым Dart пакетом без каких-либо внешних зависимостей. ::: Далее, нам нужно реализовать сам класс `AuthenticationRepository`, который будет находиться в `packages/authentication_repository/lib/src/authentication_repository.dart`. `AuthenticationRepository` предоставляет `Stream` обновлений `AuthenticationStatus`, которые будут использоваться для уведомления приложения, когда пользователь входит или выходит. Кроме того, есть методы `logIn` и `logOut`, которые заглушены для простоты, но могут быть легко расширены для аутентификации с помощью `FirebaseAuth`, например, или какого-либо другого провайдера аутентификации. :::note Поскольку мы поддерживаем `StreamController` внутренне, метод `dispose` предоставляется, чтобы контроллер мог быть закрыт, когда он больше не нужен. ::: Наконец, нам нужно создать `packages/authentication_repository/lib/authentication_repository.dart`, который будет содержать публичные экспорты: Вот и все для `AuthenticationRepository`, далее мы будем работать над `UserRepository`. ## User Repository Так же, как и с `AuthenticationRepository`, мы создадим пакет `user_repository` внутри каталога `packages`. ``` ├── android ├── ios ├── lib ├── packages │ ├── authentication_repository │ └── user_repository └── test ``` Далее, мы создадим `pubspec.yaml` для `user_repository`: `user_repository` будет отвечать за домен пользователя и будет предоставлять API для взаимодействия с текущим пользователем. Первое, что мы определим, это модель пользователя в `packages/user_repository/lib/src/models/user.dart`: Для простоты у пользователя есть только свойство `id`, но на практике у нас могут быть дополнительные свойства, такие как `firstName`, `lastName`, `avatarUrl` и т.д... :::note [`package:equatable`](https://pub.dev/packages/equatable) используется для включения сравнения значений объекта `User`. ::: Далее, мы можем создать `models.dart` в `packages/user_repository/lib/src/models`, который будет экспортировать все модели, чтобы мы могли использовать один оператор импорта для импорта нескольких моделей. Теперь, когда модели определены, мы можем реализовать класс `UserRepository` в `packages/user_repository/lib/src/user_repository.dart`. Для этого простого примера `UserRepository` предоставляет один метод `getUser`, который будет извлекать текущего пользователя. Мы заглушаем это, но на практике это где мы запрашивали бы текущего пользователя с backend. Почти закончили с пакетом `user_repository` -- осталось только создать файл `user_repository.dart` в `packages/user_repository/lib`, который определяет публичные экспорты: Теперь, когда у нас есть пакеты `authentication_repository` и `user_repository`, мы можем сосредоточиться на приложении Flutter. ## Установка зависимостей Начнем с обновления сгенерированного `pubspec.yaml` в корне нашего проекта: Мы можем установить зависимости, запустив: ## Authentication Bloc `AuthenticationBloc` будет отвечать за реагирование на изменения в состоянии аутентификации (предоставляемом `AuthenticationRepository`) и будет выдавать состояния, на которые мы можем реагировать в слое представления. Реализация `AuthenticationBloc` находится внутри `lib/authentication`, потому что мы рассматриваем аутентификацию как функцию в нашем слое приложения. ``` ├── lib │ ├── app.dart │ ├── authentication │ │ ├── authentication.dart │ │ └── bloc │ │ ├── authentication_bloc.dart │ │ ├── authentication_event.dart │ │ └── authentication_state.dart │ ├── main.dart ``` :::tip Используйте [VSCode расширение](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) или [IntelliJ плагин](https://plugins.jetbrains.com/plugin/12129-bloc) для автоматического создания blocs. ::: ### authentication_event.dart Экземпляры `AuthenticationEvent` будут входными данными для `AuthenticationBloc` и будут обрабатываться и использоваться для выдачи новых экземпляров `AuthenticationState`. В этом приложении `AuthenticationBloc` будет реагировать на два различных события: - `AuthenticationSubscriptionRequested`: начальное событие, которое уведомляет bloc о подписке на поток `AuthenticationStatus` - `AuthenticationLogoutPressed`: уведомляет bloc о действии выхода пользователя Далее, рассмотрим `AuthenticationState`. ### authentication_state.dart Экземпляры `AuthenticationState` будут выходными данными `AuthenticationBloc` и будут использоваться слоем представления. Класс `AuthenticationState` имеет три именованных конструктора: - `AuthenticationState.unknown()`: состояние по умолчанию, которое указывает, что bloc еще не знает, аутентифицирован ли текущий пользователь. - `AuthenticationState.authenticated()`: состояние, которое указывает, что пользователь в настоящее время аутентифицирован. - `AuthenticationState.unauthenticated()`: состояние, которое указывает, что пользователь в настоящее время не аутентифицирован. Теперь, когда мы видели реализации `AuthenticationEvent` и `AuthenticationState`, давайте рассмотрим `AuthenticationBloc`. ### authentication_bloc.dart `AuthenticationBloc` управляет состоянием аутентификации приложения, которое используется для определения таких вещей, как запускать ли пользователя на странице входа или домашней странице. `AuthenticationBloc` имеет зависимость как от `AuthenticationRepository`, так и от `UserRepository` и определяет начальное состояние как `AuthenticationState.unknown()`. В теле конструктора подклассы `AuthenticationEvent` сопоставляются с их соответствующими обработчиками событий. В обработчике события `_onSubscriptionRequested` `AuthenticationBloc` использует `emit.onEach` для подписки на поток `status` из `AuthenticationRepository` и выдает состояние в ответ на каждый `AuthenticationStatus`. `emit.onEach` создает подписку на поток внутренне и заботится о ее отмене, когда либо `AuthenticationBloc`, либо поток `status` закрыт. Если поток `status` выдает ошибку, `addError` пересылает ошибку и stackTrace любому слушающему `BlocObserver`. :::caution Если `onError` опущен, любые ошибки в потоке `status` считаются необработанными и будут выброшены `onEach`. В результате подписка на поток `status` будет отменена. ::: :::tip [`BlocObserver`](/ru/bloc-concepts/#blocobserver-1) отлично подходит для логирования событий Bloc, ошибок и изменений состояния, особенно в контексте аналитики и отчетов о сбоях. ::: Когда поток `status` выдает `AuthenticationStatus.unknown` или `unauthenticated`, выдается соответствующий `AuthenticationState`. Когда выдается `AuthenticationStatus.authenticated`, `AuthentictionBloc` запрашивает пользователя через `UserRepository`. ## main.dart Далее, мы можем заменить `main.dart` по умолчанию на: ## App Так же, как в [руководстве по входу](/ru/tutorials/flutter-login), наш `app.dart` будет предоставлять экземпляр `AuthenticationRepository` приложению через `RepositoryProvider` и также создавать и предоставлять экземпляр `AuthenticationBloc`. Затем `AppView` использует `AuthenticationBloc` и обрабатывает обновление текущего маршрута на основе `AuthenticationState`. :::note `app.dart` разделен на две части: `App` и `AppView`. `App` отвечает за создание/предоставление `AuthenticationBloc`, который будет использоваться `AppView`. Это разделение позволит нам легко протестировать как виджеты `App`, так и `AppView` позже. ::: :::note `RepositoryProvider` используется для предоставления единственного экземпляра `AuthenticationRepository` всему приложению, что пригодится позже. ::: По умолчанию `BlocProvider` ленив и не вызывает `create`, пока первый раз не будет осуществлен доступ к Bloc. Поскольку `AuthenticationBloc` всегда должен подписываться на поток `AuthenticationStatus` немедленно (через событие `AuthenticationSubscriptionRequested`), мы можем явно отказаться от этого поведения, установив `lazy: false`. `AppView` - это `StatefulWidget`, потому что он поддерживает `GlobalKey`, который используется для доступа к `NavigatorState`. По умолчанию `AppView` будет отображать `SplashPage` (который мы увидим позже), и он использует `BlocListener` для навигации к различным страницам на основе изменений в `AuthenticationState`. ## Splash Функция splash будет содержать только простое представление, которое будет отображаться сразу когда приложение запускается, пока приложение определяет, аутентифицирован ли пользователь. ``` lib └── splash ├── splash.dart └── view └── splash_page.dart ``` :::tip `SplashPage` предоставляет статический `Route`, что делает очень легкой навигацию к нему через `Navigator.of(context).push(SplashPage.route())`; ::: ## Login Функция login содержит `LoginPage`, `LoginForm` и `LoginBloc` и позволяет пользователям вводить имя пользователя и пароль для входа в приложение. ``` ├── lib │ ├── login │ │ ├── bloc │ │ │ ├── login_bloc.dart │ │ │ ├── login_event.dart │ │ │ └── login_state.dart │ │ ├── login.dart │ │ ├── models │ │ │ ├── models.dart │ │ │ ├── password.dart │ │ │ └── username.dart │ │ └── view │ │ ├── login_form.dart │ │ ├── login_page.dart │ │ └── view.dart ``` ### Login Models Мы используем [`package:formz`](https://pub.dev/packages/formz) для создания повторно используемых и стандартных моделей для `username` и `password`. #### Username Для простоты мы просто проверяем, что имя пользователя не пустое, но на практике вы можете применять использование специальных символов, длину и т.д... #### Password Опять же, мы просто выполняем простую проверку, чтобы убедиться, что пароль не пустой. #### Models Barrel Так же, как и раньше, есть файл barrel `models.dart` для облегчения импорта моделей `Username` и `Password` одним импортом. ### Login Bloc `LoginBloc` управляет состоянием `LoginForm` и заботится о валидации имени пользователя и пароля, а также о состоянии формы. #### login_event.dart В этом приложении есть три различных типа `LoginEvent`: - `LoginUsernameChanged`: уведомляет bloc, что имя пользователя было изменено. - `LoginPasswordChanged`: уведомляет bloc, что пароль был изменен. - `LoginSubmitted`: уведомляет bloc, что форма была отправлена. #### login_state.dart `LoginState` будет состоять из статуса формы, а также из состояний входных данных имени пользователя и пароля. :::note Модели `Username` и `Password` используются как часть `LoginState`, и статус также является частью [package:formz](https://pub.dev/packages/formz). ::: #### login_bloc.dart `LoginBloc` отвечает за реагирование на взаимодействия пользователя в `LoginForm` и обработку валидации и отправки формы. `LoginBloc` имеет зависимость от `AuthenticationRepository`, потому что когда форма отправлена, он вызывает `logIn`. Начальное состояние bloc `pure`, что означает, что ни входные данные, ни форма не были затронуты или не взаимодействовали. Всякий раз, когда изменяется `username` или `password`, bloc создает "грязный" вариант модели `Username`/`Password` и обновляет статус формы через API `Formz.validate`. Когда добавляется событие `LoginSubmitted`, если текущий статус формы valid, bloc делает вызов `logIn` и обновляет статус на основе результата запроса. Далее рассмотрим `LoginPage` и `LoginForm`. ### Login Page `LoginPage` отвечает за предоставление `Route`, а также за создание и предоставление `LoginBloc` в `LoginForm`. :::note `context.read()` используется для поиска экземпляра `AuthenticationRepository` через `BuildContext`. ::: ### Login Form `LoginForm` обрабатывает уведомление `LoginBloc` о событиях пользователя, а также реагирует на изменения состояния, используя `BlocBuilder` и `BlocListener`. `BlocListener` используется для показа `SnackBar`, если отправка входа не удалась. В дополнение, `context.select` используется для эффективного доступа к конкретным частям `LoginState` для каждого виджета, предотвращая ненужные перестроения. Обратный вызов `onChanged` используется для уведомления `LoginBloc` об изменениях имени пользователя/пароля. Виджет `_LoginButton` включен только если статус формы valid, и `CircularProgressIndicator` показывается на его месте во время отправки формы. ## Home После успешного запроса `logIn` состояние `AuthenticationBloc` изменится на `authenticated`, и пользователь будет перенаправлен на `HomePage`, где мы отображаем `id` пользователя, а также кнопку выхода. ``` ├── lib │ ├── home │ │ ├── home.dart │ │ └── view │ │ └── home_page.dart ``` ### Home Page `HomePage` может получить доступ к текущему id пользователя через `context.select((AuthenticationBloc bloc) => bloc.state.user.id)` и отображает его через виджет `Text`. Кроме того, когда нажата кнопка выхода, событие `AuthenticationLogoutPressed` добавляется в `AuthenticationBloc`. :::note `context.select((AuthenticationBloc bloc) => bloc.state.user.id)` будет вызывать обновления, если id пользователя изменяется. ::: На этом этапе у нас есть довольно надежная реализация входа, и мы разделили наш слой представления от слоя бизнес-логики, используя Bloc. Полный исходный код этого примера (включая unit и widget тесты) можно найти [здесь](https://github.com/felangel/Bloc/tree/master/examples/flutter_login). ================================================ FILE: docs/src/content/docs/ru/tutorials/flutter-timer.mdx ================================================ --- title: Таймер Flutter description: Подробное руководство по созданию приложения таймера Flutter с использованием bloc. sidebar: order: 2 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-timer/FlutterCreateSnippet.astro'; import TimerBlocEmptySnippet from '~/components/tutorials/flutter-timer/TimerBlocEmptySnippet.astro'; import TimerBlocInitialStateSnippet from '~/components/tutorials/flutter-timer/TimerBlocInitialStateSnippet.astro'; import TimerBlocTickerSnippet from '~/components/tutorials/flutter-timer/TimerBlocTickerSnippet.astro'; import TimerBlocOnStartedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnStartedSnippet.astro'; import TimerBlocOnTickedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnTickedSnippet.astro'; import TimerBlocOnPausedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnPausedSnippet.astro'; import TimerBlocOnResumedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnResumedSnippet.astro'; import TimerPageSnippet from '~/components/tutorials/flutter-timer/TimerPageSnippet.astro'; import ActionsSnippet from '~/components/tutorials/flutter-timer/ActionsSnippet.astro'; import BackgroundSnippet from '~/components/tutorials/flutter-timer/BackgroundSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) В следующем руководстве мы рассмотрим, как создать приложение таймера используя библиотеку bloc. Готовое приложение должно выглядеть так: ![demo](~/assets/tutorials/flutter-timer.gif) ## Ключевые темы - Наблюдение за изменениями состояния с помощью [BlocObserver](/ru/bloc-concepts#blocobserver). - [BlocProvider](/ru/flutter-bloc-concepts#blocprovider), виджет Flutter который предоставляет bloc своим дочерним элементам. - [BlocBuilder](/ru/flutter-bloc-concepts#blocbuilder), виджет Flutter который обрабатывает построение виджета в ответ на новые состояния. - Предотвращение ненужных перестроек с помощью [Equatable](/ru/faqs/#когда-использовать-equatable). - Изучение использования `StreamSubscription` в Bloc. - Предотвращение ненужных перестроек с помощью `buildWhen`. ## Настройка Начнем с создания нового Flutter проекта: Затем можем заменить содержимое pubspec.yaml на: :::note Мы будем использовать пакеты [flutter_bloc](https://pub.dev/packages/flutter_bloc) и [equatable](https://pub.dev/packages/equatable) в этом приложении. ::: Далее, запустите `flutter pub get` для установки всех зависимостей. ## Структура проекта ``` ├── lib | ├── timer │ │ ├── bloc │ │ │ └── timer_bloc.dart | | | └── timer_event.dart | | | └── timer_state.dart │ │ └── view │ │ | ├── timer_page.dart │ │ ├── timer.dart │ ├── app.dart │ ├── ticker.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` ## Ticker Ticker будет нашим источником данных для приложения таймера. Он предоставит поток тиков, на который мы можем подписаться и реагировать. Начните с создания `ticker.dart`. Весь наш класс `Ticker` предоставляет функцию tick, которая принимает количество тиков (секунд), которое мы хотим, и возвращает поток, который выдает оставшиеся секунды каждую секунду. Далее, нам нужно создать наш `TimerBloc`, который будет использовать `Ticker`. ## Timer Bloc ### TimerState Начнем с определения `TimerStates`, в которых может находиться наш `TimerBloc`. Наше состояние `TimerBloc` может быть одним из следующих: - `TimerInitial`: готов начать обратный отсчет с указанной продолжительности. - `TimerRunInProgress`: активно ведет обратный отсчет от указанной продолжительности. - `TimerRunPause`: приостановлен на некоторой оставшейся продолжительности. - `TimerRunComplete`: завершен с оставшейся продолжительностью 0. Каждое из этих состояний будет иметь влияние на пользовательский интерфейс и действия, которые пользователь может выполнять. Например: - если состояние `TimerInitial`, пользователь сможет запустить таймер. - если состояние `TimerRunInProgress`, пользователь сможет приостановить и сбросить таймер, а также увидеть оставшуюся продолжительность. - если состояние `TimerRunPause`, пользователь сможет возобновить таймер и сбросить таймер. - если состояние `TimerRunComplete`, пользователь сможет сбросить таймер. Чтобы держать все наши файлы bloc вместе, создадим каталог bloc с `bloc/timer_state.dart`. :::tip Вы можете использовать [IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc-code-generator) или [VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) расширения для автоматической генерации следующих файлов bloc. ::: Обратите внимание, что все `TimerStates` расширяют абстрактный базовый класс `TimerState`, который имеет свойство duration. Это потому, что независимо от того, в каком состоянии находится наш `TimerBloc`, мы хотим знать, сколько времени осталось. Кроме того, `TimerState` расширяет `Equatable` для оптимизации нашего кода, гарантируя, что наше приложение не вызывает перестроение, если происходит то же самое состояние. Далее, определим и реализуем `TimerEvents`, которые будет обрабатывать наш `TimerBloc`. ### TimerEvent Наш `TimerBloc` должен знать, как обрабатывать следующие события: - `TimerStarted`: информирует TimerBloc, что таймер должен быть запущен. - `TimerPaused`: информирует TimerBloc, что таймер должен быть приостановлен. - `TimerResumed`: информирует TimerBloc, что таймер должен быть возобновлен. - `TimerReset`: информирует TimerBloc, что таймер должен быть сброшен к исходному состоянию. - `_TimerTicked`: информирует TimerBloc, что произошел тик и что он должен обновить свое состояние соответственно. Если вы не использовали [IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc-code-generator) или [VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) расширения, то создайте `bloc/timer_event.dart` и реализуем эти события. Далее, реализуем `TimerBloc`! ### TimerBloc Если вы еще не сделали это, создайте `bloc/timer_bloc.dart` и создайте пустой `TimerBloc`. Первое, что нам нужно сделать, это определить начальное состояние нашего `TimerBloc`. В этом случае мы хотим, чтобы `TimerBloc` начинался в состоянии `TimerInitial` с предустановленной продолжительностью 1 минута (60 секунд). Далее, нам нужно определить зависимость от нашего `Ticker`. Мы также определяем `StreamSubscription` для нашего `Ticker`, к которому мы вернемся чуть позже. На этом этапе все, что осталось сделать, это реализовать обработчики событий. Для улучшения читаемости, мне нравится разбивать каждый обработчик событий на свою собственную вспомогательную функцию. Начнем с события `TimerStarted`. Если `TimerBloc` получает событие `TimerStarted`, он отправляет состояние `TimerRunInProgress` с начальной продолжительностью. Кроме того, если уже был открыт `_tickerSubscription`, нам нужно отменить его для освобождения памяти. Нам также нужно переопределить метод `close` в нашем `TimerBloc`, чтобы мы могли отменить `_tickerSubscription`, когда `TimerBloc` закрыт. Наконец, мы слушаем поток `_ticker.tick` и на каждый тик добавляем событие `_TimerTicked` с оставшейся продолжительностью. Далее, реализуем обработчик события `_TimerTicked`. Каждый раз, когда получено событие `_TimerTicked`, если продолжительность тика больше 0, нам нужно отправить обновленное состояние `TimerRunInProgress` с новой продолжительностью. В противном случае, если продолжительность тика равна 0, наш таймер завершился, и нам нужно отправить состояние `TimerRunComplete`. Теперь реализуем обработчик события `TimerPaused`. В `_onPaused`, если `state` нашего `TimerBloc` является `TimerRunInProgress`, то мы можем приостановить `_tickerSubscription` и отправить состояние `TimerRunPause` с текущей продолжительностью таймера. Далее, реализуем обработчик события `TimerResumed`, чтобы мы могли снять паузу с таймера. Обработчик события `TimerResumed` очень похож на обработчик события `TimerPaused`. Если `TimerBloc` имеет `state` равный `TimerRunPause` и получает событие `TimerResumed`, то он возобновляет `_tickerSubscription` и отправляет состояние `TimerRunInProgress` с текущей продолжительностью. Наконец, нам нужно реализовать обработчик события `TimerReset`. Если `TimerBloc` получает событие `TimerReset`, ему нужно отменить текущий `_tickerSubscription`, чтобы он не получал уведомления о дополнительных тиках и отправляет состояние `TimerInitial` с исходной продолжительностью. Это все, что касается `TimerBloc`. Теперь все, что осталось, это реализовать UI для нашего приложения таймера. ## Пользовательский интерфейс приложения ### MyApp Можем начать с удаления содержимого `main.dart` и замены его на следующее. Далее, создадим наш виджет 'App' в `app.dart`, который будет корнем нашего приложения. Далее, нам нужно реализовать наш виджет `Timer`. ### Timer Наш виджет `Timer` (`lib/timer/view/timer_page.dart`) будет отвечать за отображение оставшегося времени вместе с соответствующими кнопками, которые позволят пользователям запускать, приостанавливать и сбрасывать таймер. Пока мы просто используем `BlocProvider` для доступа к экземпляру нашего `TimerBloc`. Далее, мы реализуем наш виджет `Actions`, который будет иметь соответствующие действия (запуск, пауза и сброс). ### Barrel Чтобы очистить наши импорты из раздела `Timer`, нам нужно создать файл barrel `timer/timer.dart`. ### Actions Виджет `Actions` это просто еще один `StatelessWidget`, который использует `BlocBuilder` для перестроения UI каждый раз, когда мы получаем новый `TimerState`. `Actions` использует `context.read()` для доступа к экземпляру `TimerBloc` и возвращает различные `FloatingActionButtons` в зависимости от текущего состояния `TimerBloc`. Каждая из `FloatingActionButtons` добавляет событие в свой обратный вызов `onPressed` для уведомления `TimerBloc`. Если вы хотите более точный контроль над тем, когда вызывается функция `builder`, вы можете предоставить необязательный `buildWhen` в `BlocBuilder`. `buildWhen` принимает предыдущее состояние bloc и текущее состояние bloc и возвращает `boolean`. Если `buildWhen` возвращает `true`, `builder` будет вызван с `state` и виджет будет перестроен. Если `buildWhen` возвращает `false`, `builder` не будет вызван с `state` и перестроение не произойдет. В этом случае мы не хотим, чтобы виджет `Actions` перестраивался на каждом тике, потому что это было бы неэффективно. Вместо этого мы хотим, чтобы `Actions` перестраивался только если `runtimeType` `TimerState` изменяется (TimerInitial => TimerRunInProgress, TimerRunInProgress => TimerRunPause, и т.д...). В результате, если бы мы случайно окрашивали виджеты при каждом перестроении, это выглядело бы так: ![BlocBuilder buildWhen demo](https://cdn-images-1.medium.com/max/1600/1*YyjpH1rcZlYWxCX308l_Ew.gif) :::note Несмотря на то, что виджет `Text` перестраивается на каждом тике, мы перестраиваем только `Actions`, если они должны быть перестроены. ::: ### Background Наконец, добавьте виджет background следующим образом: ### Собираем все вместе Вот и все! На этом этапе у нас есть довольно надежное приложение таймера, которое эффективно перестраивает только те виджеты, которые необходимо перестроить. Полный исходный код этого примера можно найти [здесь](https://github.com/felangel/Bloc/tree/master/examples/flutter_timer). ================================================ FILE: docs/src/content/docs/ru/tutorials/flutter-todos.mdx ================================================ --- title: Задачи Flutter description: Подробное руководство по созданию приложения задач Flutter с использованием bloc. sidebar: order: 6 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-todos/FlutterCreateSnippet.astro'; import ActivateVeryGoodCLISnippet from '~/components/tutorials/flutter-todos/ActivateVeryGoodCLISnippet.astro'; import FlutterCreatePackagesSnippet from '~/components/tutorials/flutter-todos/FlutterCreatePackagesSnippet.astro'; import ProjectStructureSnippet from '~/components/tutorials/flutter-todos/ProjectStructureSnippet.astro'; import VeryGoodPackagesGetSnippet from '~/components/tutorials/flutter-todos/VeryGoodPackagesGetSnippet.astro'; import HomePageTreeSnippet from '~/components/tutorials/flutter-todos/HomePageTreeSnippet.astro'; import TodosOverviewPageTreeSnippet from '~/components/tutorials/flutter-todos/TodosOverviewPageTreeSnippet.astro'; import StatsPageTreeSnippet from '~/components/tutorials/flutter-todos/StatsPageTreeSnippet.astro'; import EditTodosPageTreeSnippet from '~/components/tutorials/flutter-todos/EditTodosPageTreeSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) В следующем руководстве мы собираемся создать приложение задач во Flutter, используя библиотеку Bloc. ![demo](~/assets/tutorials/flutter-todos.gif) ## Ключевые темы - [Bloc and Cubit](/ru/bloc-concepts/#cubit-против-bloc) to manage the various feature states. - [Layered Architecture](/ru/architecture) for separation of concerns and to facilitate reusability. - [BlocObserver](/ru/bloc-concepts#blocobserver) to observe state changes. - [BlocProvider](/ru/flutter-bloc-concepts#blocprovider), a Flutter widget which provides a bloc to its children. - [BlocBuilder](/ru/flutter-bloc-concepts#blocbuilder), a Flutter widget that handles building the widget in response to new states. - [BlocListener](/ru/flutter-bloc-concepts#bloclistener), a Flutter widget that handles performing side effects in response to state changes. - [RepositoryProvider](/ru/flutter-bloc-concepts#repositoryprovider), a Flutter widget to provide a repository to its children. - [Equatable](/ru/faqs/#когда-использовать-equatable) to prevent unnecessary rebuilds. - [MultiBlocListener](/ru/flutter-bloc-concepts#multibloclistener), a Flutter widget that reduces nesting when using multiple BlocListeners. ## Настройка Начнем с создания нового Flutter проекта, используя [very_good_cli](https://pub.dev/packages/very_good_cli). :::note Install `very_good_cli` using the following command ::: Next we'll create the `todos_api`, `local_storage_todos_api`, and `todos_repository` packages using `very_good_cli`: We can then replace the contents of `pubspec.yaml` with: Finally, we can install all the dependencies: ## Структура проекта Our application project structure should look like: We split the project into multiple packages in order to maintain explicit dependencies for each package with clear boundaries that enforce the [single responsibility principle](https://en.wikipedia.org/wiki/Single-responsibility_principle). Modularizing our project like this has many benefits including but not limited to: - easy to reuse packages across multiple projects - CI/CD improvements in terms of efficiency (run checks on only the code that has changed) - easy to maintain the packages in isolation with their dedicated test suites, semantic versioning, and release cycle/cadence ## Архитектура ![Todos Architecture Diagram](~/assets/tutorials/todos-architecture.png) Layering our code is incredibly important and helps us iterate quickly and with confidence. Each layer has a single responsibility and can be used and tested in isolation. This allows us to keep changes contained to a specific layer in order to minimize the impact on the entire application. In addition, layering our application allows us to easily reuse libraries across multiple projects (especially with respect to the data layer). Our application consists of three main layers: - data layer - domain layer - feature layer - presentation/UI (widgets) - business logic (blocs/cubits) **Data Layer** This layer is the lowest layer and is responsible for retrieving raw data from external sources such as a databases, APIs, and more. Packages in the data layer generally should not depend on any UI and can be reused and even published on [pub.dev](https://pub.dev) as a standalone package. In this example, our data layer consists of the `todos_api` and `local_storage_todos_api` packages. **Domain Layer** This layer combines one or more data providers and applies "business rules" to the data. Each component in this layer is called a repository and each repository generally manages a single domain. Packages in the repository layer should generally only interact with the data layer. In this example, our repository layer consists of the `todos_repository` package. **Feature Layer** This layer contains all of the application-specific features and use cases. Each feature generally consists of some UI and business logic. Features should generally be independent of other features so that they can easily be added/removed without impacting the rest of the codebase. Within each feature, the state of the feature along with any business logic is managed by blocs. Blocs interact with zero or more repositories. Blocs react to events and emit states which trigger changes in the UI. Widgets within each feature should generally only depend on the corresponding bloc and render UI based on the current state. The UI can notify the bloc of user input via events. In this example, our application will consist of the `home`, `todos_overview`, `stats`, and `edit_todos` features. Now that we've gone over the layers at a high level, let's start building our application starting with the data layer! ## Слой данных The data layer is the lowest layer in our application and consists of raw data providers. Packages in this layer are primarily concerned with where/how data is coming from. In this case our data layer will consist of the `TodosApi`, which is an interface, and the `LocalStorageTodosApi`, which is an implementation of the `TodosApi` backed by `shared_preferences`. ### TodosApi The `todos_api` package will export a generic interface for interacting/managing todos. Later we'll implement the `TodosApi` using `shared_preferences`. Having an abstraction will make it easy to support other implementations without having to change any other part of our application. For example, we can later add a `FirestoreTodosApi`, which uses `cloud_firestore` instead of `shared_preferences`, with minimal code changes to the rest of the application. #### Todo model Next we'll define our `Todo` model. The first thing of note is that the `Todo` model doesn't live in our app — it's part of the `todos_api` package. This is because the `TodosApi` defines APIs that return/accept `Todo` objects. The model is a Dart representation of the raw Todo object that will be stored/retrieved. The `Todo` model uses [json_serializable](https://pub.dev/packages/json_serializable) to handle the json (de)serialization. If you are following along, you will have to run the [code generation step](https://pub.dev/packages/json_serializable#running-the-code-generator) to resolve the compiler errors. `json_map.dart` provides a `typedef` for code checking and linting. The model of the `Todo` is defined in `todos_api/models/todo.dart` and is exported by `package:todos_api/todos_api.dart`. #### Update Exports Our `Todo` model and the `TodosApi` are exported via barrel files. Notice how we don't import the model directly, but we import it in `lib/src/todos_api.dart` with a reference to the package barrel file: `import 'package:todos_api/todos_api.dart';`. Update the barrel files to resolve any remaining import errors: #### Streams vs Futures In a previous version of this tutorial, the `TodosApi` was `Future`-based rather than `Stream`-based. For an example of a `Future`-based API see [Brian Egan's implementation in his Architecture Samples](https://github.com/brianegan/flutter_architecture_samples/tree/master/todos_repository_core). A `Future`-based implementation could consist of two methods: `loadTodos` and `saveTodos` (note the plural). This means, a full list of todos must be provided to the method each time. - One limitation of this approach is that the standard CRUD (Create, Read, Update, and Delete) operation requires sending the full list of todos with each call. For example, on an Add Todo screen, one cannot just send the added todo item. Instead, we must keep track of the entire list and provide the entire new list of todos when persisting the updated list. - A second limitation is that `loadTodos` is a one-time delivery of data. The app must contain logic to ask for updates periodically. In the current implementation, the `TodosApi` exposes a `Stream>` via `getTodos()` which will report real-time updates to all subscribers when the list of todos has changed. In addition, todos can be created, deleted, or updated individually. For example, both deleting and saving a todo are done with only the `todo` as the argument. It's not necessary to provide the newly updated list of todos each time. ### LocalStorageTodosApi This package implements the `todos_api` using the [`shared_preferences`](https://pub.dev/packages/shared_preferences) package. ## Слой Repository A [repository](/ru/architecture/#репозиторий) is part of the business layer. A repository depends on one or more data providers that have no business value, and combines their public API into APIs that provide business value. In addition, having a repository layer helps abstract data acquisition from the rest of the application, allowing us to change where/how data is being stored without affecting other parts of the app. ### TodosRepository Instantiating the repository requires specifying a `TodosApi`, which we discussed earlier in this tutorial, so we added it as a dependency in our `pubspec.yaml`: #### Library Exports In addition to exporting the `TodosRepository` class, we also export the `Todo` model from the `todos_api` package. This step prevents tight coupling between the application and the data providers. We decided to re-export the same `Todo` model from the `todos_api`, rather than redefining a separate model in the `todos_repository`, because in this case we are in complete control of the data model. In many cases, the data provider will not be something that you can control. In those cases, it becomes increasingly important to maintain your own model definitions in the repository layer to maintain full control of the interface and API contract. ## Слой функций ### Точка входа Our app's entrypoint is `main.dart`. In this case, there are three versions: The most notable thing is the concrete implementation of the `local_storage_todos_api` is instantiated within each entrypoint. ### Начальная загрузка `bootstrap.dart` loads our `BlocObserver` and creates the instance of `TodosRepository`. ### App `App` wraps a `RepositoryProvider` widget that provides the repository to all children. Since both the `EditTodoPage` and `HomePage` subtrees are descendents, all the blocs and cubits can access the repository. `AppView` creates the `MaterialApp` and configures the theme and localizations. ### Тема This provides theme definition for light and dark mode. ### Главная The home feature is responsible for managing the state of the currently-selected tab and displays the correct subtree. #### ГлавнаяState There are only two states associated with the two screens: `todos` and `stats`. :::note `EditTodo` is a separate route therefore it isn't part of the `HomeState`. ::: #### ГлавнаяCubit A cubit is appropriate in this case due to the simplicity of the business logic. We have one method `setTab` to change the tab. #### ГлавнаяView `view.dart` is a barrel file that exports all relevant UI components for the home feature. `home_page.dart` contains the UI for the root page that the user will see when the app is launched. A simplified representation of the widget tree for the `HomePage` is: The `HomePage` provides an instance of `HomeCubit` to `HomeView`. `HomeView` uses `context.select` to selectively rebuild whenever the tab changes. This allows us to easily widget test `HomeView` by providing a mock `HomeCubit` and stubbing the state. The `BottomAppBar` contains `HomeTabButton` widgets which call `setTab` on the `HomeCubit`. The instance of the cubit is looked up via `context.read` and the appropriate method is invoked on the cubit instance. :::caution `context.read` doesn't listen for changes, it is just used to access to `HomeCubit` and call `setTab`. ::: ### TodosOverview The todos overview feature allows users to manage their todos by creating, editing, deleting, and filtering todos. #### TodosOverviewEvent Let's create `todos_overview/bloc/todos_overview_event.dart` and define the events. - `TodosOverviewSubscriptionRequested`: This is the startup event. In response, the bloc subscribes to the stream of todos from the `TodosRepository`. - `TodosOverviewTodoDeleted`: This deletes a Todo. - `TodosOverviewTodoCompletionToggled`: This toggles a todo's completed status. - `TodosOverviewToggleAllRequested`: This toggles completion for all todos. - `TodosOverviewClearCompletedRequested`: This deletes all completed todos. - `TodosOverviewUndoDeletionRequested`: This undoes a todo deletion, e.g. an accidental deletion. - `TodosOverviewFilterChanged`: This takes a `TodosViewFilter` as an argument and changes the view by applying a filter. #### TodosOverviewState Let's create `todos_overview/bloc/todos_overview_state.dart` and define the state. `TodosOverviewState` will keep track of a list of todos, the active filter, the `lastDeletedTodo`, and the status. :::note In addition to the default getters and setters, we have a custom getter called `filteredTodos`. The UI uses `BlocBuilder` to access either `state.filteredTodos` or `state.todos`. ::: #### TodosOverviewBloc Let's create `todos_overview/bloc/todos_overview_bloc.dart`. :::note The bloc does not create an instance of the `TodosRepository` internally. Instead, it relies on an instance of the repository to be injected via constructor. ::: ##### onSubscriptionRequested When `TodosOverviewSubscriptionRequested` is added, the bloc starts by emitting a `loading` state. In response, the UI can then render a loading indicator. Next, we use `emit.forEach>( ... )` which creates a subscription on the todos stream from the `TodosRepository`. :::caution `emit.forEach()` is not the same `forEach()` used by lists. This `forEach` enables the bloc to subscribe to a `Stream` and emit a new state for each update from the stream. ::: :::note `stream.listen` is never called directly in this tutorial. Using `await emit.forEach()` is a newer pattern for subscribing to a stream which allows the bloc to manage the subscription internally. ::: Now that the subscription is handled, we will handle the other events, like adding, modifying, and deleting todos. ##### onTodoSaved `_onTodoSaved` simply calls `_todosRepository.saveTodo(event.todo)`. :::note `emit` is never called from within `onTodoSaved` and many other event handlers. Instead, they notify the repository which emits an updated list via the todos stream. See the [data flow](#поток-данных) section for more information. ::: ##### Undo The undo feature allows users to restore the last deleted item. `_onTodoDeleted` does two things. First, it emits a new state with the `Todo` to be deleted. Then, it deletes the `Todo` via a call to the repository. `_onUndoDeletionRequested` runs when the undo deletion request event comes from the UI. `_onUndoDeletionRequested` does the following: - Temporarily saves a copy of the last deleted todo. - Updates the state by removing the `lastDeletedTodo`. - Reverts the deletion. ##### Filtering `_onFilterChanged` emits a new state with the new event filter. #### Models There is one model file that deals with the view filtering. `todos_view_filter.dart` is an enum that represents the three view filters and the methods to apply the filter. `models.dart` is the barrel file for exports. Next, let's take a look at the `TodosOverviewPage`. #### TodosOverviewPage A simplified representation of the widget tree for the `TodosOverviewPage` is: Just as with the `Home` feature, the `TodosOverviewPage` provides an instance of the `TodosOverviewBloc` to the subtree via `BlocProvider`. This scopes the `TodosOverviewBloc` to just the widgets below `TodosOverviewPage`. There are three widgets that are listening for changes in the `TodosOverviewBloc`. 1. The first is a `BlocListener` that listens for errors. The `listener` will only be called when `listenWhen` returns `true`. If the status is `TodosOverviewStatus.failure`, a `SnackBar` is displayed. 2. We created a second `BlocListener` that listens for deletions. When a todo has been deleted, a `SnackBar` is displayed with an undo button. If the user taps undo, the `TodosOverviewUndoDeletionRequested` event will be added to the bloc. 3. Finally, we use a `BlocBuilder` to builds the ListView that displays the todos. The `AppBar`contains two actions which are dropdowns for filtering and manipulating the todos. :::note `TodosOverviewTodoCompletionToggled` and `TodosOverviewTodoDeleted` are added to the bloc via `context.read`. ::: `view.dart` is the barrel file that exports `todos_overview_page.dart`. #### Виджеты `widgets.dart` is another barrel file that exports all the components used within the `todos_overview` feature. `todo_list_tile.dart` is the `ListTile` for each todo item. `todos_overview_options_button.dart` exposes two options for manipulating todos: - `toggleAll` - `clearCompleted` `todos_overview_filter_button.dart` exposes three filter options: - `all` - `activeOnly` - `completedOnly` ### Статистика The stats feature displays statistics about the active and completed todos. #### СтатистикаState `StatsState` keeps track of summary information and the current `StatsStatus`. #### СтатистикаEvent `StatsEvent` has only one event called `StatsSubscriptionRequested`: #### СтатистикаBloc `StatsBloc` depends on the `TodosRepository` just like `TodosOverviewBloc`. It subscribes to the todos stream via `_todosRepository.getTodos`. #### Статистика View `view.dart` is the barrel file for the `stats_page`. `stats_page.dart` contains the UI for the page that displays the todos statistics. A simplified representation of the widget tree for the `StatsPage` is: :::caution The `TodosOverviewBloc` and `StatsBloc` both communicate with the `TodosRepository`, but it is important to note there is no direct communication between the blocs. See the [data flow](#поток-данных) section for more information. ::: ### EditTodo The `EditTodo` feature allows users to edit an existing todo item and save the changes. #### EditTodoState `EditTodoState` keeps track of the information needed when editing a todo. #### EditTodoEvent The different events the bloc will react to are: - `EditTodoTitleChanged` - `EditTodoDescriptionChanged` - `EditTodoSubmitted` #### EditTodoBloc `EditTodoBloc` depends on the `TodosRepository`, just like `TodosOverviewBloc` and `StatsBloc`. :::caution Unlike the other Blocs, `EditTodoBloc` does not subscribe to `_todosRepository.getTodos`. It is a "write-only" bloc meaning it doesn't need to read any information from the repository. ::: ##### Поток данных Even though there are many features that depend on the same list of todos, there is no bloc-to-bloc communication. Instead, all features are independent of each other and rely on the `TodosRepository` to listen for changes in the list of todos, as well as perform updates to the list. For example, the `EditTodos` doesn't know anything about the `TodosOverview` or `Stats` features. When the UI submits a `EditTodoSubmitted` event: - `EditTodoBloc` handles the business logic to update the `TodosRepository`. - `TodosRepository` notifies `TodosOverviewBloc` and `StatsBloc`. - `TodosOverviewBloc` and `StatsBloc` notify the UI which update with the new state. #### EditTodoPage Just like with the previous features, the `EditTodosPage` provides an instance of the `EditTodosBloc` via `BlocProvider`. Unlike the other features, the `EditTodosPage` is a separate route which is why it exposes a `static` `route` method. This makes it easy to push the `EditTodosPage` onto the navigation stack via `Navigator.of(context).push(...)`. A simplified representation of the widget tree for the `EditTodosPage` is: ## Резюме Вот и все, мы завершили руководство! 🎉 Полный исходный код этого примера, включая unit и widget тесты, можно найти [here](https://github.com/felangel/bloc/tree/master/examples/flutter_todos). ================================================ FILE: docs/src/content/docs/ru/tutorials/flutter-weather.mdx ================================================ --- title: Погода Flutter description: Подробное руководство по созданию приложения погоды Flutter с использованием bloc. sidebar: order: 5 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-weather/FlutterCreateSnippet.astro'; import FeatureTreeSnippet from '~/components/tutorials/flutter-weather/FeatureTreeSnippet.astro'; import FlutterCreateApiClientSnippet from '~/components/tutorials/flutter-weather/FlutterCreateApiClientSnippet.astro'; import OpenMeteoModelsTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoModelsTreeSnippet.astro'; import LocationJsonSnippet from '~/components/tutorials/flutter-weather/LocationJsonSnippet.astro'; import LocationDartSnippet from '~/components/tutorials/flutter-weather/LocationDartSnippet.astro'; import WeatherJsonSnippet from '~/components/tutorials/flutter-weather/WeatherJsonSnippet.astro'; import WeatherDartSnippet from '~/components/tutorials/flutter-weather/WeatherDartSnippet.astro'; import OpenMeteoModelsBarrelTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoModelsBarrelTreeSnippet.astro'; import OpenMeteoLibrarySnippet from '~/components/tutorials/flutter-weather/OpenMeteoLibrarySnippet.astro'; import BuildRunnerBuildSnippet from '~/components/tutorials/flutter-weather/BuildRunnerBuildSnippet.astro'; import OpenMeteoApiClientTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoApiClientTreeSnippet.astro'; import LocationSearchMethodSnippet from '~/components/tutorials/flutter-weather/LocationSearchMethodSnippet.astro'; import GetWeatherMethodSnippet from '~/components/tutorials/flutter-weather/GetWeatherMethodSnippet.astro'; import FlutterTestCoverageSnippet from '~/components/tutorials/flutter-weather/FlutterTestCoverageSnippet.astro'; import FlutterCreateRepositorySnippet from '~/components/tutorials/flutter-weather/FlutterCreateRepositorySnippet.astro'; import RepositoryModelsBarrelTreeSnippet from '~/components/tutorials/flutter-weather/RepositoryModelsBarrelTreeSnippet.astro'; import WeatherRepositoryLibrarySnippet from '~/components/tutorials/flutter-weather/WeatherRepositoryLibrarySnippet.astro'; import WeatherCubitTreeSnippet from '~/components/tutorials/flutter-weather/WeatherCubitTreeSnippet.astro'; import WeatherBarrelDartSnippet from '~/components/tutorials/flutter-weather/WeatherBarrelDartSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) В этом руководстве мы собираемся создать приложение погоды на Flutter, которое демонстрирует, как управлять несколькими cubits для реализации динамического оформления, обновления по pull-to-refresh и многого другого. Наше приложение погоды будет получать актуальные данные о погоде из публичного API OpenMeteo и демонстрировать, как разделить наше приложение на слои (данные, repository, бизнес-логика и представление). ![demo](~/assets/tutorials/flutter-weather.gif) ## Требования к проекту Наше приложение должно позволять пользователям - Искать город на выделенной странице поиска - Видеть приятное отображение данных о погоде, возвращаемых [Open Meteo API](https://open-meteo.com) - Изменять отображаемые единицы (метрические против имперских) Дополнительно, - Тема приложения должна отражать погоду для выбранного города - Состояние приложения должно сохраняться между сеансами: т.е. приложение должно запоминать свое состояние после закрытия и повторного открытия (используя [HydratedBloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc)) ## Ключевые концепции - Наблюдение за изменениями состояния с помощью [BlocObserver](/ru/bloc-concepts#blocobserver). - [BlocProvider](/ru/flutter-bloc-concepts#blocprovider), виджет Flutter который предоставляет bloc своим дочерним элементам. - [BlocBuilder](/ru/flutter-bloc-concepts#blocbuilder), виджет Flutter который обрабатывает построение виджета в ответ на новые состояния. - Предотвращение ненужных перестроек с помощью [Equatable](/ru/faqs/#когда-использовать-equatable). - [RepositoryProvider](/ru/flutter-bloc-concepts#repositoryprovider), виджет Flutter который предоставляет repository своим дочерним элементам. - [BlocListener](/ru/flutter-bloc-concepts#bloclistener), виджет Flutter который вызывает код слушателя в ответ на изменения состояния в bloc. - [MultiBlocProvider](/ru/flutter-bloc-concepts#multiblocprovider), виджет Flutter, который объединяет несколько виджетов BlocProvider в один - [BlocConsumer](/ru/flutter-bloc-concepts#blocconsumer), виджет Flutter, который предоставляет builder и listener для реагирования на новые состояния - [HydratedBloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) для управления и сохранения состояния ## Настройка Для начала создадим новый flutter проект ### Структура проекта Наше приложение будет состоять из изолированных функций в соответствующих каталогах. Это позволяет нам масштабироваться по мере увеличения количества функций и позволяет разработчикам работать над различными функциями параллельно. Наше приложение можно разбить на четыре основные функции: **search, settings, theme, weather**. Создадим эти каталоги. ### Архитектура Следуя руководствам [архитектуры bloc](/ru/architecture), наше приложение будет состоять из нескольких слоев. В этом руководстве вот что будут делать эти слои: - **Data**: извлечение необработанных данных о погоде из API - **Repository**: абстрагирование слоя данных и предоставление доменных моделей для потребления приложением - **Business Logic**: управление состоянием каждой функции (информация о единицах, детали города, темы и т.д.) - **Presentation**: отображение информации о погоде и сбор ввода от пользователей (страница настроек, страница поиска и т.д.) ## Слой данных Для этого приложения мы будем обращаться к [Open Meteo API](https://open-meteo.com). Мы сосредоточимся на двух конечных точках: - `https://geocoding-api.open-meteo.com/v1/search?name=$city&count=1` для получения местоположения для данного названия города - `https://api.open-meteo.com/v1/forecast?latitude=$latitude&longitude=$longitude¤t_weather=true` для получения погоды для данного местоположения Откройте [https://geocoding-api.open-meteo.com/v1/search?name=chicago&count=1](https://geocoding-api.open-meteo.com/v1/search?name=chicago&count=1) в своем браузере, чтобы увидеть ответ для города Чикаго. Мы будем использовать `latitude` и `longitude` в ответе для обращения к конечной точке погоды. `latitude`/`longitude` для Чикаго равны `41.85003`/`-87.65005`. Перейдите к [https://api.open-meteo.com/v1/forecast?latitude=43.0389&longitude=-87.90647¤t_weather=true](https://api.open-meteo.com/v1/forecast?latitude=43.0389&longitude=-87.90647¤t_weather=true) в своем браузере, и вы увидите ответ для погоды в Чикаго, который содержит все данные, которые нам понадобятся для нашего приложения. ### OpenMeteo API Client OpenMeteo API Client не зависит от нашего приложения. В результате мы создадим его как внутренний пакет (и даже можем опубликовать его на [pub.dev](https://pub.dev)). Затем мы можем использовать пакет, добавив его в `pubspec.yaml` для слоя repository, который будет обрабатывать запросы данных для нашего основного приложения погоды. Создайте новый каталог на уровне проекта под названием `packages`. Этот каталог будет хранить все наши внутренние пакеты. В этом каталоге запустите встроенную команду `flutter create` для создания нового пакета под названием `open_meteo_api` для нашего API client. ### Weather Data Model Далее, создадим `location.dart` и `weather.dart`, которые будут содержать модели для ответов конечных точек API `location` и `weather`. #### Location Model Модель `location.dart` должна хранить данные, возвращаемые API местоположения, которые выглядят следующим образом: Вот незавершенный файл `location.dart`, который хранит вышеуказанный ответ: #### Weather Model Далее, поработаем над `weather.dart`. Наша модель погоды должна хранить данные, возвращаемые API погоды, которые выглядят следующим образом: Вот незавершенный файл `weather.dart`, который хранит вышеуказанный ответ: ### Barrel Files Пока мы здесь, быстро создадим [barrel файл](https://adrianfaciu.dev/posts/barrel-files/) для очистки некоторых из наших импортов в дальнейшем. Создайте barrel файл `models.dart` и экспортируйте две модели: Также создадим barrel файл на уровне пакета, `open_meteo_api.dart` В верхнем уровне, `open_meteo_api.dart` экспортируем модели: ### Настройка Нам нужно иметь возможность [сериализовать и десериализовать](https://en.wikipedia.org/wiki/Serialization) наши модели, чтобы работать с данными API. Для этого мы добавим методы `toJson` и `fromJson` в наши модели. Дополнительно, нам нужен способ [делать HTTP сетевые запросы](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) для получения данных из API. К счастью, есть ряд популярных пакетов для этого. Мы будем использовать пакеты [json_annotation](https://pub.dev/packages/json_annotation), [json_serializable](https://pub.dev/packages/json_serializable) и [build_runner](https://pub.dev/packages/build_runner) для генерации реализаций `toJson` и `fromJson` для нас. На более позднем этапе мы также будем использовать пакет [http](https://pub.dev/packages/http) для отправки сетевых запросов к API погоды, чтобы наше приложение могло отображать текущие данные о погоде. Добавим эти зависимости в `pubspec.yaml`. :::note Не забудьте запустить `flutter pub get` после добавления зависимостей. ::: ### (Де)Сериализация Чтобы генерация кода работала, нам нужно аннотировать наш код, используя следующее: - `@JsonSerializable` для пометки классов, которые могут быть сериализованы - `@JsonKey` для предоставления строковых представлений имен полей - `@JsonValue` для предоставления строковых представлений значений полей - Реализовать `JSONConverter` для преобразования объектных представлений в JSON представления Для каждого файла нам также нужно: - Импортировать `json_annotation` - Включить сгенерированный код, используя ключевое слово [part](https://dart.dev/tools/pub/create-packages#organizing-a-package) - Включить методы `fromJson` для десериализации #### Location Model Вот наш полный файл модели `location.dart`: #### Weather Model Вот наш полный файл модели `weather.dart`: #### Create Build File В папке `open_meteo_api` создайте файл `build.yaml`. Цель этого файла - обработка несоответствий между соглашениями именования в именах полей `json_serializable`. #### Генерация кода Используем `build_runner` для генерации кода. `build_runner` должен сгенерировать файлы `location.g.dart` и `weather.g.dart`. ### OpenMeteo API Client Создадим наш API client в `open_meteo_api_client.dart` в каталоге `src`. Структура нашего проекта теперь должна выглядеть так: Теперь мы можем использовать пакет [http](https://pub.dev/packages/http), который мы добавили ранее в файл `pubspec.yaml`, чтобы делать HTTP запросы к API погоды и использовать эту информацию в нашем приложении. Наш API client будет предоставлять два метода: - `locationSearch`, который возвращает `Future` - `getWeather`, который возвращает `Future` #### Location Search Метод `locationSearch` обращается к API местоположения и выбрасывает ошибки `LocationRequestFailure` по мере необходимости. Завершенный метод выглядит так: #### Get Weather Аналогично, метод `getWeather` обращается к API погоды и выбрасывает ошибки `WeatherRequestFailure` по мере необходимости. Завершенный метод выглядит так: Завершенный файл выглядит так: #### Обновления Barrel File Завершим этот пакет, добавив наш API client в barrel файл. ### Unit Tests Особенно важно писать unit тесты для слоя данных, поскольку это основа нашего приложения. Unit тесты дадут нам уверенность, что пакет ведет себя, как ожидается. #### Настройка Ранее мы добавили пакет [test](https://pub.dev/packages/test) в наш pubspec.yaml, который позволяет легко писать unit тесты. Мы создадим тестовый файл для api client, а также для двух моделей. #### Location Tests #### Weather Tests #### API Client Tests Далее, протестируем наш API client. Мы должны убедиться, что наш API client обрабатывает оба вызова API правильно, включая граничные случаи. :::note Мы не хотим, чтобы наши тесты делали реальные вызовы API, поскольку наша цель - протестировать логику API client (включая все граничные случаи), а не сам API. Чтобы иметь согласованную, контролируемую тестовую среду, мы будем использовать [mocktail](https://github.com/felangel/mocktail) (который мы добавили в файл pubspec.yaml ранее) для мока http client. ::: #### Покрытие тестами Наконец, соберем покрытие тестами, чтобы убедиться, что мы покрыли каждую строку кода хотя бы одним тестовым случаем. ## Слой Repository Цель нашего слоя repository - абстрагировать наш слой данных и облегчить коммуникацию со слоем bloc. Делая это, остальная часть нашей кодовой базы зависит только от функций, предоставляемых нашим слоем repository, вместо конкретных реализаций провайдеров данных. Это позволяет нам изменять провайдеры данных без нарушения какого-либо кода на уровне приложения. Например, если мы решим мигрировать от этого конкретного API погоды, мы должны быть в состоянии создать новый API client и заменить его без необходимости вносить изменения в публичный API слоев repository или приложения. ### Настройка Внутри каталога packages запустите следующую команду: Мы будем использовать те же пакеты, что и в пакете `open_meteo_api`, включая пакет `open_meteo_api` из последнего шага. Обновите свой `pubspec.yaml` и запустите `flutter pub get`. :::note Мы используем `path` для указания местоположения `open_meteo_api`, что позволяет нам рассматривать его так же, как внешний пакет из `pub.dev`. ::: ### Weather Repository Models Мы создадим новый файл `weather.dart` для предоставления доменной модели погоды. Эта модель будет содержать только данные, относящиеся к нашим бизнес-случаям -- другими словами, она должна быть полностью отделена от API client и формата необработанных данных. Как обычно, мы также создадим barrel файл `models.dart`. На этот раз наша модель погоды будет хранить только свойства `location, temperature, condition`. Мы также продолжим аннотировать наш код, чтобы разрешить сериализацию и десериализацию. Обновите barrel файл, который мы создали ранее, чтобы включить модели. #### Create Build File Как и раньше, нам нужно создать файл `build.yaml` со следующим содержимым: #### Генерация кода Как мы делали ранее, запустите следующую команду для генерации реализации (де)сериализации. #### Barrel File Также создадим barrel файл на уровне пакета с именем `packages/weather_repository/lib/weather_repository.dart` для экспорта наших моделей: ### Weather Repository Основная цель `WeatherRepository` - предоставить интерфейс, который абстрагирует провайдер данных. В данном случае `WeatherRepository` будет иметь зависимость от `WeatherApiClient` и предоставлять один публичный метод, `getWeather(String city)`. :::note Потребители `WeatherRepository` не знают о детайлях реализации, таких как тот факт, что два сетевых запроса выполняются к API погоды. Цель `WeatherRepository` - отделить "что" от "как" -- другими словами, мы хотим иметь способ получить погоду для данного города, но не заботимся о том, как или откуда эти данные приходят. ::: #### Настройка Создадим файл `weather_repository.dart` в каталоге `src` нашего пакета и поработаем над реализацией repository. Основной метод, на котором мы сосредоточимся, это `getWeather(String city)`. Мы можем реализовать его, используя два вызова API client следующим образом: #### Barrel File Обновите barrel файл, который мы создали ранее. ### Unit Tests Так же, как и со слоем данных, критически важно тестировать слой repository, чтобы убедиться, что логика на уровне домена корректна. Для тестирования нашего `WeatherRepository` мы будем использовать библиотеку [mocktail](https://github.com/felangel/mocktail). Мы замокаем базовый api client, чтобы протестировать логику `WeatherRepository` в изолированной, контролируемой среде. ## Слой бизнес-логики В слое бизнес-логики мы будем использовать доменную модель погоды из `WeatherRepository` и предоставлять модель на уровне функции, которая будет представлена пользователю через UI. :::note Это третья различная модель погоды, которую мы реализуем. В API client наша модель погоды содержала всю информацию, возвращаемую API. В слое repository наша модель погоды содержала только абстрагированную модель на основе нашего бизнес-случая. На этом слое наша модель погоды будет содержать соответствующую информацию, необходимую специально для текущего набора функций. ::: ### Настройка Поскольку наш слой бизнес-логики находится в нашем основном приложении, нам нужно отредактировать `pubspec.yaml` для всего проекта `flutter_weather` и включить все пакеты, которые мы будем использовать. - Использование [equatable](https://pub.dev/packages/equatable) позволяет экземплярам класса состояния нашего приложения сравниваться с помощью оператора equals `==`. Под капотом bloc будет сравнивать наши состояния, чтобы увидеть, равны ли они, и если они не равны, это вызовет перестроение. Это гарантирует, что наше дерево виджетов будет перестраиваться только когда это необходимо, чтобы поддерживать производительность быстрой и отзывчивой. - Мы можем приукрасить наш пользовательский интерфейс с помощью [google_fonts](https://pub.dev/packages/google_fonts). - [HydratedBloc](https://pub.dev/packages/hydrated_bloc) позволяет нам сохранять состояние приложения, когда приложение закрывается и снова открывается. - Мы включим пакет `weather_repository`, который мы только что создали, чтобы позволить нам получать текущие данные о погоде! Для тестирования мы захотим включить обычный пакет `test`, вместе с `mocktail` для мока зависимостей и [bloc_test](https://pub.dev/packages/bloc_test), чтобы обеспечить легкое тестирование единиц бизнес-логики или blocs! Далее, мы будем работать над слоем приложения внутри каталога функции `weather`. ### Weather Model Цель нашей модели погоды - отслеживать данные о погоде, отображаемые нашим приложением, а также настройки температуры (Celsius или Fahrenheit). Создайте `flutter_weather/lib/weather/models/weather.dart`: ### Create Build File Создайте файл `build.yaml` для слоя бизнес-логики. ### Генерация кода Запустите `build_runner` для генерации реализаций (де)сериализации. ### Barrel File Экспортируем наши модели из barrel файла (`flutter_weather/lib/weather/models/models.dart`): Затем создадим верхнеуровневый barrel файл weather (`flutter_weather/lib/weather/weather.dart`); ### Weather Мы будем использовать `HydratedCubit` чтобы позволить нашему приложению запоминать его состояние приложения, даже после того, как оно закрыто и снова открыто. :::note `HydratedCubit` - это расширение `Cubit`, которое обрабатывает сохранение и восстановление состояния между сеансами. ::: #### Weather State Используя [Bloc VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) или [Bloc IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) расширение, щелкните правой кнопкой мыши на каталоге `weather` и создайте новый cubit с именем `Weather`. Структура проекта должна выглядеть так: Есть четыре состояния, в которых может находиться наше приложение погоды: - `initial` до того, как что-либо загрузится - `loading` во время вызова API - `success` если вызов API успешен - `failure` если вызов API неуспешен Перечисление `WeatherStatus` будет представлять вышеуказанное. Полное состояние погоды должно выглядеть так: #### Weather Cubit Теперь, когда мы определили `WeatherState`, напишем `WeatherCubit`, который предоставляет следующие методы: - `fetchWeather(String? city)` использует наш weather repository, чтобы попытаться получить объект погоды для данного города - `refreshWeather()` извлекает новый объект погоды, используя weather repository при данном текущем состоянии погоды - `toggleUnits()` переключает состояние между Celsius и Fahrenheit - `fromJson(Map json)`, `toJson(WeatherState state)` используются для сохранения :::note Не забудьте сгенерировать код (де)сериализации с помощью: ::: ### Unit Tests Подобно слоям данных и repository, критически важно протестировать слой бизнес-логики, чтобы убедиться, что логика на уровне функции ведет себя так, как мы ожидаем. Мы будем полагаться на [bloc_test](https://pub.dev/packages/bloc_test) в дополнение к `mocktail` и `test`. Добавим пакеты `test`, `bloc_test` и `mocktail` в `dev_dependencies`. :::note Пакет [bloc_test](https://pub.dev/packages/bloc_test) позволяет нам легко подготавливать наши blocs для тестирования, обрабатывать изменения состояния и проверять результаты в согласованном порядке. ::: #### Weather Cubit Tests ## Слой представления ### Weather Page Начнем с `WeatherPage`, который использует `BlocProvider` для предоставления экземпляра `WeatherCubit` дереву виджетов. Вы заметите, что страница зависит от виджетов `SettingsPage` и `SearchPage`, которые мы создадим далее. ### SettingsPage Страница настроек позволяет пользователям обновлять свои предпочтения для единиц температуры. ### SearchPage Страница поиска позволяет пользователям ввести название желаемого города и предоставляет результат поиска предыдущему маршруту через `Navigator.of(context).pop`. ### Weather Widgets Приложение будет отображать разные экраны в зависимости от четырех возможных состояний `WeatherCubit`. #### WeatherEmpty Этот экран будет показан, когда нет данных для отображения, потому что пользователь еще не выбрал город. #### WeatherError Этот экран будет отображаться, если есть ошибка. #### WeatherLoading Этот экран будет отображаться, пока приложение получает данные. #### WeatherPopulated Этот экран будет отображаться после того, как пользователь выбрал город, и мы получили данные. ### Barrel File Добавим эти состояния в barrel файл для очистки наших импортов. ### Entrypoint Наш `main.dart` файл должен инициализировать наш `WeatherApp` и `BlocObserver` (для целей отладки), а также настроить наш `HydratedStorage` для сохранения состояния между сеансами. Наш виджет `app.dart` будет обрабатывать построение представления `WeatherPage`, которое мы ранее создали, и использовать `BlocProvider` для внедрения нашего `WeatherCubit`. ### Widget Tests Библиотека [`bloc_test`](https://pub.dev/packages/bloc_test) также предоставляет `MockBlocs` и `MockCubits`, которые упрощают тестирование UI. Мы можем мокать состояния различных cubits и убедиться, что UI реагирует правильно. :::note Мы используем `MockWeatherCubit` вместе с API `when` из `mocktail`, чтобы заглушить состояние cubit в каждом из тестовых случаев. Это позволяет нам симулировать все состояния и проверять, что UI ведет себя правильно при всех обстоятельствах. ::: ## Резюме Вот и все, мы завершили руководство! 🎉 Мы можем запустить финальное приложение, используя команду `flutter run`. Полный исходный код этого примера, включая unit и widget тесты, можно найти [здесь](https://github.com/felangel/bloc/tree/master/examples/flutter_weather). ================================================ FILE: docs/src/content/docs/ru/tutorials/github-search.mdx ================================================ --- title: Поиск GitHub description: Подробное руководство по созданию приложения поиска GitHub во Flutter и AngularDart с использованием bloc. sidebar: order: 9 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import SetupSnippet from '~/components/tutorials/github-search/SetupSnippet.astro'; import DartPubGetSnippet from '~/components/tutorials/github-search/DartPubGetSnippet.astro'; import FlutterCreateSnippet from '~/components/tutorials/github-search/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; import StagehandSnippet from '~/components/tutorials/github-search/StagehandSnippet.astro'; import ActivateStagehandSnippet from '~/components/tutorials/github-search/ActivateStagehandSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) В следующем руководстве мы собираемся создать приложение поиска GitHub во Flutter и AngularDart, чтобы продемонстрировать, как мы можем делиться данными и слоями бизнес-логики между двумя проектами. ![demo](~/assets/tutorials/flutter-github-search.gif) ![demo](~/assets/tutorials/ngdart-github-search.gif) ## Ключевые темы - [BlocProvider](/ru/flutter-bloc-concepts#blocprovider), Flutter widget which provides a bloc to its children. - [BlocBuilder](/ru/flutter-bloc-concepts#blocbuilder), Flutter widget that handles building the widget in response to new states. - Using Cubit instead of Bloc. [What's the difference?](/ru/bloc-concepts/#cubit-против-bloc) - Prevent unnecessary rebuilds with [Equatable](/ru/faqs/#когда-использовать-equatable). - Use a custom `EventTransformer` with [`bloc_concurrency`](https://pub.dev/packages/bloc_concurrency). - Making network requests using the `http` package. ## Общая библиотека поиска GitHub The Common GitHub Search library will contain models, the data provider, the repository, as well as the bloc that will be shared between AngularDart and Flutter. ### Настройка We'll start off by creating a new directory for our application. :::note The `common_github_search` directory will contain the shared library. ::: We need to create a `pubspec.yaml` with the required dependencies. Lastly, we need to install our dependencies. That's it for the project setup! Now we can get to work on building out the `common_github_search` package. ### Github Client The `GithubClient` which will be providing raw data from the [GitHub API](https://developer.github.com/v3/). :::note You can see a sample of what the data we get back will look like [here](https://api.github.com/search/repositories?q=dartlang). ::: Let's create `github_client.dart`. :::note Our `GithubClient` is simply making a network request to Github's Repository Search API and converting the result into either a `SearchResult` or `SearchResultError` as a `Future`. ::: :::note The `GithubClient` implementation depends on `SearchResult.fromJson`, which we have not yet implemented. ::: Next we need to define our `SearchResult` and `SearchResultError` models. #### Модель результата поиска Create `search_result.dart`, which represents a list of `SearchResultItems` based on the user's query: :::note The `SearchResult` implementation depends on `SearchResultItem.fromJson`, which we have not yet implemented. ::: :::note We aren't including properties that aren't going to be used in our model. ::: #### Модель элемента результата поиска Next, we'll create `search_result_item.dart`. :::note Again, the `SearchResultItem` implementation dependes on `GithubUser.fromJson`, which we have not yet implemented. ::: #### Модель пользователя GitHub Next, we'll create `github_user.dart`. At this point, we have finished implementing `SearchResult` and its dependencies. Now we'll move onto `SearchResultError`. #### Модель ошибки результата поиска Create `search_result_error.dart`. Our `GithubClient` is finished so next we'll move onto the `GithubCache`, which will be responsible for [memoizing](https://en.wikipedia.org/wiki/Memoization) as a performance optimization. ### GitHub Cache Our `GithubCache` will be responsible for remembering all past queries so that we can avoid making unnecessary network requests to the GitHub API. This will also help improve our application's performance. Create `github_cache.dart`. Now we're ready to create our `GithubRepository`! ### GitHub Repository The Github Repository is responsible for creating an abstraction between the data layer (`GithubClient`) and the Business Logic Layer (`Bloc`). This is also where we're going to put our `GithubCache` to use. Create `github_repository.dart`. :::note The `GithubRepository` has a dependency on the `GithubCache` and the `GithubClient` and abstracts the underlying implementation. Our application never has to know about how the data is being retrieved or where it's coming from since it shouldn't care. We can change how the repository works at any time and as long as we don't change the interface we shouldn't need to change any client code. ::: At this point, we've completed the data provider layer and the repository layer so we're ready to move on to the business logic layer. ### GitHub Search Event Our Bloc will be notified when a user has typed the name of a repository which we will represent as a `TextChanged` `GithubSearchEvent`. Create `github_search_event.dart`. :::note We extend [`Equatable`](https://pub.dev/packages/equatable) so that we can compare instances of `GithubSearchEvent`. By default, the equality operator returns true if and only if this and other are the same instance. ::: ### Github Search State Our presentation layer will need to have several pieces of information in order to properly lay itself out: - `SearchStateEmpty`- will tell the presentation layer that no input has been given by the user. - `SearchStateLoading`- will tell the presentation layer it has to display some sort of loading indicator. - `SearchStateSuccess`- will tell the presentation layer that it has data to present. - `items`- will be the `List` which will be displayed. - `SearchStateError`- will tell the presentation layer that an error has occurred while fetching repositories. - `error`- will be the exact error that occurred. We can now create `github_search_state.dart` and implement it like so. :::note We extend [`Equatable`](https://pub.dev/packages/equatable) so that we can compare instances of `GithubSearchState`. By default, the equality operator returns true if and only if this and other are the same instance. ::: Now that we have our Events and States implemented, we can create our `GithubSearchBloc`. ### GitHub Search Bloc Create `github_search_bloc.dart`: :::note Our `GithubSearchBloc` converts `GithubSearchEvent` to `GithubSearchState` and has a dependency on the `GithubRepository`. ::: :::note We create a custom `EventTransformer` to [debounce](https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/debounce.html) the `GithubSearchEvents`. One of the reasons why we created a `Bloc` instead of a `Cubit` was to take advantage of stream transformers. ::: Awesome! We're all done with our `common_github_search` package. The finished product should look like [this](https://github.com/felangel/bloc/tree/master/examples/github_search/common_github_search). Next, we'll work on the Flutter implementation. ## Flutter GitHub Search Flutter Github Search will be a Flutter application which reuses the models, data providers, repositories, and blocs from `common_github_search` to implement Github Search. ### Настройка We need to start by creating a new Flutter project in our `github_search` directory at the same level as `common_github_search`. Next, we need to update our `pubspec.yaml` to include all the necessary dependencies. :::note We are including our newly created `common_github_search` library as a dependency. ::: Now, we need to install the dependencies. That's it for project setup. Since the `common_github_search` package contains our data layer as well as our business logic layer, all we need to build is the presentation layer. ### Форма поиска We're going to need to create a form with a `_SearchBar` and `_SearchBody` widget. - `_SearchBar` will be responsible for taking user input. - `_SearchBody` will be responsible for displaying search results, loading indicators, and errors. Let's create `search_form.dart`. Our `SearchForm` will be a `StatelessWidget` which renders the `_SearchBar` and `_SearchBody` widgets. `_SearchBar` is also going to be a `StatefulWidget` because it will need to maintain its own `TextEditingController` so that we can keep track of what a user has entered as input. `_SearchBody` is a `StatelessWidget` which will be responsible for displaying search results, errors, and loading indicators. It will be the consumer of the `GithubSearchBloc`. If our state is `SearchStateSuccess`, we render `_SearchResults` which we will implement next. `_SearchResults` is a `StatelessWidget` which takes a `List` and displays them as a list of `_SearchResultItems`. `_SearchResultItem` is a `StatelessWidget` and is responsible for rendering the information for a single search result. It is also responsible for handling user interaction and navigating to the repository url on a user tap. :::note `_SearchBar` accesses `GitHubSearchBloc` via `context.read()` and notifies the bloc of `TextChanged` events. ::: :::note `_SearchBody` uses `BlocBuilder` in order to rebuild in response to state changes. Since the bloc parameter of the `BlocBuilder` object was omitted, `BlocBuilder` will automatically perform a lookup using `BlocProvider` and the current `BuildContext`. Read more [here.](/ru/flutter-bloc-concepts#blocbuilder) ::: :::note We use `ListView.builder` in order to construct a scrollable list of `_SearchResultItem`. ::: :::note We use the [url_launcher](https://pub.dev/packages/url_launcher) package to open external urls. ::: ### Собираем все вместе Now all that's left to do is implement our main app in `main.dart`. :::note Our `GithubRepository` is created in `main` and injected into our `App`. Our `SearchForm` is wrapped in a `BlocProvider` which is responsible for initializing, closing, and making the instance of `GithubSearchBloc` available to the `SearchForm` widget and its children. ::: That's all there is to it! We've now successfully implemented a GitHub search app in Flutter using the [bloc](https://pub.dev/packages/bloc) and [flutter_bloc](https://pub.dev/packages/flutter_bloc) packages and we've successfully separated our presentation layer from our business logic. Полный исходный код можно найти [here](https://github.com/felangel/bloc/tree/master/examples/github_search/flutter_github_search). Finally, we're going to build our AngularDart GitHub Search app. ## AngularDart GitHub Search AngularDart GitHub Search will be an AngularDart application which reuses the models, data providers, repositories, and blocs from `common_github_search` to implement Github Search. ### Настройка We need to start by creating a new AngularDart project in our github_search directory at the same level as `common_github_search`. :::note You can install `stagehand` via: ::: We can then go ahead and replace the contents of `pubspec.yaml` with: ### Форма поиска Just like in our Flutter app, we're going to need to create a `SearchForm` with a `SearchBar` and `SearchBody` component. Our `SearchForm` component will implement `OnInit` and `OnDestroy` because it will need to create and close a `GithubSearchBloc`. - `SearchBar` will be responsible for taking user input. - `SearchBody` will be responsible for displaying search results, loading indicators, and errors. Let's create `search_form_component.dart.` :::note The `GithubRepository` is injected into the `SearchFormComponent`. ::: :::note The `GithubSearchBloc` is created and closed by the `SearchFormComponent`. ::: Our template (`search_form_component.html`) will look like: Next, we'll implement the `SearchBar` component. ### Панель поиска `SearchBar` is a component which will be responsible for taking in user input and notifying the `GithubSearchBloc` of text changes. Create `search_bar_component.dart`. :::note `SearchBarComponent` has a dependency on `GitHubSearchBloc` because it is responsible for notifying the bloc of `TextChanged` events. ::: Next, we can create `search_bar_component.html`. We're done with `SearchBar`, now onto `SearchBody`. ### Тело поиска `SearchBody` is a component which will be responsible for displaying search results, errors, and loading indicators. It will be the consumer of the `GithubSearchBloc`. Create `search_body_component.dart`. :::note `SearchBodyComponent` has a dependency on `GithubSearchState` which is provided by the `GithubSearchBloc` using the `angular_bloc` bloc pipe. ::: Create `search_body_component.html`. If our state `isSuccess`, we render `SearchResults`. We will implement it next. ### Результаты поиска `SearchResults` is a component which takes a `List` and displays them as a list of `SearchResultItems`. Create `search_results_component.dart`. Next up we'll create `search_results_component.html`. :::note We use `ngFor` in order to construct a list of `SearchResultItem` components. ::: It's time to implement `SearchResultItem`. ### Элемент результата поиска `SearchResultItem` is a component that is responsible for rendering the information for a single search result. It is also responsible for handling user interaction and navigating to the repository url on a user tap. Create `search_result_item_component.dart`. and the corresponding template in `search_result_item_component.html`. ### Собираем все вместе We have all of our components and now it's time to put them all together in our `app_component.dart`. :::note We're creating the `GithubRepository` in the `AppComponent` and injecting it into the `SearchForm` component. ::: That's all there is to it! We've now successfully implemented a GitHub search app in AngularDart using the `bloc` and `angular_bloc` packages and we've successfully separated our presentation layer from our business logic. Полный исходный код можно найти [here](https://github.com/felangel/bloc/tree/master/examples/github_search/angular_github_search). ## Резюме В этом руководстве мы создали приложение Flutter и AngularDart, делясь всеми моделями, провайдерами данных и blocs между ними. The only thing we actually had to write twice was the presentation layer (UI) which is awesome in terms of efficiency and development speed. In addition, it's fairly common for web apps and mobile apps to have different user experiences and styles and this approach really demonstrates how easy it is to build two apps that look totally different but share the same data and business logic layers. Полный исходный код можно найти [here](https://github.com/felangel/bloc/tree/master/examples/github_search). ================================================ FILE: docs/src/content/docs/ru/tutorials/ngdart-counter.mdx ================================================ --- title: Счетчик AngularDart description: Подробное руководство по созданию приложения-счетчика AngularDart с использованием bloc. sidebar: order: 8 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import ActivateStagehandSnippet from '~/components/tutorials/ngdart-counter/ActivateStagehandSnippet.astro'; import StagehandSnippet from '~/components/tutorials/ngdart-counter/StagehandSnippet.astro'; import InstallDependenciesSnippet from '~/components/tutorials/ngdart-counter/InstallDependenciesSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) В следующем руководстве мы собираемся создать счетчик в AngularDart используя библиотеку Bloc. ![demo](~/assets/tutorials/ngdart-counter.gif) ## Настройка Начнем с создания нового проекта AngularDart с [stagehand](https://github.com/dart-lang/stagehand). Если у вас не установлен stagehand, активируйте его с помощью: Затем сгенерируйте новый проект с помощью: Затем можем заменить содержимое `pubspec.yaml` на: и затем установить все наши зависимости Наше приложение счетчика будет иметь только две кнопки для увеличения/уменьшения значения счетчика и элемент для отображения текущего значения. Начнем проектировать `CounterEvents`. ## Counter Bloc Поскольку состояние нашего счетчика может быть представлено целым числом, нам не нужно создавать пользовательский класс, и мы можем разместить события и bloc вместе. :::note Просто из объявления класса мы можем сказать, что наш `CounterBloc` будет принимать `CounterEvents` в качестве входных данных и выводить целые числа. ::: ## Counter App Теперь, когда у нас есть полностью реализованный `CounterBloc`, мы можем начать создавать наш компонент AngularDart App. Наш `app.component.dart` должен выглядеть так: и наш `app.component.html` должен выглядеть так: ## Counter Page Наконец, все, что осталось, это построить наш компонент Counter Page. Наш `counter_page_component.dart` должен выглядеть так: :::note Мы можем получить доступ к экземпляру `CounterBloc`, используя систему внедрения зависимостей AngularDart. Поскольку мы зарегистрировали его как `Provider`, AngularDart может правильно разрешить `CounterBloc`. ::: :::note Мы закрываем `CounterBloc` в `ngOnDestroy`. ::: :::note Мы импортируем `BlocPipe`, чтобы мы могли использовать его в нашем шаблоне. ::: Наконец, наш `counter_page_component.html` должен выглядеть так: :::note Мы используем `BlocPipe`, чтобы мы могли отображать состояние нашего `CounterBloc` по мере его обновления. ::: Вот и все! Мы отделили наш слой представления от нашего слоя бизнес-логики. Наш `CounterPageComponent` не знает, что происходит, когда пользователь нажимает кнопку; он просто добавляет событие, чтобы уведомить `CounterBloc`. Кроме того, наш `CounterBloc` не знает, что происходит с состоянием (значением счетчика); он просто преобразует `CounterEvents` в целые числа. Мы можем запустить наше приложение с помощью `webdev serve` и можем просмотреть его локально. Полный исходный код этого примера можно найти [здесь](https://github.com/felangel/bloc/tree/master/examples/angular_counter). ================================================ FILE: docs/src/content/docs/ru/why-bloc.mdx ================================================ --- title: Почему Bloc? description: Обзор того, что делает Bloc надежным решением для управления состоянием. sidebar: order: 1 --- Bloc упрощает отделение представления от бизнес-логики, делая ваш код _быстрым_, _легко тестируемым_ и _переиспользуемым_. При создании качественных приложений управление состоянием становится критически важным. Как разработчики мы хотим: - знать, в каком состоянии находится наше приложение в любой момент времени. - легко тестировать каждый случай, чтобы убедиться, что наше приложение реагирует надлежащим образом. - записывать каждое взаимодействие пользователя в нашем приложении, чтобы мы могли принимать решения на основе данных. - работать максимально эффективно и повторно использовать компоненты как внутри нашего приложения, так и в других приложениях. - иметь возможность многим разработчикам без проблем работать в одной кодовой базе, следуя одним и тем же шаблонам и соглашениям. - разрабатывать быстрые и отзывчивые приложения. Bloc был разработан для удовлетворения всех этих потребностей и многих других. Существует множество решений для управления состоянием, и выбор подходящего может стать сложной задачей. Не существует одного идеального решения для управления состоянием! Важно выбрать то, которое лучше всего подходит для вашей команды и вашего проекта. Bloc был разработан с учетом трех основных ценностей: - **Простой:** Легко понять и может использоваться разработчиками с разным уровнем навыков. - **Мощный:** Помогает создавать потрясающие, сложные приложения, составляя их из более мелких компонентов. - **Тестируемый:** Легко тестировать каждый аспект приложения, чтобы мы могли итерировать с уверенностью. В целом, Bloc пытается сделать изменения состояния предсказуемыми, регулируя, когда может произойти изменение состояния, и обеспечивая единый способ изменения состояния во всём приложении. ================================================ FILE: docs/src/content/docs/testing.mdx ================================================ --- title: Testing description: The basics of how to write tests for your blocs. --- import CounterBlocSnippet from '~/components/testing/CounterBlocSnippet.astro'; import AddDevDependenciesSnippet from '~/components/testing/AddDevDependenciesSnippet.astro'; import CounterBlocTestImportsSnippet from '~/components/testing/CounterBlocTestImportsSnippet.astro'; import CounterBlocTestMainSnippet from '~/components/testing/CounterBlocTestMainSnippet.astro'; import CounterBlocTestSetupSnippet from '~/components/testing/CounterBlocTestSetupSnippet.astro'; import CounterBlocTestInitialStateSnippet from '~/components/testing/CounterBlocTestInitialStateSnippet.astro'; import CounterBlocTestBlocTestSnippet from '~/components/testing/CounterBlocTestBlocTestSnippet.astro'; Bloc was designed to be extremely easy to test. In this section, we'll walk through how to unit test a bloc. For the sake of simplicity, let's write tests for the `CounterBloc` we created in [Core Concepts](/bloc-concepts). To recap, the `CounterBloc` implementation looks like: ## Setup Before we start writing our tests we're going to need to add a testing framework to our dependencies. We need to add [test](https://pub.dev/packages/test) and [bloc_test](https://pub.dev/packages/bloc_test) to our project. ## Testing Let's get started by creating the file for our `CounterBloc` Tests, `counter_bloc_test.dart` and importing the test package. Next, we need to create our `main` as well as our test group. :::note Groups are for organizing individual tests as well as for creating a context in which you can share a common `setUp` and `tearDown` across all of the individual tests. ::: Let's start by creating an instance of our `CounterBloc` which will be used across all of our tests. Now we can start writing our individual tests. :::note We can run all of our tests with the `dart test` command. ::: At this point we should have our first passing test! Now let's write a more complex test using the [bloc_test](https://pub.dev/packages/bloc_test) package. We should be able to run the tests and see that all are passing. That's all there is to it, testing should be a breeze and we should feel confident when making changes and refactoring our code. You can refer to the [Weather App](https://github.com/felangel/bloc/tree/master/examples/flutter_weather) for an example of a fully tested application. ================================================ FILE: docs/src/content/docs/tutorials/flutter-counter.mdx ================================================ --- title: Flutter Counter description: An in-depth guide on how to build a Flutter counter app with bloc. sidebar: order: 1 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-counter/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) In the following tutorial, we're going to build a Counter in Flutter using the Bloc library. ![demo](~/assets/tutorials/flutter-counter.gif) ## Key Topics - Observe state changes with [BlocObserver](/bloc-concepts#blocobserver). - [BlocProvider](/flutter-bloc-concepts#blocprovider), Flutter widget which provides a bloc to its children. - [BlocBuilder](/flutter-bloc-concepts#blocbuilder), Flutter widget that handles building the widget in response to new states. - Using Cubit instead of Bloc. [What's the difference?](/bloc-concepts#cubit-vs-bloc) - Adding events with [context.read](/flutter-bloc-concepts#contextread). ## Setup We'll start off by creating a brand new Flutter project We can then go ahead and replace the contents of `pubspec.yaml` with and then install all of our dependencies ## Project Structure ``` ├── lib │ ├── app.dart │ ├── counter │ │ ├── counter.dart │ │ ├── cubit │ │ │ └── counter_cubit.dart │ │ └── view │ │ ├── counter_page.dart │ │ ├── counter_view.dart │ │ └── view.dart │ ├── counter_observer.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` The application uses a feature-driven directory structure. This project structure enables us to scale the project by having self-contained features. In this example we will only have a single feature (the counter itself) but in more complex applications we can have hundreds of different features. ## BlocObserver The first thing we're going to take a look at is how to create a `BlocObserver` which will help us observe all state changes in the application. Let's create `lib/counter_observer.dart`: In this case, we're only overriding `onChange` to see all state changes that occur. :::note `onChange` works the same way for both `Bloc` and `Cubit` instances. ::: ## main.dart Next, let's replace the contents of `lib/main.dart` with: We're initializing the `CounterObserver` we just created and calling `runApp` with the `CounterApp` widget which we'll look at next. ## Counter App Let's create `lib/app.dart`: `CounterApp` will be a `MaterialApp` and is specifying the `home` as `CounterPage`. :::note We are extending `MaterialApp` because `CounterApp` _is_ a `MaterialApp`. In most cases, we're going to be creating `StatelessWidget` or `StatefulWidget` instances and composing widgets in `build` but in this case there are no widgets to compose so it's simpler to just extend `MaterialApp`. ::: Let's take a look at `CounterPage` next! ## Counter Page Let's create `lib/counter/view/counter_page.dart`: The `CounterPage` widget is responsible for creating a `CounterCubit` (which we will look at next) and providing it to the `CounterView`. :::note It's important to separate or decouple the creation of a `Cubit` from the consumption of a `Cubit` in order to have code that is much more testable and reusable. ::: ## Counter Cubit Let's create `lib/counter/cubit/counter_cubit.dart`: The `CounterCubit` class will expose two methods: - `increment`: adds 1 to the current state - `decrement`: subtracts 1 from the current state The type of state the `CounterCubit` is managing is just an `int` and the initial state is `0`. :::tip Use the [VSCode Extension](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) or [IntelliJ Plugin](https://plugins.jetbrains.com/plugin/12129-bloc) to create new cubits automatically. ::: Next, let's take a look at the `CounterView` which will be responsible for consuming the state and interacting with the `CounterCubit`. ## Counter View Let's create `lib/counter/view/counter_view.dart`: The `CounterView` is responsible for rendering the current count and rendering two FloatingActionButtons to increment/decrement the counter. A `BlocBuilder` is used to wrap the `Text` widget in order to update the text any time the `CounterCubit` state changes. In addition, `context.read()` is used to look-up the closest `CounterCubit` instance. :::note Only the `Text` widget is wrapped in a `BlocBuilder` because that is the only widget that needs to be rebuilt in response to state changes in the `CounterCubit`. Avoid unnecessarily wrapping widgets that don't need to be rebuilt when a state changes. ::: ## Barrel Create `lib/counter/view/view.dart`: Add `view.dart` to export all public facing parts of counter view. Let's create `lib/counter/counter.dart`: Add `counter.dart` to export all the public facing parts of the counter feature. That's it! We've separated the presentation layer from the business logic layer. The `CounterView` has no idea what happens when a user presses a button; it just notifies the `CounterCubit`. Furthermore, the `CounterCubit` has no idea what is happening with the state (counter value); it's simply emitting new states in response to the methods being called. We can run our app with `flutter run` and can view it on our device or simulator/emulator. The full source (including unit and widget tests) for this example can be found [here](https://github.com/felangel/Bloc/tree/master/examples/flutter_counter). ================================================ FILE: docs/src/content/docs/tutorials/flutter-firebase-login.mdx ================================================ --- title: Flutter Firebase Login description: An in-depth guide on how to build a Flutter login flow with bloc and Firebase. sidebar: order: 7 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-firebase-login/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) In the following tutorial, we're going to build a Firebase Login Flow in Flutter using the Bloc library. ![demo](~/assets/tutorials/flutter-firebase-login.gif) ## Key Topics - [BlocProvider](/flutter-bloc-concepts#blocprovider), a Flutter widget which provides a bloc to its children. - Using Cubit instead of Bloc. [What's the difference?](/bloc-concepts#cubit-vs-bloc) - Adding events with [context.read](/flutter-bloc-concepts#contextread). - Prevent unnecessary rebuilds with [Equatable](/faqs#when-to-use-equatable). - [RepositoryProvider](/flutter-bloc-concepts#repositoryprovider), a Flutter widget which provides a repository to its children. - [BlocListener](/flutter-bloc-concepts#bloclistener), a Flutter widget which invokes the listener code in response to state changes in the bloc. - Adding events with [context.read](/flutter-bloc-concepts#contextselect). ## Setup We'll start off by creating a brand new Flutter project. Just like in the [login tutorial](/tutorials/flutter-login), we're going to create internal packages to better layer our application architecture and maintain clear boundaries and to maximize both reusability as well as improve testability. In this case, the [firebase_auth](https://pub.dev/packages/firebase_auth) and [google_sign_in](https://pub.dev/packages/google_sign_in) packages are going to be our data layer so we're only going to be creating an `AuthenticationRepository` to compose data from the two API clients. ## Authentication Repository The `AuthenticationRepository` will be responsible for abstracting the internal implementation details of how we authenticate and fetch user information. In this case, it will be integrating with Firebase but we can always change the internal implementation later on and our application will be unaffected. ### Setup We'll start by creating `packages/authentication_repository` and a `pubspec.yaml` at the root of the project. Next, we can install the dependencies by running: in the `authentication_repository` directory. Just like most packages, the `authentication_repository` will define it's API surface via `packages/authentication_repository/lib/authentication_repository.dart` :::note The `authentication_repository` package will be exposing an `AuthenticationRepository` as well as models. ::: Next, let's take a look at the models. ### User The `User` model will describe a user in the context of the authentication domain. For the purposes of this example, a user will consist of an `email`, `id`, `name`, and `photo`. :::note It's completely up to you to define what a user needs to look like in the context of your domain. ::: [user.dart](https://raw.githubusercontent.com/felangel/bloc/master/examples/flutter_firebase_login/packages/authentication_repository/lib/src/models/user.dart ':include') :::note The `User` class is extending [equatable](https://pub.dev/packages/equatable) in order to override equality comparisons so that we can compare different instances of `User` by value. ::: :::tip It's useful to define a `static` empty `User` so that we don't have to handle `null` Users and can always work with a concrete `User` object. ::: ### Repository The `AuthenticationRepository` is responsible for abstracting the underlying implementation of how a user is authenticated, as well as how a user is fetched. The `AuthenticationRepository` exposes a `Stream` which we can subscribe to in order to be notified of when a `User` changes. In addition, it exposes methods to `signUp`, `logInWithGoogle`, `logInWithEmailAndPassword`, and `logOut`. :::note The `AuthenticationRepository` is also responsible for handling low-level errors that can occur in the data layer and exposes a clean, simple set of errors that align with the domain. ::: That's it for the `AuthenticationRepository`. Next, let's take a look at how to integrate it into the Flutter project we created. ## Firebase Setup We need to follow the [firebase_auth usage instructions](https://pub.dev/packages/firebase_auth#usage) in order to hook up our application to Firebase and enable [google_sign_in](https://pub.dev/packages/google_sign_in). :::caution Remember to update the `google-services.json` on Android and the `GoogleService-Info.plist` & `Info.plist` on iOS, otherwise the application will crash. ::: ## Project Dependencies We can replace the generated `pubspec.yaml` at the root of the project with the following: Notice that we are specifying an assets directory for all of our applications local assets. Create an `assets` directory in the root of your project and add the [bloc logo](https://github.com/felangel/bloc/blob/master/examples/flutter_firebase_login/assets/bloc_logo_small.png) asset (which we'll use later). Then install all of the dependencies: :::note We are depending on the `authentication_repository` package via path which will allow us to iterate quickly while still maintaining a clear separation. ::: ## main.dart The `main.dart` file can be replaced with the following: It's simply setting up some global configuration for the application and calling `runApp` with an instance of `App`. :::note We're injecting a single instance of `AuthenticationRepository` into the `App` and it is an explicit constructor dependency. ::: ## App Just like in the [login tutorial](/tutorials/flutter-login), our `app.dart` will provide an instance of the `AuthenticationRepository` to the application via `RepositoryProvider` and also creates and provides an instance of `AuthenticationBloc`. Then `AppView` consumes the `AuthenticationBloc` and handles updating the current route based on the `AuthenticationState`. ## App Bloc The `AppBloc` is responsible for managing the global state of the application. It has a dependency on the `AuthenticationRepository` and subscribes to the `user` Stream in order to emit new states in response to changes in the current user. ### State The `AppState` consists of an `AppStatus` and a `User`. The default constructor accepts an optional `User` and redirects to the private constructor with the appropriate authentication status. ### Event The `AppEvent` has two subclasses: - `AppUserSubscriptionRequested` which notifies the bloc to subscribe to the user stream. - `AppLogoutPressed` which notifies the bloc of a user logout action. ### Bloc In the constructor body, `AppEvent` subclasses are mapped to their corresponding event handlers. In the `_onUserSubscriptionRequested` event handler, the `AppBloc` uses `emit.onEach` to subscribe to the user stream of the `AuthenticationRepository` and emit a state in response to each `User`. `emit.onEach` creates a stream subscription internally and takes care of canceling it when either `AppBloc` or the user stream is closed. If the user stream emits an error, `addError` forwards the error and stack trace to any `BlocObserver` listening. :::caution If `onError` is omitted, any errors on the user stream are considered unhandled, and will be thrown by `onEach`. As a result, the subscription to the user stream will be canceled. ::: :::tip A [`BlocObserver`](/bloc-concepts/#blocobserver-1) is great for logging Bloc events, errors, and state changes especially in the context analytics and crash reporting. ::: ## Models An `Email` and `Password` input model are useful for encapsulating the validation logic and will be used in both the `LoginForm` and `SignUpForm` (later in the tutorial). Both input models are made using the [formz](https://pub.dev/packages/formz) package and allow us to work with a validated object rather than a primitive type like a `String`. ### Email ### Password ## Login Page The `LoginPage` is responsible for creating and providing an instance of `LoginCubit` to the `LoginForm`. :::tip It's very important to keep the creation of blocs/cubits separate from where they are consumed. This will allow you to easily inject mock instances and test your view in isolation. ::: ## Login Cubit The `LoginCubit` is responsible for managing the `LoginState` of the form. It exposes APIs to `logInWithCredentials`, `logInWithGoogle`, as well as gets notified when the email/password are updated. ### State The `LoginState` consists of an `Email`, `Password`, and `FormzStatus`. The `Email` and `Password` models extend `FormzInput` from the [formz](https://pub.dev/packages/formz) package. ### Cubit The `LoginCubit` has a dependency on the `AuthenticationRepository` in order to sign the user in either via credentials or via google sign in. :::note We used a `Cubit` instead of a `Bloc` here because the `LoginState` is fairly simple and localized. Even without events, we can still have a fairly good sense of what happened just by looking at the changes from one state to another and our code is a lot simpler and more concise. ::: ## Login Form The `LoginForm` is responsible for rendering the form in response to the `LoginState` and invokes methods on the `LoginCubit` in response to user interactions. The `LoginForm` also renders a "Create Account" button which navigates to the `SignUpPage` where a user can create a brand new account. ## Sign Up Page The `SignUp` structure mirrors the `Login` structure and consists of a `SignUpPage`, `SignUpView`, and `SignUpCubit`. The `SignUpPage` is just responsible for creating and providing an instance of the `SignUpCubit` to the `SignUpForm` (exactly like in `LoginPage`). :::note Just as in the `LoginCubit`, the `SignUpCubit` has a dependency on the `AuthenticationRepository` in order to create new user accounts. ::: ## Sign Up Cubit The `SignUpCubit` manages the state of the `SignUpForm` and communicates with the `AuthenticationRepository` in order to create new user accounts. ### State The `SignUpState` reuses the same `Email` and `Password` form input models because the validation logic is the same. ### Cubit The `SignUpCubit` is extremely similar to the `LoginCubit` with the main exception being it exposes an API to submit the form as opposed to login. ## Sign Up Form The `SignUpForm` is responsible for rendering the form in response to the `SignUpState` and invokes methods on the `SignUpCubit` in response to user interactions. ## Home Page After a user either successfully logs in or signs up, the `user` stream will be updated which will trigger a state change in the `AuthenticationBloc` and will result in the `AppView` pushing the `HomePage` route onto the navigation stack. From the `HomePage`, the user can view their profile information and log out by tapping the exit icon in the `AppBar`. :::note A `widgets` directory was created alongside the `view` directory within the `home` feature for reusable components that are specific to that particular feature. In this case a simple `Avatar` widget is exported and used within the `HomePage`. ::: :::note When the logout `IconButton` is tapped, an `AuthenticationLogoutRequested` event is added to the `AuthenticationBloc` which signs the user out and navigates them back to the `LoginPage`. ::: At this point we have a pretty solid login implementation using Firebase and we have decoupled our presentation layer from the business logic layer by using the Bloc Library. The full source for this example can be found [here](https://github.com/felangel/bloc/tree/master/examples/flutter_firebase_login). ================================================ FILE: docs/src/content/docs/tutorials/flutter-infinite-list.mdx ================================================ --- title: Flutter Infinite List description: An in-depth guide on how to build a Flutter infinite list with bloc. sidebar: order: 3 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-infinite-list/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/flutter-infinite-list/FlutterPubGetSnippet.astro'; import PostsJsonSnippet from '~/components/tutorials/flutter-infinite-list/PostsJsonSnippet.astro'; import PostBlocInitialStateSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocInitialStateSnippet.astro'; import PostBlocOnPostFetchedSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocOnPostFetchedSnippet.astro'; import PostBlocTransformerSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocTransformerSnippet.astro'; ![intermediate](https://img.shields.io/badge/level-intermediate-orange.svg) In this tutorial, we're going to be implementing an app which fetches data over the network and loads it as a user scrolls using Flutter and the bloc library. ![demo](~/assets/tutorials/flutter-infinite-list.gif) ## Key Topics - Observe state changes with [BlocObserver](/bloc-concepts#blocobserver). - [BlocProvider](/flutter-bloc-concepts#blocprovider), Flutter widget which provides a bloc to its children. - [BlocBuilder](/flutter-bloc-concepts#blocbuilder), Flutter widget that handles building the widget in response to new states. - Adding events with [context.read](/flutter-bloc-concepts#contextread). - Prevent unnecessary rebuilds with [Equatable](/faqs#when-to-use-equatable). - Use the `transformEvents` method with Rx. ## Setup We'll start off by creating a brand new Flutter project We can then go ahead and replace the contents of pubspec.yaml with and then install all of our dependencies ## Project Structure ``` ├── lib | ├── posts │ │ ├── bloc │ │ │ └── post_bloc.dart | | | └── post_event.dart | | | └── post_state.dart | | └── models | | | └── models.dart* | | | └── post.dart │ │ └── view │ │ | ├── posts_page.dart │ │ | └── posts_list.dart | | | └── view.dart* | | └── widgets | | | └── bottom_loader.dart | | | └── post_list_item.dart | | | └── widgets.dart* │ │ ├── posts.dart* │ ├── app.dart │ ├── simple_bloc_observer.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` The application uses a feature-driven directory structure. This project structure enables us to scale the project by having self-contained features. In this example we will only have a single feature (the post feature) and it's split up into respective folders with barrel files, indicated by the asterisk (\*). ## REST API For this demo application, we'll be using [jsonplaceholder](http://jsonplaceholder.typicode.com) as our data source. :::note jsonplaceholder is an online REST API which serves fake data; it's very useful for building prototypes. ::: Open a new tab in your browser and visit https://jsonplaceholder.typicode.com/posts?_start=0&_limit=2 to see what the API returns. :::note In our url we specified the start and limit as query parameters to the GET request. ::: Great, now that we know what our data is going to look like, let's create the model. ## Data Model Create `post.dart` and let's get to work creating the model of our Post object. `Post` is just a class with an `id`, `title`, and `body`. :::note We extend [`Equatable`](https://pub.dev/packages/equatable) so that we can compare `Posts`. Without this, we would need to manually change our class to override equality and hashCode so that we could tell the difference between two `Posts` objects. See [the package](https://pub.dev/packages/equatable) for more details. ::: Now that we have our `Post` object model, let's start working on the Business Logic Component (bloc). ## Post Events Before we dive into the implementation, we need to define what our `PostBloc` is going to be doing. At a high level, it will be responding to user input (scrolling) and fetching more posts in order for the presentation layer to display them. Let's start by creating our `Event`. Our `PostBloc` will only be responding to a single event; `PostFetched` which will be added by the presentation layer whenever it needs more Posts to present. Since our `PostFetched` event is a type of `PostEvent` we can create `bloc/post_event.dart` and implement the event like so. To recap, our `PostBloc` will be receiving `PostEvents` and converting them to `PostStates`. We have defined all of our `PostEvents` (PostFetched) so next let's define our `PostState`. ## Post States Our presentation layer will need to have several pieces of information in order to properly lay itself out: - `PostInitial`- will tell the presentation layer it needs to render a loading indicator while the initial batch of posts are loaded - `PostSuccess`- will tell the presentation layer it has content to render - `posts`- will be the `List` which will be displayed - `hasReachedMax`- will tell the presentation layer whether or not it has reached the maximum number of posts - `PostFailure`- will tell the presentation layer that an error has occurred while fetching posts We can now create `bloc/post_state.dart` and implement it like so. :::note We implemented `copyWith` so that we can copy an instance of `PostSuccess` and update zero or more properties conveniently (this will come in handy later). ::: Now that we have our `Events` and `States` implemented, we can create our `PostBloc`. ## Post Bloc For simplicity, our `PostBloc` will have a direct dependency on an `http client`; however, in a production application we suggest instead you inject an api client and use the repository pattern [docs](/architecture). Let's create `post_bloc.dart` and create our empty `PostBloc`. :::note Just from the class declaration we can tell that our PostBloc will be taking PostEvents as input and outputting PostStates. ::: Next, we need to register an event handler to handle incoming `PostFetched` events. In response to a `PostFetched` event, we will call `_fetchPosts` to fetch posts from the API. Our `PostBloc` will `emit` new states via the `Emitter` provided in the event handler. Check out [core concepts](/bloc-concepts#streams) for more information. Now every time a `PostEvent` is added, if it is a `PostFetched` event and there are more posts to fetch, our `PostBloc` will fetch the next 20 posts. The API will return an empty array if we try to fetch beyond the maximum number of posts (100), so if we get back an empty array, our bloc will `emit` the currentState except we will set `hasReachedMax` to true. If we cannot retrieve the posts, we emit `PostStatus.failure`. If we can retrieve the posts, we emit `PostStatus.success` and the entire list of posts. One optimization we can make is to `throttle` the `PostFetched` event in order to prevent spamming our API unnecessarily. We can do this by using the `transform` parameter when we register the `_onFetched` event handler. :::note Passing a `transformer` to `on` allows us to customize how events are processed. ::: :::note Make sure to import [`package:stream_transform`](https://pub.dev/packages/stream_transform) to use the `throttle` api. ::: Our finished `PostBloc` should now look like this: Great! Now that we've finished implementing the business logic all that's left to do is implement the presentation layer. ## Presentation Layer In our `main.dart` we can start by implementing our main function and calling `runApp` to render our root widget. Here, we can also include our bloc observer to log transitions and any errors. In our `App` widget, the root of our project, we can then set the home to `PostsPage` In our `PostsPage` widget, we use `BlocProvider` to create and provide an instance of `PostBloc` to the subtree. Also, we add a `PostFetched` event so that when the app loads, it requests the initial batch of Posts. Next, we need to implement our `PostsList` view which will present our posts and hook up to our `PostBloc`. :::note `PostsList` is a `StatefulWidget` because it will need to maintain a `ScrollController`. In `initState`, we add a listener to our `ScrollController` so that we can respond to scroll events. We also access our `PostBloc` instance via `context.read()`. ::: Moving along, our build method returns a `BlocBuilder`. `BlocBuilder` is a Flutter widget from the [flutter_bloc package](https://pub.dev/packages/flutter_bloc) which handles building a widget in response to new bloc states. Any time our `PostBloc` state changes, our builder function will be called with the new `PostState`. :::caution We need to remember to clean up after ourselves and dispose of our `ScrollController` when the StatefulWidget is disposed. ::: Whenever the user scrolls, we calculate how far you have scrolled down the page and if our distance is ≥ 90% of our `maxScrollextent` we add a `PostFetched` event in order to load more posts. Next, we need to implement our `BottomLoader` widget which will indicate to the user that we are loading more posts. Lastly, we need to implement our `PostListItem` which will render an individual `Post`. At this point, we should be able to run our app and everything should work; however, there's one more thing we can do. One added bonus of using the bloc library is that we can have access to all `Transitions` in one place. The change from one state to another is called a `Transition`. :::note A `Transition` consists of the current state, the event, and the next state. ::: Even though in this application we only have one bloc, it's fairly common in larger applications to have many blocs managing different parts of the application's state. If we want to be able to do something in response to all `Transitions` we can simply create our own `BlocObserver`. :::note All we need to do is extend `BlocObserver` and override the `onTransition` method. ::: Now every time a Bloc `Transition` occurs we can see the transition printed to the console. :::note In practice, you can create different `BlocObservers` and because every state change is recorded, we are able to very easily instrument our applications and track all user interactions and state changes in one place! ::: That's all there is to it! We've now successfully implemented an infinite list in flutter using the [bloc](https://pub.dev/packages/bloc) and [flutter_bloc](https://pub.dev/packages/flutter_bloc) packages and we've successfully separated our presentation layer from our business logic. Our `PostsPage` has no idea where the `Posts` are coming from or how they are being retrieved. Conversely, our `PostBloc` has no idea how the `State` is being rendered, it simply converts events into states. The full source for this example can be found [here](https://github.com/felangel/Bloc/tree/master/examples/flutter_infinite_list). ================================================ FILE: docs/src/content/docs/tutorials/flutter-login.mdx ================================================ --- title: Flutter Login description: An in-depth guide on how to build a Flutter login flow with bloc. sidebar: order: 4 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-login/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![intermediate](https://img.shields.io/badge/level-intermediate-orange.svg) In the following tutorial, we're going to build a Login Flow in Flutter using the Bloc library. ![demo](~/assets/tutorials/flutter-login.gif) ## Key Topics - [BlocProvider](/flutter-bloc-concepts#blocprovider), Flutter widget which provides a bloc to its children. - Adding events with [context.read](/flutter-bloc-concepts#contextread). - Prevent unnecessary rebuilds with [Equatable](/faqs#when-to-use-equatable). - [RepositoryProvider](/flutter-bloc-concepts#repositoryprovider), a Flutter widget which provides a repository to its children. - [BlocListener](/flutter-bloc-concepts#bloclistener), a Flutter widget which invokes the listener code in response to state changes in the bloc. - Updating the UI based on a part of a bloc state with [context.select](/flutter-bloc-concepts#contextselect). ## Project Setup We'll start off by creating a brand new Flutter project Next, we can install all of our dependencies ## Authentication Repository The first thing we're going to do is create an `authentication_repository` package which will be responsible for managing the authentication domain. We'll start by creating a `packages/authentication_repository` directory at the root of the project which will contain all internal packages. At a high level, the directory structure should look like this: ``` ├── android ├── ios ├── lib ├── packages │ └── authentication_repository └── test ``` Next, we can create a `pubspec.yaml` for the `authentication_repository` package: :::note `package:authentication_repository` will be a pure Dart package without any external dependencies. ::: Next up, we need to implement the `AuthenticationRepository` class itself which will be in `packages/authentication_repository/lib/src/authentication_repository.dart`. The `AuthenticationRepository` exposes a `Stream` of `AuthenticationStatus` updates which will be used to notify the application when a user signs in or out. In addition, there are `logIn` and `logOut` methods which are stubbed for simplicity but can easily be extended to authenticate with `FirebaseAuth` for example or some other authentication provider. :::note Since we are maintaining a `StreamController` internally, a `dispose` method is exposed so that the controller can be closed when it is no longer needed. ::: Lastly, we need to create `packages/authentication_repository/lib/authentication_repository.dart` which will contain the public exports: That's it for the `AuthenticationRepository`, next we'll work on the `UserRepository`. ## User Repository Just like with the `AuthenticationRepository`, we will create a `user_repository` package inside the `packages` directory. ``` ├── android ├── ios ├── lib ├── packages │ ├── authentication_repository │ └── user_repository └── test ``` Next, we'll create the `pubspec.yaml` for the `user_repository`: The `user_repository` will be responsible for the user domain and will expose APIs to interact with the current user. The first thing we will define is the user model in `packages/user_repository/lib/src/models/user.dart`: For simplicity, a user just has an `id` property but in practice we might have additional properties like `firstName`, `lastName`, `avatarUrl`, etc... :::note [`package:equatable`](https://pub.dev/packages/equatable) is used to enable value comparisons of the `User` object. ::: Next, we can create a `models.dart` in `packages/user_repository/lib/src/models` which will export all models so that we can use a single import state to import multiple models. Now that the models have been defined, we can implement the `UserRepository` class in `packages/user_repository/lib/src/user_repository.dart`. For this simple example, the `UserRepository` exposes a single method `getUser` which will retrieve the current user. We are stubbing this but in practice this is where we would query the current user from the backend. Almost done with the `user_repository` package -- the only thing left to do is to create the `user_repository.dart` file in `packages/user_repository/lib` which defines the public exports: Now that we have the `authentication_repository` and `user_repository` packages complete, we can focus on the Flutter application. ## Installing Dependencies Let's start by updating the generated `pubspec.yaml` at the root of our project: We can install the dependencies by running: ## Authentication Bloc The `AuthenticationBloc` will be responsible for reacting to changes in the authentication state (exposed by the `AuthenticationRepository`) and will emit states we can react to in the presentation layer. The implementation for the `AuthenticationBloc` is inside of `lib/authentication` because we treat authentication as a feature in our application layer. ``` ├── lib │ ├── app.dart │ ├── authentication │ │ ├── authentication.dart │ │ └── bloc │ │ ├── authentication_bloc.dart │ │ ├── authentication_event.dart │ │ └── authentication_state.dart │ ├── main.dart ``` :::tip Use the [VSCode Extension](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) or [IntelliJ Plugin](https://plugins.jetbrains.com/plugin/12129-bloc) to create blocs automatically. ::: ### authentication_event.dart `AuthenticationEvent` instances will be the input to the `AuthenticationBloc` and will be processed and used to emit new `AuthenticationState` instances. In this application, the `AuthenticationBloc` will be reacting to two different events: - `AuthenticationSubscriptionRequested`: initial event that notifies the bloc to subscribe to the `AuthenticationStatus` stream - `AuthenticationLogoutPressed`: notifies the bloc of a user logout action Next, let's take a look at the `AuthenticationState`. ### authentication_state.dart `AuthenticationState` instances will be the output of the `AuthenticationBloc` and will be consumed by the presentation layer. The `AuthenticationState` class has three named constructors: - `AuthenticationState.unknown()`: the default state which indicates that the bloc does not yet know whether the current user is authenticated or not. - `AuthenticationState.authenticated()`: the state which indicates that the user is current authenticated. - `AuthenticationState.unauthenticated()`: the state which indicates that the user is current not authenticated. Now that we have seen the `AuthenticationEvent` and `AuthenticationState` implementations let's take a look at `AuthenticationBloc`. ### authentication_bloc.dart The `AuthenticationBloc` manages the authentication state of the application which is used to determine things like whether or not to start the user at a login page or a home page. The `AuthenticationBloc` has a dependency on both the `AuthenticationRepository` and `UserRepository` and defines the initial state as `AuthenticationState.unknown()`. In the constructor body, `AuthenticationEvent` subclasses are mapped to their corresponding event handlers. In the `_onSubscriptionRequested` event handler, the `AuthenticationBloc` uses `emit.onEach` to subscribe to the `status` stream of the `AuthenticationRepository` and emit a state in response to each `AuthenticationStatus`. `emit.onEach` creates a stream subscription internally and takes care of canceling it when either `AuthenticationBloc` or the `status` stream is closed. If the `status` stream emits an error, `addError` forwards the error and stackTrace to any `BlocObserver` listening. :::caution If `onError` is omitted, any errors on the `status` stream are considered unhandled, and will be thrown by `onEach`. As a result, the subscription to the `status` stream will be canceled. ::: :::tip A [`BlocObserver`](/bloc-concepts/#blocobserver-1) is great for logging Bloc events, errors, and state changes especially in the context analytics and crash reporting.; ::: When the `status` stream emits `AuthenticationStatus.unknown` or `unauthenticated`, the corresponding `AuthenticationState` is emitted. When `AuthenticationStatus.authenticated` is emitted, the `AuthentictionBloc` queries the user via the `UserRepository`. ## main.dart Next, we can replace the default `main.dart` with: ## App `app.dart` will contain the root `App` widget for the entire application. :::note `app.dart` is split into two parts `App` and `AppView`. `App` is responsible for creating/providing the `AuthenticationBloc` which will be consumed by the `AppView`. This decoupling will enable us to easily test both the `App` and `AppView` widgets later on. ::: :::note `RepositoryProvider` is used to provide the single instance of `AuthenticationRepository` to the entire application which will come in handy later on. ::: By default, `BlocProvider` is lazy and does not call `create` until the first time the Bloc is accessed. Since `AuthenticationBloc` should always subscribe to the `AuthenticationStatus` stream immediately (via the `AuthenticationSubscriptionRequested` event), we can explicitly opt out of this behavior by setting `lazy: false`. `AppView` is a `StatefulWidget` because it maintains a `GlobalKey` which is used to access the `NavigatorState`. By default, `AppView` will render the `SplashPage` (which we will see later) and it uses `BlocListener` to navigate to different pages based on changes in the `AuthenticationState`. ## Splash The splash feature will just contain a simple view which will be rendered right when the app is launched while the app determines whether the user is authenticated. ``` lib └── splash ├── splash.dart └── view └── splash_page.dart ``` :::tip `SplashPage` exposes a static `Route` which makes it very easy to navigate to via `Navigator.of(context).push(SplashPage.route())`; ::: ## Login The login feature contains a `LoginPage`, `LoginForm` and `LoginBloc` and allows users to enter a username and password to log into the application. ``` ├── lib │ ├── login │ │ ├── bloc │ │ │ ├── login_bloc.dart │ │ │ ├── login_event.dart │ │ │ └── login_state.dart │ │ ├── login.dart │ │ ├── models │ │ │ ├── models.dart │ │ │ ├── password.dart │ │ │ └── username.dart │ │ └── view │ │ ├── login_form.dart │ │ ├── login_page.dart │ │ └── view.dart ``` ### Login Models We are using [`package:formz`](https://pub.dev/packages/formz) to create reusable and standard models for the `username` and `password`. #### Username For simplicity, we are just validating the username to ensure that it is not empty but in practice you can enforce special character usage, length, etc... #### Password Again, we are just performing a simple check to ensure the password is not empty. #### Models Barrel Just like before, there is a `models.dart` barrel to make it easy to import the `Username` and `Password` models with a single import. ### Login Bloc The `LoginBloc` manages the state of the `LoginForm` and takes care validating the username and password input as well as the state of the form. #### login_event.dart In this application there are three different `LoginEvent` types: - `LoginUsernameChanged`: notifies the bloc that the username has been modified. - `LoginPasswordChanged`: notifies the bloc that the password has been modified. - `LoginSubmitted`: notifies the bloc that the form has been submitted. #### login_state.dart The `LoginState` will contain the status of the form as well as the username and password input states. :::note The `Username` and `Password` models are used as part of the `LoginState` and the status is also part of [package:formz](https://pub.dev/packages/formz). ::: #### login_bloc.dart The `LoginBloc` is responsible for reacting to user interactions in the `LoginForm` and handling the validation and submission of the form. The `LoginBloc` has a dependency on the `AuthenticationRepository` because when the form is submitted, it invokes `logIn`. The initial state of the bloc is `pure` meaning neither the inputs nor the form has been touched or interacted with. Whenever either the `username` or `password` change, the bloc will create a dirty variant of the `Username`/`Password` model and update the form status via the `Formz.validate` API. When the `LoginSubmitted` event is added, if the current status of the form is valid, the bloc makes a call to `logIn` and updates the status based on the outcome of the request. Next let's take a look at the `LoginPage` and `LoginForm`. ### Login Page The `LoginPage` is responsible for exposing the `Route` as well as creating and providing the `LoginBloc` to the `LoginForm`. :::note `context.read()` is used to lookup the instance of `AuthenticationRepository` via the `BuildContext`. ::: ### Login Form The `LoginForm` handles notifying the `LoginBloc` of user events and also responds to state changes using `BlocBuilder` and `BlocListener`. `BlocListener` is used to show a `SnackBar` if the login submission fails. In addition, `context.select` is used to efficiently access specific parts of the `LoginState` for each widget, preventing unnecessary rebuilds. The `onChanged` callback is used to notify the `LoginBloc` of changes to the username/password. The `_LoginButton` widget is only enabled if the status of the form is valid and a `CircularProgressIndicator` is shown in its place while the form is being submitted. ## Home Upon a successful `logIn` request, the state of the `AuthenticationBloc` will change to `authenticated` and the user will be navigated to the `HomePage` where we display the user's `id` as well as a button to log out. ``` ├── lib │ ├── home │ │ ├── home.dart │ │ └── view │ │ └── home_page.dart ``` ### Home Page The `HomePage` can access the current user id via `context.select((AuthenticationBloc bloc) => bloc.state.user.id)` and displays it via a `Text` widget. In addition, when the logout button is tapped, an `AuthenticationLogoutPressed` event is added to the `AuthenticationBloc`. :::note `context.select((AuthenticationBloc bloc) => bloc.state.user.id)` will trigger updates if the user id changes. ::: At this point we have a pretty solid login implementation and we have decoupled our presentation layer from the business logic layer by using Bloc. The full source for this example (including unit and widget tests) can be found [here](https://github.com/felangel/Bloc/tree/master/examples/flutter_login). ================================================ FILE: docs/src/content/docs/tutorials/flutter-timer.mdx ================================================ --- title: Flutter Timer description: An in-depth guide on how to build a Flutter timer app with bloc. sidebar: order: 2 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-timer/FlutterCreateSnippet.astro'; import TimerBlocEmptySnippet from '~/components/tutorials/flutter-timer/TimerBlocEmptySnippet.astro'; import TimerBlocInitialStateSnippet from '~/components/tutorials/flutter-timer/TimerBlocInitialStateSnippet.astro'; import TimerBlocTickerSnippet from '~/components/tutorials/flutter-timer/TimerBlocTickerSnippet.astro'; import TimerBlocOnStartedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnStartedSnippet.astro'; import TimerBlocOnTickedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnTickedSnippet.astro'; import TimerBlocOnPausedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnPausedSnippet.astro'; import TimerBlocOnResumedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnResumedSnippet.astro'; import TimerPageSnippet from '~/components/tutorials/flutter-timer/TimerPageSnippet.astro'; import ActionsSnippet from '~/components/tutorials/flutter-timer/ActionsSnippet.astro'; import BackgroundSnippet from '~/components/tutorials/flutter-timer/BackgroundSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) In the following tutorial we're going to cover how to build a timer application using the bloc library. The finished application should look like this: ![demo](~/assets/tutorials/flutter-timer.gif) ## Key Topics - Observe state changes with [BlocObserver](/bloc-concepts#blocobserver). - [BlocProvider](/flutter-bloc-concepts#blocprovider), Flutter widget which provides a bloc to its children. - [BlocBuilder](/flutter-bloc-concepts#blocbuilder), Flutter widget that handles building the widget in response to new states. - Prevent unnecessary rebuilds with [Equatable](/faqs#when-to-use-equatable). - Learn to use `StreamSubscription` in a Bloc. - Prevent unnecessary rebuilds with `buildWhen`. ## Setup We'll start off by creating a brand new Flutter project: We can then replace the contents of pubspec.yaml with: :::note We'll be using the [flutter_bloc](https://pub.dev/packages/flutter_bloc) and [equatable](https://pub.dev/packages/equatable) packages in this app. ::: Next, run `flutter pub get` to install all the dependencies. ## Project Structure ``` ├── lib | ├── timer │ │ ├── bloc │ │ │ └── timer_bloc.dart | | | └── timer_event.dart | | | └── timer_state.dart │ │ └── view │ │ | ├── timer_page.dart │ │ ├── timer.dart │ ├── app.dart │ ├── ticker.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` ## Ticker The ticker will be our data source for the timer application. It will expose a stream of ticks which we can subscribe and react to. Start off by creating `ticker.dart`. All our `Ticker` class does is expose a tick function which takes the number of ticks (seconds) we want and returns a stream which emits the remaining seconds every second. Next up, we need to create our `TimerBloc` which will consume the `Ticker`. ## Timer Bloc ### TimerState We'll start off by defining the `TimerStates` which our `TimerBloc` can be in. Our `TimerBloc` state can be one of the following: - `TimerInitial`: ready to start counting down from the specified duration. - `TimerRunInProgress`: actively counting down from the specified duration. - `TimerRunPause`: paused at some remaining duration. - `TimerRunComplete`: completed with a remaining duration of 0. Each of these states will have an implication on the user interface and actions that the user can perform. For example: - if the state is `TimerInitial` the user will be able to start the timer. - if the state is `TimerRunInProgress` the user will be able to pause and reset the timer as well as see the remaining duration. - if the state is `TimerRunPause` the user will be able to resume the timer and reset the timer. - if the state is `TimerRunComplete` the user will be able to reset the timer. In order to keep all of our bloc files together, let's create a bloc directory with `bloc/timer_state.dart`. :::tip You can use the [IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc-code-generator) or [VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) extensions to autogenerate the following bloc files for you. ::: Note that all of the `TimerStates` extend the abstract base class `TimerState` which has a duration property. This is because no matter what state our `TimerBloc` is in, we want to know how much time is remaining. Additionally, `TimerState` extends `Equatable` to optimize our code by ensuring that our app does not trigger rebuilds if the same state occurs. Next up, let's define and implement the `TimerEvents` which our `TimerBloc` will be processing. ### TimerEvent Our `TimerBloc` will need to know how to process the following events: - `TimerStarted`: informs the TimerBloc that the timer should be started. - `TimerPaused`: informs the TimerBloc that the timer should be paused. - `TimerResumed`: informs the TimerBloc that the timer should be resumed. - `TimerReset`: informs the TimerBloc that the timer should be reset to the original state. - `_TimerTicked`: informs the TimerBloc that a tick has occurred and that it needs to update its state accordingly. If you didn't use the [IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc-code-generator) or [VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) extensions, then create `bloc/timer_event.dart` and let's implement those events. Next up, let's implement the `TimerBloc`! ### TimerBloc If you haven't already, create `bloc/timer_bloc.dart` and create an empty `TimerBloc`. The first thing we need to do is define the initial state of our `TimerBloc`. In this case, we want the `TimerBloc` to start off in the `TimerInitial` state with a preset duration of 1 minute (60 seconds). Next, we need to define the dependency on our `Ticker`. We are also defining a `StreamSubscription` for our `Ticker` which we will get to in a bit. At this point, all that's left to do is implement the event handlers. For improved readability, I like to break out each event handler into its own helper function. We'll start with the `TimerStarted` event. If the `TimerBloc` receives a `TimerStarted` event, it pushes a `TimerRunInProgress` state with the start duration. In addition, if there was already an open `_tickerSubscription` we need to cancel it to deallocate the memory. We also need to override the `close` method on our `TimerBloc` so that we can cancel the `_tickerSubscription` when the `TimerBloc` is closed. Lastly, we listen to the `_ticker.tick` stream and on every tick we add a `_TimerTicked` event with the remaining duration. Next, let's implement the `_TimerTicked` event handler. Every time a `_TimerTicked` event is received, if the tick's duration is greater than 0, we need to push an updated `TimerRunInProgress` state with the new duration. Otherwise, if the tick's duration is 0, our timer has ended and we need to push a `TimerRunComplete` state. Now let's implement the `TimerPaused` event handler. In `_onPaused` if the `state` of our `TimerBloc` is `TimerRunInProgress`, then we can pause the `_tickerSubscription` and push a `TimerRunPause` state with the current timer duration. Next, let's implement the `TimerResumed` event handler so that we can unpause the timer. The `TimerResumed` event handler is very similar to the `TimerPaused` event handler. If the `TimerBloc` has a `state` of `TimerRunPause` and it receives a `TimerResumed` event, then it resumes the `_tickerSubscription` and pushes a `TimerRunInProgress` state with the current duration. Lastly, we need to implement the `TimerReset` event handler. If the `TimerBloc` receives a `TimerReset` event, it needs to cancel the current `_tickerSubscription` so that it isn't notified of any additional ticks and pushes a `TimerInitial` state with the original duration. That's all there is to the `TimerBloc`. Now all that's left is implement the UI for our Timer Application. ## Application UI ### MyApp We can start off by deleting the contents of `main.dart` and replacing it with the following. Next, let's create our 'App' widget in `app.dart`, which will be the root of our application. Next, we need to implement our `Timer` widget. ### Timer Our `Timer` widget (`lib/timer/view/timer_page.dart`) will be responsible for displaying the remaining time along with the proper buttons which will enable users to start, pause, and reset the timer. So far, we're just using `BlocProvider` to access the instance of our `TimerBloc`. Next, we're going to implement our `Actions` widget which will have the proper actions (start, pause, and reset). ### Barrel In order to clean up our imports from the `Timer` section, we need to create a barrel file `timer/timer.dart`. ### Actions The `Actions` widget is just another `StatelessWidget` which uses a `BlocBuilder` to rebuild the UI every time we get a new `TimerState`. `Actions` uses `context.read()` to access the `TimerBloc` instance and returns different `FloatingActionButtons` based on the current state of the `TimerBloc`. Each of the `FloatingActionButtons` adds an event in its `onPressed` callback to notify the `TimerBloc`. If you want fine-grained control over when the `builder` function is called you can provide an optional `buildWhen` to `BlocBuilder`. The `buildWhen` takes the previous bloc state and current bloc state and returns a `boolean`. If `buildWhen` returns `true`, `builder` will be called with `state` and the widget will rebuild. If `buildWhen` returns `false`, `builder` will not be called with `state` and no rebuild will occur. In this case, we don't want the `Actions` widget to be rebuilt on every tick because that would be inefficient. Instead, we only want `Actions` to rebuild if the `runtimeType` of the `TimerState` changes (TimerInitial => TimerRunInProgress, TimerRunInProgress => TimerRunPause, etc...). As a result, if we randomly colored the widgets on every rebuild, it would look like: ![BlocBuilder buildWhen demo](https://cdn-images-1.medium.com/max/1600/1*YyjpH1rcZlYWxCX308l_Ew.gif) :::note Even though the `Text` widget is rebuilt on every tick, we only rebuild the `Actions` if they need to be rebuilt. ::: ### Background Lastly, add the background widget as follows: ### Putting it all together That's all there is to it! At this point we have a pretty solid timer application which efficiently rebuilds only widgets that need to be rebuilt. The full source for this example can be found [here](https://github.com/felangel/Bloc/tree/master/examples/flutter_timer). ================================================ FILE: docs/src/content/docs/tutorials/flutter-todos.mdx ================================================ --- title: Flutter Todos description: An in-depth guide on how to build a Flutter todos app with bloc. sidebar: order: 6 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-todos/FlutterCreateSnippet.astro'; import ActivateVeryGoodCLISnippet from '~/components/tutorials/flutter-todos/ActivateVeryGoodCLISnippet.astro'; import FlutterCreatePackagesSnippet from '~/components/tutorials/flutter-todos/FlutterCreatePackagesSnippet.astro'; import ProjectStructureSnippet from '~/components/tutorials/flutter-todos/ProjectStructureSnippet.astro'; import VeryGoodPackagesGetSnippet from '~/components/tutorials/flutter-todos/VeryGoodPackagesGetSnippet.astro'; import HomePageTreeSnippet from '~/components/tutorials/flutter-todos/HomePageTreeSnippet.astro'; import TodosOverviewPageTreeSnippet from '~/components/tutorials/flutter-todos/TodosOverviewPageTreeSnippet.astro'; import StatsPageTreeSnippet from '~/components/tutorials/flutter-todos/StatsPageTreeSnippet.astro'; import EditTodosPageTreeSnippet from '~/components/tutorials/flutter-todos/EditTodosPageTreeSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) In the following tutorial, we're going to build a todos app in Flutter using the Bloc library. ![demo](~/assets/tutorials/flutter-todos.gif) ## Key Topics - [Bloc and Cubit](/bloc-concepts#cubit-vs-bloc) to manage the various feature states. - [Layered Architecture](/architecture) for separation of concerns and to facilitate reusability. - [BlocObserver](/bloc-concepts#blocobserver) to observe state changes. - [BlocProvider](/flutter-bloc-concepts#blocprovider), a Flutter widget which provides a bloc to its children. - [BlocBuilder](/flutter-bloc-concepts#blocbuilder), a Flutter widget that handles building the widget in response to new states. - [BlocListener](/flutter-bloc-concepts#bloclistener), a Flutter widget that handles performing side effects in response to state changes. - [RepositoryProvider](/flutter-bloc-concepts#repositoryprovider), a Flutter widget to provide a repository to its children. - [Equatable](/faqs#when-to-use-equatable) to prevent unnecessary rebuilds. - [MultiBlocListener](/flutter-bloc-concepts#multibloclistener), a Flutter widget that reduces nesting when using multiple BlocListeners. ## Setup We'll start off by creating a brand new Flutter project using the [very_good_cli](https://pub.dev/packages/very_good_cli). :::note Install `very_good_cli` using the following command ::: Next we'll create the `todos_api`, `local_storage_todos_api`, and `todos_repository` packages using `very_good_cli`: We can then replace the contents of `pubspec.yaml` with: Finally, we can install all the dependencies: ## Project Structure Our application project structure should look like: We split the project into multiple packages in order to maintain explicit dependencies for each package with clear boundaries that enforce the [single responsibility principle](https://en.wikipedia.org/wiki/Single-responsibility_principle). Modularizing our project like this has many benefits including but not limited to: - easy to reuse packages across multiple projects - CI/CD improvements in terms of efficiency (run checks on only the code that has changed) - easy to maintain the packages in isolation with their dedicated test suites, semantic versioning, and release cycle/cadence ## Architecture ![Todos Architecture Diagram](~/assets/tutorials/todos-architecture.png) Layering our code is incredibly important and helps us iterate quickly and with confidence. Each layer has a single responsibility and can be used and tested in isolation. This allows us to keep changes contained to a specific layer in order to minimize the impact on the entire application. In addition, layering our application allows us to easily reuse libraries across multiple projects (especially with respect to the data layer). Our application consists of three main layers: - data layer - domain layer - feature layer - presentation/UI (widgets) - business logic (blocs/cubits) **Data Layer** This layer is the lowest layer and is responsible for retrieving raw data from external sources such as a databases, APIs, and more. Packages in the data layer generally should not depend on any UI and can be reused and even published on [pub.dev](https://pub.dev) as a standalone package. In this example, our data layer consists of the `todos_api` and `local_storage_todos_api` packages. **Domain Layer** This layer combines one or more data providers and applies "business rules" to the data. Each component in this layer is called a repository and each repository generally manages a single domain. Packages in the repository layer should generally only interact with the data layer. In this example, our repository layer consists of the `todos_repository` package. **Feature Layer** This layer contains all of the application-specific features and use cases. Each feature generally consists of some UI and business logic. Features should generally be independent of other features so that they can easily be added/removed without impacting the rest of the codebase. Within each feature, the state of the feature along with any business logic is managed by blocs. Blocs interact with zero or more repositories. Blocs react to events and emit states which trigger changes in the UI. Widgets within each feature should generally only depend on the corresponding bloc and render UI based on the current state. The UI can notify the bloc of user input via events. In this example, our application will consist of the `home`, `todos_overview`, `stats`, and `edit_todos` features. Now that we've gone over the layers at a high level, let's start building our application starting with the data layer! ## Data Layer The data layer is the lowest layer in our application and consists of raw data providers. Packages in this layer are primarily concerned with where/how data is coming from. In this case our data layer will consist of the `TodosApi`, which is an interface, and the `LocalStorageTodosApi`, which is an implementation of the `TodosApi` backed by `shared_preferences`. ### TodosApi The `todos_api` package will export a generic interface for interacting/managing todos. Later we'll implement the `TodosApi` using `shared_preferences`. Having an abstraction will make it easy to support other implementations without having to change any other part of our application. For example, we can later add a `FirestoreTodosApi`, which uses `cloud_firestore` instead of `shared_preferences`, with minimal code changes to the rest of the application. #### Todo model Next we'll define our `Todo` model. The first thing of note is that the `Todo` model doesn't live in our app — it's part of the `todos_api` package. This is because the `TodosApi` defines APIs that return/accept `Todo` objects. The model is a Dart representation of the raw Todo object that will be stored/retrieved. The `Todo` model uses [json_serializable](https://pub.dev/packages/json_serializable) to handle the json (de)serialization. If you are following along, you will have to run the [code generation step](https://pub.dev/packages/json_serializable#running-the-code-generator) to resolve the compiler errors. `json_map.dart` provides a `typedef` for code checking and linting. The model of the `Todo` is defined in `todos_api/models/todo.dart` and is exported by `package:todos_api/todos_api.dart`. #### Update Exports Our `Todo` model and the `TodosApi` are exported via barrel files. Notice how we don't import the model directly, but we import it in `lib/src/todos_api.dart` with a reference to the package barrel file: `import 'package:todos_api/todos_api.dart';`. Update the barrel files to resolve any remaining import errors: #### Streams vs Futures In a previous version of this tutorial, the `TodosApi` was `Future`-based rather than `Stream`-based. For an example of a `Future`-based API see [Brian Egan's implementation in his Architecture Samples](https://github.com/brianegan/flutter_architecture_samples/tree/master/todos_repository_core). A `Future`-based implementation could consist of two methods: `loadTodos` and `saveTodos` (note the plural). This means, a full list of todos must be provided to the method each time. - One limitation of this approach is that the standard CRUD (Create, Read, Update, and Delete) operation requires sending the full list of todos with each call. For example, on an Add Todo screen, one cannot just send the added todo item. Instead, we must keep track of the entire list and provide the entire new list of todos when persisting the updated list. - A second limitation is that `loadTodos` is a one-time delivery of data. The app must contain logic to ask for updates periodically. In the current implementation, the `TodosApi` exposes a `Stream>` via `getTodos()` which will report real-time updates to all subscribers when the list of todos has changed. In addition, todos can be created, deleted, or updated individually. For example, both deleting and saving a todo are done with only the `todo` as the argument. It's not necessary to provide the newly updated list of todos each time. ### LocalStorageTodosApi This package implements the `todos_api` using the [`shared_preferences`](https://pub.dev/packages/shared_preferences) package. ## Repository Layer A [repository](/architecture#repository) is part of the business layer. A repository depends on one or more data providers that have no business value, and combines their public API into APIs that provide business value. In addition, having a repository layer helps abstract data acquisition from the rest of the application, allowing us to change where/how data is being stored without affecting other parts of the app. ### TodosRepository Instantiating the repository requires specifying a `TodosApi`, which we discussed earlier in this tutorial, so we added it as a dependency in our `pubspec.yaml`: #### Library Exports In addition to exporting the `TodosRepository` class, we also export the `Todo` model from the `todos_api` package. This step prevents tight coupling between the application and the data providers. We decided to re-export the same `Todo` model from the `todos_api`, rather than redefining a separate model in the `todos_repository`, because in this case we are in complete control of the data model. In many cases, the data provider will not be something that you can control. In those cases, it becomes increasingly important to maintain your own model definitions in the repository layer to maintain full control of the interface and API contract. ## Feature Layer ### Entrypoint Our app's entrypoint is `main.dart`. In this case, there are three versions: The most notable thing is the concrete implementation of the `local_storage_todos_api` is instantiated within each entrypoint. ### Bootstrapping `bootstrap.dart` loads our `BlocObserver` and creates the instance of `TodosRepository`. ### App `App` wraps a `RepositoryProvider` widget that provides the repository to all children. Since both the `EditTodoPage` and `HomePage` subtrees are descendents, all the blocs and cubits can access the repository. `AppView` creates the `MaterialApp` and configures the theme and localizations. ### Theme This provides theme definition for light and dark mode. ### Home The home feature is responsible for managing the state of the currently-selected tab and displays the correct subtree. #### HomeState There are only two states associated with the two screens: `todos` and `stats`. :::note `EditTodo` is a separate route therefore it isn't part of the `HomeState`. ::: #### HomeCubit A cubit is appropriate in this case due to the simplicity of the business logic. We have one method `setTab` to change the tab. #### HomeView `view.dart` is a barrel file that exports all relevant UI components for the home feature. `home_page.dart` contains the UI for the root page that the user will see when the app is launched. A simplified representation of the widget tree for the `HomePage` is: The `HomePage` provides an instance of `HomeCubit` to `HomeView`. `HomeView` uses `context.select` to selectively rebuild whenever the tab changes. This allows us to easily widget test `HomeView` by providing a mock `HomeCubit` and stubbing the state. The `BottomAppBar` contains `HomeTabButton` widgets which call `setTab` on the `HomeCubit`. The instance of the cubit is looked up via `context.read` and the appropriate method is invoked on the cubit instance. :::caution `context.read` doesn't listen for changes, it is just used to access to `HomeCubit` and call `setTab`. ::: ### TodosOverview The todos overview feature allows users to manage their todos by creating, editing, deleting, and filtering todos. #### TodosOverviewEvent Let's create `todos_overview/bloc/todos_overview_event.dart` and define the events. - `TodosOverviewSubscriptionRequested`: This is the startup event. In response, the bloc subscribes to the stream of todos from the `TodosRepository`. - `TodosOverviewTodoDeleted`: This deletes a Todo. - `TodosOverviewTodoCompletionToggled`: This toggles a todo's completed status. - `TodosOverviewToggleAllRequested`: This toggles completion for all todos. - `TodosOverviewClearCompletedRequested`: This deletes all completed todos. - `TodosOverviewUndoDeletionRequested`: This undoes a todo deletion, e.g. an accidental deletion. - `TodosOverviewFilterChanged`: This takes a `TodosViewFilter` as an argument and changes the view by applying a filter. #### TodosOverviewState Let's create `todos_overview/bloc/todos_overview_state.dart` and define the state. `TodosOverviewState` will keep track of a list of todos, the active filter, the `lastDeletedTodo`, and the status. :::note In addition to the default getters and setters, we have a custom getter called `filteredTodos`. The UI uses `BlocBuilder` to access either `state.filteredTodos` or `state.todos`. ::: #### TodosOverviewBloc Let's create `todos_overview/bloc/todos_overview_bloc.dart`. :::note The bloc does not create an instance of the `TodosRepository` internally. Instead, it relies on an instance of the repository to be injected via constructor. ::: ##### onSubscriptionRequested When `TodosOverviewSubscriptionRequested` is added, the bloc starts by emitting a `loading` state. In response, the UI can then render a loading indicator. Next, we use `emit.forEach>( ... )` which creates a subscription on the todos stream from the `TodosRepository`. :::caution `emit.forEach()` is not the same `forEach()` used by lists. This `forEach` enables the bloc to subscribe to a `Stream` and emit a new state for each update from the stream. ::: :::note `stream.listen` is never called directly in this tutorial. Using `await emit.forEach()` is a newer pattern for subscribing to a stream which allows the bloc to manage the subscription internally. ::: Now that the subscription is handled, we will handle the other events, like adding, modifying, and deleting todos. ##### onTodoSaved `_onTodoSaved` simply calls `_todosRepository.saveTodo(event.todo)`. :::note `emit` is never called from within `onTodoSaved` and many other event handlers. Instead, they notify the repository which emits an updated list via the todos stream. See the [data flow](#data-flow) section for more information. ::: ##### Undo The undo feature allows users to restore the last deleted item. `_onTodoDeleted` does two things. First, it emits a new state with the `Todo` to be deleted. Then, it deletes the `Todo` via a call to the repository. `_onUndoDeletionRequested` runs when the undo deletion request event comes from the UI. `_onUndoDeletionRequested` does the following: - Temporarily saves a copy of the last deleted todo. - Updates the state by removing the `lastDeletedTodo`. - Reverts the deletion. ##### Filtering `_onFilterChanged` emits a new state with the new event filter. #### Models There is one model file that deals with the view filtering. `todos_view_filter.dart` is an enum that represents the three view filters and the methods to apply the filter. `models.dart` is the barrel file for exports. Next, let's take a look at the `TodosOverviewPage`. #### TodosOverviewPage A simplified representation of the widget tree for the `TodosOverviewPage` is: Just as with the `Home` feature, the `TodosOverviewPage` provides an instance of the `TodosOverviewBloc` to the subtree via `BlocProvider`. This scopes the `TodosOverviewBloc` to just the widgets below `TodosOverviewPage`. There are three widgets that are listening for changes in the `TodosOverviewBloc`. 1. The first is a `BlocListener` that listens for errors. The `listener` will only be called when `listenWhen` returns `true`. If the status is `TodosOverviewStatus.failure`, a `SnackBar` is displayed. 2. We created a second `BlocListener` that listens for deletions. When a todo has been deleted, a `SnackBar` is displayed with an undo button. If the user taps undo, the `TodosOverviewUndoDeletionRequested` event will be added to the bloc. 3. Finally, we use a `BlocBuilder` to builds the ListView that displays the todos. The `AppBar`contains two actions which are dropdowns for filtering and manipulating the todos. :::note `TodosOverviewTodoCompletionToggled` and `TodosOverviewTodoDeleted` are added to the bloc via `context.read`. ::: `view.dart` is the barrel file that exports `todos_overview_page.dart`. #### Widgets `widgets.dart` is another barrel file that exports all the components used within the `todos_overview` feature. `todo_list_tile.dart` is the `ListTile` for each todo item. `todos_overview_options_button.dart` exposes two options for manipulating todos: - `toggleAll` - `clearCompleted` `todos_overview_filter_button.dart` exposes three filter options: - `all` - `activeOnly` - `completedOnly` ### Stats The stats feature displays statistics about the active and completed todos. #### StatsState `StatsState` keeps track of summary information and the current `StatsStatus`. #### StatsEvent `StatsEvent` has only one event called `StatsSubscriptionRequested`: #### StatsBloc `StatsBloc` depends on the `TodosRepository` just like `TodosOverviewBloc`. It subscribes to the todos stream via `_todosRepository.getTodos`. #### Stats View `view.dart` is the barrel file for the `stats_page`. `stats_page.dart` contains the UI for the page that displays the todos statistics. A simplified representation of the widget tree for the `StatsPage` is: :::caution The `TodosOverviewBloc` and `StatsBloc` both communicate with the `TodosRepository`, but it is important to note there is no direct communication between the blocs. See the [data flow](#data-flow) section for more information. ::: ### EditTodo The `EditTodo` feature allows users to edit an existing todo item and save the changes. #### EditTodoState `EditTodoState` keeps track of the information needed when editing a todo. #### EditTodoEvent The different events the bloc will react to are: - `EditTodoTitleChanged` - `EditTodoDescriptionChanged` - `EditTodoSubmitted` #### EditTodoBloc `EditTodoBloc` depends on the `TodosRepository`, just like `TodosOverviewBloc` and `StatsBloc`. :::caution Unlike the other Blocs, `EditTodoBloc` does not subscribe to `_todosRepository.getTodos`. It is a "write-only" bloc meaning it doesn't need to read any information from the repository. ::: ##### Data Flow Even though there are many features that depend on the same list of todos, there is no bloc-to-bloc communication. Instead, all features are independent of each other and rely on the `TodosRepository` to listen for changes in the list of todos, as well as perform updates to the list. For example, the `EditTodos` doesn't know anything about the `TodosOverview` or `Stats` features. When the UI submits a `EditTodoSubmitted` event: - `EditTodoBloc` handles the business logic to update the `TodosRepository`. - `TodosRepository` notifies `TodosOverviewBloc` and `StatsBloc`. - `TodosOverviewBloc` and `StatsBloc` notify the UI which update with the new state. #### EditTodoPage Just like with the previous features, the `EditTodosPage` provides an instance of the `EditTodosBloc` via `BlocProvider`. Unlike the other features, the `EditTodosPage` is a separate route which is why it exposes a `static` `route` method. This makes it easy to push the `EditTodosPage` onto the navigation stack via `Navigator.of(context).push(...)`. A simplified representation of the widget tree for the `EditTodosPage` is: ## Summary That's it, we have completed the tutorial! 🎉 The full source code for this example, including unit and widget tests, can be found [here](https://github.com/felangel/bloc/tree/master/examples/flutter_todos). ================================================ FILE: docs/src/content/docs/tutorials/flutter-weather.mdx ================================================ --- title: Flutter Weather description: An in-depth guide on how to build a Flutter weather app with bloc. sidebar: order: 5 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-weather/FlutterCreateSnippet.astro'; import FeatureTreeSnippet from '~/components/tutorials/flutter-weather/FeatureTreeSnippet.astro'; import FlutterCreateApiClientSnippet from '~/components/tutorials/flutter-weather/FlutterCreateApiClientSnippet.astro'; import OpenMeteoModelsTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoModelsTreeSnippet.astro'; import LocationJsonSnippet from '~/components/tutorials/flutter-weather/LocationJsonSnippet.astro'; import LocationDartSnippet from '~/components/tutorials/flutter-weather/LocationDartSnippet.astro'; import WeatherJsonSnippet from '~/components/tutorials/flutter-weather/WeatherJsonSnippet.astro'; import WeatherDartSnippet from '~/components/tutorials/flutter-weather/WeatherDartSnippet.astro'; import OpenMeteoModelsBarrelTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoModelsBarrelTreeSnippet.astro'; import OpenMeteoLibrarySnippet from '~/components/tutorials/flutter-weather/OpenMeteoLibrarySnippet.astro'; import BuildRunnerBuildSnippet from '~/components/tutorials/flutter-weather/BuildRunnerBuildSnippet.astro'; import OpenMeteoApiClientTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoApiClientTreeSnippet.astro'; import LocationSearchMethodSnippet from '~/components/tutorials/flutter-weather/LocationSearchMethodSnippet.astro'; import GetWeatherMethodSnippet from '~/components/tutorials/flutter-weather/GetWeatherMethodSnippet.astro'; import FlutterTestCoverageSnippet from '~/components/tutorials/flutter-weather/FlutterTestCoverageSnippet.astro'; import FlutterCreateRepositorySnippet from '~/components/tutorials/flutter-weather/FlutterCreateRepositorySnippet.astro'; import RepositoryModelsBarrelTreeSnippet from '~/components/tutorials/flutter-weather/RepositoryModelsBarrelTreeSnippet.astro'; import WeatherRepositoryLibrarySnippet from '~/components/tutorials/flutter-weather/WeatherRepositoryLibrarySnippet.astro'; import WeatherCubitTreeSnippet from '~/components/tutorials/flutter-weather/WeatherCubitTreeSnippet.astro'; import WeatherBarrelDartSnippet from '~/components/tutorials/flutter-weather/WeatherBarrelDartSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) In this tutorial, we're going to build a Weather app in Flutter which demonstrates how to manage multiple cubits to implement dynamic theming, pull-to-refresh, and much more. Our weather app will pull live weather data from the public OpenMeteo API and demonstrate how to separate our application into layers (data, repository, business logic, and presentation). ![demo](~/assets/tutorials/flutter-weather.gif) ## Project Requirements Our app should let users - Search for a city on a dedicated search page - See a pleasant depiction of the weather data returned by [Open Meteo API](https://open-meteo.com) - Change the units displayed (metric vs imperial) Additionally, - The theme of the application should reflect the weather for the chosen city - Application state should persist across sessions: i.e., the app should remember its state after closing and reopening it (using [HydratedBloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc)) ## Key Concepts - Observe state changes with [BlocObserver](/bloc-concepts#blocobserver). - [BlocProvider](/flutter-bloc-concepts#blocprovider), Flutter widget which provides a bloc to its children. - [BlocBuilder](/flutter-bloc-concepts#blocbuilder), Flutter widget that handles building the widget in response to new states. - Prevent unnecessary rebuilds with [Equatable](/faqs#when-to-use-equatable). - [RepositoryProvider](/flutter-bloc-concepts#repositoryprovider), a Flutter widget which provides a repository to its children. - [BlocListener](/flutter-bloc-concepts#bloclistener), a Flutter widget which invokes the listener code in response to state changes in the bloc. - [MultiBlocProvider](/flutter-bloc-concepts#multiblocprovider), a Flutter widget that merges multiple BlocProvider widgets into one - [BlocConsumer](/flutter-bloc-concepts#blocconsumer), a Flutter widget that exposes a builder and listener in order to react to new states - [HydratedBloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) to manage and persist state ## Setup To begin, create a new flutter project ### Project Structure Our app will consist of isolated features in corresponding directories. This enables us to scale as the number of features increases and allows developers to work on different features in parallel. Our app can be broken down into four main features: **search, settings, theme, weather**. Let's create those directories. ### Architecture Following the [bloc architecture](/architecture) guidelines, our application will consist of several layers. In this tutorial, here's what these layers will do: - **Data**: retrieve raw weather data from the API - **Repository**: abstract the data layer and expose domain models for the application to consume - **Business Logic**: manage the state of each feature (unit information, city details, themes, etc.) - **Presentation**: display weather information and collect input from users (settings page, search page etc.) ## Data Layer For this application we'll be hitting the [Open Meteo API](https://open-meteo.com). We'll be focusing on two endpoints: - `https://geocoding-api.open-meteo.com/v1/search?name=$city&count=1` to get a location for a given city name - `https://api.open-meteo.com/v1/forecast?latitude=$latitude&longitude=$longitude¤t_weather=true` to get the weather for a given location Open [https://geocoding-api.open-meteo.com/v1/search?name=chicago&count=1](https://geocoding-api.open-meteo.com/v1/search?name=chicago&count=1) in your browser to see the response for the city of Chicago. We will use the `latitude` and `longitude` in the response to hit the weather endpoint. The `latitude`/`longitutde` for Chicago is `41.85003`/`-87.65005`. Navigate to [https://api.open-meteo.com/v1/forecast?latitude=43.0389&longitude=-87.90647¤t_weather=true](https://api.open-meteo.com/v1/forecast?latitude=43.0389&longitude=-87.90647¤t_weather=true) in your browser and you'll see the response for weather in Chicago which contains all the data we will need for our app. ### OpenMeteo API Client The OpenMeteo API Client is independent of our application. As a result, we will create it as an internal package (and could even publish it on [pub.dev](https://pub.dev)). We can then use the package by adding it to the `pubspec.yaml` for the repository layer, which will handle data requests for our main weather application. Create a new directory on the project level called `packages`. This directory will store all of our internal packages. Within this directory, run the built-in `flutter create` command to create a new package called `open_meteo_api` for our API client. ### Weather Data Model Next, let's create `location.dart` and `weather.dart` which will contain the models for the `location` and `weather` API endpoint responses. #### Location Model The `location.dart` model should store data returned by the location API, which looks like the following: Here's the in-progress `location.dart` file which stores the above response: #### Weather Model Next, let's work on `weather.dart`. Our weather model should store data returned by the weather API, which looks like the following: Here's the in-progress `weather.dart` file which stores the above response: ### Barrel Files While we're here, let's quickly create a [barrel file](https://adrianfaciu.dev/posts/barrel-files/) to clean up some of our imports down the road. Create a `models.dart` barrel file and export the two models: Let's also create a package level barrel file, `open_meteo_api.dart` In the top level, `open_meteo_api.dart` let's export the models: ### Setup We need to be able to [serialize and deserialize](https://en.wikipedia.org/wiki/Serialization) our models in order to work with the API data. To do this, we will add `toJson` and `fromJson` methods to our models. Additionally, we need a way to [make HTTP network requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) to fetch data from an API. Fortunately, there are a number of popular packages for doing just that. We will be using the [json_annotation](https://pub.dev/packages/json_annotation), [json_serializable](https://pub.dev/packages/json_serializable), and [build_runner](https://pub.dev/packages/build_runner) packages to generate the `toJson` and `fromJson` implementations for us. In a later step, we will also use the [http](https://pub.dev/packages/http) package to send network requests to the weather API so our application can display the current weather data. Let's add these dependencies to the `pubspec.yaml`. :::note Remember to run `flutter pub get` after adding the dependencies. ::: ### (De)Serialization In order for code generation to work, we need to annotate our code using the following: - `@JsonSerializable` to label classes which can be serialized - `@JsonKey` to provide string representations of field names - `@JsonValue` to provide string representations of field values - Implement `JSONConverter` to convert object representations into JSON representations For each file we also need to: - Import `json_annotation` - Include the generated code using the [part](https://dart.dev/tools/pub/create-packages#organizing-a-package) keyword - Include `fromJson` methods for deserialization #### Location Model Here is our complete `location.dart` model file: #### Weather Model Here is our complete `weather.dart` model file: #### Create Build File In the `open_meteo_api` folder, create a `build.yaml` file. The purpose of this file is to handle discrepancies between naming conventions in the `json_serializable` field names. #### Code Generation Let's use `build_runner` to generate the code. `build_runner` should generate the `location.g.dart` and `weather.g.dart` files. ### OpenMeteo API Client Let's create our API client in `open_meteo_api_client.dart` within the `src` directory. Our project structure should now look like this: We can now use the [http](https://pub.dev/packages/http) package we added earlier to the `pubspec.yaml` file to make HTTP requests to the weather API and use this information in our application. Our API client will expose two methods: - `locationSearch` which returns a `Future` - `getWeather` which returns a `Future` #### Location Search The `locationSearch` method hits the location API and throws `LocationRequestFailure` errors as applicable. The completed method looks as follows: #### Get Weather Similarly, the `getWeather` method hits the weather API and throws `WeatherRequestFailure` errors as applicable. The completed method looks as follows: The completed file looks like this: #### Barrel File Updates Let's wrap up this package by adding our API client to the barrel file. ### Unit Tests It's especially important to write unit tests for the data layer since it's the foundation of our application. Unit tests will give us confidence that the package behaves as expected. #### Setup Earlier, we added the [test](https://pub.dev/packages/test) package to our pubspec.yaml which allows to easily write unit tests. We will be creating a test file for the api client as well as the two models. #### Location Tests #### Weather Tests #### API Client Tests Next, let's test our API client. We should test to ensure that our API client handles both API calls correctly, including edge cases. :::note We don't want our tests to make real API calls since our goal is to test the API client logic (including all edge cases) and not the API itself. In order to have a consistent, controlled test environment, we will use [mocktail](https://github.com/felangel/mocktail) (which we added to the pubspec.yaml file earlier) to mock the `http` client. ::: #### Test Coverage Finally, let's gather test coverage to verify that we've covered each line of code with at least one test case. ## Repository Layer The goal of our repository layer is to abstract our data layer and facilitate communication with the bloc layer. In doing this, the rest of our code base depends only on functions exposed by our repository layer instead of specific data provider implementations. This allows us to change data providers without disrupting any of the application-level code. For example, if we decide to migrate away from this particular weather API, we should be able to create a new API client and swap it out without having to make changes to the public API of the repository or application layers. ### Setup Inside the packages directory, run the following command: We will use the same packages as in the `open_meteo_api` package including the `open_meteo_api` package from the last step. Update your `pubspec.yaml` and run `flutter pub get`. :::note We're using a `path` to specify the location of the `open_meteo_api` which allows us to treat it just like an external package from `pub.dev`. ::: ### Weather Repository Models We will be creating a new `weather.dart` file to expose a domain-specific weather model. This model will contain only data relevant to our business cases -- in other words it should be completely decoupled from the API client and raw data format. As usual, we will also create a `models.dart` barrel file. This time, our weather model will only store the `location, temperature, condition` properties. We will also continue to annotate our code to allow for serialization and deserialization. Update the barrel file we created previously to include the models. #### Create Build File As before, we need to create a `build.yaml` file with the following contents: #### Code Generation As we have done previously, run the following command to generate the (de)serialization implementation. #### Barrel File Let's also create a package-level barrel file named `packages/weather_repository/lib/weather_repository.dart` to export our models: ### Weather Repository The main goal of the `WeatherRepository` is to provide an interface which abstracts the data provider. In this case, the `WeatherRepository` will have a dependency on the `WeatherApiClient` and expose a single public method, `getWeather(String city)`. :::note Consumers of the `WeatherRepository` are not privy to the underlying implementation details such as the fact that two network requests are made to the weather API. The goal of the `WeatherRepository` is to separate the "what" from the "how" -- in other words, we want to have a way to fetch weather for a given city, but don't care about how or where that data is coming from. ::: #### Setup Let's create the `weather_repository.dart` file within the `src` directory of our package and work on the repository implementation. The main method we will focus on is `getWeather(String city)`. We can implement it using two calls to the API client as follows: #### Barrel File Update the barrel file we created previously. ### Unit Tests Just as with the data layer, it's critical to test the repository layer in order to make sure the domain level logic is correct. To test our `WeatherRepository`, we will use the [mocktail](https://github.com/felangel/mocktail) library. We will mock the underlying api client in order to unit test the `WeatherRepository` logic in an isolated, controlled environment. ## Business Logic Layer In the business logic layer, we will be consuming the weather domain model from the `WeatherRepository` and exposing a feature-level model which will be surfaced to the user via the UI. :::note This is the third different type of weather model we're implementing. In the API client, our weather model contained all the info returned by the API. In the repository layer, our weather model contained only the abstracted model based on our business case. In this layer, our weather model will contain relevant information needed specifically for the current feature set. ::: ### Setup Because our business logic layer resides in our main app, we need to edit the `pubspec.yaml` for the entire `flutter_weather` project and include all the packages we'll be using. - Using [equatable](https://pub.dev/packages/equatable) enables our app's state class instances to be compared using the equals `==` operator. Under the hood, bloc will compare our states to see if they're equal, and if they're not, it will trigger a rebuild. This guarantees that our widget tree will only rebuild when necessary to keep performance fast and responsive. - We can spice up our user interface with [google_fonts](https://pub.dev/packages/google_fonts). - [HydratedBloc](https://pub.dev/packages/hydrated_bloc) allows us to persist application state when the app is closed and reopened. - We'll include the `weather_repository` package we just created to allow us to fetch the current weather data! For testing, we'll want to include the usual `test` package, along with `mocktail` for mocking dependencies and [bloc_test](https://pub.dev/packages/bloc_test), to enable easy testing of business logic units, or blocs! Next, we will be working on the application layer within the `weather` feature directory. ### Weather Model The goal of our weather model is to keep track of weather data displayed by our app, as well as temperature settings (Celsius or Fahrenheit). Create `flutter_weather/lib/weather/models/weather.dart`: ### Create Build File Create a `build.yaml` file for the business logic layer. ### Code Generation Run `build_runner` to generate the (de)serialization implementations. ### Barrel File Let's export our models from the barrel file (`flutter_weather/lib/weather/models/models.dart`): Then, let's create a top-level weather barrel file (`flutter_weather/lib/weather/weather.dart`); ### Weather We will use `HydratedCubit` to enable our app to remember its application state, even after it's been closed and reopened. :::note `HydratedCubit` is an extension of `Cubit` which handles persisting and restoring state across sessions. ::: #### Weather State Using the [Bloc VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) or [Bloc IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) extension, right click on the `weather` directory and create a new cubit called `Weather`. The project structure should look like this: There are four states our weather app can be in: - `initial` before anything loads - `loading` during the API call - `success` if the API call is successful - `failure` if the API call is unsuccessful The `WeatherStatus` enum will represent the above. The complete weather state should look like this: #### Weather Cubit Now that we've defined the `WeatherState`, let's write the `WeatherCubit` which will expose the following methods: - `fetchWeather(String? city)` uses our weather repository to try and retrieve a weather object for the given city - `refreshWeather()` retrieves a new weather object using the weather repository given the current weather state - `toggleUnits()` toggles the state between Celsius and Fahrenheit - `fromJson(Map json)`, `toJson(WeatherState state)` used for persistence :::note Remember to generate the (de)serialization code via: ::: ### Unit Tests Similar to the data and repository layers, it's critical to unit test the business logic layer to ensure that the feature-level logic behaves as we expect. We will be relying on the [bloc_test](https://pub.dev/packages/bloc_test) in addition to `mocktail` and `test`. Let's add the `test`, `bloc_test`, and `mocktail` packages to the `dev_dependencies`. :::note The [bloc_test](https://pub.dev/packages/bloc_test) package allows us to easily prepare our blocs for testing, handle state changes, and check results in a consistent way. ::: #### Weather Cubit Tests ## Presentation Layer ### Weather Page We will start with the `WeatherPage` which uses `BlocProvider` in order to provide an instance of the `WeatherCubit` to the widget tree. You'll notice that page depends on `SettingsPage` and `SearchPage` widgets, which we will create next. ### SettingsPage The settings page allows users to update their preferences for the temperature units. ### SearchPage The search page allows users to enter the name of their desired city and provides the search result to the previous route via `Navigator.of(context).pop`. ### Weather Widgets The app will display different screens depending on the four possible states of the `WeatherCubit`. #### WeatherEmpty This screen will show when there is no data to display because the user has not yet selected a city. #### WeatherError This screen will display if there is an error. #### WeatherLoading This screen will display as the application fetches the data. #### WeatherPopulated This screen will display after the user has selected a city and we have retrieved the data. ### Barrel File Let's add these states to a barrel file to clean up our imports. ### Entrypoint Our `main.dart` file should initialize our `WeatherApp` and `BlocObserver` (for debugging purposes), as well as setup our `HydratedStorage` to persist state across sessions. Our `app.dart` widget will handle building the `WeatherPage` view we previously created and use `BlocProvider` to inject our `WeatherCubit`. ### Widget Tests The [`bloc_test`](https://pub.dev/packages/bloc_test) library also exposes `MockBlocs` and `MockCubits` which make it easy to test UI. We can mock the states of the various cubits and ensure that the UI reacts correctly. :::note We're using a `MockWeatherCubit` together with the `when` API from `mocktail` in order to stub the state of the cubit in each of the test cases. This allows us to simulate all states and verify the UI behaves correctly under all circumstances. ::: ## Summary That's it, we have completed the tutorial! 🎉 We can run the final app using the `flutter run` command. The full source code for this example, including unit and widget tests, can be found [here](https://github.com/felangel/bloc/tree/master/examples/flutter_weather). ================================================ FILE: docs/src/content/docs/tutorials/github-search.mdx ================================================ --- title: GitHub Search description: An in-depth guide on how to build GitHub Search app in Flutter and AngularDart with bloc. sidebar: order: 9 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import SetupSnippet from '~/components/tutorials/github-search/SetupSnippet.astro'; import DartPubGetSnippet from '~/components/tutorials/github-search/DartPubGetSnippet.astro'; import FlutterCreateSnippet from '~/components/tutorials/github-search/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; import StagehandSnippet from '~/components/tutorials/github-search/StagehandSnippet.astro'; import ActivateStagehandSnippet from '~/components/tutorials/github-search/ActivateStagehandSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) In the following tutorial, we're going to build a GitHub Search app in Flutter and AngularDart to demonstrate how we can share the data and business logic layers between the two projects. ![demo](~/assets/tutorials/flutter-github-search.gif) ![demo](~/assets/tutorials/ngdart-github-search.gif) ## Key Topics - [BlocProvider](/flutter-bloc-concepts#blocprovider), Flutter widget which provides a bloc to its children. - [BlocBuilder](/flutter-bloc-concepts#blocbuilder), Flutter widget that handles building the widget in response to new states. - Using Cubit instead of Bloc. [What's the difference?](/bloc-concepts#cubit-vs-bloc) - Prevent unnecessary rebuilds with [Equatable](/faqs#when-to-use-equatable). - Use a custom `EventTransformer` with [`bloc_concurrency`](https://pub.dev/packages/bloc_concurrency). - Making network requests using the `http` package. ## Common GitHub Search Library The Common GitHub Search library will contain models, the data provider, the repository, as well as the bloc that will be shared between AngularDart and Flutter. ### Setup We'll start off by creating a new directory for our application. :::note The `common_github_search` directory will contain the shared library. ::: We need to create a `pubspec.yaml` with the required dependencies. Lastly, we need to install our dependencies. That's it for the project setup! Now we can get to work on building out the `common_github_search` package. ### Github Client The `GithubClient` which will be providing raw data from the [GitHub API](https://developer.github.com/v3/). :::note You can see a sample of what the data we get back will look like [here](https://api.github.com/search/repositories?q=dartlang). ::: Let's create `github_client.dart`. :::note Our `GithubClient` is simply making a network request to Github's Repository Search API and converting the result into either a `SearchResult` or `SearchResultError` as a `Future`. ::: :::note The `GithubClient` implementation depends on `SearchResult.fromJson`, which we have not yet implemented. ::: Next we need to define our `SearchResult` and `SearchResultError` models. #### Search Result Model Create `search_result.dart`, which represents a list of `SearchResultItems` based on the user's query: :::note The `SearchResult` implementation depends on `SearchResultItem.fromJson`, which we have not yet implemented. ::: :::note We aren't including properties that aren't going to be used in our model. ::: #### Search Result Item Model Next, we'll create `search_result_item.dart`. :::note Again, the `SearchResultItem` implementation dependes on `GithubUser.fromJson`, which we have not yet implemented. ::: #### GitHub User Model Next, we'll create `github_user.dart`. At this point, we have finished implementing `SearchResult` and its dependencies. Now we'll move onto `SearchResultError`. #### Search Result Error Model Create `search_result_error.dart`. Our `GithubClient` is finished so next we'll move onto the `GithubCache`, which will be responsible for [memoizing](https://en.wikipedia.org/wiki/Memoization) as a performance optimization. ### GitHub Cache Our `GithubCache` will be responsible for remembering all past queries so that we can avoid making unnecessary network requests to the GitHub API. This will also help improve our application's performance. Create `github_cache.dart`. Now we're ready to create our `GithubRepository`! ### GitHub Repository The Github Repository is responsible for creating an abstraction between the data layer (`GithubClient`) and the Business Logic Layer (`Bloc`). This is also where we're going to put our `GithubCache` to use. Create `github_repository.dart`. :::note The `GithubRepository` has a dependency on the `GithubCache` and the `GithubClient` and abstracts the underlying implementation. Our application never has to know about how the data is being retrieved or where it's coming from since it shouldn't care. We can change how the repository works at any time and as long as we don't change the interface we shouldn't need to change any client code. ::: At this point, we've completed the data provider layer and the repository layer so we're ready to move on to the business logic layer. ### GitHub Search Event Our Bloc will be notified when a user has typed the name of a repository which we will represent as a `TextChanged` `GithubSearchEvent`. Create `github_search_event.dart`. :::note We extend [`Equatable`](https://pub.dev/packages/equatable) so that we can compare instances of `GithubSearchEvent`. By default, the equality operator returns true if and only if this and other are the same instance. ::: ### Github Search State Our presentation layer will need to have several pieces of information in order to properly lay itself out: - `SearchStateEmpty`- will tell the presentation layer that no input has been given by the user. - `SearchStateLoading`- will tell the presentation layer it has to display some sort of loading indicator. - `SearchStateSuccess`- will tell the presentation layer that it has data to present. - `items`- will be the `List` which will be displayed. - `SearchStateError`- will tell the presentation layer that an error has occurred while fetching repositories. - `error`- will be the exact error that occurred. We can now create `github_search_state.dart` and implement it like so. :::note We extend [`Equatable`](https://pub.dev/packages/equatable) so that we can compare instances of `GithubSearchState`. By default, the equality operator returns true if and only if this and other are the same instance. ::: Now that we have our Events and States implemented, we can create our `GithubSearchBloc`. ### GitHub Search Bloc Create `github_search_bloc.dart`: :::note Our `GithubSearchBloc` converts `GithubSearchEvent` to `GithubSearchState` and has a dependency on the `GithubRepository`. ::: :::note We create a custom `EventTransformer` to [debounce](https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/debounce.html) the `GithubSearchEvents`. One of the reasons why we created a `Bloc` instead of a `Cubit` was to take advantage of stream transformers. ::: Awesome! We're all done with our `common_github_search` package. The finished product should look like [this](https://github.com/felangel/bloc/tree/master/examples/github_search/common_github_search). Next, we'll work on the Flutter implementation. ## Flutter GitHub Search Flutter Github Search will be a Flutter application which reuses the models, data providers, repositories, and blocs from `common_github_search` to implement Github Search. ### Setup We need to start by creating a new Flutter project in our `github_search` directory at the same level as `common_github_search`. Next, we need to update our `pubspec.yaml` to include all the necessary dependencies. :::note We are including our newly created `common_github_search` library as a dependency. ::: Now, we need to install the dependencies. That's it for project setup. Since the `common_github_search` package contains our data layer as well as our business logic layer, all we need to build is the presentation layer. ### Search Form We're going to need to create a form with a `_SearchBar` and `_SearchBody` widget. - `_SearchBar` will be responsible for taking user input. - `_SearchBody` will be responsible for displaying search results, loading indicators, and errors. Let's create `search_form.dart`. Our `SearchForm` will be a `StatelessWidget` which renders the `_SearchBar` and `_SearchBody` widgets. `_SearchBar` is also going to be a `StatefulWidget` because it will need to maintain its own `TextEditingController` so that we can keep track of what a user has entered as input. `_SearchBody` is a `StatelessWidget` which will be responsible for displaying search results, errors, and loading indicators. It will be the consumer of the `GithubSearchBloc`. If our state is `SearchStateSuccess`, we render `_SearchResults` which we will implement next. `_SearchResults` is a `StatelessWidget` which takes a `List` and displays them as a list of `_SearchResultItems`. `_SearchResultItem` is a `StatelessWidget` and is responsible for rendering the information for a single search result. It is also responsible for handling user interaction and navigating to the repository url on a user tap. :::note `_SearchBar` accesses `GitHubSearchBloc` via `context.read()` and notifies the bloc of `TextChanged` events. ::: :::note `_SearchBody` uses `BlocBuilder` in order to rebuild in response to state changes. Since the bloc parameter of the `BlocBuilder` object was omitted, `BlocBuilder` will automatically perform a lookup using `BlocProvider` and the current `BuildContext`. Read more [here.](/flutter-bloc-concepts#blocbuilder) ::: :::note We use `ListView.builder` in order to construct a scrollable list of `_SearchResultItem`. ::: :::note We use the [url_launcher](https://pub.dev/packages/url_launcher) package to open external urls. ::: ### Putting it all together Now all that's left to do is implement our main app in `main.dart`. :::note Our `GithubRepository` is created in `main` and injected into our `App`. Our `SearchForm` is wrapped in a `BlocProvider` which is responsible for initializing, closing, and making the instance of `GithubSearchBloc` available to the `SearchForm` widget and its children. ::: That's all there is to it! We've now successfully implemented a GitHub search app in Flutter using the [bloc](https://pub.dev/packages/bloc) and [flutter_bloc](https://pub.dev/packages/flutter_bloc) packages and we've successfully separated our presentation layer from our business logic. The full source can be found [here](https://github.com/felangel/bloc/tree/master/examples/github_search/flutter_github_search). Finally, we're going to build our AngularDart GitHub Search app. ## AngularDart GitHub Search AngularDart GitHub Search will be an AngularDart application which reuses the models, data providers, repositories, and blocs from `common_github_search` to implement Github Search. ### Setup We need to start by creating a new AngularDart project in our github_search directory at the same level as `common_github_search`. :::note You can install `stagehand` via: ::: We can then go ahead and replace the contents of `pubspec.yaml` with: ### Search Form Just like in our Flutter app, we're going to need to create a `SearchForm` with a `SearchBar` and `SearchBody` component. Our `SearchForm` component will implement `OnInit` and `OnDestroy` because it will need to create and close a `GithubSearchBloc`. - `SearchBar` will be responsible for taking user input. - `SearchBody` will be responsible for displaying search results, loading indicators, and errors. Let's create `search_form_component.dart.` :::note The `GithubRepository` is injected into the `SearchFormComponent`. ::: :::note The `GithubSearchBloc` is created and closed by the `SearchFormComponent`. ::: Our template (`search_form_component.html`) will look like: Next, we'll implement the `SearchBar` component. ### Search Bar `SearchBar` is a component which will be responsible for taking in user input and notifying the `GithubSearchBloc` of text changes. Create `search_bar_component.dart`. :::note `SearchBarComponent` has a dependency on `GitHubSearchBloc` because it is responsible for notifying the bloc of `TextChanged` events. ::: Next, we can create `search_bar_component.html`. We're done with `SearchBar`, now onto `SearchBody`. ### Search Body `SearchBody` is a component which will be responsible for displaying search results, errors, and loading indicators. It will be the consumer of the `GithubSearchBloc`. Create `search_body_component.dart`. :::note `SearchBodyComponent` has a dependency on `GithubSearchState` which is provided by the `GithubSearchBloc` using the `angular_bloc` bloc pipe. ::: Create `search_body_component.html`. If our state `isSuccess`, we render `SearchResults`. We will implement it next. ### Search Results `SearchResults` is a component which takes a `List` and displays them as a list of `SearchResultItems`. Create `search_results_component.dart`. Next up we'll create `search_results_component.html`. :::note We use `ngFor` in order to construct a list of `SearchResultItem` components. ::: It's time to implement `SearchResultItem`. ### Search Result Item `SearchResultItem` is a component that is responsible for rendering the information for a single search result. It is also responsible for handling user interaction and navigating to the repository url on a user tap. Create `search_result_item_component.dart`. and the corresponding template in `search_result_item_component.html`. ### Putting it all together We have all of our components and now it's time to put them all together in our `app_component.dart`. :::note We're creating the `GithubRepository` in the `AppComponent` and injecting it into the `SearchForm` component. ::: That's all there is to it! We've now successfully implemented a GitHub search app in AngularDart using the `bloc` and `angular_bloc` packages and we've successfully separated our presentation layer from our business logic. The full source can be found [here](https://github.com/felangel/bloc/tree/master/examples/github_search/angular_github_search). ## Summary In this tutorial we created a Flutter and AngularDart app while sharing all of the models, data providers, and blocs between the two. The only thing we actually had to write twice was the presentation layer (UI) which is awesome in terms of efficiency and development speed. In addition, it's fairly common for web apps and mobile apps to have different user experiences and styles and this approach really demonstrates how easy it is to build two apps that look totally different but share the same data and business logic layers. The full source can be found [here](https://github.com/felangel/bloc/tree/master/examples/github_search). ================================================ FILE: docs/src/content/docs/tutorials/ngdart-counter.mdx ================================================ --- title: AngularDart Counter description: An in-depth guide on how to build an AngularDart counter app with bloc. sidebar: order: 8 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import ActivateStagehandSnippet from '~/components/tutorials/ngdart-counter/ActivateStagehandSnippet.astro'; import StagehandSnippet from '~/components/tutorials/ngdart-counter/StagehandSnippet.astro'; import InstallDependenciesSnippet from '~/components/tutorials/ngdart-counter/InstallDependenciesSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) In the following tutorial, we're going to build a Counter in AngularDart using the Bloc library. ![demo](~/assets/tutorials/ngdart-counter.gif) ## Setup We'll start off by creating a brand new AngularDart project with [stagehand](https://github.com/dart-lang/stagehand). If you don't have stagehand installed, activate it via: Then generate a new project via: We can then go ahead and replace the contents of `pubspec.yaml` with: and then install all of our dependencies Our counter app is just going to have two buttons to increment/decrement the counter value and an element to display the current value. Let's get started designing the `CounterEvents`. ## Counter Bloc Since our counter's state can be represented by an integer we don't need to create a custom class and we can co-locate the events and bloc. :::note Just from the class declaration we can tell that our `CounterBloc` will be taking `CounterEvents` as input and outputting integers. ::: ## Counter App Now that we have our `CounterBloc` fully implemented, we can get started creating our AngularDart App Component. Our `app.component.dart` should look like: and our `app.component.html` should look like: ## Counter Page Finally, all that's left is to build our Counter Page Component. Our `counter_page_component.dart` should look like: :::note We are able to access the `CounterBloc` instance using AngularDart's dependency injection system. Because we have registered it as a `Provider`, AngularDart can properly resolve `CounterBloc`. ::: :::note We are closing the `CounterBloc` in `ngOnDestroy`. ::: :::note We are importing the `BlocPipe` so that we can use it in our template. ::: Lastly, our `counter_page_component.html` should look like: :::note We are using the `BlocPipe` so that we can display our `CounterBloc` state as it is updated. ::: That's it! We've separated our presentation layer from our business logic layer. Our `CounterPageComponent` has no idea what happens when a user presses a button; it just adds an event to notify the `CounterBloc`. Furthermore, our `CounterBloc` has no idea what is happening with the state (counter value); it's simply converting the `CounterEvents` into integers. We can run our app with `webdev serve` and can view it locally. The full source for this example can be found [here](https://github.com/felangel/bloc/tree/master/examples/angular_counter). ================================================ FILE: docs/src/content/docs/uk/architecture.mdx ================================================ --- title: Архітектура description: Огляд рекомендованих архітектурних шаблонів при використанні bloc. --- import DataProviderSnippet from '~/components/architecture/DataProviderSnippet.astro'; import RepositorySnippet from '~/components/architecture/RepositorySnippet.astro'; import BusinessLogicComponentSnippet from '~/components/architecture/BusinessLogicComponentSnippet.astro'; import BlocTightCouplingSnippet from '~/components/architecture/BlocTightCouplingSnippet.astro'; import BlocLooseCouplingPresentationSnippet from '~/components/architecture/BlocLooseCouplingPresentationSnippet.astro'; import AppIdeasRepositorySnippet from '~/components/architecture/AppIdeasRepositorySnippet.astro'; import AppIdeaRankingBlocSnippet from '~/components/architecture/AppIdeaRankingBlocSnippet.astro'; import PresentationComponentSnippet from '~/components/architecture/PresentationComponentSnippet.astro'; ![Архітектура Bloc](~/assets/concepts/bloc_architecture_full.png) Використання бібліотеки bloc дозволяє нам розділити наш додаток на три шари: - Представлення - Бізнес-логіка - Дані - Сховище - Постачальник даних Ми почнемо з найнижчого шару (найбільш віддаленого від користувацького інтерфейсу) та рухатимемося вгору до шару представлення. ## Шар даних Відповідальність шару даних полягає в отриманні/маніпулюванні даними з одного або кількох джерел. Шар даних можна розділити на дві частини: - Сховище - Постачальник даних Цей шар є найнижчим рівнем додатку та взаємодіє з базами даних, мережевими запитами та іншими асинхронними джерелами даних. ### Постачальник даних Відповідальність постачальника даних полягає в наданні необроблених даних. Постачальник даних повинен бути універсальним та багатофункціональним. Постачальник даних зазвичай надає прості API для виконання [CRUD](https://uk.wikipedia.org/wiki/CRUD) операцій. Ми можемо мати методи `createData`, `readData`, `updateData` та `deleteData` як частину нашого шару даних. ### Сховище Шар сховища — це обгортка навколо одного або кількох постачальників даних, з якими спілкується шар Bloc. Як ви можете бачити, наш шар сховища може взаємодіяти з кількома постачальниками даних та виконувати перетворення даних перед передачею результату на шар бізнес-логіки. ## Шар бізнес-логіки Відповідальність шару бізнес-логіки полягає у відповіді на введення з шару представлення новими станами. Цей шар може залежати від одного або кількох сховищ для отримання даних, необхідних для побудови стану додатку. Думайте про шар бізнес-логіки як про міст між користувацьким інтерфейсом (шар представлення) та шаром даних. Шар бізнес-логіки сповіщається про події/дії з шару представлення, а потім взаємодіє зі сховищем, щоб побудувати новий стан для використання шаром представлення. ### Взаємодія між блоками Оскільки блоки надають потоки, може виникнути спокуса створити блок, який прослуховує інший блок. Ви **не повинні** робити цього. Існують кращі альтернативи, ніж вдаватися до коду нижче: Хоча наведений вище код не містить помилок (і навіть очищується за собою), він має більш серйозну проблему: він створює залежність між двома блоками. Як правило, залежностей між двома сутностями на одному архітектурному шарі слід уникати за будь-яку ціну, оскільки це створює тісний зв'язок, який важко підтримувати. Оскільки блоки знаходяться на архітектурному шарі бізнес-логіки, жоден блок не повинен знати про будь-який інший блок. ![Шари архітектури додатку](~/assets/architecture/architecture.png) Блок повинен отримувати інформацію лише через події та з впроваджених сховищ (тобто сховищ, переданих блоку в його конструкторі). Якщо ви перебуваєте в ситуації, коли блок повинен реагувати на інший блок, у вас є два інших варіанти. Ви можете перемістити проблему на шар вище (у шар представлення) або на шар нижче (у шар домену). #### З'єднання блоків через представлення Ви можете використовувати `BlocListener` для прослуховування одного блоку та додавання події до іншого блоку щоразу, коли перший блок змінюється. Наведений вище код запобігає необхідності `SecondBloc` знати про `FirstBloc`, заохочуючи слабкий зв'язок. Додаток [flutter_weather](/uk/tutorials/flutter-weather) [використовує цю техніку](https://github.com/felangel/bloc/blob/b4c8db938ad71a6b60d4a641ec357905095c3965/examples/flutter_weather/lib/weather/view/weather_page.dart#L38-L42) для зміни теми додатку на основі отриманої інформації про погоду. У деяких ситуаціях ви можете не захотіти зв'язувати два блоки в шарі представлення. Замість цього часто має сенс, щоб два блоки використовували одне й те саме джерело даних та оновлювалися при зміні даних. #### З'єднання блоків через домен Два блоки можуть прослуховувати потік зі сховища та оновлювати свої стани незалежно один від одного щоразу, коли змінюються дані сховища. Використання реактивних сховищ для синхронізації стану є поширеним у великомасштабних корпоративних додатках. Спочатку створіть або використовуйте сховище, яке надає `Stream` даних. Наприклад, наступне сховище надає нескінченний потік тих самих кількох ідей додатків: Те саме сховище може бути впроваджене в кожний блок, який повинен реагувати на нові ідеї додатків. Нижче наведено `AppIdeaRankingBloc`, який видає стан для кожної вхідної ідеї додатку зі сховища вище: Докладніше про використання потоків з Bloc див. у статті [Як використовувати Bloc з потоками та конкурентністю](https://verygood.ventures/blog/how-to-use-bloc-with-streams-and-concurrency). ## Шар представлення Відповідальність шару представлення полягає у визначенні того, як відмалювати себе на основі одного або кількох станів блоків. Крім того, він повинен обробляти введення користувача та події життєвого циклу додатку. Більшість потоків додатків починаються з події `AppStart`, яка запускає додаток для отримання деяких даних для представлення користувачеві. У цьому сценарії шар представлення додасть подію `AppStart`. Крім того, шар представлення повинен буде визначити, що відмалювати на екрані на основі стану з шару bloc. До цього моменту, хоча у нас були деякі фрагменти коду, все це було досить високорівневим. У розділі посібників ми об'єднаємо все це разом, коли будемо створювати кілька різних прикладів додатків. ================================================ FILE: docs/src/content/docs/uk/bloc-concepts.mdx ================================================ --- title: Концепції Bloc description: Огляд основних концепцій для package:bloc. sidebar: order: 1 --- import CountStreamSnippet from '~/components/concepts/bloc/CountStreamSnippet.astro'; import SumStreamSnippet from '~/components/concepts/bloc/SumStreamSnippet.astro'; import StreamsMainSnippet from '~/components/concepts/bloc/StreamsMainSnippet.astro'; import CounterCubitSnippet from '~/components/concepts/bloc/CounterCubitSnippet.astro'; import CounterCubitInitialStateSnippet from '~/components/concepts/bloc/CounterCubitInitialStateSnippet.astro'; import CounterCubitInstantiationSnippet from '~/components/concepts/bloc/CounterCubitInstantiationSnippet.astro'; import CounterCubitIncrementSnippet from '~/components/concepts/bloc/CounterCubitIncrementSnippet.astro'; import CounterCubitBasicUsageSnippet from '~/components/concepts/bloc/CounterCubitBasicUsageSnippet.astro'; import CounterCubitStreamUsageSnippet from '~/components/concepts/bloc/CounterCubitStreamUsageSnippet.astro'; import CounterCubitOnChangeSnippet from '~/components/concepts/bloc/CounterCubitOnChangeSnippet.astro'; import CounterCubitOnChangeUsageSnippet from '~/components/concepts/bloc/CounterCubitOnChangeUsageSnippet.astro'; import CounterCubitOnChangeOutputSnippet from '~/components/concepts/bloc/CounterCubitOnChangeOutputSnippet.astro'; import SimpleBlocObserverOnChangeSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeSnippet.astro'; import SimpleBlocObserverOnChangeUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeUsageSnippet.astro'; import SimpleBlocObserverOnChangeOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeOutputSnippet.astro'; import CounterCubitOnErrorSnippet from '~/components/concepts/bloc/CounterCubitOnErrorSnippet.astro'; import SimpleBlocObserverOnErrorSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnErrorSnippet.astro'; import CounterCubitOnErrorOutputSnippet from '~/components/concepts/bloc/CounterCubitOnErrorOutputSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/bloc/CounterBlocSnippet.astro'; import CounterBlocEventHandlerSnippet from '~/components/concepts/bloc/CounterBlocEventHandlerSnippet.astro'; import CounterBlocIncrementSnippet from '~/components/concepts/bloc/CounterBlocIncrementSnippet.astro'; import CounterBlocUsageSnippet from '~/components/concepts/bloc/CounterBlocUsageSnippet.astro'; import CounterBlocStreamUsageSnippet from '~/components/concepts/bloc/CounterBlocStreamUsageSnippet.astro'; import CounterBlocOnChangeSnippet from '~/components/concepts/bloc/CounterBlocOnChangeSnippet.astro'; import CounterBlocOnChangeUsageSnippet from '~/components/concepts/bloc/CounterBlocOnChangeUsageSnippet.astro'; import CounterBlocOnChangeOutputSnippet from '~/components/concepts/bloc/CounterBlocOnChangeOutputSnippet.astro'; import CounterBlocOnTransitionSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionSnippet.astro'; import CounterBlocOnTransitionOutputSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionOutputSnippet.astro'; import SimpleBlocObserverOnTransitionSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionSnippet.astro'; import SimpleBlocObserverOnTransitionUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionUsageSnippet.astro'; import SimpleBlocObserverOnTransitionOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionOutputSnippet.astro'; import CounterBlocOnEventSnippet from '~/components/concepts/bloc/CounterBlocOnEventSnippet.astro'; import SimpleBlocObserverOnEventSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventSnippet.astro'; import SimpleBlocObserverOnEventOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventOutputSnippet.astro'; import CounterBlocOnErrorSnippet from '~/components/concepts/bloc/CounterBlocOnErrorSnippet.astro'; import CounterBlocOnErrorOutputSnippet from '~/components/concepts/bloc/CounterBlocOnErrorOutputSnippet.astro'; import CounterCubitFullSnippet from '~/components/concepts/bloc/CounterCubitFullSnippet.astro'; import CounterBlocFullSnippet from '~/components/concepts/bloc/CounterBlocFullSnippet.astro'; import AuthenticationStateSnippet from '~/components/concepts/bloc/AuthenticationStateSnippet.astro'; import AuthenticationTransitionSnippet from '~/components/concepts/bloc/AuthenticationTransitionSnippet.astro'; import AuthenticationChangeSnippet from '~/components/concepts/bloc/AuthenticationChangeSnippet.astro'; import DebounceEventTransformerSnippet from '~/components/concepts/bloc/DebounceEventTransformerSnippet.astro'; :::note Будь ласка, уважно прочитайте наступні розділи перед початком роботи з [`package:bloc`](https://pub.dev/packages/bloc). ::: Існує кілька ключових концепцій, які є критично важливими для розуміння того, як використовувати пакет bloc. У наступних розділах ми детально обговоримо кожну з них, а також розглянемо, як вони застосовуються на прикладі додатку-лічильника. ## Потоки (Streams) :::note Ознайомтеся з офіційною [документацією Dart](https://dart.dev/tutorials/language/streams) для отримання додаткової інформації про `Streams`. ::: Потік (stream) — це послідовність асинхронних даних. Для використання бібліотеки bloc критично важливо мати базове розуміння `Streams` та того, як вони працюють. Якщо ви не знайомі з `Streams`, просто уявіть трубу з водою, що тече через неї. Труба — це `Stream`, а вода — це асинхронні дані. Ми можемо створити `Stream` у Dart, написавши функцію `async*` (асинхронний генератор). Позначаючи функцію як `async*`, ми отримуємо можливість використовувати ключове слово `yield` та повертати `Stream` даних. У наведеному вище прикладі ми повертаємо `Stream` цілих чисел до значення параметра `max`. Щоразу, коли ми використовуємо `yield` у функції `async*`, ми проштовхуємо цей фрагмент даних через `Stream`. Ми можемо використовувати вищевказаний `Stream` кількома способами. Якби ми хотіли написати функцію для повернення суми `Stream` цілих чисел, вона могла б виглядати так: Позначаючи вищевказану функцію як `async`, ми отримуємо можливість використовувати ключове слово `await` та повертати `Future` цілих чисел. У цьому прикладі ми очікуємо кожне значення в потоці та повертаємо суму всіх цілих чисел у потоці. Ми можемо зібрати все це разом наступним чином: Тепер, коли ми маємо базове розуміння того, як працюють `Streams` у Dart, ми готові дізнатися про основний компонент пакету bloc: `Cubit`. ## Cubit `Cubit` — це клас, який розширює `BlocBase` та може бути розширений для керування будь-яким типом стану. ![Cubit Architecture](~/assets/concepts/cubit_architecture_full.png) `Cubit` може надавати функції, які можна викликати для ініціювання змін стану. Стани — це вихідні дані `Cubit` і представляють частину стану вашого додатку. Компоненти UI можуть бути сповіщені про стани та перемальовувати частини себе на основі поточного стану. :::note Для отримання додаткової інформації про походження `Cubit` ознайомтеся з [цим issue](https://github.com/felangel/cubit/issues/69). ::: ### Створення Cubit Ми можемо створити `CounterCubit` наступним чином: При створенні `Cubit` нам необхідно визначити тип стану, яким буде керувати `Cubit`. У випадку `CounterCubit` вище стан може бути представлений через `int`, але в більш складних випадках може бути необхідно використовувати `class` замість примітивного типу. Друге, що нам потрібно зробити при створенні `Cubit`, — це вказати початковий стан. Ми можемо зробити це, викликавши `super` зі значенням початкового стану. У наведеному вище фрагменті ми встановлюємо початковий стан у `0` внутрішньо, але ми також можемо дозволити `Cubit` бути більш гнучким, приймаючи зовнішнє значення: Це дозволило б нам створювати екземпляри `CounterCubit` з різними початковими станами, наприклад: ### Зміни стану Cubit Кожний `Cubit` має можливість видавати новий стан через `emit`. У наведеному вище фрагменті `CounterCubit` надає публічний метод `increment`, який може бути викликаний ззовні для сповіщення `CounterCubit` про збільшення його стану. Коли викликається `increment`, ми можемо отримати доступ до поточного стану `Cubit` через геттер `state` та викликати `emit` нового стану, додавши 1 до поточного стану. :::caution Метод `emit` є захищеним, що означає, що він повинен використовуватися лише всередині `Cubit`. ::: ### Використання Cubit Тепер ми можемо взяти реалізований `CounterCubit` та використати його! #### Базове використання У наведеному вище фрагменті ми починаємо зі створення екземпляра `CounterCubit`. Потім ми виводимо поточний стан cubit, який є початковим станом (оскільки нові стани ще не були видані). Далі ми викликаємо функцію `increment` для ініціювання зміни стану. Нарешті, ми знову виводимо стан `Cubit`, який змінився з `0` на `1`, та викликаємо `close` на `Cubit` для закриття внутрішнього потоку станів. #### Використання Stream `Cubit` надає `Stream`, який дозволяє нам отримувати оновлення стану в реальному часі: У наведеному вище фрагменті ми підписуємося на `CounterCubit` та викликаємо print при кожній зміні стану. Потім ми викликаємо функцію `increment`, яка видасть новий стан. Нарешті, ми викликаємо `cancel` на `subscription`, коли більше не хочемо отримувати оновлення, та закриваємо `Cubit`. :::note `await Future.delayed(Duration.zero)` додано для цього прикладу, щоб уникнути негайного скасування підписки. ::: :::caution Лише наступні зміни стану будуть отримані при виклику `listen` на `Cubit`. ::: ### Спостереження за Cubit Коли `Cubit` видає новий стан, відбувається `Change`. Ми можемо спостерігати за всіма змінами для даного `Cubit`, перевизначивши `onChange`. Потім ми можемо взаємодіяти з `Cubit` та спостерігати за всіма змінами, що виводяться в консоль. Наведений вище приклад виведе: :::note `Change` відбувається безпосередньо перед оновленням стану `Cubit`. `Change` складається з `currentState` та `nextState`. ::: #### BlocObserver Одним з додаткових переваг використання бібліотеки bloc є те, що ми можемо мати доступ до всіх `Changes` в одному місці. Хоча в цьому додатку у нас є лише один `Cubit`, у більших додатках досить часто зустрічається багато `Cubits`, що керують різними частинами стану додатку. Якщо ми хочемо мати можливість щось робити у відповідь на всі `Changes`, ми можемо просто створити власний `BlocObserver`. :::note Все, що нам потрібно зробити, — це розширити `BlocObserver` та перевизначити метод `onChange`. ::: Щоб використовувати `SimpleBlocObserver`, нам просто потрібно змінити функцію `main`: Наведений вище фрагмент потім виведе: :::note Внутрішнє перевизначення `onChange` викликається першим, яке викликає `super.onChange`, сповіщаючи `onChange` у `BlocObserver`. ::: :::tip У `BlocObserver` ми маємо доступ до екземпляра `Cubit` на додаток до самого `Change`. ::: ### Обробка помилок у Cubit Кожний `Cubit` має метод `addError`, який можна використовувати для позначення того, що сталася помилка. :::note `onError` може бути перевизначений всередині `Cubit` для обробки всіх помилок для конкретного `Cubit`. ::: `onError` також може бути перевизначений у `BlocObserver` для глобальної обробки всіх повідомлених помилок. Якщо ми знову запустимо ту ж програму, ми повинні побачити наступний вивід: ## Bloc `Bloc` — це більш просунутий клас, який покладається на `події` для ініціювання змін `стану`, а не на функції. `Bloc` також розширює `BlocBase`, що означає, що він має аналогічний публічний API, як `Cubit`. Однак замість виклику `функції` на `Bloc` та безпосереднього видавання нового `стану`, `Bloc`-и отримують `події` та перетворюють вхідні `події` у вихідні `стани`. ![Bloc Architecture](~/assets/concepts/bloc_architecture_full.png) ### Створення Bloc Створення `Bloc` аналогічне створенню `Cubit`, за винятком того, що на додаток до визначення стану, яким ми будемо керувати, ми також повинні визначити подію, яку `Bloc` зможе обробляти. Події — це вхідні дані для Bloc. Вони зазвичай додаються у відповідь на взаємодії користувача, такі як натискання кнопок, або події життєвого циклу, такі як завантаження сторінки. Так само, як при створенні `CounterCubit`, ми повинні вказати початковий стан, передавши його в суперклас через `super`. ### Зміни стану Bloc `Bloc` вимагає, щоб ми реєстрували обробники подій через API `on`, на відміну від функцій у `Cubit`. Обробник подій відповідає за перетворення будь-яких вхідних подій у нуль або більше вихідних станів. :::tip `EventHandler` має доступ до доданої події, а також до `Emitter`, який може використовуватися для видавання нуля або більше станів у відповідь на вхідну подію. ::: Потім ми можемо оновити `EventHandler` для обробки події `CounterIncrementPressed`: У наведеному вище фрагменті ми зареєстрували `EventHandler` для керування всіма подіями `CounterIncrementPressed`. Для кожної вхідної події `CounterIncrementPressed` ми можемо отримати доступ до поточного стану bloc через геттер `state` та викликати `emit(state + 1)`. :::note Оскільки клас `Bloc` розширює `BlocBase`, ми маємо доступ до поточного стану bloc у будь-який момент часу через геттер `state`, так само як у `Cubit`. ::: :::caution Bloc-и ніколи не повинні безпосередньо викликати `emit` для нових станів. Замість цього кожна зміна стану повинна бути виведена у відповідь на вхідну подію всередині `EventHandler`. ::: :::caution І bloc-и, і cubit-и будуть ігнорувати дублікати станів. Якщо ми видамо `State nextState`, де `state == nextState`, то зміна стану не відбудеться. ::: ### Використання Bloc На цьому етапі ми можемо створити екземпляр нашого `CounterBloc` та використати його! #### Базове використання У наведеному вище фрагменті ми починаємо зі створення екземпляра `CounterBloc`. Потім ми виводимо поточний стан `Bloc`, який є початковим станом (оскільки нові стани ще не були видані). Далі ми додаємо подію `CounterIncrementPressed` для ініціювання зміни стану. Нарешті, ми знову виводимо стан `Bloc`, який змінився з `0` на `1`, та викликаємо `close` на `Bloc` для закриття внутрішнього потоку станів. :::note `await Future.delayed(Duration.zero)` додано, щоб переконатися, що ми чекаємо наступної ітерації циклу подій (дозволяючи `EventHandler` обробити подію). ::: #### Використання Stream Так само, як з `Cubit`, `Bloc` — це спеціальний тип `Stream`, що означає, що ми також можемо підписатися на `Bloc` для отримання оновлень його стану в реальному часі: У наведеному вище фрагменті ми підписуємося на `CounterBloc` та викликаємо print при кожній зміні стану. Потім ми додаємо подію `CounterIncrementPressed`, яка запускає `EventHandler` `on` та видає новий стан. Нарешті, ми викликаємо `cancel` на підписці, коли більше не хочемо отримувати оновлення, та закриваємо `Bloc`. :::note `await Future.delayed(Duration.zero)` додано для цього прикладу, щоб уникнути негайного скасування підписки. ::: ### Спостереження за Bloc Оскільки `Bloc` розширює `BlocBase`, ми можемо спостерігати за всіма змінами стану для `Bloc` за допомогою `onChange`. Потім ми можемо оновити `main.dart` до: Тепер, якщо ми запустимо наведений вище фрагмент, вивід буде: Однією з ключових відмінностей між `Bloc` та `Cubit` є те, що оскільки `Bloc` керується подіями, ми також можемо захопити інформацію про те, що спричинило зміну стану. Ми можемо зробити це, перевизначивши `onTransition`. Зміна від одного стану до іншого називається `Transition`. `Transition` складається з поточного стану, події та наступного стану. Якщо ми потім повторно запустимо той самий фрагмент `main.dart` як раніше, ми повинні побачити наступний вивід: :::note `onTransition` викликається перед `onChange` та містить подію, яка спричинила зміну від `currentState` до `nextState`. ::: #### BlocObserver Так само, як і раніше, ми можемо перевизначити `onTransition` у користувацькому `BlocObserver` для спостереження за всіма переходами, що відбуваються з одного місця. Ми можемо ініціалізувати `SimpleBlocObserver` так само, як і раніше: Тепер, якщо ми запустимо наведений вище фрагмент, вивід повинен виглядати так: :::note `onTransition` викликається першим (локальний перед глобальним), а потім `onChange`. ::: Ще однією унікальною особливістю екземплярів `Bloc` є те, що вони дозволяють нам перевизначити `onEvent`, який викликається щоразу, коли нова подія додається до `Bloc`. Так само, як з `onChange` та `onTransition`, `onEvent` може бути перевизначений локально, а також глобально. Ми можемо запустити той самий `main.dart`, як і раніше, та повинні побачити наступний вивід: :::note `onEvent` викликається, як тільки подію додано. Локальний `onEvent` викликається перед глобальним `onEvent` у `BlocObserver`. ::: ### Обробка помилок у Bloc Так само, як з `Cubit`, кожний `Bloc` має методи `addError` та `onError`. Ми можемо вказати, що сталася помилка, викликавши `addError` з будь-якого місця всередині нашого `Bloc`. Потім ми можемо реагувати на всі помилки, перевизначивши `onError`, так само як з `Cubit`. Якщо ми повторно запустимо той самий `main.dart`, як раніше, ми можемо побачити, як це виглядає, коли про помилку повідомляється: :::note Локальний `onError` викликається першим, а потім глобальний `onError` у `BlocObserver`. ::: :::note `onError` та `onChange` працюють абсолютно однаково для екземплярів як `Bloc`, так і `Cubit`. ::: :::caution Будь-які необроблені винятки, що виникають всередині `EventHandler`, також повідомляються до `onError`. ::: ## Cubit проти Bloc Тепер, коли ми розглянули основи класів `Cubit` та `Bloc`, вам може бути цікаво, коли слід використовувати `Cubit`, а коли — `Bloc`. ### Переваги Cubit #### Простота Однією з найбільших переваг використання `Cubit` є простота. При створенні `Cubit` нам потрібно визначити лише стан, а також функції, які ми хочемо надати для зміни стану. Для порівняння, при створенні `Bloc` ми повинні визначити стани, події та реалізацію `EventHandler`. Це робить `Cubit` більш зрозумілим та потребує менше коду. Тепер давайте розглянемо дві реалізації лічильника: ##### CounterCubit ##### CounterBloc Реалізація `Cubit` є більш лаконічною, і замість окремого визначення подій функції діють як події. Крім того, при використанні `Cubit` ми можемо просто викликати `emit` з будь-якого місця, щоб ініціювати зміну стану. ### Переваги Bloc #### Відстежуваність Однією з найбільших переваг використання `Bloc` є знання послідовності змін стану, а також того, що саме спричинило ці зміни. Для стану, який є критично важливим для функціональності додатку, може бути дуже корисно використовувати більш подієво-орієнтований підхід, щоб захопити всі події на додаток до змін стану. Поширеним випадком використання може бути керування `AuthenticationState`. Для простоти припустимо, що ми можемо представити `AuthenticationState` через `enum`: Може бути багато причин, через які стан додатку міг змінитися з `authenticated` на `unauthenticated`. Наприклад, користувач міг натиснути кнопку виходу та запросити вихід з додатку. З іншого боку, можливо, токен доступу користувача було відкликано, і його було примусово розлогінено. При використанні `Bloc` ми можемо чітко відстежити, як стан додатку потрапив у певний стан. Наведений вище `Transition` надає нам всю інформацію, необхідну для розуміння того, чому змінився стан. Якби ми використовували `Cubit` для керування `AuthenticationState`, наші логи виглядали б так: Це повідомляє нам, що користувача було розлогінено, але не пояснює чому, що може бути критично важливим для налагодження та розуміння того, як стан додатку змінюється з часом. #### Розширені перетворення подій Ще одна область, в якій `Bloc` перевершує `Cubit`, — це коли нам потрібно скористатися реактивними операторами, такими як `buffer`, `debounceTime`, `throttle` тощо. :::tip Див. [`package:stream_transform`](https://pub.dev/packages/stream_transform) та [`package:rxdart`](https://pub.dev/packages/rxdart) для перетворювачів потоків. ::: `Bloc` має приймач подій, який дозволяє нам контролювати та перетворювати вхідний потік подій. Наприклад, якби ми створювали пошук у реальному часі, ми, ймовірно, хотіли б відкласти запити до бекенду, щоб уникнути обмеження швидкості, а також зменшити витрати/навантаження на бекенд. З `Bloc` ми можемо надати користувацький `EventTransformer` для зміни способу обробки вхідних подій `Bloc`. З наведеним вище кодом ми можемо легко відкласти вхідні події з дуже невеликою кількістю додаткового коду. :::tip Ознайомтеся з [`package:bloc_concurrency`](https://pub.dev/packages/bloc_concurrency) для набору перетворювачів подій з визначеною думкою. ::: Якщо ви не впевнені, що використовувати, починайте з `Cubit`, і ви зможете пізніше відрефакторити або масштабуватися до `Bloc` за потреби. ================================================ FILE: docs/src/content/docs/uk/faqs.mdx ================================================ --- title: Часті запитання description: Відповіді на часті запитання щодо бібліотеки bloc. --- import StateNotUpdatingGood1Snippet from '~/components/faqs/StateNotUpdatingGood1Snippet.astro'; import StateNotUpdatingGood2Snippet from '~/components/faqs/StateNotUpdatingGood2Snippet.astro'; import StateNotUpdatingGood3Snippet from '~/components/faqs/StateNotUpdatingGood3Snippet.astro'; import StateNotUpdatingBad1Snippet from '~/components/faqs/StateNotUpdatingBad1Snippet.astro'; import StateNotUpdatingBad2Snippet from '~/components/faqs/StateNotUpdatingBad2Snippet.astro'; import StateNotUpdatingBad3Snippet from '~/components/faqs/StateNotUpdatingBad3Snippet.astro'; import EquatableEmitSnippet from '~/components/faqs/EquatableEmitSnippet.astro'; import EquatableBlocTestSnippet from '~/components/faqs/EquatableBlocTestSnippet.astro'; import NoEquatableBlocTestSnippet from '~/components/faqs/NoEquatableBlocTestSnippet.astro'; import SingleStateSnippet from '~/components/faqs/SingleStateSnippet.astro'; import SingleStateUsageSnippet from '~/components/faqs/SingleStateUsageSnippet.astro'; import BlocProviderGood1Snippet from '~/components/faqs/BlocProviderGood1Snippet.astro'; import BlocProviderGood2Snippet from '~/components/faqs/BlocProviderGood2Snippet.astro'; import BlocProviderBad1Snippet from '~/components/faqs/BlocProviderBad1Snippet.astro'; import BlocInternalAddEventSnippet from '~/components/faqs/BlocInternalAddEventSnippet.astro'; import BlocInternalEventSnippet from '~/components/faqs/BlocInternalEventSnippet.astro'; import BlocExternalForEachSnippet from '~/components/faqs/BlocExternalForEachSnippet.astro'; ## Стан не оновлюється ❔ **Запитання**: Я випускаю стан у моєму блоці, але користувацький інтерфейс не оновлюється. Що я роблю неправильно? 💡 **Відповідь**: Якщо ви використовуєте Equatable, переконайтеся, що ви передаєте всі властивості у геттер props. ✅ **ДОБРЕ** ❌ **ПОГАНО** Крім того, переконайтеся, що ви випускаєте новий екземпляр стану у вашому блоці. ✅ **ДОБРЕ** ❌ **ПОГАНО** :::caution Властивості `Equatable` завжди повинні копіюватися, а не змінюватися. Якщо клас `Equatable` містить `List` або `Map` як властивості, обов'язково використовуйте `List.of` або `Map.of` відповідно, щоб гарантувати, що рівність оцінюється на основі значень властивостей, а не посилання. ::: ## Коли використовувати Equatable ❔**Запитання**: Коли мені слід використовувати Equatable? 💡**Відповідь**: У наведеному вище сценарії, якщо `StateA` розширює `Equatable`, відбудеться лише одна зміна стану (другий emit буде проігноровано). Загалом, ви повинні використовувати `Equatable`, якщо хочете оптимізувати свій код для зменшення кількості перебудов. Ви не повинні використовувати `Equatable`, якщо хочете, щоб однаковий стан підряд викликав кілька переходів. Крім того, використання `Equatable` значно спрощує тестування блоків, оскільки ми можемо очікувати конкретні екземпляри станів блоку, а не використовувати `Matchers` або `Predicates`. Без `Equatable` наведений вище тест не пройде, і його потрібно буде переписати так: ## Обробка помилок ❔ **Запитання**: Як я можу обробити помилку, зберігаючи при цьому попередні дані? 💡 **Відповідь**: Це значною мірою залежить від того, як було змодельовано стан блоку. У випадках, коли дані повинні зберігатися навіть за наявності помилки, розгляньте використання одного класу стану. Це дозволить віджетам мати доступ до властивостей `data` та `error` одночасно, і блок може використовувати `state.copyWith` для збереження старих даних навіть коли сталася помилка. ## Bloc vs. Redux ❔ **Запитання**: У чому різниця між Bloc та Redux? 💡 **Відповідь**: BLoC -- це шаблон проєктування, що визначається наступними правилами: 1. Вхід та вихід BLoC -- це прості потоки та приймачі. 2. Залежності повинні бути впроваджуваними та незалежними від платформи. 3. Розгалуження платформи не допускається. 4. Реалізація може бути будь-якою, якщо ви дотримуєтесь наведених вище правил. Рекомендації щодо користувацького інтерфейсу: 1. Кожен "достатньо складний" компонент має відповідний BLoC. 2. Компоненти повинні надсилати входи "як є". 3. Компоненти повинні показувати виходи якомога ближче до "як є". 4. Усі розгалуження повинні базуватися на простих логічних виходах BLoC. Бібліотека Bloc реалізує шаблон проєктування BLoC і спрямована на абстрагування RxDart для спрощення досвіду розробника. Три принципи Redux: 1. Єдине джерело істини 2. Стан доступний лише для читання 3. Зміни виконуються чистими функціями Бібліотека bloc порушує перший принцип; з bloc стан розподілений по кількох блоках. Крім того, у bloc немає концепції middleware, і bloc призначений для спрощення асинхронних змін стану, дозволяючи вам випускати кілька станів для однієї події. ## Bloc vs. Provider ❔ **Запитання**: У чому різниця між Bloc та Provider? 💡 **Відповідь**: `provider` призначений для впровадження залежностей (він обгортає `InheritedWidget`). Вам все одно потрібно з'ясувати, як керувати вашим станом (через `ChangeNotifier`, `Bloc`, `Mobx` тощо...). Бібліотека Bloc використовує `provider` внутрішньо, щоб спростити надання та доступ до блоків по всьому дереву віджетів. ## BlocProvider.of() не може знайти Bloc ❔ **Запитання**: При використанні `BlocProvider.of(context)` він не може знайти блок. Як це виправити? 💡 **Відповідь**: Ви не можете отримати доступ до блоку з того самого контексту, в якому він був наданий, тому ви маєте переконатися, що `BlocProvider.of()` викликається всередині дочірнього `BuildContext`. ✅ **ДОБРЕ** ❌ **ПОГАНО** ## Структура проєкту ❔ **Запитання**: Як мені структурувати мій проєкт? 💡 **Відповідь**: Хоча на це запитання дійсно немає правильної/неправильної відповіді, деякі рекомендовані посилання: - [I/O Photobooth](https://github.com/flutter/photobooth) - [I/O Pinball](https://github.com/flutter/pinball) - [Flutter News Toolkit](https://github.com/flutter/news_toolkit) Найважливіше -- мати **послідовну** та **продуману** структуру проєкту. ## Додавання подій всередині блоку ❔ **Запитання**: Чи можна додавати події всередині блоку? 💡 **Відповідь**: У більшості випадків події повинні додаватися ззовні, але в деяких окремих випадках може мати сенс додавати події внутрішньо. Найпоширеніша ситуація, в якій використовуються внутрішні події, -- це коли зміни стану повинні відбуватися у відповідь на оновлення в реальному часі з репозиторію. У цих ситуаціях репозиторій є стимулом для зміни стану замість зовнішньої події, такої як натискання кнопки. У наступному прикладі стан `MyBloc` залежить від поточного користувача, який надається через `Stream` з `UserRepository`. `MyBloc` прослуховує зміни поточного користувача та додає внутрішню подію `_UserChanged` щоразу, коли користувач випускається з потоку користувачів. Додаючи внутрішню подію, ми також можемо вказати користувацький `transformer` для події, щоб визначити, як будуть оброблятися кілька подій `_UserChanged` -- за замовчуванням вони будуть оброблятися одночасно. Наполегливо рекомендується, щоб внутрішні події були приватними. Це явний спосіб сигналізувати, що конкретна подія використовується лише всередині самого блоку та запобігає обізнаності зовнішніх компонентів про подію. Як альтернативу, ми можемо визначити зовнішню подію `Started` та використати API `emit.forEach` для обробки реагування на оновлення користувачів у реальному часі: Переваги наведеного вище підходу: - Нам не потрібна внутрішня подія `_UserChanged` - Нам не потрібно керувати `StreamSubscription` вручну - Ми маємо повний контроль над тим, коли блок підписується на потік оновлень користувачів Недоліки наведеного вище підходу: - Ми не можемо легко призупинити `pause` або відновити `resume` підписку - Нам потрібно надати публічну подію `Started`, яка повинна бути додана ззовні - Ми не можемо використовувати користувацький `transformer` для налаштування того, як ми реагуємо на оновлення користувачів ## Надання публічних методів ❔ **Запитання**: Чи можна надавати публічні методи в моїх екземплярах bloc та cubit? 💡 **Відповідь** При створенні cubit рекомендується надавати лише публічні методи для цілей ініціювання змін стану. У результаті, як правило, всі публічні методи в екземплярі cubit повинні повертати `void` або `Future`. При створенні bloc рекомендується уникати надання будь-яких користувацьких публічних методів і замість цього сповіщати блок про події, викликаючи `add`. ================================================ FILE: docs/src/content/docs/uk/flutter-bloc-concepts.mdx ================================================ --- title: Концепції Flutter Bloc description: Огляд основних концепцій для package:flutter_bloc. sidebar: order: 2 --- import BlocBuilderSnippet from '~/components/concepts/flutter-bloc/BlocBuilderSnippet.astro'; import BlocBuilderExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocBuilderExplicitBlocSnippet.astro'; import BlocBuilderConditionSnippet from '~/components/concepts/flutter-bloc/BlocBuilderConditionSnippet.astro'; import BlocSelectorSnippet from '~/components/concepts/flutter-bloc/BlocSelectorSnippet.astro'; import BlocProviderSnippet from '~/components/concepts/flutter-bloc/BlocProviderSnippet.astro'; import BlocProviderEagerSnippet from '~/components/concepts/flutter-bloc/BlocProviderEagerSnippet.astro'; import BlocProviderValueSnippet from '~/components/concepts/flutter-bloc/BlocProviderValueSnippet.astro'; import BlocProviderLookupSnippet from '~/components/concepts/flutter-bloc/BlocProviderLookupSnippet.astro'; import NestedBlocProviderSnippet from '~/components/concepts/flutter-bloc/NestedBlocProviderSnippet.astro'; import MultiBlocProviderSnippet from '~/components/concepts/flutter-bloc/MultiBlocProviderSnippet.astro'; import BlocListenerSnippet from '~/components/concepts/flutter-bloc/BlocListenerSnippet.astro'; import BlocListenerExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocListenerExplicitBlocSnippet.astro'; import BlocListenerConditionSnippet from '~/components/concepts/flutter-bloc/BlocListenerConditionSnippet.astro'; import NestedBlocListenerSnippet from '~/components/concepts/flutter-bloc/NestedBlocListenerSnippet.astro'; import MultiBlocListenerSnippet from '~/components/concepts/flutter-bloc/MultiBlocListenerSnippet.astro'; import BlocConsumerSnippet from '~/components/concepts/flutter-bloc/BlocConsumerSnippet.astro'; import BlocConsumerConditionSnippet from '~/components/concepts/flutter-bloc/BlocConsumerConditionSnippet.astro'; import RepositoryProviderSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderSnippet.astro'; import RepositoryProviderLookupSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderLookupSnippet.astro'; import RepositoryProviderDisposeSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderDisposeSnippet.astro'; import NestedRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/NestedRepositoryProviderSnippet.astro'; import MultiRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/MultiRepositoryProviderSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/flutter-bloc/CounterBlocSnippet.astro'; import CounterMainSnippet from '~/components/concepts/flutter-bloc/CounterMainSnippet.astro'; import CounterPageSnippet from '~/components/concepts/flutter-bloc/CounterPageSnippet.astro'; import WeatherRepositorySnippet from '~/components/concepts/flutter-bloc/WeatherRepositorySnippet.astro'; import WeatherMainSnippet from '~/components/concepts/flutter-bloc/WeatherMainSnippet.astro'; import WeatherAppSnippet from '~/components/concepts/flutter-bloc/WeatherAppSnippet.astro'; import WeatherPageSnippet from '~/components/concepts/flutter-bloc/WeatherPageSnippet.astro'; :::note Будь ласка, уважно прочитайте наступні розділи перед роботою з [`package:flutter_bloc`](https://pub.dev/packages/flutter_bloc). ::: :::note Усі віджети, що експортуються пакетом `flutter_bloc`, інтегруються як з екземплярами `Cubit`, так і з екземплярами `Bloc`. ::: ## Віджети Bloc ### BlocBuilder **BlocBuilder** — це Flutter віджет, якому потрібен `Bloc` та функція `builder`. `BlocBuilder` обробляє побудову віджета у відповідь на нові стани. `BlocBuilder` дуже схожий на `StreamBuilder`, але має простіший API для зменшення кількості шаблонного коду. Функція `builder` може потенційно викликатися багато разів і повинна бути [чистою функцією](https://en.wikipedia.org/wiki/Pure_function), яка повертає віджет у відповідь на стан. Дивіться `BlocListener`, якщо ви хочете "робити" щось у відповідь на зміни стану, такі як навігація, показ діалогу тощо. Якщо параметр `bloc` опущено, `BlocBuilder` автоматично виконає пошук, використовуючи `BlocProvider` та поточний `BuildContext`. Вказуйте bloc лише в тому випадку, якщо ви хочете надати bloc, який буде обмежений одним віджетом і не доступний через батьківський `BlocProvider` та поточний `BuildContext`. Для точного контролю над тим, коли викликається функція `builder`, можна надати необов'язковий параметр `buildWhen`. `buildWhen` приймає попередній стан bloc та поточний стан bloc і повертає булеве значення. Якщо `buildWhen` повертає true, `builder` буде викликаний з `state` і віджет буде перебудований. Якщо `buildWhen` повертає false, `builder` не буде викликаний з `state` і перебудова не відбудеться. ### BlocSelector **BlocSelector** — це Flutter віджет, який аналогічний `BlocBuilder`, але дозволяє розробникам фільтрувати оновлення, обираючи нове значення на основі поточного стану bloc. Непотрібні побудови запобігаються, якщо обране значення не змінюється. Обране значення повинно бути незмінним, щоб `BlocSelector` міг точно визначити, чи повинен `builder` бути викликаний знову. Якщо параметр `bloc` опущено, `BlocSelector` автоматично виконає пошук, використовуючи `BlocProvider` та поточний `BuildContext`. ### BlocProvider **BlocProvider** — це Flutter віджет, який надає bloc своїм дочірнім елементам через `BlocProvider.of(context)`. Він використовується як віджет впровадження залежностей (DI), щоб один екземпляр bloc міг бути наданий кільком віджетам у піддереві. У більшості випадків `BlocProvider` повинен використовуватися для створення нових bloc, які будуть доступні решті піддерева. У цьому випадку, оскільки `BlocProvider` відповідає за створення bloc, він автоматично обробить закриття bloc. За замовчуванням `BlocProvider` створить bloc ліниво, що означає, що `create` буде виконаний, коли bloc буде знайдений через `BlocProvider.of(context)`. Щоб перевизначити цю поведінку та примусово запустити `create` негайно, `lazy` можна встановити у `false`. У деяких випадках `BlocProvider` може використовуватися для надання існуючого bloc новій частині дерева віджетів. Це найчастіше використовується, коли існуючий bloc потрібно зробити доступним для нового маршруту. У цьому випадку `BlocProvider` не буде автоматично закривати bloc, оскільки він його не створював. потім з `ChildA` або `ScreenA` ми можемо отримати `BlocA` за допомогою: ### MultiBlocProvider **MultiBlocProvider** — це Flutter віджет, який об'єднує кілька віджетів `BlocProvider` в один. `MultiBlocProvider` покращує читабельність та усуває необхідність вкладати кілька `BlocProviders`. Використовуючи `MultiBlocProvider`, ми можемо перейти від: до: :::caution Коли `BlocProvider` визначений у контексті `MultiBlocProvider`, будь-який `child` буде ігноруватися. ::: ### BlocListener **BlocListener** — це Flutter віджет, який приймає `BlocWidgetListener` та необов'язковий `Bloc` і викликає `listener` у відповідь на зміни стану в bloc. Він повинен використовуватися для функціональності, яка повинна виконуватися один раз на кожну зміну стану, такої як навігація, показ `SnackBar`, показ `Dialog` тощо. `listener` викликається лише один раз для кожної зміни стану (**НЕ** включаючи початковий стан), на відміну від `builder` у `BlocBuilder`, і є функцією `void`. Якщо параметр `bloc` опущено, `BlocListener` автоматично виконає пошук, використовуючи `BlocProvider` та поточний `BuildContext`. Вказуйте bloc лише в тому випадку, якщо ви хочете надати bloc, який інакше не доступний через `BlocProvider` та поточний `BuildContext`. Для точного контролю над тим, коли викликається функція `listener`, можна надати необов'язковий параметр `listenWhen`. `listenWhen` приймає попередній стан bloc та поточний стан bloc і повертає булеве значення. Якщо `listenWhen` повертає true, `listener` буде викликаний з `state`. Якщо `listenWhen` повертає false, `listener` не буде викликаний з `state`. ### MultiBlocListener **MultiBlocListener** — це Flutter віджет, який об'єднує кілька віджетів `BlocListener` в один. `MultiBlocListener` покращує читабельність та усуває необхідність вкладати кілька `BlocListeners`. Використовуючи `MultiBlocListener`, ми можемо перейти від: до: :::caution Коли `BlocListener` визначений у контексті `MultiBlocListener`, будь-який `child` буде ігноруватися. ::: ### BlocConsumer **BlocConsumer** надає `builder` та `listener` для реагування на нові стани. `BlocConsumer` аналогічний вкладеним `BlocListener` та `BlocBuilder`, але зменшує кількість необхідного шаблонного коду. `BlocConsumer` повинен використовуватися лише коли необхідно як перебудувати UI, так і виконати інші реакції на зміни стану в `bloc`. `BlocConsumer` приймає обов'язкові `BlocWidgetBuilder` та `BlocWidgetListener` і необов'язкові `bloc`, `BlocBuilderCondition` та `BlocListenerCondition`. Якщо параметр `bloc` опущено, `BlocConsumer` автоматично виконає пошук, використовуючи `BlocProvider` та поточний `BuildContext`. Необов'язкові `listenWhen` та `buildWhen` можуть бути реалізовані для більш детального контролю над тим, коли викликаються `listener` та `builder`. `listenWhen` та `buildWhen` будуть викликані при кожній зміні `state` в `bloc`. Кожний приймає попередній `state` та поточний `state` і повинен повернути `bool`, який визначає, чи буде викликана функція `builder` та/або `listener`. Попередній `state` буде ініціалізований станом `state` блоку `bloc` при ініціалізації `BlocConsumer`. `listenWhen` та `buildWhen` є необов'язковими, і якщо вони не реалізовані, за замовчуванням буде `true`. ### RepositoryProvider **RepositoryProvider** — це Flutter віджет, який надає сховище своїм дочірнім елементам через `RepositoryProvider.of(context)`. Він використовується як віджет впровадження залежностей (DI), щоб один екземпляр сховища міг бути наданий кільком віджетам у піддереві. `BlocProvider` повинен використовуватися для надання bloc, тоді як `RepositoryProvider` повинен використовуватися лише для сховищ. потім з `ChildA` ми можемо отримати екземпляр `Repository` за допомогою: Сховища, які керують ресурсами, що повинні бути звільнені, можуть зробити це через зворотний виклик `dispose`: ### MultiRepositoryProvider **MultiRepositoryProvider** — це Flutter віджет, який об'єднує кілька віджетів `RepositoryProvider` в один. `MultiRepositoryProvider` покращує читабельність та усуває необхідність вкладати кілька `RepositoryProvider`. Використовуючи `MultiRepositoryProvider`, ми можемо перейти від: до: :::caution Коли `RepositoryProvider` визначений у контексті `MultiRepositoryProvider`, будь-який `child` буде ігноруватися. ::: ## Використання BlocProvider Давайте розглянемо, як використовувати `BlocProvider` для надання `CounterBloc` у `CounterPage` та реагувати на зміни стану за допомогою `BlocBuilder`. На цьому етапі ми успішно відокремили наш шар представлення від шару бізнес-логіки. Зверніть увагу, що віджет `CounterPage` нічого не знає про те, що відбувається, коли користувач натискає на кнопки. Віджет просто повідомляє `CounterBloc`, що користувач натиснув кнопку збільшення або зменшення. ## Використання RepositoryProvider Ми розглянемо, як використовувати `RepositoryProvider` у контексті прикладу [`flutter_weather`][flutter_weather_link]. У нашому `main.dart` ми викликаємо `runApp` з нашим віджетом `WeatherApp`. Ми впровадимо наш екземпляр `WeatherRepository` у дерево віджетів через `RepositoryProvider`. При створенні екземпляра bloc ми можемо отримати доступ до екземпляра сховища через `context.read` та впровадити сховище в bloc через конструктор. :::tip Якщо у вас більше одного сховища, ви можете використовувати `MultiRepositoryProvider` для надання кількох екземплярів сховищ піддереву. ::: :::note Використовуйте зворотний виклик `dispose` для звільнення будь-яких ресурсів, коли `RepositoryProvider` демонтується. ::: [flutter_weather_link]: https://github.com/felangel/bloc/blob/master/examples/flutter_weather ## Методи розширення [Методи розширення](https://dart.dev/guides/language/extension-methods), представлені в Dart 2.7, — це спосіб додати функціональність до існуючих бібліотек. У цьому розділі ми розглянемо методи розширення, включені в `package:flutter_bloc`, та як їх можна використовувати. `flutter_bloc` має залежність від [package:provider](https://pub.dev/packages/provider), яка спрощує використання [`InheritedWidget`](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html). Внутрішньо `package:flutter_bloc` використовує `package:provider` для реалізації: `BlocProvider`, `MultiBlocProvider`, `RepositoryProvider` та віджетів `MultiRepositoryProvider`. `package:flutter_bloc` експортує розширення `ReadContext`, `WatchContext` та `SelectContext` з `package:provider`. :::note Дізнайтеся більше про [`package:provider`](https://pub.dev/packages/provider). ::: ### context.read `context.read()` шукає найближчий екземпляр предка типу `T` і функціонально еквівалентний `BlocProvider.of(context)`. `context.read` найчастіше використовується для отримання екземпляра bloc, щоб додати подію у зворотних викликах `onPressed`. :::note `context.read()` не прослуховує `T` — якщо наданий `Object` типу `T` змінюється, `context.read` не викличе перебудову віджета. ::: #### Використання ✅ **ВИКОРИСТОВУЙТЕ** `context.read` для додавання подій у зворотних викликах. ```dart onPressed() { context.read().add(CounterIncrementPressed()), } ``` ❌ **УНИКАЙТЕ** використання `context.read` для отримання стану в методі `build`. ```dart @override Widget build(BuildContext context) { final state = context.read().state; return Text('$state'); } ``` Вищевказане використання є схильним до помилок, тому що віджет `Text` не буде перебудований, якщо стан bloc зміниться. :::caution Використовуйте `BlocBuilder` або `context.watch` замість цього, щоб перебудовувати у відповідь на зміни стану. ::: ### context.watch Як і `context.read()`, `context.watch()` надає найближчий екземпляр предка типу `T`, однак він також прослуховує зміни екземпляра. Це функціонально еквівалентно `BlocProvider.of(context, listen: true)`. Якщо наданий `Object` типу `T` змінюється, `context.watch` викличе перебудову. :::caution `context.watch` доступний лише в методі `build` класу `StatelessWidget` або `State`. ::: #### Використання ✅ **ВИКОРИСТОВУЙТЕ** `BlocBuilder` замість `context.watch` для явного обмеження перебудов. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (context, state) { // Коли стан змінюється, перебудовується лише Text. return Text(state.value); }, ), ), ); } ``` Альтернативно, використовуйте `Builder` для обмеження перебудов. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Коли стан змінюється, перебудовується лише Text. final state = context.watch().state; return Text(state.value); }, ), ), ); } ``` ✅ **ВИКОРИСТОВУЙТЕ** `Builder` та `context.watch` як `MultiBlocBuilder`. ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // повертає віджет, який залежить від стану BlocA, BlocB та BlocC } ); ``` ❌ **УНИКАЙТЕ** використання `context.watch`, коли батьківський віджет в методі `build` не залежить від стану. ```dart @override Widget build(BuildContext context) { // Коли стан змінюється, MaterialApp перебудовується // навіть якщо він використовується лише у віджеті Text. final state = context.watch().state; return MaterialApp( home: Scaffold( body: Text(state.value), ), ); } ``` :::caution Використання `context.watch` в корені методу `build` призведе до перебудови всього віджета при зміні стану bloc. ::: ### context.select Як і `context.watch()`, `context.select(R function(T value))` надає найближчий екземпляр предка типу `T` та прослуховує зміни `T`. На відміну від `context.watch`, `context.select` дозволяє прослуховувати зміни в меншій частині стану. ```dart Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); } ``` Вищевказане буде перебудовувати віджет лише коли властивість `name` стану `ProfileBloc` зміниться. #### Використання ✅ **ВИКОРИСТОВУЙТЕ** `BlocSelector` замість `context.select` для явного обмеження перебудов. ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector( selector: (state) => state.name, builder: (context, name) { // Коли state.name змінюється, перебудовується лише Text. return Text(name); }, ), ), ); } ``` Альтернативно, використовуйте `Builder` для обмеження перебудов. ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // Коли state.name змінюється, перебудовується лише Text. final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), ); } ``` ❌ **УНИКАЙТЕ** використання `context.select`, коли батьківський віджет в методі build не залежить від стану. ```dart @override Widget build(BuildContext context) { // Коли state.value змінюється, MaterialApp перебудовується // навіть якщо він використовується лише у віджеті Text. final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), ); } ``` :::caution Використання `context.select` в корені методу `build` призведе до перебудови всього віджета при зміні обраного значення. ::: ================================================ FILE: docs/src/content/docs/uk/getting-started.mdx ================================================ --- title: Початок роботи description: Все, що потрібно для початку роботи з Bloc. --- import InstallationTabs from '~/components/getting-started/InstallationTabs.astro'; import ImportTabs from '~/components/getting-started/ImportTabs.astro'; ## Пакети Екосистема bloc складається з кількох пакетів, перелічених нижче: | Пакет | Опис | Посилання | | ------------------------------------------------------------------------------------------ | ------------------------------- | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | Компоненти AngularDart | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | Основні API Dart | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | Перетворювачі подій | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | Користувацький лінтер | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | API для тестування | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | Інструменти командного рядка | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | Віджети Flutter | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | Підтримка кешування/збереження | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | Підтримка скасування/повторення | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | ## Встановлення :::note Для початку використання bloc у вас має бути встановлений [Dart SDK](https://dart.dev/get-dart) на вашому комп'ютері. ::: ## Імпорти Тепер, коли ми успішно встановили bloc, ми можемо створити наш `main.dart` та імпортувати відповідний пакет `bloc`. ================================================ FILE: docs/src/content/docs/uk/index.mdx ================================================ --- template: splash title: Бібліотека керування станом Bloc description: Офіційна документація бібліотеки керування станом bloc. Підтримка Dart, Flutter та AngularDart. Включає приклади та посібники. banner: content: | ✨ Відвідайте Крамницю Bloc ✨ editUrl: false lastUpdated: false hero: title: Bloc v9.2.0 tagline: Передбачувана бібліотека керування станом для Dart. image: alt: Логотип Bloc file: ~/assets/bloc.svg actions: - text: Почати link: /uk/getting-started/ variant: primary icon: rocket - text: Переглянути на GitHub link: https://github.com/felangel/bloc icon: github variant: secondary --- import { CardGrid } from '@astrojs/starlight/components'; import SponsorsGrid from '~/components/landing/SponsorsGrid.astro'; import Card from '~/components/landing/Card.astro'; import ListCard from '~/components/landing/ListCard.astro'; import SplitCard from '~/components/landing/SplitCard.astro'; import Discord from '~/components/landing/Discord.astro';
```sh # Додайте bloc до вашого проєкту. dart pub add bloc ``` Наш [посібник для початку роботи](/uk/getting-started) містить покрокові інструкції щодо того, як почати використовувати Bloc всього за кілька хвилин. Пройдіть [офіційні посібники](/uk/tutorials/flutter-counter), щоб вивчити найкращі практики та створити різноманітні додатки на основі Bloc. Досліджуйте високоякісні, повністю протестовані [приклади додатків](https://github.com/felangel/bloc/tree/master/examples), такі як лічильник, таймер, нескінченний список, погода, завдання та багато іншого! - [Чому Bloc?](/uk/why-bloc) - [Основні концепції](/uk/bloc-concepts) - [Архітектура](/uk/architecture) - [Тестування](/uk/testing) - [Угоди про найменування](/uk/naming-conventions) - [Часті запитання](/uk/faqs) - [Інтеграція з VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) - [Інтеграція з IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) - [Інтеграція з Neovim](https://github.com/wa11breaker/flutter-bloc.nvim) - [Інтеграція з Mason CLI](https://github.com/felangel/bloc/blob/master/bricks/README.md) - [Користувацькі шаблони](https://brickhub.dev/search?q=bloc) - [Інструменти розробника](https://github.com/felangel/bloc/blob/master/packages/bloc_tools/README.md) ================================================ FILE: docs/src/content/docs/uk/lint/configuration.mdx ================================================ --- title: Конфігурація лінтера description: Налаштування лінтера bloc. sidebar: order: 3 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import BlocLintBasicAnalysisOptionsSnippet from '~/components/lint/BlocLintBasicAnalysisOptionsSnippet.astro'; import RunBlocLintInCurrentDirectorySnippet from '~/components/lint/RunBlocLintInCurrentDirectorySnippet.astro'; import RunBlocLintInSrcTestSnippet from '~/components/lint/RunBlocLintInSrcTestSnippet.astro'; import AvoidFlutterImportsWarningSnippet from '~/components/lint/ImportFlutterWarningSnippet.mdx'; import RunBlocLintCounterCubitSnippet from '~/components/lint/RunBlocLintCounterCubitSnippet.astro'; import AvoidFlutterImportsWarningOutputSnippet from '~/components/lint/ImportFlutterWarningOutputSnippet.astro'; За замовчуванням лінтер bloc не видаватиме жодної діагностики, якщо ви явно не налаштували параметри аналізу проєкту. Для початку створіть або змініть наявний файл `analysis_options.yaml` у корені вашого проєкту, щоб включити список правил під ключем верхнього рівня bloc: Запустіть лінтер за допомогою наступної команди в терміналі: Наведена вище команда проаналізує всі файли в поточному каталозі та його підкаталогах, але ви також можете перевірити конкретні файли та каталоги, передавши їх як аргументи командного рядка: Наведена вище команда проаналізує весь код у каталогах `src` та `test`. Якщо правило `avoid_flutter_imports` увімкнено, будь-який файл bloc або cubit, що містить імпорт flutter, буде позначено як попередження: Ви можете побачити попередження, запустивши команду `bloc lint`: Вивід має виглядати так: :::note Ось усі підтримувані правила лінтера: ::: ================================================ FILE: docs/src/content/docs/uk/lint/customizing-rules.mdx ================================================ --- title: Налаштування правил лінтера description: Налаштування правил лінтера bloc sidebar: order: 4 --- import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import BlocLintEnablingRulesSnippet from '~/components/lint/BlocLintEnablingRulesSnippet.astro'; import BlocLintDisablingRulesSnippet from '~/components/lint/BlocLintDisablingRulesSnippet.astro'; import BlocLintChangingSeveritySnippet from '~/components/lint/BlocLintChangingSeveritySnippet.astro'; import ImportFlutterInfoSnippet from '~/components/lint/ImportFlutterInfoSnippet.mdx'; import ImportFlutterInfoOutputSnippet from '~/components/lint/ImportFlutterInfoOutputSnippet.astro'; import BlocLintExcludingFilesSnippet from '~/components/lint/BlocLintExcludingFilesSnippet.astro'; import BlocLintIgnoreForLineSnippet from '~/components/lint/BlocLintIgnoreForLineSnippet.astro'; import BlocLintIgnoreForFileSnippet from '~/components/lint/BlocLintIgnoreForFileSnippet.astro'; Ви можете налаштувати поведінку лінтера bloc, змінюючи серйозність окремих правил, індивідуально вмикаючи або вимикаючи правила, а також виключаючи файли зі статичного аналізу. ## Увімкнення та вимкнення правил Лінтер bloc підтримує зростаючий список правил лінтера. Зверніть увагу, що правила лінтера не обов'язково повинні узгоджуватися між собою. Наприклад, деякі розробники можуть надавати перевагу використанню блоків (`prefer_bloc`), тоді як інші можуть надавати перевагу використанню кубітів (`prefer_cubit`). :::note На відміну від статичного аналізу, правила лінтера можуть містити хибні спрацювання. Не соромтеся повідомляти про будь-які хибні спрацювання або інші проблеми, [створивши issue](https://github.com/felangel/bloc/issues/new/choose). ::: ### Увімкнення рекомендованих правил Бібліотека bloc надає набір рекомендованих правил лінтера у складі пакета [`bloc_lint`](https://pub.dev/packages/bloc_lint). Для увімкнення рекомендованого набору лінтів додайте пакет `bloc_lint` як dev-залежність: Потім відредагуйте ваш файл `analysis_options.yaml`, щоб включити набір правил: :::note Коли публікується нова версія `bloc_lint`, код, який раніше проходив статичний аналіз, може почати не проходити його. Ми рекомендуємо оновити ваш код для роботи з новими правилами, або ви також можете за бажанням увімкнути чи вимкнути окремі правила. ::: ### Увімкнення окремих правил Для увімкнення окремих правил додайте `bloc:` до файлу `analysis_options.yaml` як ключ верхнього рівня та `rules:` як ключ другого рівня. У наступних рядках вкажіть правила, які ви бажаєте, у вигляді списку YAML (з префіксом у вигляді дефісів). Наприклад: ### Вимкнення окремих правил Якщо ви включаєте наявний набір правил, такий як набір `recommended`, ви можете захотіти вимкнути одне або кілька включених правил лінтера. Вимкнення правил аналогічне їх увімкненню, але потребує використання YAML-карти замість списку. Наприклад, наступне включає рекомендований набір правил лінтера за винятком `avoid_public_bloc_methods` та додатково вмикає правило `prefer_bloc`: ## Налаштування серйозності правил Ви можете налаштувати серйозність будь-якого правила таким чином: Тепер те саме правило лінтера відображатиметься з серйозністю `info` замість `warning`: Вивід команди `bloc lint` має виглядати так: Підтримувані варіанти серйозності: | Серйозність | Опис | | ----------- | ------------------------------------------------- | | `error` | Вказує, що шаблон не дозволений. | | `warning` | Вказує, що шаблон підозрілий, але дозволений. | | `info` | Надає інформацію користувачам, але не є проблемою | | `hint` | Пропонує кращий спосіб досягнення результату. | ## Виключення файлів Іноді допустимо, що статичний аналіз не проходить. Наприклад, ви можете захотіти ігнорувати попередження або помилки, що відображаються у згенерованому коді, який не був написаний вами та вашою командою. Так само, як і з офіційними правилами лінтера Dart, ви можете використовувати опцію аналізатора `exclude:`, щоб виключити файли зі статичного аналізу. Ви можете або перелічити окремі файли, або використовувати патерни [`glob`](https://pub.dev/packages/glob). :::note Усі використання патернів glob мають бути відносно каталогу, що містить відповідний файл `analysis_options.yaml`. ::: Наприклад, ми можемо виключити весь згенерований код Dart за допомогою наступних параметрів аналізу: ## Ігнорування правил Так само, як і з офіційними правилами лінтера Dart, ви можете ігнорувати правила лінтера bloc для певного файлу або рядка коду, використовуючи `// ignore_for_file` та `// ignore` відповідно. :::note Щоб ігнорувати кілька правил для певного рядка або файлу, вкажіть список, розділений комами. ::: ### Ігнорування рядків Ми можемо ігнорувати конкретні випадки порушень правил, додавши коментар `ignore` безпосередньо над проблемним рядком або додавши його в кінець проблемного рядка. Наприклад, ми можемо ігнорувати конкретні випадки `prefer_file_naming_conventions` у певному файлі: ### Ігнорування файлів Ми можемо ігнорувати всі випадки порушень правил у файлі, додавши коментар `ignore_for_file` у будь-якому місці файлу. Наприклад, ми можемо ігнорувати всі випадки `prefer_file_naming_conventions` у певному файлі: ================================================ FILE: docs/src/content/docs/uk/lint/index.mdx ================================================ --- title: Огляд лінтера description: Вступ до лінтера bloc. sidebar: order: 1 --- import AvoidFlutterImportsWarningSnippet from '~/components/lint/ImportFlutterWarningSnippet.mdx'; import AvoidFlutterImportsWarningOutputSnippet from '~/components/lint/ImportFlutterWarningOutputSnippet.astro'; import InstallBlocToolsSnippet from '~/components/lint/InstallBlocToolsSnippet.astro'; import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import RunBlocLintInCurrentDirectorySnippet from '~/components/lint/RunBlocLintInCurrentDirectorySnippet.astro'; Лінтинг — це процес статичного аналізу коду для виявлення потенційних помилок, а також програмних і стилістичних проблем. Bloc має вбудований лінтер, який можна використовувати через вашу IDE або [`інструменти командного рядка bloc`](https://pub.dev/packages/bloc_tools) за допомогою команди `bloc lint`. За допомогою лінтера bloc ви можете підвищити якість вашої кодової бази та забезпечити узгодженість без виконання жодного рядка коду. Наприклад, можливо, ви випадково імпортували залежність Flutter у ваш cubit: За правильного налаштування лінтер bloc вкаже на імпорт і видасть наступне попередження: У наступних розділах ми розглянемо, як встановити, налаштувати та кастомізувати лінтер bloc, щоб ви могли скористатися перевагами статичного аналізу у вашій кодовій базі. ## Швидкий старт Почніть використовувати лінтер bloc всього за кілька швидких і простих кроків. :::note Для початку використання bloc у вас має бути встановлений [Dart SDK](https://dart.dev/get-dart) на вашому комп'ютері. ::: 1. Встановіть [інструменти командного рядка bloc](https://pub.dev/packages/bloc_tools) 1. Встановіть пакет [bloc_lint](https://pub.dev/packages/bloc_lint) 1. Додайте файл `analysis_options.yaml` у корінь вашого проєкту з рекомендованими правилами 1. Запустіть лінтер Ось і все 🎉 Продовжуйте читання для більш детального вивчення налаштування та кастомізації лінтера bloc. ================================================ FILE: docs/src/content/docs/uk/lint/installation.mdx ================================================ --- title: Встановлення лінтера description: Встановлення лінтера bloc. sidebar: order: 2 --- import { CardGrid } from '@astrojs/starlight/components'; import Card from '~/components/landing/Card.astro'; import InstallBlocToolsSnippet from '~/components/lint/InstallBlocToolsSnippet.astro'; import BlocToolsLintHelpOutputSnippet from '~/components/lint/BlocToolsLintHelpOutputSnippet.astro'; import InstallBlocLintSnippet from '~/components/lint/InstallBlocLintSnippet.astro'; import BlocLintRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintRecommendedAnalysisOptionsSnippet.astro'; import BlocLintMultipleRecommendedAnalysisOptionsSnippet from '~/components/lint/BlocLintMultipleRecommendedAnalysisOptionsSnippet.astro'; ## Інструменти командного рядка Для використання лінтера з командного рядка встановіть [`package:bloc_tools`](https://pub.dev/packages/bloc_tools) за допомогою наступної команди: Після встановлення інструментів командного рядка bloc ви можете запустити лінтер bloc за допомогою команди `bloc lint`: ## Рекомендований набір правил Для встановлення рекомендованого набору правил лінтера встановіть [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) як dev-залежність за допомогою наступної команди: Потім додайте файл `analysis_options.yaml` у корінь вашого проєкту з рекомендованим набором правил: За потреби ви можете включити кілька наборів правил, визначивши їх у вигляді списку: ## Інтеграції з IDE Наступні IDE офіційно підтримують лінтер bloc та мовний сервер для надання миттєвої діагностики безпосередньо у вашій IDE. Підтримка у [Bloc VSCode Extension](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) доступна починаючи з версії v6.8.0. Підтримка у [Bloc IntelliJ Plugin](https://plugins.jetbrains.com/plugin/12129-bloc) доступна починаючи з версії v4.1.0. ================================================ FILE: docs/src/content/docs/uk/lint-rules/avoid_build_context_extensions.mdx ================================================ --- title: Уникайте розширень BuildContext description: Правило avoid_build_context_extensions. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_build_context_extensions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_build_context_extensions/GoodSnippet.astro';
Уникайте використання розширень `BuildContext` для доступу до екземплярів `Bloc` або `Cubit`. :::note Це правило лінтера було введено у версії `0.3.0` пакета [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Обґрунтування Для узгодженості та заради явності краще використовувати безпосередньо базові методи замість розширень `BuildContext`. Це також корисно для тестування, оскільки неможливо замокати метод розширення. | розширення | явний метод | | ---------------- | -------------------------------------------------------------------- | | `context.read` | `BlocProvider.of(context, listen: false)` | | `context.watch` | `BlocBuilder(...)` або `BlocProvider.of(context)` | | `context.select` | `BlocSelector(...)` | ## Приклади **Уникайте** використання розширень `BuildContext` для взаємодії з екземплярами `Bloc` або `Cubit`. **ПОГАНО**: **ДОБРЕ**: ## Увімкнення Щоб увімкнути правило `avoid_build_context_extensions`, додайте його до `analysis_options.yaml` у розділі `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/uk/lint-rules/avoid_flutter_imports.mdx ================================================ --- title: Уникайте імпортів Flutter description: Правило лінтера bloc avoid_flutter_imports. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_flutter_imports/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_flutter_imports/GoodSnippet.astro';
Уникайте введення залежностей від Flutter у компонентах бізнес-логіки (екземплярах `Bloc` або `Cubit`). ## Обґрунтування Розділення застосунку на шари є ключовою частиною побудови підтримуваної кодової бази та допомагає розробникам ітерувати швидко й впевнено. Кожен шар повинен мати єдину відповідальність і бути здатним функціонувати та тестуватися ізольовано. Це дозволяє обмежувати зміни конкретними шарами, мінімізуючи вплив на весь застосунок. У результаті компоненти бізнес-логіки зазвичай повинні керувати станом функцій і бути відокремленими від шару користувацького інтерфейсу. Події повинні надходити до компонентів бізнес-логіки з шару UI, а стан повинен витікати з шару бізнес-логіки у шар UI. Збереження компонентів бізнес-логіки відокремленими від Flutter надає можливість повторно використовувати бізнес-логіку на кількох платформах/фреймворках (наприклад, Flutter, AngularDart, Jaspr тощо). ## Приклади **НЕ ІМПОРТУЙТЕ** Flutter у ваших компонентах бізнес-логіки. **ПОГАНО**: **ДОБРЕ**: ## Увімкнення Щоб увімкнути правило `avoid_flutter_imports`, додайте його до `analysis_options.yaml` у розділі `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/uk/lint-rules/avoid_public_bloc_methods.mdx ================================================ --- title: Уникайте публічних методів Bloc description: Правило avoid_public_bloc_methods. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_public_bloc_methods/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_public_bloc_methods/GoodSnippet.astro';
Уникайте надання публічних методів в екземплярах `Bloc`. ## Обґрунтування Блоки реагують на вхідні події та видають вихідні стани. У результаті рекомендований спосіб взаємодії з екземпляром bloc — через метод `add`. У більшості випадків немає потреби створювати додаткові абстракції поверх API `add`. ![Архітектура Bloc](~/assets/concepts/bloc_architecture_full.png) ## Приклади **НЕ НАДАВАЙТЕ** публічні методи в екземплярах bloc. **ПОГАНО**: **ДОБРЕ**: ## Увімкнення Щоб увімкнути правило `avoid_public_bloc_methods`, додайте його до `analysis_options.yaml` у розділі `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/uk/lint-rules/avoid_public_fields.mdx ================================================ --- title: Уникайте публічних полів description: Правило avoid_public_fields. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/avoid_public_fields/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/avoid_public_fields/GoodSnippet.astro';
Уникайте надання публічних полів в екземплярах `Bloc` та `Cubit`. ## Обґрунтування Компоненти бізнес-логіки підтримують власний `state` та видають зміни стану через API `emit`. У результаті весь публічно доступний стан повинен бути наданий через об'єкт `state`. ## Приклади **НЕ НАДАВАЙТЕ** публічні поля в екземплярах bloc та cubit. **ПОГАНО**: **ДОБРЕ**: ## Увімкнення Щоб увімкнути правило `avoid_public_fields`, додайте його до `analysis_options.yaml` у розділі `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/uk/lint-rules/prefer_bloc.mdx ================================================ --- title: Надавайте перевагу Bloc description: Правило prefer_bloc. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_bloc/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_bloc/GoodSnippet.astro';
Надавайте перевагу використанню екземплярів `Bloc` замість екземплярів `Cubit`. ## Обґрунтування Це правило є суто стилістичним. У деяких випадках команди можуть надавати перевагу стандартизації використання лише екземплярів `Bloc` у всьому застосунку заради узгодженості. :::tip Дізнайтеся більше про переваги `Bloc` у розділі [Основні концепції](/uk/bloc-concepts/#переваги-bloc). ::: ## Приклади **Уникайте** використання екземплярів `Cubit`. **ПОГАНО**: **ДОБРЕ**: ## Увімкнення Щоб увімкнути правило `prefer_bloc`, додайте його до `analysis_options.yaml` у розділі `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/uk/lint-rules/prefer_build_context_extensions.mdx ================================================ --- title: Надавайте перевагу розширенням BuildContext description: Правило prefer_build_context_extensions. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_build_context_extensions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_build_context_extensions/GoodSnippet.astro';
Надавайте перевагу використанню розширень `BuildContext` для доступу до екземпляра `Bloc` або `Repository`. :::note Це правило лінтера було введено у версії `0.3.2` пакета [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Обґрунтування Для узгодженості надавайте перевагу використанню розширень `BuildContext`, таких як `context.read`, `context.watch` та `context.select`, замість `BlocProvider.of`, `RepositoryProvider.of`, `BlocBuilder` або `BlocSelector`. | явний метод | розширення | | -------------------------------------------------------------------- | --------------------- | | `BlocProvider.of(context, listen: false)` | `context.read` | | `BlocBuilder(...)` або `BlocProvider.of(context)` | `context.watch` | | `BlocSelector(...)` | `context.select` | ## Приклади **Уникайте** використання `BlocProvider.of(context)` для доступу до екземпляра `Bloc`. **ПОГАНО**: **ДОБРЕ**: ## Увімкнення Щоб увімкнути правило `prefer_build_context_extensions`, додайте його до `analysis_options.yaml` у розділі `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/uk/lint-rules/prefer_cubit.mdx ================================================ --- title: Надавайте перевагу Cubit description: Правило prefer_cubit. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_cubit/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_cubit/GoodSnippet.astro';
Надавайте перевагу використанню екземплярів `Cubit` замість екземплярів `Bloc`. ## Обґрунтування Це правило є суто стилістичним. У деяких випадках команди можуть надавати перевагу стандартизації використання лише екземплярів `Cubit` у всьому застосунку заради узгодженості. :::tip Дізнайтеся більше про переваги `Cubit` у розділі [Основні концепції](/uk/bloc-concepts/#переваги-cubit). ::: ## Приклади **Уникайте** використання екземплярів `Bloc`. **ПОГАНО**: **ДОБРЕ**: ## Увімкнення Щоб увімкнути правило `prefer_cubit`, додайте його до `analysis_options.yaml` у розділі `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/uk/lint-rules/prefer_file_naming_conventions.mdx ================================================ --- title: Надавайте перевагу конвенціям іменування файлів description: Правило prefer_file_naming_conventions. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_file_naming_conventions/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_file_naming_conventions/GoodSnippet.astro';
Надавайте перевагу дотриманню конвенцій іменування файлів. :::note Це правило лінтера було введено у версії `0.3.0` пакета [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Обґрунтування Для узгодженості, простоти обслуговування та розділення відповідальності краще визначати екземпляри bloc та cubit у їхніх відповідних файлах Dart замість вбудовування їх безпосередньо. :::tip Розгляньте можливість використання команди `bloc new ` з пакета [package:bloc_tools](https://pub.dev/packages/bloc_tools) для швидкого та послідовного генерування нових екземплярів bloc/cubit. ::: ## Приклади **Надавайте перевагу** оголошенню екземплярів bloc/cubit у їхніх власних відповідних файлах. **ДОБРЕ**: **ПОГАНО**: ## Увімкнення Щоб увімкнути правило `prefer_file_naming_conventions`, додайте його до `analysis_options.yaml` у розділі `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/uk/lint-rules/prefer_void_public_cubit_methods.mdx ================================================ --- title: Надавайте перевагу void публічним методам Cubit description: Правило prefer_void_public_cubit_methods. --- import { Badge } from '@astrojs/starlight/components'; import EnableRuleSnippet from '~/components/lint-rules/EnableRuleSnippet.astro'; import BadSnippet from '~/components/lint-rules/prefer_void_public_cubit_methods/BadSnippet.mdx'; import GoodSnippet from '~/components/lint-rules/prefer_void_public_cubit_methods/GoodSnippet.astro';
Надавайте перевагу void публічним методам в екземплярах `Cubit`. :::note Це правило лінтера було введено у версії `0.2.0-dev.2` пакета [`package:bloc_lint`](https://pub.dev/packages/bloc_lint) ::: ## Обґрунтування Публічні методи в екземплярах `Cubit` повинні використовуватися для сповіщення `Cubit` та ініціювання змін стану через метод `emit`. Якщо викликаючій стороні потрібен доступ до будь-якої інформації про стан, вона повинна отримувати її з `state`. :::note Наступні правила є пов'язаними і зазвичай вмикаються в комбінації з `prefer_void_public_cubit_methods`. - [`avoid_public_bloc_methods`](/uk/lint-rules/avoid_public_bloc_methods) - [`avoid_public_fields`](/uk/lint-rules/avoid_public_fields) ::: ## Приклади **Уникайте** не-void публічних методів в екземплярах `Cubit`. **ПОГАНО**: **ДОБРЕ**: ## Увімкнення Щоб увімкнути правило `prefer_void_public_cubit_methods`, додайте його до `analysis_options.yaml` у розділі `bloc` > `rules`: ================================================ FILE: docs/src/content/docs/uk/migration.mdx ================================================ --- title: Посібник з міграції description: Мігруйте на останню стабільну версію Bloc. --- import { Code, Tabs, TabItem } from '@astrojs/starlight/components'; :::tip Будь ласка, зверніться до [журналу релізів](https://github.com/felangel/bloc/releases) для отримання додаткової інформації про те, що змінилося в кожному релізі. ::: ## v10.0.0 ### `package:bloc_test` #### ❗✨ Відокремлення `blocTest` від `BlocBase` :::note[Що змінилося?] У bloc_test v10.0.0 API `blocTest` більше не тісно пов'язаний з `BlocBase`. ::: ##### Обґрунтування `blocTest` повинен використовувати основні інтерфейси bloc, коли це можливо, для підвищення гнучкості та можливості повторного використання. Раніше це було неможливо, оскільки `BlocBase` реалізовував `StateStreamableSource`, чого було недостатньо для `blocTest` через внутрішню залежність від API `emit`. ### `package:hydrated_bloc` #### ❗✨ Підтримка WebAssembly :::note[Що змінилося?] У hydrated_bloc v10.0.0 було додано підтримку компіляції у WebAssembly (wasm). ::: ##### Обґрунтування Раніше було неможливо компілювати додатки у wasm при використанні `hydrated_bloc`. У v10.0.0 пакет було перероблено для підтримки компіляції у wasm. **v9.x.x** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` **v10.x.x** ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorageDirectory.web : HydratedStorageDirectory((await getTemporaryDirectory()).path), ); runApp(const App()); } ``` ## v9.0.0 ### `package:bloc` #### ❗🧹 Видалення застарілих API :::note[Що змінилося?] У bloc v9.0.0 всі раніше застарілі API були видалені. ::: ##### Короткий виклад - `BlocOverrides` видалено на користь `Bloc.observer` та `Bloc.transformer` #### ❗✨ Введення нового інтерфейсу `EmittableStateStreamableSource` :::note[Що змінилося?] У bloc v9.0.0 було введено новий основний інтерфейс `EmittableStateStreamableSource`. ::: ##### Обґрунтування `package:bloc_test` раніше був тісно пов'язаний з `BlocBase`. Інтерфейс `EmittableStateStreamableSource` було введено для того, щоб дозволити `blocTest` відокремитися від конкретної реалізації `BlocBase`. ### `package:hydrated_bloc` #### ✨ Повернення API `HydratedBloc.storage` :::note[Що змінилося?] У hydrated_bloc v9.0.0 `HydratedBlocOverrides` було видалено на користь API `HydratedBloc.storage`.\*\* ::: ##### Обґрунтування Зверніться до [обґрунтування повернення перевизначень Bloc.observer та Bloc.transformer](/uk/migration#-повернення-api-blocobserver-та-bloctransformer). **v8.x.x** ```dart Future main() async { final storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); HydratedBlocOverrides.runZoned( () => runApp(App()), storage: storage, ); } ``` **v9.0.0** ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorage.webStorageDirectory : await getTemporaryDirectory(), ); runApp(App()); } ``` ## v8.1.0 ### `package:bloc` #### ✨ Повернення API `Bloc.observer` та `Bloc.transformer` :::note[Що змінилося?] У bloc v8.1.0 `BlocOverrides` було оголошено застарілим на користь API `Bloc.observer` та `Bloc.transformer`. ::: ##### Обґрунтування API `BlocOverrides` було введено у v8.0.0 у спробі підтримати обмеження області дії конфігурацій, специфічних для bloc, таких як `BlocObserver`, `EventTransformer` та `HydratedStorage`. У чистих додатках Dart зміни працювали добре; однак у додатках Flutter новий API спричинив більше проблем, ніж вирішив. API `BlocOverrides` було натхненне подібними API у Flutter/Dart: - [HttpOverrides](https://api.flutter.dev/flutter/dart-io/HttpOverrides-class.html) - [IOOverrides](https://api.flutter.dev/flutter/dart-io/IOOverrides-class.html) **Проблеми** Хоча це не було основною причиною цих змін, API `BlocOverrides` привніс додаткову складність для розробників. Окрім збільшення кількості вкладеності та рядків коду, необхідних для досягнення того ж ефекту, API `BlocOverrides` вимагав від розробників глибокого розуміння [Zones](https://api.dart.dev/stable/2.17.6/dart-async/Zone-class.html) у Dart. `Zones` -- це не зручна для початківців концепція, і нерозуміння того, як працюють Zones, може призвести до появи помилок (таких як неініціалізовані спостерігачі, трансформери, екземпляри сховища). Наприклад, багато розробників мали б щось на кшталт: ```dart void main() { WidgetsFlutterBinding.ensureInitialized(); BlocOverrides.runZoned(...); } ``` Наведений вище код, хоча й виглядає нешкідливим, насправді може призвести до багатьох важко відстежуваних помилок. Яка б зона не викликала первісно `WidgetsFlutterBinding.ensureInitialized`, буде зоною, в якій обробляються події жестів (наприклад, колбеки `onTap`, `onPressed`) через `GestureBinding.initInstances`. Це лише одна з багатьох проблем, спричинених використанням `zoneValues`. Крім того, Flutter робить багато речей за лаштунками, які включають розгалуження/маніпуляцію Zones (особливо при запуску тестів), що може призвести до неочікуваної поведінки (і у багатьох випадках до поведінки, яка знаходиться поза контролем розробника -- див. проблеми нижче). Через використання [runZoned](https://api.flutter.dev/flutter/dart-async/runZoned.html), перехід на API `BlocOverrides` призвів до виявлення кількох помилок/обмежень у Flutter (зокрема у віджетних та інтеграційних тестах): - https://github.com/flutter/flutter/issues/96939 - https://github.com/flutter/flutter/issues/94123 - https://github.com/flutter/flutter/issues/93676 які вплинули на багатьох розробників, що використовують бібліотеку bloc: - https://github.com/felangel/bloc/issues/3394 - https://github.com/felangel/bloc/issues/3350 - https://github.com/felangel/bloc/issues/3319 **v8.0.x** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` **v8.1.0** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` ## v8.0.0 ### `package:bloc` #### ❗✨ Введення нового API `BlocOverrides` :::note[Що змінилося?] У bloc v8.0.0 `Bloc.observer` та `Bloc.transformer` були видалені на користь API `BlocOverrides`. ::: ##### Обґрунтування Попередній API, який використовувався для перевизначення `BlocObserver` та `EventTransformer` за замовчуванням, покладався на глобальний сінглтон як для `BlocObserver`, так і для `EventTransformer`. У результаті було неможливо: - Мати кілька реалізацій `BlocObserver` або `EventTransformer`, обмежених різними частинами додатку - Мати перевизначення `BlocObserver` або `EventTransformer`, обмежені пакетом - Якщо пакет залежав від `package:bloc` і реєстрував свій власний `BlocObserver`, будь-який споживач пакету повинен був би або перезаписати `BlocObserver` пакету, або звітувати перед `BlocObserver` пакету. Також було складніше тестувати через спільний глобальний стан у тестах. Bloc v8.0.0 вводить клас `BlocOverrides`, який дозволяє розробникам перевизначати `BlocObserver` та/або `EventTransformer` для конкретної `Zone`, а не покладатися на глобальний змінний сінглтон. **v7.x.x** ```dart void main() { Bloc.observer = CustomBlocObserver(); Bloc.transformer = customEventTransformer(); // ... } ``` **v8.0.0** ```dart void main() { BlocOverrides.runZoned( () { // ... }, blocObserver: CustomBlocObserver(), eventTransformer: customEventTransformer(), ); } ``` Екземпляри `Bloc` будуть використовувати `BlocObserver` та/або `EventTransformer` для поточної `Zone` через `BlocOverrides.current`. Якщо для зони немає `BlocOverrides`, вони будуть використовувати існуючі внутрішні значення за замовчуванням (жодних змін у поведінці/функціональності). Це дозволяє кожній `Zone` функціонувати незалежно зі своїми `BlocOverrides`. ```dart BlocOverrides.runZoned( () { // BlocObserverA та eventTransformerA final overrides = BlocOverrides.current; // Bloc'и в цій зоні звітують перед BlocObserverA // та використовують eventTransformerA як трансформер за замовчуванням. // ... // Пізніше... BlocOverrides.runZoned( () { // BlocObserverB та eventTransformerB final overrides = BlocOverrides.current; // Bloc'и в цій зоні звітують перед BlocObserverB // та використовують eventTransformerB як трансформер за замовчуванням. // ... }, blocObserver: BlocObserverB(), eventTransformer: eventTransformerB(), ); }, blocObserver: BlocObserverA(), eventTransformer: eventTransformerA(), ); ``` #### ❗✨ Покращення обробки та звітування про помилки :::note[Що змінилося?] У bloc v8.0.0 `BlocUnhandledErrorException` видалено. Крім того, будь-які неперехоплені виключення завжди повідомляються в `onError` та пробрасуються повторно (незалежно від режиму налагодження чи релізу). API `addError` повідомляє про помилки в `onError`, але не розглядає повідомлені помилки як неперехоплені виключення. ::: ##### Обґрунтування Мета цих змін: - зробити внутрішні необроблені виключення надзвичайно очевидними, зберігаючи при цьому функціональність bloc - підтримувати `addError` без порушення потоку керування Раніше обробка та звітування про помилки відрізнялися залежно від того, чи працював додаток у режимі налагодження чи релізу. Крім того, помилки, повідомлені через `addError`, розглядалися як неперехоплені виключення у режимі налагодження, що призводило до поганого досвіду розробника при використанні API `addError` (особливо при написанні модульних тестів). У v8.0.0 `addError` можна безпечно використовувати для повідомлення про помилки, а `blocTest` можна використовувати для перевірки того, що помилки повідомляються. Всі помилки, як і раніше, повідомляються в `onError`, однак повторно пробрасуються лише неперехоплені виключення (незалежно від режиму налагодження чи релізу). #### ❗🧹 Зробити `BlocObserver` абстрактним :::note[Що змінилося?] У bloc v8.0.0 `BlocObserver` було перетворено на `abstract` клас, що означає, що екземпляр `BlocObserver` не може бути створений. ::: ##### Обґрунтування `BlocObserver` призначався для використання як інтерфейс. Оскільки реалізація API за замовчуванням -- це no-ops, `BlocObserver` тепер є `abstract` класом, щоб чітко показати, що клас призначений для розширення, а не для прямого створення екземпляру. **v7.x.x** ```dart void main() { // Було можливо створити екземпляр базового класу. final observer = BlocObserver(); } ``` **v8.0.0** ```dart class MyBlocObserver extends BlocObserver {...} void main() { // Неможливо створити екземпляр базового класу. final observer = BlocObserver(); // ПОМИЛКА // Замість цього розширте `BlocObserver`. final observer = MyBlocObserver(); // OK } ``` #### ❗✨ `add` викидає `StateError`, якщо Bloc закритий :::note[Що змінилося?] У bloc v8.0.0 виклик `add` на закритому bloc призведе до `StateError`. ::: ##### Обґрунтування Раніше було можливо викликати `add` на закритому bloc, і внутрішня помилка проковтувалася, що ускладнювало налагодження того, чому додана подія не оброблялася. Щоб зробити цей сценарій більш видимим, у v8.0.0 виклик `add` на закритому bloc викине `StateError`, який буде повідомлений як неперехоплене виключення та переданий в `onError`. #### ❗✨ `emit` викидає `StateError`, якщо Bloc закритий :::note[Що змінилося?] У bloc v8.0.0 виклик `emit` всередині закритого bloc призведе до `StateError`. ::: ##### Обґрунтування Раніше було можливо викликати `emit` всередині закритого bloc, і жодної зміни стану не відбувалося, але також не було жодної вказівки на те, що пішло не так, що ускладнювало налагодження. Щоб зробити цей сценарій більш видимим, у v8.0.0 виклик `emit` всередині закритого bloc викине `StateError`, який буде повідомлений як неперехоплене виключення та переданий в `onError`. #### ❗🧹 Видалення застарілих API :::note[Що змінилося?] У bloc v8.0.0 всі раніше застарілі API були видалені. ::: ##### Короткий виклад - `mapEventToState` видалено на користь `on` - `transformEvents` видалено на користь API `EventTransformer` - typedef `TransitionFunction` видалено на користь API `EventTransformer` - `listen` видалено на користь `stream.listen` ### `package:bloc_test` #### ✨ `MockBloc` та `MockCubit` більше не потребують `registerFallbackValue` :::note[Що змінилося?] У bloc_test v9.0.0 розробникам більше не потрібно явно викликати `registerFallbackValue` при використанні `MockBloc` або `MockCubit`. ::: ##### Короткий виклад `registerFallbackValue` потрібен лише при використанні матчера `any()` з `package:mocktail` для користувацького типу. Раніше `registerFallbackValue` був необхідний для кожного `Event` та `State` при використанні `MockBloc` або `MockCubit`. **v8.x.x** ```dart class FakeMyEvent extends Fake implements MyEvent {} class FakeMyState extends Fake implements MyState {} class MyMockBloc extends MockBloc implements MyBloc {} void main() { setUpAll(() { registerFallbackValue(FakeMyEvent()); registerFallbackValue(FakeMyState()); }); // Тести... } ``` **v9.0.0** ```dart class MyMockBloc extends MockBloc implements MyBloc {} void main() { // Тести... } ``` ### `package:hydrated_bloc` #### ❗✨ Введення нового API `HydratedBlocOverrides` :::note[Що змінилося?] У hydrated_bloc v8.0.0 `HydratedBloc.storage` було видалено на користь API `HydratedBlocOverrides`. ::: ##### Обґрунтування Раніше для перевизначення реалізації `Storage` використовувався глобальний сінглтон. У результаті було неможливо мати кілька реалізацій `Storage`, обмежених різними частинами додатку. Також було складніше тестувати через спільний глобальний стан у тестах. `HydratedBloc` v8.0.0 вводить клас `HydratedBlocOverrides`, який дозволяє розробникам перевизначати `Storage` для конкретної `Zone`, а не покладатися на глобальний змінний сінглтон. **v7.x.x** ```dart void main() async { HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); // ... } ``` **v8.0.0** ```dart void main() { final storage = await HydratedStorage.build( storageDirectory: await getApplicationSupportDirectory(), ); HydratedBlocOverrides.runZoned( () { // ... }, storage: storage, ); } ``` Екземпляри `HydratedBloc` будуть використовувати `Storage` для поточної `Zone` через `HydratedBlocOverrides.current`. Це дозволяє кожній `Zone` функціонувати незалежно зі своїми `BlocOverrides`. ## v7.2.0 ### `package:bloc` #### ✨ Введення нового API `on` :::note[Що змінилося?] У bloc v7.2.0 `mapEventToState` було оголошено застарілим на користь `on`. `mapEventToState` буде видалено у bloc v8.0.0. ::: ##### Обґрунтування API `on` було введено в рамках [[Пропозиція] Замінити mapEventToState на on\ у Bloc](https://github.com/felangel/bloc/issues/2526). Через [проблему в Dart](https://github.com/dart-lang/sdk/issues/44616) не завжди очевидно, яким буде значення `state` при роботі з вкладеними асинхронними генераторами (`async*`). Незважаючи на те, що існують способи обійти цю проблему, одним з основних принципів бібліотеки bloc є передбачуваність. API `on` було створено, щоб зробити бібліотеку максимально безпечною для використання та усунути будь-яку невизначеність щодо змін стану. :::tip Для отримання додаткової інформації [прочитайте повну пропозицію](https://github.com/felangel/bloc/issues/2526). ::: **Короткий виклад** `on` дозволяє зареєструвати обробник подій для всіх подій типу `E`. За замовчуванням події будуть оброблятися паралельно при використанні `on`, на відміну від `mapEventToState`, який обробляє події `послідовно`. **v7.1.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0); @override Stream mapEventToState(CounterEvent event) async* { if (event is Increment) { yield state + 1; } } } ``` **v7.2.0** ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } } ``` :::note Кожна зареєстрована функція `EventHandler` працює незалежно, тому важливо реєструвати обробники подій на основі типу трансформера, який ви хочете застосувати. ::: Якщо ви хочете зберегти точно таку ж поведінку, як у v7.1.0, ви можете зареєструвати один обробник подій для всіх подій та застосувати трансформер `sequential`: ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; class MyBloc extends Bloc { MyBloc() : super(MyState()) { on(_onEvent, transformer: sequential()) } FutureOr _onEvent(MyEvent event, Emitter emit) async { // TODO: логіка тут... } } ``` Ви також можете перевизначити `EventTransformer` за замовчуванням для всіх bloc'ів у вашому додатку: ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; void main() { Bloc.transformer = sequential(); ... } ``` #### ✨ Введення нового API `EventTransformer` :::note[Що змінилося?] У bloc v7.2.0 `transformEvents` було оголошено застарілим на користь API `EventTransformer`. `transformEvents` буде видалено у bloc v8.0.0. ::: ##### Обґрунтування API `on` відкрив можливість надання користувацького трансформера подій для кожного обробника подій. Було введено новий typedef `EventTransformer`, який дозволяє розробникам трансформувати вхідний потік подій для кожного обробника подій, а не вказувати один трансформер подій для всіх подій. **Короткий виклад** `EventTransformer` відповідає за прийняття вхідного потоку подій разом з `EventMapper` (ваш обробник події) та повернення нового потоку подій. ```dart typedef EventTransformer = Stream Function(Stream events, EventMapper mapper) ``` `EventTransformer` за замовчуванням обробляє всі події паралельно і виглядає приблизно так: ```dart EventTransformer concurrent() { return (events, mapper) => events.flatMap(mapper); } ``` :::tip Ознайомтеся з [package:bloc_concurrency](https://pub.dev/packages/bloc_concurrency) для отримання набору користувацьких трансформерів подій ::: **v7.1.0** ```dart @override Stream> transformEvents(events, transitionFn) { return events .debounceTime(const Duration(milliseconds: 300)) .flatMap(transitionFn); } ``` **v7.2.0** ```dart /// Визначте користувацький `EventTransformer` EventTransformer debounce(Duration duration) { return (events, mapper) => events.debounceTime(duration).flatMap(mapper); } MyBloc() : super(MyState()) { /// Застосуйте користувацький `EventTransformer` до `EventHandler` on(_onEvent, transformer: debounce(const Duration(milliseconds: 300))) } ``` #### ⚠️ Застаріння API `transformTransitions` :::note[Що змінилося?] У bloc v7.2.0 `transformTransitions` було оголошено застарілим на користь перевизначення API `stream`. `transformTransitions` буде видалено у bloc v8.0.0. ::: ##### Обґрунтування Геттер `stream` у `Bloc` спрощує перевизначення вихідного потоку станів, тому більше немає необхідності підтримувати окремий API `transformTransitions`. **Короткий виклад** **v7.1.0** ```dart @override Stream> transformTransitions( Stream> transitions, ) { return transitions.debounceTime(const Duration(milliseconds: 42)); } ``` **v7.2.0** ```dart @override Stream get stream => super.stream.debounceTime(const Duration(milliseconds: 42)); ``` ## v7.0.0 ### `package:bloc` #### ❗ Bloc та Cubit розширюють BlocBase ##### Обґрунтування Як розробник, відносини між bloc'ами та cubit'ами були дещо незручними. Коли cubit було вперше представлено, він починався як базовий клас для bloc'ів, що мало сенс, тому що він мав підмножину функціональності, а bloc'и просто розширювали Cubit та визначали додаткові API. Це призвело до кількох недоліків: - Усі API повинні були б або бути перейменовані, щоб приймати cubit для точності, або вони повинні були б бути залишені як bloc для послідовності, навіть незважаючи на те, що ієрархічно це неточно ([#1708](https://github.com/felangel/bloc/issues/1708), [#1560](https://github.com/felangel/bloc/issues/1560)). - Cubit повинен був розширювати Stream та реалізовувати EventSink, щоб мати спільну базу, на основі якої можна реалізувати віджети, такі як BlocBuilder, BlocListener тощо ([#1429](https://github.com/felangel/bloc/issues/1429)). Пізніше ми експериментували з інверсією відносин та зробили bloc базовим класом, що частково вирішило перший пункт вище, але призвело до інших проблем: - API cubit роздутий через базові API bloc, такі як mapEventToState, add тощо ([#2228](https://github.com/felangel/bloc/issues/2228)) - Розробники технічно можуть викликати ці API та ламати речі - Ми все ще маємо ту ж проблему з cubit, що розкриває весь API stream, як і раніше ([#1429](https://github.com/felangel/bloc/issues/1429)) Щоб вирішити ці проблеми, ми ввели базовий клас як для `Bloc`, так і для `Cubit` під назвою `BlocBase`, щоб вищестоящі компоненти як і раніше могли взаємодіяти як з екземплярами bloc, так і з cubit, але без розкриття всього API `Stream` та `EventSink` напряму. **Короткий виклад** **BlocObserver** **v6.1.x** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(Cubit cubit) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(Cubit cubit, Object event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(Cubit cubit, Object error, StackTrace stackTrace) {...} @override void onClose(Cubit cubit) {...} } ``` **v7.0.0** ```dart class SimpleBlocObserver extends BlocObserver { @override void onCreate(BlocBase bloc) {...} @override void onEvent(Bloc bloc, Object event) {...} @override void onChange(BlocBase bloc, Object? event) {...} @override void onTransition(Bloc bloc, Transition transition) {...} @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) {...} @override void onClose(BlocBase bloc) {...} } ``` **Bloc/Cubit** **v6.1.x** ```dart final bloc = MyBloc(); bloc.listen((state) {...}); final cubit = MyCubit(); cubit.listen((state) {...}); ``` **v7.0.0** ```dart final bloc = MyBloc(); bloc.stream.listen((state) {...}); final cubit = MyCubit(); cubit.stream.listen((state) {...}); ``` ### `package:bloc_test` #### ❗seed повертає функцію для підтримки динамічних значень ##### Обґрунтування Щоб підтримувати змінне початкове значення, яке можна динамічно оновлювати в `setUp`, `seed` повертає функцію. **Короткий виклад** **v7.x.x** ```dart blocTest( '...', seed: MyState(), ... ); ``` **v8.0.0** ```dart blocTest( '...', seed: () => MyState(), ... ); ``` #### ❗expect повертає функцію для підтримки динамічних значень та включає підтримку матчерів ##### Обґрунтування Щоб підтримувати змінне очікування, яке можна динамічно оновлювати в `setUp`, `expect` повертає функцію. `expect` також підтримує `Matchers`. **Короткий виклад** **v7.x.x** ```dart blocTest( '...', expect: [MyStateA(), MyStateB()], ... ); ``` **v8.0.0** ```dart blocTest( '...', expect: () => [MyStateA(), MyStateB()], ... ); // Це також може бути `Matcher` blocTest( '...', expect: () => contains(MyStateA()), ... ); ``` #### ❗errors повертає функцію для підтримки динамічних значень та включає підтримку матчерів ##### Обґрунтування Щоб підтримувати змінні помилки, які можна динамічно оновлювати в `setUp`, `errors` повертає функцію. `errors` також підтримує `Matchers`. **Короткий виклад** **v7.x.x** ```dart blocTest( '...', errors: [MyError()], ... ); ``` **v8.0.0** ```dart blocTest( '...', errors: () => [MyError()], ... ); // Це також може бути `Matcher` blocTest( '...', errors: () => contains(MyError()), ... ); ``` #### ❗MockBloc та MockCubit ##### Обґрунтування Для підтримки заглушок різних основних API `MockBloc` та `MockCubit` експортуються як частина пакету `bloc_test`. Раніше `MockBloc` доводилося використовувати як для екземплярів `Bloc`, так і для `Cubit`, що було неінтуїтивно. **Короткий виклад** **v7.x.x** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockBloc implements MyBloc {} ``` **v8.0.0** ```dart class MockMyBloc extends MockBloc implements MyBloc {} class MockMyCubit extends MockCubit implements MyCubit {} ``` #### ❗Інтеграція з Mocktail ##### Обґрунтування Через різні обмеження null-safe [package:mockito](https://pub.dev/packages/mockito), описані [тут](https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#problems-with-typical-mocking-and-stubbing), [package:mocktail](https://pub.dev/packages/mocktail) використовується `MockBloc` та `MockCubit`. Це дозволяє розробникам продовжувати використовувати знайомий API для моків без необхідності вручну писати заглушки або покладатися на генерацію коду. **Короткий виклад** **v7.x.x** ```dart import 'package:mockito/mockito.dart'; ... when(bloc.state).thenReturn(MyState()); verify(bloc.add(any)).called(1); ``` **v8.0.0** ```dart import 'package:mocktail/mocktail.dart'; ... when(() => bloc.state).thenReturn(MyState()); verify(() => bloc.add(any())).called(1); ``` > Будь ласка, зверніться до > [#347](https://github.com/dart-lang/mockito/issues/347), а також до > [документації mocktail](https://github.com/felangel/mocktail/tree/main/packages/mocktail) > для отримання додаткової інформації. ### `package:flutter_bloc` #### ❗ перейменування параметра `cubit` на `bloc` ##### Обґрунтування У результаті рефакторингу в `package:bloc` для введення `BlocBase`, який розширюють `Bloc` та `Cubit`, параметри `BlocBuilder`, `BlocConsumer` та `BlocListener` були перейменовані з `cubit` на `bloc`, тому що віджети працюють з типом `BlocBase`. Це також додатково узгоджується з назвою бібліотеки та, сподіваємося, покращує читабельність. **Короткий виклад** **v6.1.x** ```dart BlocBuilder( cubit: myBloc, ... ) BlocListener( cubit: myBloc, ... ) BlocConsumer( cubit: myBloc, ... ) ``` **v7.0.0** ```dart BlocBuilder( bloc: myBloc, ... ) BlocListener( bloc: myBloc, ... ) BlocConsumer( bloc: myBloc, ... ) ``` ### `package:hydrated_bloc` #### ❗storageDirectory є обов'язковим при виклику HydratedStorage.build ##### Обґрунтування Щоб зробити `package:hydrated_bloc` чистим пакетом Dart, залежність від [package:path_provider](https://pub.dev/packages/path_provider) була видалена, і параметр `storageDirectory` при виклику `HydratedStorage.build` є обов'язковим і більше не використовує `getTemporaryDirectory` за замовчуванням. **Короткий виклад** **v6.x.x** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` **v7.0.0** ```dart import 'package:path_provider/path_provider.dart'; ... HydratedBloc.storage = await HydratedStorage.build( storageDirectory: await getTemporaryDirectory(), ); ``` ## v6.1.0 ### `package:flutter_bloc` #### ❗context.bloc та context.repository застаріли на користь context.read та context.watch ##### Обґрунтування `context.read`, `context.watch` та `context.select` були додані для узгодження з існуючим API [provider](https://pub.dev/packages/provider), з яким знайомі багато розробників, та для вирішення проблем, піднятих спільнотою. Щоб підвищити безпеку коду та підтримувати узгодженість, `context.bloc` було оголошено застарілим, тому що його можна замінити або `context.read`, або `context.watch` залежно від того, чи використовується він безпосередньо всередині `build`. **context.watch** `context.watch` вирішує запит на наявність [MultiBlocBuilder](https://github.com/felangel/bloc/issues/538), тому що ми можемо спостерігати за кількома bloc'ами в одному `Builder`, щоб відрендерити UI на основі кількох станів: ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // повернути Widget, який залежить від стану BlocA, BlocB та BlocC } ); ``` **context.select** `context.select` дозволяє розробникам рендерити/оновлювати UI на основі частини стану bloc та вирішує запит на наявність [простішого buildWhen](https://github.com/felangel/bloc/issues/1521). ```dart final name = context.select((UserBloc bloc) => bloc.state.user.name); ``` Наведений вище фрагмент дозволяє нам отримати доступ та перебудувати віджет лише тоді, коли ім'я поточного користувача змінюється. **context.read** Хоча здається, що `context.read` ідентичний `context.bloc`, є деякі тонкі, але значні відмінності. Обидва дозволяють отримати доступ до bloc за допомогою `BuildContext` і не призводять до перебудов; однак `context.read` не може бути викликаний безпосередньо всередині методу `build`. Є дві основні причини використовувати `context.bloc` всередині `build`: 1. **Щоб отримати доступ до стану bloc** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` Наведене вище використання схильне до помилок, тому що віджет `Text` не буде перебудований, якщо стан bloc зміниться. У цьому сценарії слід використовувати або `BlocBuilder`, або `context.watch`. ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` або ```dart @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) => Text('$state'), ); } ``` :::note Використання `context.watch` в корені методу `build` призведе до того, що весь віджет буде перебудований при зміні стану bloc. Якщо весь віджет не потрібно перебудовувати, або використовуйте `BlocBuilder`, щоб обгорнути частини, які повинні перебудовуватися, використовуйте `Builder` з `context.watch` для обмеження перебудов, або розбийте віджет на менші віджети. ::: 2. **Щоб отримати доступ до bloc для додавання події** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` Наведене вище використання неефективне, тому що воно призводить до пошуку bloc на кожній перебудові, коли bloc потрібен лише тоді, коли користувач натискає `ElevatedButton`. У цьому сценарії краще використовувати `context.read` для доступу до bloc безпосередньо там, де він потрібен (у даному випадку в колбеці `onPressed`). ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` **Короткий виклад** **v6.0.x** ```dart @override Widget build(BuildContext context) { final bloc = context.bloc(); return ElevatedButton( onPressed: () => bloc.add(MyEvent()), ... ) } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () => context.read().add(MyEvent()), ... ) } ``` ?> Якщо доступ до bloc для додавання події, виконуйте доступ до bloc, використовуючи `context.read` у колбеці, де це необхідно. **v6.0.x** ```dart @override Widget build(BuildContext context) { final state = context.bloc().state; return Text('$state'); } ``` **v6.1.x** ```dart @override Widget build(BuildContext context) { final state = context.watch().state; return Text('$state'); } ``` ?> Використовуйте `context.watch` при доступі до стану bloc, щоб переконатися, що віджет перебудовується при зміні стану. ## v6.0.0 ### `package:bloc` #### ❗BlocObserver onError приймає Cubit ##### Обґрунтування Через інтеграцію `Cubit`, `onError` тепер є спільним як для екземплярів `Bloc`, так і для `Cubit`. Оскільки `Cubit` є базовим, `BlocObserver` буде приймати тип `Cubit`, а не тип `Bloc` у перевизначенні `onError`. **v5.x.x** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Bloc bloc, Object error, StackTrace stackTrace) { super.onError(bloc, error, stackTrace); } } ``` **v6.0.0** ```dart class MyBlocObserver extends BlocObserver { @override void onError(Cubit cubit, Object error, StackTrace stackTrace) { super.onError(cubit, error, stackTrace); } } ``` #### ❗Bloc не емітить останній стан при підписці ##### Обґрунтування Цю зміну було внесено для узгодження `Bloc` та `Cubit` із вбудованою поведінкою `Stream` у `Dart`. Крім того, відповідність старій поведінці в контексті `Cubit` призвела до багатьох непередбачених побічних ефектів і в цілому ускладнила внутрішні реалізації інших пакетів, таких як `flutter_bloc` та `bloc_test`, без необхідності (потребуючи `skip(1)` тощо). **v5.x.x** ```dart final bloc = MyBloc(); bloc.listen(print); ``` Раніше наведений вище фрагмент виводив початковий стан bloc, за яким слідували подальші зміни стану. **v6.x.x** У v6.0.0 наведений вище фрагмент не виводить початковий стан і виводить лише подальші зміни стану. Попередню поведінку можна досягти наступним чином: ```dart final bloc = MyBloc(); print(bloc.state); bloc.listen(print); ``` ?> **Примітка**: Ця зміна вплине лише на код, який залежить від прямих підписок на bloc. При використанні `BlocBuilder`, `BlocListener` або `BlocConsumer` не буде помітної зміни в поведінці. ### `package:bloc_test` #### ❗MockBloc потребує лише тип State ##### Обґрунтування Це не обов'язково і усуває зайвий код, а також робить `MockBloc` сумісним з `Cubit`. **v5.x.x** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` **v6.0.0** ```dart class MockCounterBloc extends MockBloc implements CounterBloc {} ``` #### ❗whenListen потребує лише тип State ##### Обґрунтування Це не обов'язково і усуває зайвий код, а також робить `whenListen` сумісним з `Cubit`. **v5.x.x** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` **v6.0.0** ```dart whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); ``` #### ❗blocTest не потребує тип Event ##### Обґрунтування Це не обов'язково і усуває зайвий код, а також робить `blocTest` сумісним з `Cubit`. **v5.x.x** ```dart blocTest( 'emits [1] when increment is called', build: () async => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` **v6.0.0** ```dart blocTest( 'emits [1] when increment is called', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [1], ); ``` #### ❗blocTest skip за замовчуванням дорівнює 0 ##### Обґрунтування Оскільки екземпляри `bloc` та `cubit` більше не будуть емітити останній стан для нових підписок, більше не було необхідності встановлювати `skip` за замовчуванням рівним `1`. **v5.x.x** ```dart blocTest( 'emits [0] when skip is 0', build: () async => CounterBloc(), skip: 0, expect: const [0], ); ``` **v6.0.0** ```dart blocTest( 'emits [] when skip is 0', build: () => CounterBloc(), skip: 0, expect: const [], ); ``` Початковий стан bloc або cubit можна протестувати наступним чином: ```dart test('initial state is correct', () { expect(MyBloc().state, InitialState()); }); ``` #### ❗blocTest робить build синхронним ##### Обґрунтування Раніше `build` було зроблено `async`, щоб можна було виконати різні підготовчі дії для приведення тестованого bloc у певний стан. Це більше не потрібно і також вирішує кілька проблем через додаткову затримку між build та підпискою всередині. Замість того, щоб виконувати асинхронну підготовку для приведення bloc у бажаний стан, ми тепер можемо встановити стан bloc, зв'язавши `emit` з бажаним станом. **v5.x.x** ```dart blocTest( 'emits [2] when increment is added', build: () async { final bloc = CounterBloc(); bloc.add(CounterEvent.increment); await bloc.take(2); return bloc; } act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` **v6.0.0** ```dart blocTest( 'emits [2] when increment is added', build: () => CounterBloc()..emit(1), act: (bloc) => bloc.add(CounterEvent.increment), expect: const [2], ); ``` :::note `emit` видимий лише для тестування і ніколи не повинен використовуватися поза тестами. ::: ### `package:flutter_bloc` #### ❗параметр bloc у BlocBuilder перейменовано на cubit ##### Обґрунтування Щоб зробити `BlocBuilder` сумісним з екземплярами `bloc` та `cubit`, параметр `bloc` було перейменовано на `cubit` (оскільки `Cubit` є базовим класом). **v5.x.x** ```dart BlocBuilder( bloc: myBloc, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocBuilder( cubit: myBloc, builder: (context, state) {...} ) ``` #### ❗параметр bloc у BlocListener перейменовано на cubit ##### Обґрунтування Щоб зробити `BlocListener` сумісним з екземплярами `bloc` та `cubit`, параметр `bloc` було перейменовано на `cubit` (оскільки `Cubit` є базовим класом). **v5.x.x** ```dart BlocListener( bloc: myBloc, listener: (context, state) {...} ) ``` **v6.0.0** ```dart BlocListener( cubit: myBloc, listener: (context, state) {...} ) ``` #### ❗параметр bloc у BlocConsumer перейменовано на cubit ##### Обґрунтування Щоб зробити `BlocConsumer` сумісним з екземплярами `bloc` та `cubit`, параметр `bloc` було перейменовано на `cubit` (оскільки `Cubit` є базовим класом). **v5.x.x** ```dart BlocConsumer( bloc: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` **v6.0.0** ```dart BlocConsumer( cubit: myBloc, listener: (context, state) {...}, builder: (context, state) {...} ) ``` --- ## v5.0.0 ### `package:bloc` #### ❗initialState було видалено ##### Обґрунтування Як розробник, необхідність перевизначати `initialState` при створенні bloc становить дві основні проблеми: - `initialState` bloc може бути динамічним і також може бути вказаний пізніше (навіть поза самим bloc). У деякому сенсі це можна розглядати як витік внутрішньої інформації bloc на рівень UI. - Це багатослівно. **v4.x.x** ```dart class CounterBloc extends Bloc { @override int get initialState => 0; ... } ``` **v5.0.0** ```dart class CounterBloc extends Bloc { CounterBloc() : super(0); ... } ``` ?> Для отримання додаткової інформації ознайомтеся з [#1304](https://github.com/felangel/bloc/issues/1304) #### ❗BlocDelegate перейменовано на BlocObserver ##### Обґрунтування Назва `BlocDelegate` не була точним описом ролі, яку відігравав клас. `BlocDelegate` передбачає, що клас відіграє активну роль, тоді як насправді передбачувана роль `BlocDelegate` полягала у тому, щоб бути пасивним компонентом, який просто спостерігає за всіма bloc'ами у додатку. :::note В ідеалі не повинно бути жодних користувацьких функцій або можливостей, що обробляються всередині `BlocObserver`. ::: **v4.x.x** ```dart class MyBlocDelegate extends BlocDelegate { ... } ``` **v5.0.0** ```dart class MyBlocObserver extends BlocObserver { ... } ``` #### ❗BlocSupervisor було видалено ##### Обґрунтування `BlocSupervisor` був ще одним компонентом, про який розробникам потрібно було знати та взаємодіяти з ним виключно для вказівки користувацького `BlocDelegate`. Зі зміною на `BlocObserver` ми відчули, що це покращило досвід розробника, встановивши спостерігача безпосередньо на самому bloc. ?> Ця зміна також дозволила нам відокремити інші доповнення до bloc, такі як `HydratedStorage`, від `BlocObserver`. **v4.x.x** ```dart BlocSupervisor.delegate = MyBlocDelegate(); ``` **v5.0.0** ```dart Bloc.observer = MyBlocObserver(); ``` ### `package:flutter_bloc` #### ❗condition у BlocBuilder перейменовано на buildWhen ##### Обґрунтування При використанні `BlocBuilder` ми раніше могли вказати `condition`, щоб визначити, чи повинен `builder` перебудовуватися. ```dart BlocBuilder( condition: (previous, current) { // повернути true/false, щоб визначити, чи потрібно викликати builder }, builder: (context, state) {...} ) ``` Назва `condition` не дуже самоочевидна або зрозуміла, і, що важливіше, при взаємодії з `BlocConsumer` API став неузгодженим, тому що розробники можуть надати дві умови (одну для `builder` і одну для `listener`). У результаті API `BlocConsumer` розкрив `buildWhen` та `listenWhen` ```dart BlocConsumer( listenWhen: (previous, current) { // повернути true/false, щоб визначити, чи потрібно викликати listener }, listener: (context, state) {...}, buildWhen: (previous, current) { // повернути true/false, щоб визначити, чи потрібно викликати builder }, builder: (context, state) {...}, ) ``` Щоб узгодити API та забезпечити більш послідовний досвід розробника, `condition` було перейменовано на `buildWhen`. **v4.x.x** ```dart BlocBuilder( condition: (previous, current) { // повернути true/false, щоб визначити, чи потрібно викликати builder }, builder: (context, state) {...} ) ``` **v5.0.0** ```dart BlocBuilder( buildWhen: (previous, current) { // повернути true/false, щоб визначити, чи потрібно викликати builder }, builder: (context, state) {...} ) ``` #### ❗condition у BlocListener перейменовано на listenWhen ##### Обґрунтування З тих самих причин, що описані вище, умову `BlocListener` також було перейменовано. **v4.x.x** ```dart BlocListener( condition: (previous, current) { // повернути true/false, щоб визначити, чи потрібно викликати listener }, listener: (context, state) {...} ) ``` **v5.0.0** ```dart BlocListener( listenWhen: (previous, current) { // повернути true/false, щоб визначити, чи потрібно викликати listener }, listener: (context, state) {...} ) ``` ### `package:hydrated_bloc` #### ❗HydratedStorage та HydratedBlocStorage перейменовані ##### Обґрунтування Щоб покращити повторне використання коду між [hydrated_bloc](https://pub.dev/packages/hydrated_bloc) та [hydrated_cubit](https://pub.dev/packages/hydrated_cubit), конкретна реалізація сховища за замовчуванням була перейменована з `HydratedBlocStorage` на `HydratedStorage`. Крім того, інтерфейс `HydratedStorage` було перейменовано з `HydratedStorage` на `Storage`. **v4.0.0** ```dart class MyHydratedStorage implements HydratedStorage { ... } ``` **v5.0.0** ```dart class MyHydratedStorage implements Storage { ... } ``` #### ❗HydratedStorage відокремлено від BlocDelegate ##### Обґрунтування Як згадувалося раніше, `BlocDelegate` було перейменовано на `BlocObserver` і було встановлено безпосередньо як частину `bloc` через: ```dart Bloc.observer = MyBlocObserver(); ``` Наступну зміну було внесено для: - Залишатися узгодженим з новим API спостерігача bloc - Обмежити область дії сховища лише `HydratedBloc` - Відокремити `BlocObserver` від `Storage` **v4.0.0** ```dart BlocSupervisor.delegate = await HydratedBlocDelegate.build(); ``` **v5.0.0** ```dart HydratedBloc.storage = await HydratedStorage.build(); ``` #### ❗Спрощена ініціалізація ##### Обґрунтування Раніше розробникам доводилося вручну викликати `super.initialState ?? DefaultInitialState()` для налаштування своїх екземплярів `HydratedBloc`. Це незручно і багатослівно, а також несумісно з критичними змінами в `initialState` у `bloc`. У результаті в v5.0.0 ініціалізація `HydratedBloc` ідентична звичайній ініціалізації `Bloc`. **v4.0.0** ```dart class CounterBloc extends HydratedBloc { @override int get initialState => super.initialState ?? 0; } ``` **v5.0.0** ```dart class CounterBloc extends HydratedBloc { CounterBloc() : super(0); ... } ``` ================================================ FILE: docs/src/content/docs/uk/modeling-state.mdx ================================================ --- title: Моделювання стану description: Огляд кількох способів моделювання станів при використанні package:bloc. --- import ConcreteClassAndStatusEnumSnippet from '~/components/modeling-state/ConcreteClassAndStatusEnumSnippet.astro'; import SealedClassAndSubclassesSnippet from '~/components/modeling-state/SealedClassAndSubclassesSnippet.astro'; Існує безліч різних підходів до структурування стану додатку. Кожен з них має свої переваги та недоліки. У цьому розділі ми розглянемо кілька підходів, їх плюси та мінуси, а також коли використовувати кожен з них. Наведені нижче підходи є лише рекомендаціями і є повністю необов'язковими. Використовуйте будь-який підхід, який вам подобається. Ви можете виявити, що деякі приклади/документація не дотримуються цих підходів, переважно для простоти/стислості. :::tip Наведені нижче фрагменти коду зосереджені на структурі стану. На практиці ви також можете захотіти: - Розширити `Equatable` з [`package:equatable`](https://pub.dev/packages/equatable) - Анотувати клас за допомогою `@Data()` з [`package:data_class`](https://pub.dev/packages/data_class) - Анотувати клас за допомогою **@immutable** з [`package:meta`](https://pub.dev/packages/meta) - Реалізувати метод `copyWith` - Використовувати ключове слово `const` для конструкторів ::: ## Конкретний клас та enum статусу Цей підхід складається з **одного конкретного класу** для всіх станів разом з `enum`, що представляє різні статуси. Властивості робляться nullable і обробляються на основі поточного статусу. Цей підхід найкраще працює для станів, які не є строго ексклюзивними та/або містять багато спільних властивостей. #### Плюси - **Просто**: Легко керувати одним класом та enum статусу, і всі властивості легко доступні. - **Стисло**: Зазвичай потребує менше рядків коду порівняно з іншими підходами. #### Мінуси - **Не типобезпечно**: Потребує перевірки `status` перед доступом до властивостей. Можливо `emit` неправильно сформований стан, що може призвести до помилок. Властивості для конкретних станів є nullable, що може бути обтяжливим для керування і потребує або примусового розгортання, або виконання перевірок на null. Деякі з цих мінусів можна пом'якшити написанням модульних тестів та написанням спеціалізованих іменованих конструкторів. - **Роздутий**: Призводить до одного стану, який може стати роздутим з багатьма властивостями з часом. #### Вердикт Цей підхід найкраще працює для простих станів або коли вимоги потребують станів, які не є ексклюзивними (наприклад, показ snackbar при виникненні помилки при збереженні старих даних з останнього успішного стану). Цей підхід забезпечує гнучкість та стислість за рахунок типобезпеки. ## Запечатаний клас та підкласи Цей підхід складається з **запечатаного класу**, який містить будь-які спільні властивості, та кількох підкласів для окремих станів. Цей підхід чудово підходить для окремих, ексклюзивних станів. #### Плюси - **Типобезпечно**: Код безпечний на етапі компіляції, і неможливо випадково отримати доступ до недопустимої властивості. Кожен підклас містить свої власні властивості, що робить зрозумілим, які властивості належать якому стану. - **Явно:** Розділяє спільні властивості від специфічних для стану властивостей. - **Вичерпно**: Використання оператора `switch` для перевірки вичерпності, щоб гарантувати, що кожен стан явно оброблений. - Якщо ви не хочете [вичерпного перемикання](https://dart.dev/language/branches#exhaustiveness-checking) або хочете мати можливість додавати підтипи пізніше без порушення API, використовуйте модифікатор [final](https://dart.dev/language/class-modifiers#final). - Див. [документацію по запечатаних класах](https://dart.dev/language/class-modifiers#sealed) для отримання детальнішої інформації. #### Мінуси - **Багатослівно**: Потребує більше коду (один базовий клас та підклас для кожного стану). Також може потребувати дублювання коду для спільних властивостей у підкласах. - **Складно**: Додавання нових властивостей потребує оновлення кожного підкласу та базового класу, що може бути обтяжливим і призвести до збільшення складності стану. Крім того, може потребувати непотрібної/надмірної перевірки типів для доступу до властивостей. #### Вердикт Цей підхід найкраще працює для добре визначених ексклюзивних станів з унікальними властивостями. Цей підхід забезпечує типобезпеку та вичерпні перевірки і наголошує на безпеці над стислістю та простотою. ================================================ FILE: docs/src/content/docs/uk/naming-conventions.mdx ================================================ --- title: Угоди про іменування description: Огляд рекомендованих угод про іменування при використанні bloc. --- import EventExamplesGood1 from '~/components/naming-conventions/EventExamplesGood1Snippet.astro'; import EventExamplesBad1 from '~/components/naming-conventions/EventExamplesBad1Snippet.astro'; import StateExamplesGood1Snippet from '~/components/naming-conventions/StateExamplesGood1Snippet.astro'; import SingleStateExamplesGood1Snippet from '~/components/naming-conventions/SingleStateExamplesGood1Snippet.astro'; import StateExamplesBad1Snippet from '~/components/naming-conventions/StateExamplesBad1Snippet.astro'; Наведені нижче угоди про іменування є лише рекомендаціями і є повністю необов'язковими. Використовуйте будь-які угоди про іменування, які вам подобаються. Ви можете виявити, що деякі приклади/документація не дотримуються угод про іменування, переважно для простоти/стислості. Ці угоди настійно рекомендуються для великих проєктів з кількома розробниками. ## Угоди про події Події повинні бути названі в **минулому часі**, тому що події -- це те, що вже відбулося з точки зору блоку. ### Анатомія `BlocSubject` + `Іменник (необов'язково)` + `Дієслово (подія)` Події початкового завантаження повинні дотримуватися угоди: `BlocSubject` + `Started` :::note Базовий клас події повинен бути названий: `BlocSubject` + `Event`. ::: ### Приклади ✅ **Добре** ❌ **Погано** ## Угоди про стани Стани повинні бути іменниками, тому що стан -- це просто знімок у певний момент часу. Існує два поширені способи представлення стану: використання підкласів або використання одного класу. ### Анатомія #### Підкласи `BlocSubject` + `Дієслово (дія)` + `State` При представленні стану у вигляді кількох підкласів `State` повинен бути одним з наступних: `Initial` | `Success` | `Failure` | `InProgress` :::note Початкові стани повинні дотримуватися угоди: `BlocSubject` + `Initial`. ::: #### Один клас `BlocSubject` + `State` При представленні стану у вигляді одного базового класу повинен використовуватися enum з назвою `BlocSubject` + `Status` для представлення статусу стану: `initial` | `success` | `failure` | `loading`. :::note Базовий клас стану завжди повинен бути названий: `BlocSubject` + `State`. ::: ### Приклади ✅ **Добре** ##### Підкласи ##### Один клас ❌ **Погано** ================================================ FILE: docs/src/content/docs/uk/testing.mdx ================================================ --- title: Тестування description: Основи написання тестів для ваших блоків. --- import CounterBlocSnippet from '~/components/testing/CounterBlocSnippet.astro'; import AddDevDependenciesSnippet from '~/components/testing/AddDevDependenciesSnippet.astro'; import CounterBlocTestImportsSnippet from '~/components/testing/CounterBlocTestImportsSnippet.astro'; import CounterBlocTestMainSnippet from '~/components/testing/CounterBlocTestMainSnippet.astro'; import CounterBlocTestSetupSnippet from '~/components/testing/CounterBlocTestSetupSnippet.astro'; import CounterBlocTestInitialStateSnippet from '~/components/testing/CounterBlocTestInitialStateSnippet.astro'; import CounterBlocTestBlocTestSnippet from '~/components/testing/CounterBlocTestBlocTestSnippet.astro'; Bloc був розроблений так, щоб його було надзвичайно легко тестувати. У цьому розділі ми розглянемо, як провести модульне тестування блоку. Для простоти давайте напишемо тести для `CounterBloc`, який ми створили в розділі [Основні концепції](/uk/bloc-concepts). Нагадаємо, що реалізація `CounterBloc` виглядає так: ## Налаштування Перш ніж почати писати наші тести, нам потрібно додати фреймворк для тестування до наших залежностей. Нам потрібно додати [test](https://pub.dev/packages/test) та [bloc_test](https://pub.dev/packages/bloc_test) до нашого проєкту. ## Тестування Давайте почнемо зі створення файлу для наших тестів `CounterBloc`, `counter_bloc_test.dart`, та імпортуємо пакет test. Далі нам потрібно створити нашу функцію `main`, а також нашу групу тестів. :::note Групи використовуються для організації окремих тестів, а також для створення контексту, в якому ви можете спільно використовувати загальні `setUp` та `tearDown` для всіх окремих тестів. ::: Давайте почнемо зі створення екземпляру нашого `CounterBloc`, який буде використовуватися у всіх наших тестах. Тепер ми можемо почати писати наші окремі тести. :::note Ми можемо запустити всі наші тести за допомогою команди `dart test`. ::: На цьому етапі у нас повинен бути перший пройдений тест! Тепер давайте напишемо складніший тест, використовуючи пакет [bloc_test](https://pub.dev/packages/bloc_test). Ми повинні мати можливість запустити тести та побачити, що всі вони проходять. Ось і все, тестування має бути легким, і ми повинні відчувати впевненість при внесенні змін та рефакторингу нашого коду. Ви можете звернутися до [додатку Weather](https://github.com/felangel/bloc/tree/master/examples/flutter_weather) як прикладу повністю протестованого додатку. ================================================ FILE: docs/src/content/docs/uk/tutorials/flutter-counter.mdx ================================================ --- title: Лічильник Flutter description: Детальний посібник зі створення додатку-лічильника на Flutter з використанням bloc. sidebar: order: 1 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-counter/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) У наступному підручнику ми створимо лічильник на Flutter з використанням бібліотеки Bloc. ![demo](~/assets/tutorials/flutter-counter.gif) ## Ключові теми - Спостереження за змінами стану за допомогою [BlocObserver](/uk/bloc-concepts#blocobserver). - [BlocProvider](/uk/flutter-bloc-concepts#blocprovider), віджет Flutter, який надає bloc своїм дочірнім елементам. - [BlocBuilder](/uk/flutter-bloc-concepts#blocbuilder), віджет Flutter, який обробляє побудову віджета у відповідь на нові стани. - Використання Cubit замість Bloc. [У чому різниця?](/uk/bloc-concepts#cubit-проти-bloc) - Додавання подій за допомогою [context.read](/uk/flutter-bloc-concepts#contextread). ## Налаштування Почнемо зі створення нового Flutter проєкту Потім ми можемо замінити вміст `pubspec.yaml` на а потім встановити всі наші залежності ## Структура проєкту ``` ├── lib │ ├── app.dart │ ├── counter │ │ ├── counter.dart │ │ ├── cubit │ │ │ └── counter_cubit.dart │ │ └── view │ │ ├── counter_page.dart │ │ ├── counter_view.dart │ │ └── view.dart │ ├── counter_observer.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` Додаток використовує структуру каталогів, орієнтовану на функціональність. Ця структура проєкту дозволяє нам масштабувати проєкт, маючи самодостатні функціональні модулі. У цьому прикладі ми матимемо лише один модуль (сам лічильник), але у складніших додатках може бути сотні різних модулів. ## BlocObserver Перше, на що ми звернемо увагу — як створити `BlocObserver`, який допоможе нам спостерігати за всіма змінами стану в додатку. Створимо `lib/counter_observer.dart`: У цьому випадку ми перевизначаємо лише `onChange`, щоб бачити всі зміни стану, які відбуваються. :::note `onChange` працює однаково для екземплярів `Bloc` та `Cubit`. ::: ## main.dart Далі замінимо вміст `lib/main.dart` на: Ми ініціалізуємо щойно створений `CounterObserver` і викликаємо `runApp` з віджетом `CounterApp`, який ми розглянемо далі. ## Counter App Створимо `lib/app.dart`: `CounterApp` буде `MaterialApp` і вказує `home` як `CounterPage`. :::note Ми розширюємо `MaterialApp`, тому що `CounterApp` _є_ `MaterialApp`. У більшості випадків ми будемо створювати екземпляри `StatelessWidget` або `StatefulWidget` та компонувати віджети в `build`, але в цьому випадку немає віджетів для компонування, тому простіше просто розширити `MaterialApp`. ::: Давайте розглянемо `CounterPage` далі! ## Counter Page Створимо `lib/counter/view/counter_page.dart`: Віджет `CounterPage` відповідає за створення `CounterCubit` (який ми розглянемо далі) та надання його `CounterView`. :::note Важливо відокремити або розділити створення `Cubit` від споживання `Cubit`, щоб мати код, який значно легше тестувати та повторно використовувати. ::: ## Counter Cubit Створимо `lib/counter/cubit/counter_cubit.dart`: Клас `CounterCubit` надасть два методи: - `increment`: додає 1 до поточного стану - `decrement`: віднімає 1 від поточного стану Тип стану, яким керує `CounterCubit` — це просто `int`, а початковий стан дорівнює `0`. :::tip Використовуйте [розширення VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) або [плагін IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) для автоматичного створення нових cubit-ів. ::: Далі розглянемо `CounterView`, який відповідатиме за споживання стану та взаємодію з `CounterCubit`. ## Counter View Створимо `lib/counter/view/counter_view.dart`: `CounterView` відповідає за відображення поточного значення лічильника та відображення двох FloatingActionButton для збільшення/зменшення лічильника. `BlocBuilder` використовується для обгортання віджета `Text`, щоб оновлювати текст кожного разу, коли змінюється стан `CounterCubit`. Крім того, `context.read()` використовується для пошуку найближчого екземпляра `CounterCubit`. :::note Тільки віджет `Text` обгорнутий у `BlocBuilder`, оскільки це єдиний віджет, який потрібно перебудовувати у відповідь на зміни стану в `CounterCubit`. Уникайте зайвого обгортання віджетів, які не потребують перебудови при зміні стану. ::: ## Barrel Створимо `lib/counter/view/view.dart`: Додамо `view.dart` для експорту всіх публічних частин counter view. Створимо `lib/counter/counter.dart`: Додамо `counter.dart` для експорту всіх публічних частин функціональності лічильника. Ось і все! Ми відокремили шар представлення від шару бізнес-логіки. `CounterView` не знає, що відбувається, коли користувач натискає кнопку; він просто повідомляє `CounterCubit`. Крім того, `CounterCubit` не знає, що відбувається зі станом (значенням лічильника); він просто випускає нові стани у відповідь на виклики методів. Ми можемо запустити наш додаток за допомогою `flutter run` та переглянути його на нашому пристрої або симуляторі/емуляторі. Повний вихідний код (включаючи unit та widget тести) цього прикладу можна знайти [тут](https://github.com/felangel/Bloc/tree/master/examples/flutter_counter). ================================================ FILE: docs/src/content/docs/uk/tutorials/flutter-firebase-login.mdx ================================================ --- title: Вхід Flutter Firebase description: Детальний посібник зі створення потоку входу Flutter з використанням bloc та Firebase. sidebar: order: 7 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-firebase-login/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) У наступному підручнику ми створимо потік входу через Firebase у Flutter з використанням бібліотеки Bloc. ![demo](~/assets/tutorials/flutter-firebase-login.gif) ## Ключові теми - [BlocProvider](/uk/flutter-bloc-concepts#blocprovider), віджет Flutter, який надає bloc своїм дочірнім елементам. - Використання Cubit замість Bloc. [У чому різниця?](/uk/bloc-concepts#cubit-проти-bloc) - Додавання подій за допомогою [context.read](/uk/flutter-bloc-concepts#contextread). - Запобігання зайвим перебудовам за допомогою [Equatable](/uk/faqs#коли-використовувати-equatable). - [RepositoryProvider](/uk/flutter-bloc-concepts#repositoryprovider), віджет Flutter, який надає сховище своїм дочірнім елементам. - [BlocListener](/uk/flutter-bloc-concepts#bloclistener), віджет Flutter, який викликає код слухача у відповідь на зміни стану в bloc. - Додавання подій за допомогою [context.read](/uk/flutter-bloc-concepts#contextselect). ## Налаштування Почнемо зі створення нового Flutter проєкту. Як і в [підручнику з входу](/uk/tutorials/flutter-login), ми створимо внутрішні пакети для кращого шарування архітектури додатку та підтримки чітких меж, а також для максимального повторного використання та покращення тестованості. У цьому випадку пакети [firebase_auth](https://pub.dev/packages/firebase_auth) та [google_sign_in](https://pub.dev/packages/google_sign_in) будуть нашим шаром даних, тому ми створимо лише `AuthenticationRepository` для компонування даних від двох API-клієнтів. ## Authentication Repository `AuthenticationRepository` відповідатиме за абстрагування внутрішніх деталей реалізації автентифікації та отримання інформації про користувача. У цьому випадку він буде інтегруватися з Firebase, але ми завжди можемо змінити внутрішню реалізацію пізніше, і наш додаток не буде порушено. ### Налаштування Почнемо зі створення `packages/authentication_repository` та `pubspec.yaml` в корені проєкту. Далі ми можемо встановити залежності, виконавши: у каталозі `authentication_repository`. Як і більшість пакетів, `authentication_repository` визначає свій API через `packages/authentication_repository/lib/authentication_repository.dart` :::note Пакет `authentication_repository` експортуватиме `AuthenticationRepository`, а також моделі. ::: Далі розглянемо моделі. ### User Модель `User` описуватиме користувача в контексті домену автентифікації. Для цілей цього прикладу користувач складатиметься з `email`, `id`, `name` та `photo`. :::note Ви самі вирішуєте, як має виглядати користувач у контексті вашого домену. ::: [user.dart](https://raw.githubusercontent.com/felangel/bloc/master/examples/flutter_firebase_login/packages/authentication_repository/lib/src/models/user.dart ':include') :::note Клас `User` розширює [equatable](https://pub.dev/packages/equatable), щоб перевизначити порівняння рівності та мати можливість порівнювати різні екземпляри `User` за значенням. ::: :::tip Корисно визначити `static` порожнього `User`, щоб не обробляти `null` Users і завжди працювати з конкретним об'єктом `User`. ::: ### Repository `AuthenticationRepository` відповідає за абстрагування базової реалізації автентифікації користувача, а також отримання даних про користувача. `AuthenticationRepository` надає `Stream`, на який ми можемо підписатися, щоб отримувати сповіщення про зміну `User`. Крім того, він надає методи `signUp`, `logInWithGoogle`, `logInWithEmailAndPassword` та `logOut`. :::note `AuthenticationRepository` також відповідає за обробку низькорівневих помилок, які можуть виникнути в шарі даних, та надає чистий, простий набір помилок, що відповідають домену. ::: На цьому з `AuthenticationRepository` все. Далі розглянемо, як інтегрувати його у Flutter проєкт, який ми створили. ## Налаштування Firebase Нам потрібно виконати [інструкції з використання firebase_auth](https://pub.dev/packages/firebase_auth#usage), щоб підключити наш додаток до Firebase та увімкнути [google_sign_in](https://pub.dev/packages/google_sign_in). :::caution Не забудьте оновити `google-services.json` на Android та `GoogleService-Info.plist` й `Info.plist` на iOS, інакше додаток аварійно завершить роботу. ::: ## Залежності проєкту Ми можемо замінити згенерований `pubspec.yaml` у корені проєкту наступним: Зверніть увагу, що ми вказуємо каталог assets для всіх локальних ресурсів нашого додатку. Створіть каталог `assets` у корені проєкту та додайте [логотип bloc](https://github.com/felangel/bloc/blob/master/examples/flutter_firebase_login/assets/bloc_logo_small.png) (який ми використаємо пізніше). Потім встановіть усі залежності: :::note Ми залежимо від пакету `authentication_repository` через шлях, що дозволяє нам швидко ітерувати, зберігаючи чітке розділення. ::: ## main.dart Файл `main.dart` можна замінити наступним: Він просто налаштовує деяку глобальну конфігурацію для додатку та викликає `runApp` з екземпляром `App`. :::note Ми впроваджуємо єдиний екземпляр `AuthenticationRepository` в `App`, і це є явною залежністю конструктора. ::: ## App Як і в [підручнику з входу](/uk/tutorials/flutter-login), наш `app.dart` надає екземпляр `AuthenticationRepository` додатку через `RepositoryProvider`, а також створює та надає екземпляр `AuthenticationBloc`. Потім `AppView` споживає `AuthenticationBloc` та обробляє оновлення поточного маршруту на основі `AuthenticationState`. ## App Bloc `AppBloc` відповідає за керування глобальним станом додатку. Він залежить від `AuthenticationRepository` та підписується на потік `user`, щоб випускати нові стани у відповідь на зміни поточного користувача. ### State `AppState` складається з `AppStatus` та `User`. Конструктор за замовчуванням приймає необов'язкового `User` та перенаправляє до приватного конструктора з відповідним статусом автентифікації. ### Event `AppEvent` має два підкласи: - `AppUserSubscriptionRequested` — повідомляє bloc про підписку на потік користувача. - `AppLogoutPressed` — повідомляє bloc про дію виходу користувача. ### Bloc У тілі конструктора підкласи `AppEvent` зіставляються з відповідними обробниками подій. В обробнику подій `_onUserSubscriptionRequested` `AppBloc` використовує `emit.onEach` для підписки на потік користувача `AuthenticationRepository` та випуску стану у відповідь на кожного `User`. `emit.onEach` створює підписку на потік внутрішньо та піклується про її скасування, коли `AppBloc` або потік користувача закривається. Якщо потік користувача випускає помилку, `addError` пересилає помилку та стек виклику будь-якому `BlocObserver`, що слухає. :::caution Якщо `onError` пропущено, будь-які помилки в потоці користувача вважаються необробленими та будуть викинуті через `onEach`. Як наслідок, підписка на потік користувача буде скасована. ::: :::tip [`BlocObserver`](/uk/bloc-concepts/#blocobserver-1) чудово підходить для логування подій, помилок та змін стану Bloc, особливо в контексті аналітики та звітування про збої. ::: ## Моделі Моделі введення `Email` та `Password` корисні для інкапсуляції логіки валідації та будуть використовуватися як у `LoginForm`, так і в `SignUpForm` (пізніше у підручнику). Обидві моделі введення створені за допомогою пакету [formz](https://pub.dev/packages/formz) і дозволяють нам працювати з валідованим об'єктом замість примітивного типу, такого як `String`. ### Email ### Password ## Сторінка входу `LoginPage` відповідає за створення та надання екземпляра `LoginCubit` для `LoginForm`. :::tip Дуже важливо тримати створення блоків/кубітів окремо від місця їх споживання. Це дозволить вам легко впроваджувати мок-екземпляри та тестувати представлення ізольовано. ::: ## Login Cubit `LoginCubit` відповідає за керування `LoginState` форми. Він надає API для `logInWithCredentials`, `logInWithGoogle`, а також отримує сповіщення при оновленні email/пароля. ### State `LoginState` складається з `Email`, `Password` та `FormzStatus`. Моделі `Email` та `Password` розширюють `FormzInput` з пакету [formz](https://pub.dev/packages/formz). ### Cubit `LoginCubit` залежить від `AuthenticationRepository` для входу користувача через облікові дані або через вхід через Google. :::note Ми використали `Cubit` замість `Bloc` тут, оскільки `LoginState` досить простий та локалізований. Навіть без подій ми все ще можемо мати досить гарне уявлення про те, що відбулось, просто дивлячись на зміни від одного стану до іншого, а наш код значно простіший та лаконічніший. ::: ## Форма входу `LoginForm` відповідає за відображення форми у відповідь на `LoginState` та викликає методи `LoginCubit` у відповідь на взаємодії користувача. `LoginForm` також відображає кнопку "Створити обліковий запис", яка переходить на `SignUpPage`, де користувач може створити новий обліковий запис. ## Сторінка реєстрації Структура `SignUp` дзеркально відображає структуру `Login` та складається з `SignUpPage`, `SignUpView` та `SignUpCubit`. `SignUpPage` відповідає лише за створення та надання екземпляра `SignUpCubit` для `SignUpForm` (так само, як у `LoginPage`). :::note Як і у `LoginCubit`, `SignUpCubit` залежить від `AuthenticationRepository` для створення нових облікових записів користувачів. ::: ## Sign Up Cubit `SignUpCubit` керує станом `SignUpForm` та взаємодіє з `AuthenticationRepository` для створення нових облікових записів користувачів. ### State `SignUpState` повторно використовує ті самі моделі введення `Email` та `Password`, оскільки логіка валідації однакова. ### Cubit `SignUpCubit` дуже схожий на `LoginCubit`, з основною відмінністю в тому, що він надає API для відправки форми, а не для входу. ## Форма реєстрації `SignUpForm` відповідає за відображення форми у відповідь на `SignUpState` та викликає методи `SignUpCubit` у відповідь на взаємодії користувача. ## Домашня сторінка Після успішного входу або реєстрації користувача потік `user` буде оновлено, що призведе до зміни стану в `AuthenticationBloc` і `AppView` додасть маршрут `HomePage` до стеку навігації. На `HomePage` користувач може переглянути інформацію свого профілю та вийти, натиснувши іконку виходу в `AppBar`. :::note Каталог `widgets` було створено поряд з каталогом `view` у функціональному модулі `home` для багаторазових компонентів, специфічних для цього модуля. У цьому випадку простий віджет `Avatar` експортується та використовується в `HomePage`. ::: :::note Коли натискається `IconButton` виходу, подія `AuthenticationLogoutRequested` додається до `AuthenticationBloc`, який здійснює вихід користувача та переводить його назад на `LoginPage`. ::: На цьому етапі ми маємо досить надійну реалізацію входу з використанням Firebase, і ми відокремили шар представлення від шару бізнес-логіки за допомогою бібліотеки Bloc. Повний вихідний код цього прикладу можна знайти [тут](https://github.com/felangel/bloc/tree/master/examples/flutter_firebase_login). ================================================ FILE: docs/src/content/docs/uk/tutorials/flutter-infinite-list.mdx ================================================ --- title: Нескінченний список Flutter description: Детальний посібник зі створення нескінченного списку на Flutter з використанням bloc. sidebar: order: 3 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-infinite-list/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/flutter-infinite-list/FlutterPubGetSnippet.astro'; import PostsJsonSnippet from '~/components/tutorials/flutter-infinite-list/PostsJsonSnippet.astro'; import PostBlocInitialStateSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocInitialStateSnippet.astro'; import PostBlocOnPostFetchedSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocOnPostFetchedSnippet.astro'; import PostBlocTransformerSnippet from '~/components/tutorials/flutter-infinite-list/PostBlocTransformerSnippet.astro'; ![intermediate](https://img.shields.io/badge/level-intermediate-orange.svg) У цьому підручнику ми реалізуємо додаток, який завантажує дані з мережі та відображає їх у міру прокручування користувачем, використовуючи Flutter та бібліотеку bloc. ![demo](~/assets/tutorials/flutter-infinite-list.gif) ## Ключові теми - Спостереження за змінами стану за допомогою [BlocObserver](/uk/bloc-concepts#blocobserver). - [BlocProvider](/uk/flutter-bloc-concepts#blocprovider), віджет Flutter, який надає bloc своїм дочірнім елементам. - [BlocBuilder](/uk/flutter-bloc-concepts#blocbuilder), віджет Flutter, який обробляє побудову віджета у відповідь на нові стани. - Додавання подій за допомогою [context.read](/uk/flutter-bloc-concepts#contextread). - Запобігання зайвим перебудовам за допомогою [Equatable](/uk/faqs#коли-використовувати-equatable). - Використання методу `transformEvents` з Rx. ## Налаштування Почнемо зі створення нового Flutter проєкту Потім ми можемо замінити вміст pubspec.yaml на а потім встановити всі наші залежності ## Структура проєкту ``` ├── lib | ├── posts │ │ ├── bloc │ │ │ └── post_bloc.dart | | | └── post_event.dart | | | └── post_state.dart | | └── models | | | └── models.dart* | | | └── post.dart │ │ └── view │ │ | ├── posts_page.dart │ │ | └── posts_list.dart | | | └── view.dart* | | └── widgets | | | └── bottom_loader.dart | | | └── post_list_item.dart | | | └── widgets.dart* │ │ ├── posts.dart* │ ├── app.dart │ ├── simple_bloc_observer.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` Додаток використовує структуру каталогів, орієнтовану на функціональність. Ця структура проєкту дозволяє нам масштабувати проєкт, маючи самодостатні функціональні модулі. У цьому прикладі ми матимемо лише один модуль (модуль постів), який розділений на відповідні каталоги з barrel-файлами, позначеними зірочкою (\*). ## REST API Для цього демо-додатку ми використаємо [jsonplaceholder](http://jsonplaceholder.typicode.com) як джерело даних. :::note jsonplaceholder — це онлайн REST API, який надає фейкові дані; він дуже корисний для створення прототипів. ::: Відкрийте нову вкладку у браузері та перейдіть за адресою https://jsonplaceholder.typicode.com/posts?_start=0&_limit=2, щоб побачити, що повертає API. :::note У нашому URL ми вказали start та limit як параметри запиту для GET-запиту. ::: Чудово, тепер, коли ми знаємо, як виглядатимуть наші дані, створимо модель. ## Модель даних Створіть `post.dart` і почнемо створювати модель нашого об'єкта Post. `Post` — це просто клас з `id`, `title` та `body`. :::note Ми розширюємо [`Equatable`](https://pub.dev/packages/equatable), щоб мати можливість порівнювати `Post`. Без цього нам довелося б вручну змінювати наш клас для перевизначення рівності та hashCode, щоб відрізняти два об'єкти `Post`. Дивіться [пакет](https://pub.dev/packages/equatable) для детальної інформації. ::: Тепер, коли у нас є модель об'єкта `Post`, почнемо працювати над компонентом бізнес-логіки (bloc). ## Post Events Перш ніж зануритися в реалізацію, нам потрібно визначити, що буде робити наш `PostBloc`. На високому рівні він реагуватиме на введення користувача (прокручування) та завантажуватиме більше постів, щоб шар представлення міг їх відобразити. Почнемо зі створення нашої `Event`. Наш `PostBloc` реагуватиме лише на одну подію — `PostFetched`, яку буде додавати шар представлення, коли йому потрібно більше постів для відображення. Оскільки наш `PostFetched` є типом `PostEvent`, ми можемо створити `bloc/post_event.dart` та реалізувати подію так. Підсумовуючи, наш `PostBloc` отримуватиме `PostEvents` та перетворюватиме їх на `PostStates`. Ми визначили всі наші `PostEvents` (PostFetched), тому далі визначимо наш `PostState`. ## Post States Наш шар представлення потребуватиме кілька фрагментів інформації для коректного відображення: - `PostInitial` — повідомляє шар представлення, що потрібно відобразити індикатор завантаження під час завантаження початкової партії постів - `PostSuccess` — повідомляє шар представлення, що є контент для відображення - `posts` — буде `List`, який відображатиметься - `hasReachedMax` — повідомляє шар представлення, чи досягнуто максимальну кількість постів - `PostFailure` — повідомляє шар представлення, що сталася помилка під час завантаження постів Тепер ми можемо створити `bloc/post_state.dart` та реалізувати його так. :::note Ми реалізували `copyWith`, щоб мати можливість копіювати екземпляр `PostSuccess` та зручно оновлювати нуль або більше властивостей (це знадобиться пізніше). ::: Тепер, коли ми реалізували наші `Events` та `States`, можемо створити наш `PostBloc`. ## Post Bloc Для простоти наш `PostBloc` матиме пряму залежність від `http client`; однак у продакшн-додатку ми рекомендуємо замість цього впроваджувати api клієнт та використовувати патерн репозиторій [документація](/uk/architecture). Створимо `post_bloc.dart` та наш порожній `PostBloc`. :::note Лише з оголошення класу ми бачимо, що наш PostBloc прийматиме PostEvents як вхід та виводитиме PostStates. ::: Далі нам потрібно зареєструвати обробник подій для обробки вхідних подій `PostFetched`. У відповідь на подію `PostFetched` ми викликатимемо `_fetchPosts` для завантаження постів з API. Наш `PostBloc` буде `emit` нові стани через `Emitter`, наданий в обробнику подій. Перегляньте [основні концепції](/uk/bloc-concepts#потоки-streams) для детальної інформації. Тепер кожного разу, коли додається `PostEvent`, якщо це подія `PostFetched` і є ще пости для завантаження, наш `PostBloc` завантажить наступні 20 постів. API поверне порожній масив, якщо ми спробуємо завантажити більше максимальної кількості постів (100), тому якщо ми отримаємо порожній масив, наш bloc `emit`-не поточний стан, але встановить `hasReachedMax` у true. Якщо ми не можемо отримати пости, ми випускаємо `PostStatus.failure`. Якщо ми можемо отримати пости, ми випускаємо `PostStatus.success` та весь список постів. Одна оптимізація, яку ми можемо зробити — це `throttle` подій `PostFetched`, щоб не спамити наш API без потреби. Ми можемо це зробити, використовуючи параметр `transform` при реєстрації обробника подій `_onFetched`. :::note Передача `transformer` до `on` дозволяє налаштувати спосіб обробки подій. ::: :::note Переконайтеся, що імпортували [`package:stream_transform`](https://pub.dev/packages/stream_transform) для використання API `throttle`. ::: Наш завершений `PostBloc` тепер має виглядати так: Чудово! Тепер, коли ми завершили реалізацію бізнес-логіки, залишилося реалізувати шар представлення. ## Шар представлення У нашому `main.dart` ми можемо почати з реалізації головної функції та виклику `runApp` для відображення нашого кореневого віджета. Тут ми також можемо підключити наш bloc observer для логування переходів та помилок. У нашому віджеті `App`, кореневому елементі нашого проєкту, ми можемо встановити home як `PostsPage` У нашому віджеті `PostsPage` ми використовуємо `BlocProvider` для створення та надання екземпляра `PostBloc` піддереву. Також ми додаємо подію `PostFetched`, щоб при завантаженні додатку він запитав початкову партію постів. Далі нам потрібно реалізувати наше представлення `PostsList`, яке відображатиме наші пости та підключатиметься до нашого `PostBloc`. :::note `PostsList` є `StatefulWidget`, оскільки потребує підтримки `ScrollController`. У `initState` ми додаємо слухача до нашого `ScrollController`, щоб реагувати на події прокручування. Також ми отримуємо екземпляр `PostBloc` через `context.read()`. ::: Далі наш метод build повертає `BlocBuilder`. `BlocBuilder` — це віджет Flutter з пакету [flutter_bloc](https://pub.dev/packages/flutter_bloc), який обробляє побудову віджета у відповідь на нові стани bloc. Кожного разу, коли змінюється стан нашого `PostBloc`, функція builder буде викликана з новим `PostState`. :::caution Нам потрібно пам'ятати про очищення ресурсів та утилізацію нашого `ScrollController`, коли StatefulWidget утилізується. ::: Кожного разу, коли користувач прокручує, ми обчислюємо, наскільки далеко він прокрутив сторінку, і якщо наша відстань >= 90% нашого `maxScrollExtent`, ми додаємо подію `PostFetched` для завантаження більше постів. Далі нам потрібно реалізувати наш віджет `BottomLoader`, який показуватиме користувачу, що ми завантажуємо більше постів. Нарешті, нам потрібно реалізувати наш `PostListItem`, який відображатиме окремий `Post`. На цьому етапі ми повинні мати можливість запустити наш додаток, і все має працювати; однак є ще одна річ, яку ми можемо зробити. Додатковою перевагою використання бібліотеки bloc є те, що ми можемо мати доступ до всіх `Transitions` в одному місці. Зміна від одного стану до іншого називається `Transition`. :::note `Transition` складається з поточного стану, події та наступного стану. ::: Навіть хоча в цьому додатку ми маємо лише один bloc, у більших додатках досить поширено мати багато блоків, що керують різними частинами стану додатку. Якщо ми хочемо мати можливість виконувати дії у відповідь на всі `Transitions`, ми можемо просто створити наш власний `BlocObserver`. :::note Все, що нам потрібно зробити — це розширити `BlocObserver` та перевизначити метод `onTransition`. ::: Тепер кожного разу, коли відбувається `Transition` Bloc, ми бачитимемо перехід, виведений у консоль. :::note На практиці ви можете створювати різні `BlocObservers`, і оскільки кожна зміна стану записується, ми можемо дуже легко інструментувати наші додатки та відстежувати всі взаємодії користувачів та зміни стану в одному місці! ::: Ось і все! Ми успішно реалізували нескінченний список у Flutter, використовуючи пакети [bloc](https://pub.dev/packages/bloc) та [flutter_bloc](https://pub.dev/packages/flutter_bloc), і ми успішно відокремили наш шар представлення від бізнес-логіки. Наш `PostsPage` не знає, звідки беруться `Post` і як вони отримуються. І навпаки, наш `PostBloc` не знає, як відображається `State`, він просто перетворює події на стани. Повний вихідний код цього прикладу можна знайти [тут](https://github.com/felangel/Bloc/tree/master/examples/flutter_infinite_list). ================================================ FILE: docs/src/content/docs/uk/tutorials/flutter-login.mdx ================================================ --- title: Вхід Flutter description: Детальний посібник зі створення потоку входу Flutter з використанням bloc. sidebar: order: 4 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-login/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![intermediate](https://img.shields.io/badge/level-intermediate-orange.svg) У наступному підручнику ми створимо потік входу у Flutter з використанням бібліотеки Bloc. ![demo](~/assets/tutorials/flutter-login.gif) ## Ключові теми - [BlocProvider](/uk/flutter-bloc-concepts#blocprovider), віджет Flutter, який надає bloc своїм дочірнім елементам. - Додавання подій за допомогою [context.read](/uk/flutter-bloc-concepts#contextread). - Запобігання зайвим перебудовам за допомогою [Equatable](/uk/faqs#коли-використовувати-equatable). - [RepositoryProvider](/uk/flutter-bloc-concepts#repositoryprovider), віджет Flutter, який надає сховище своїм дочірнім елементам. - [BlocListener](/uk/flutter-bloc-concepts#bloclistener), віджет Flutter, який викликає код слухача у відповідь на зміни стану в bloc. - Оновлення UI на основі частини стану bloc за допомогою [context.select](/uk/flutter-bloc-concepts#contextselect). ## Налаштування проєкту Почнемо зі створення нового Flutter проєкту Далі ми можемо встановити всі наші залежності ## Authentication Repository Перше, що ми зробимо — створимо пакет `authentication_repository`, який відповідатиме за керування доменом автентифікації. Почнемо зі створення каталогу `packages/authentication_repository` в корені проєкту, який міститиме всі внутрішні пакети. На високому рівні структура каталогів повинна виглядати так: ``` ├── android ├── ios ├── lib ├── packages │ └── authentication_repository └── test ``` Далі створимо `pubspec.yaml` для пакету `authentication_repository`: :::note `package:authentication_repository` буде чистим Dart-пакетом без зовнішніх залежностей. ::: Далі нам потрібно реалізувати сам клас `AuthenticationRepository` у `packages/authentication_repository/lib/src/authentication_repository.dart`. `AuthenticationRepository` надає `Stream` оновлень `AuthenticationStatus`, який використовуватиметься для сповіщення додатку про вхід або вихід користувача. Крім того, є методи `logIn` та `logOut`, які для простоти є заглушками, але легко можуть бути розширені для автентифікації через `FirebaseAuth`, наприклад, або іншого провайдера автентифікації. :::note Оскільки ми підтримуємо `StreamController` внутрішньо, метод `dispose` доступний, щоб контролер можна було закрити, коли він більше не потрібен. ::: Нарешті, нам потрібно створити `packages/authentication_repository/lib/authentication_repository.dart`, який міститиме публічні експорти: На цьому з `AuthenticationRepository` все, далі ми попрацюємо над `UserRepository`. ## User Repository Так само, як і з `AuthenticationRepository`, ми створимо пакет `user_repository` всередині каталогу `packages`. ``` ├── android ├── ios ├── lib ├── packages │ ├── authentication_repository │ └── user_repository └── test ``` Далі створимо `pubspec.yaml` для `user_repository`: `user_repository` відповідатиме за домен користувача та надаватиме API для взаємодії з поточним користувачем. Перше, що ми визначимо — це модель користувача в `packages/user_repository/lib/src/models/user.dart`: Для простоти користувач має лише властивість `id`, але на практиці ми можемо мати додаткові властивості, такі як `firstName`, `lastName`, `avatarUrl` тощо... :::note [`package:equatable`](https://pub.dev/packages/equatable) використовується для порівняння об'єктів `User` за значенням. ::: Далі створимо `models.dart` у `packages/user_repository/lib/src/models`, який експортуватиме всі моделі, щоб можна було використовувати один імпорт для декількох моделей. Тепер, коли моделі визначені, ми можемо реалізувати клас `UserRepository` у `packages/user_repository/lib/src/user_repository.dart`. Для цього простого прикладу `UserRepository` надає єдиний метод `getUser`, який повертає поточного користувача. Ми використовуємо заглушку, але на практиці тут ми б запитували поточного користувача з бекенду. Майже все з пакетом `user_repository` готово — залишилося лише створити файл `user_repository.dart` у `packages/user_repository/lib`, який визначає публічні експорти: Тепер, коли пакети `authentication_repository` та `user_repository` готові, ми можемо зосередитися на Flutter-додатку. ## Встановлення залежностей Почнемо з оновлення згенерованого `pubspec.yaml` в корені нашого проєкту: Ми можемо встановити залежності, виконавши: ## Authentication Bloc `AuthenticationBloc` відповідатиме за реагування на зміни стану автентифікації (наданого `AuthenticationRepository`) та випускатиме стани, на які ми зможемо реагувати в шарі представлення. Реалізація `AuthenticationBloc` знаходиться всередині `lib/authentication`, оскільки ми розглядаємо автентифікацію як функціональний модуль у нашому шарі додатку. ``` ├── lib │ ├── app.dart │ ├── authentication │ │ ├── authentication.dart │ │ └── bloc │ │ ├── authentication_bloc.dart │ │ ├── authentication_event.dart │ │ └── authentication_state.dart │ ├── main.dart ``` :::tip Використовуйте [розширення VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) або [плагін IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc) для автоматичного створення блоків. ::: ### authentication_event.dart Екземпляри `AuthenticationEvent` будуть вхідними даними для `AuthenticationBloc` та оброблятимуться для випуску нових екземплярів `AuthenticationState`. У цьому додатку `AuthenticationBloc` реагуватиме на дві різні події: - `AuthenticationSubscriptionRequested`: початкова подія, яка повідомляє bloc підписатися на потік `AuthenticationStatus` - `AuthenticationLogoutPressed`: повідомляє bloc про дію виходу користувача Далі розглянемо `AuthenticationState`. ### authentication_state.dart Екземпляри `AuthenticationState` будуть виходом `AuthenticationBloc` та споживатимуться шаром представлення. Клас `AuthenticationState` має три іменовані конструктори: - `AuthenticationState.unknown()`: стан за замовчуванням, який вказує, що bloc ще не знає, чи автентифікований поточний користувач. - `AuthenticationState.authenticated()`: стан, який вказує, що користувач наразі автентифікований. - `AuthenticationState.unauthenticated()`: стан, який вказує, що користувач наразі не автентифікований. Тепер, коли ми розглянули реалізації `AuthenticationEvent` та `AuthenticationState`, давайте подивимося на `AuthenticationBloc`. ### authentication_bloc.dart `AuthenticationBloc` керує станом автентифікації додатку, який використовується для визначення, чи показувати користувачу сторінку входу чи домашню сторінку. `AuthenticationBloc` залежить як від `AuthenticationRepository`, так і від `UserRepository` та визначає початковий стан як `AuthenticationState.unknown()`. У тілі конструктора підкласи `AuthenticationEvent` зіставляються з відповідними обробниками подій. В обробнику подій `_onSubscriptionRequested` `AuthenticationBloc` використовує `emit.onEach` для підписки на потік `status` `AuthenticationRepository` та випуску стану у відповідь на кожен `AuthenticationStatus`. `emit.onEach` створює підписку на потік внутрішньо та піклується про її скасування, коли `AuthenticationBloc` або потік `status` закривається. Якщо потік `status` випускає помилку, `addError` пересилає помилку та stackTrace будь-якому `BlocObserver`, що слухає. :::caution Якщо `onError` пропущено, будь-які помилки в потоці `status` вважаються необробленими та будуть викинуті через `onEach`. Як наслідок, підписка на потік `status` буде скасована. ::: :::tip [`BlocObserver`](/uk/bloc-concepts/#blocobserver-1) чудово підходить для логування подій, помилок та змін стану Bloc, особливо в контексті аналітики та звітування про збої. ::: Коли потік `status` випускає `AuthenticationStatus.unknown` або `unauthenticated`, випускається відповідний `AuthenticationState`. Коли випускається `AuthenticationStatus.authenticated`, `AuthenticationBloc` запитує користувача через `UserRepository`. ## main.dart Далі ми можемо замінити стандартний `main.dart` на: ## App `app.dart` міститиме кореневий віджет `App` для всього додатку. :::note `app.dart` розділений на дві частини: `App` та `AppView`. `App` відповідає за створення/надання `AuthenticationBloc`, який споживатиметься `AppView`. Це розділення дозволить нам легко тестувати обидва віджети `App` та `AppView` надалі. ::: :::note `RepositoryProvider` використовується для надання єдиного екземпляра `AuthenticationRepository` всьому додатку, що знадобиться пізніше. ::: За замовчуванням `BlocProvider` є лінивим і не викликає `create`, поки до Bloc не звернуться вперше. Оскільки `AuthenticationBloc` повинен негайно підписатися на потік `AuthenticationStatus` (через подію `AuthenticationSubscriptionRequested`), ми можемо явно відмовитися від цієї поведінки, встановивши `lazy: false`. `AppView` є `StatefulWidget`, оскільки підтримує `GlobalKey`, який використовується для доступу до `NavigatorState`. За замовчуванням `AppView` відображатиме `SplashPage` (яку ми побачимо пізніше) та використовує `BlocListener` для навігації на різні сторінки на основі змін `AuthenticationState`. ## Splash Функціональність splash міститиме просте представлення, яке відображатиметься при запуску додатку, поки додаток визначає, чи автентифікований користувач. ``` lib └── splash ├── splash.dart └── view └── splash_page.dart ``` :::tip `SplashPage` надає статичний `Route`, що дуже спрощує навігацію через `Navigator.of(context).push(SplashPage.route())`; ::: ## Login Функціональність входу містить `LoginPage`, `LoginForm` та `LoginBloc` і дозволяє користувачам вводити ім'я користувача та пароль для входу в додаток. ``` ├── lib │ ├── login │ │ ├── bloc │ │ │ ├── login_bloc.dart │ │ │ ├── login_event.dart │ │ │ └── login_state.dart │ │ ├── login.dart │ │ ├── models │ │ │ ├── models.dart │ │ │ ├── password.dart │ │ │ └── username.dart │ │ └── view │ │ ├── login_form.dart │ │ ├── login_page.dart │ │ └── view.dart ``` ### Моделі входу Ми використовуємо [`package:formz`](https://pub.dev/packages/formz) для створення повторно використовуваних та стандартних моделей для `username` та `password`. #### Username Для простоти ми лише перевіряємо, що ім'я користувача не порожнє, але на практиці можна вимагати використання спеціальних символів, довжину тощо... #### Password Знову ж таки, ми просто виконуємо просту перевірку, щоб пароль не був порожнім. #### Barrel моделей Як і раніше, є barrel-файл `models.dart` для зручного імпорту моделей `Username` та `Password` одним імпортом. ### Login Bloc `LoginBloc` керує станом `LoginForm` та відповідає за валідацію введення імені користувача та пароля, а також стану форми. #### login_event.dart У цьому додатку є три різні типи `LoginEvent`: - `LoginUsernameChanged`: повідомляє bloc про зміну імені користувача. - `LoginPasswordChanged`: повідомляє bloc про зміну пароля. - `LoginSubmitted`: повідомляє bloc про відправку форми. #### login_state.dart `LoginState` міститиме статус форми, а також стани введення імені користувача та пароля. :::note Моделі `Username` та `Password` використовуються як частина `LoginState`, а статус також є частиною [package:formz](https://pub.dev/packages/formz). ::: #### login_bloc.dart `LoginBloc` відповідає за реагування на взаємодії користувача в `LoginForm` та обробку валідації та відправки форми. `LoginBloc` залежить від `AuthenticationRepository`, оскільки при відправці форми він викликає `logIn`. Початковий стан bloc є `pure`, що означає, що ні поля введення, ні форма ще не були торкнуті або взаємодіяли з ними. Кожного разу, коли змінюється `username` або `password`, bloc створює "брудний" варіант моделі `Username`/`Password` та оновлює статус форми через API `Formz.validate`. Коли додається подія `LoginSubmitted`, якщо поточний статус форми дійсний, bloc робить виклик `logIn` та оновлює статус на основі результату запиту. Далі розглянемо `LoginPage` та `LoginForm`. ### Login Page `LoginPage` відповідає за надання `Route`, а також за створення та надання `LoginBloc` для `LoginForm`. :::note `context.read()` використовується для пошуку екземпляра `AuthenticationRepository` через `BuildContext`. ::: ### Login Form `LoginForm` обробляє сповіщення `LoginBloc` про події користувача, а також реагує на зміни стану за допомогою `BlocBuilder` та `BlocListener`. `BlocListener` використовується для показу `SnackBar`, якщо відправка входу не вдалася. Крім того, `context.select` використовується для ефективного доступу до конкретних частин `LoginState` для кожного віджета, запобігаючи зайвим перебудовам. Зворотний виклик `onChanged` використовується для сповіщення `LoginBloc` про зміни імені користувача/пароля. Віджет `_LoginButton` увімкнений лише тоді, коли статус форми дійсний, а `CircularProgressIndicator` показується на його місці під час відправки форми. ## Home Після успішного запиту `logIn` стан `AuthenticationBloc` зміниться на `authenticated`, і користувач буде переведений на `HomePage`, де ми відображаємо `id` користувача, а також кнопку виходу. ``` ├── lib │ ├── home │ │ ├── home.dart │ │ └── view │ │ └── home_page.dart ``` ### Home Page `HomePage` може отримати id поточного користувача через `context.select((AuthenticationBloc bloc) => bloc.state.user.id)` та відображає його за допомогою віджета `Text`. Крім того, при натисканні кнопки виходу подія `AuthenticationLogoutPressed` додається до `AuthenticationBloc`. :::note `context.select((AuthenticationBloc bloc) => bloc.state.user.id)` викликатиме оновлення при зміні id користувача. ::: На цьому етапі ми маємо досить надійну реалізацію входу, і ми відокремили шар представлення від шару бізнес-логіки за допомогою Bloc. Повний вихідний код (включаючи unit та widget тести) цього прикладу можна знайти [тут](https://github.com/felangel/Bloc/tree/master/examples/flutter_login). ================================================ FILE: docs/src/content/docs/uk/tutorials/flutter-timer.mdx ================================================ --- title: Таймер Flutter description: Детальний посібник зі створення додатку-таймера на Flutter з використанням bloc. sidebar: order: 2 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-timer/FlutterCreateSnippet.astro'; import TimerBlocEmptySnippet from '~/components/tutorials/flutter-timer/TimerBlocEmptySnippet.astro'; import TimerBlocInitialStateSnippet from '~/components/tutorials/flutter-timer/TimerBlocInitialStateSnippet.astro'; import TimerBlocTickerSnippet from '~/components/tutorials/flutter-timer/TimerBlocTickerSnippet.astro'; import TimerBlocOnStartedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnStartedSnippet.astro'; import TimerBlocOnTickedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnTickedSnippet.astro'; import TimerBlocOnPausedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnPausedSnippet.astro'; import TimerBlocOnResumedSnippet from '~/components/tutorials/flutter-timer/TimerBlocOnResumedSnippet.astro'; import TimerPageSnippet from '~/components/tutorials/flutter-timer/TimerPageSnippet.astro'; import ActionsSnippet from '~/components/tutorials/flutter-timer/ActionsSnippet.astro'; import BackgroundSnippet from '~/components/tutorials/flutter-timer/BackgroundSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) У наступному підручнику ми розглянемо, як створити додаток-таймер з використанням бібліотеки bloc. Готовий додаток повинен виглядати так: ![demo](~/assets/tutorials/flutter-timer.gif) ## Ключові теми - Спостереження за змінами стану за допомогою [BlocObserver](/uk/bloc-concepts#blocobserver). - [BlocProvider](/uk/flutter-bloc-concepts#blocprovider), віджет Flutter, який надає bloc своїм дочірнім елементам. - [BlocBuilder](/uk/flutter-bloc-concepts#blocbuilder), віджет Flutter, який обробляє побудову віджета у відповідь на нові стани. - Запобігання зайвим перебудовам за допомогою [Equatable](/uk/faqs#коли-використовувати-equatable). - Навчитися використовувати `StreamSubscription` у Bloc. - Запобігання зайвим перебудовам за допомогою `buildWhen`. ## Налаштування Почнемо зі створення нового Flutter проєкту: Потім ми можемо замінити вміст pubspec.yaml на: :::note Ми використовуватимемо пакети [flutter_bloc](https://pub.dev/packages/flutter_bloc) та [equatable](https://pub.dev/packages/equatable) у цьому додатку. ::: Далі виконайте `flutter pub get` для встановлення всіх залежностей. ## Структура проєкту ``` ├── lib | ├── timer │ │ ├── bloc │ │ │ └── timer_bloc.dart | | | └── timer_event.dart | | | └── timer_state.dart │ │ └── view │ │ | ├── timer_page.dart │ │ ├── timer.dart │ ├── app.dart │ ├── ticker.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` ## Ticker Ticker буде нашим джерелом даних для додатку-таймера. Він надаватиме потік тіків, на які ми зможемо підписатися та реагувати. Почнемо зі створення `ticker.dart`. Все, що робить наш клас `Ticker` — це надає функцію tick, яка приймає кількість тіків (секунд) і повертає потік, який кожну секунду випускає секунди, що залишилися. Далі нам потрібно створити наш `TimerBloc`, який споживатиме `Ticker`. ## Timer Bloc ### TimerState Почнемо з визначення `TimerStates`, в яких може перебувати наш `TimerBloc`. Стан нашого `TimerBloc` може бути одним з наступних: - `TimerInitial`: готовий почати зворотний відлік від вказаної тривалості. - `TimerRunInProgress`: активно відлічує від вказаної тривалості. - `TimerRunPause`: призупинений на певній тривалості, що залишилася. - `TimerRunComplete`: завершений з тривалістю 0, що залишилася. Кожен з цих станів матиме вплив на інтерфейс користувача та дії, які може виконувати користувач. Наприклад: - якщо стан `TimerInitial`, користувач зможе запустити таймер. - якщо стан `TimerRunInProgress`, користувач зможе призупинити та скинути таймер, а також бачити тривалість, що залишилася. - якщо стан `TimerRunPause`, користувач зможе відновити таймер та скинути таймер. - якщо стан `TimerRunComplete`, користувач зможе скинути таймер. Для зберігання всіх файлів bloc разом створимо каталог bloc з `bloc/timer_state.dart`. :::tip Ви можете використовувати розширення [IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc-code-generator) або [VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) для автоматичної генерації наступних файлів bloc. ::: Зверніть увагу, що всі `TimerStates` розширюють абстрактний базовий клас `TimerState`, який має властивість duration. Це тому, що незалежно від стану нашого `TimerBloc`, ми хочемо знати, скільки часу залишилося. Крім того, `TimerState` розширює `Equatable` для оптимізації коду, забезпечуючи, що наш додаток не запускає перебудови, якщо відбувається однаковий стан. Далі визначимо та реалізуємо `TimerEvents`, які оброблятиме наш `TimerBloc`. ### TimerEvent Наш `TimerBloc` повинен знати, як обробляти наступні події: - `TimerStarted`: інформує TimerBloc про запуск таймера. - `TimerPaused`: інформує TimerBloc про призупинення таймера. - `TimerResumed`: інформує TimerBloc про відновлення таймера. - `TimerReset`: інформує TimerBloc про скидання таймера до початкового стану. - `_TimerTicked`: інформує TimerBloc про те, що стався тік, і йому потрібно оновити свій стан відповідно. Якщо ви не використовували розширення [IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc-code-generator) або [VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc), створіть `bloc/timer_event.dart` і реалізуємо ці події. Далі реалізуємо `TimerBloc`! ### TimerBloc Якщо ви ще цього не зробили, створіть `bloc/timer_bloc.dart` та порожній `TimerBloc`. Перше, що нам потрібно зробити — визначити початковий стан нашого `TimerBloc`. У цьому випадку ми хочемо, щоб `TimerBloc` починав зі стану `TimerInitial` з попередньо встановленою тривалістю 1 хвилина (60 секунд). Далі нам потрібно визначити залежність від нашого `Ticker`. Ми також визначаємо `StreamSubscription` для нашого `Ticker`, до якої повернемося трохи пізніше. На цьому етапі залишилося лише реалізувати обробники подій. Для кращої читабельності я виношу кожен обробник подій у окрему допоміжну функцію. Почнемо з події `TimerStarted`. Якщо `TimerBloc` отримує подію `TimerStarted`, він встановлює стан `TimerRunInProgress` з початковою тривалістю. Крім того, якщо вже була відкрита `_tickerSubscription`, нам потрібно скасувати її для вивільнення пам'яті. Нам також потрібно перевизначити метод `close` нашого `TimerBloc`, щоб скасувати `_tickerSubscription`, коли `TimerBloc` закривається. Нарешті, ми слухаємо потік `_ticker.tick` і на кожному тіку додаємо подію `_TimerTicked` з тривалістю, що залишилася. Далі реалізуємо обробник подій `_TimerTicked`. Кожного разу, коли отримується подія `_TimerTicked`, якщо тривалість тіку більше 0, нам потрібно встановити оновлений стан `TimerRunInProgress` з новою тривалістю. Інакше, якщо тривалість тіку дорівнює 0, наш таймер завершився, і нам потрібно встановити стан `TimerRunComplete`. Тепер реалізуємо обробник подій `TimerPaused`. В `_onPaused`, якщо `state` нашого `TimerBloc` є `TimerRunInProgress`, ми можемо призупинити `_tickerSubscription` та встановити стан `TimerRunPause` з поточною тривалістю таймера. Далі реалізуємо обробник подій `TimerResumed`, щоб мати можливість відновити таймер. Обробник подій `TimerResumed` дуже схожий на обробник подій `TimerPaused`. Якщо `TimerBloc` має `state` `TimerRunPause` та отримує подію `TimerResumed`, то він відновлює `_tickerSubscription` та встановлює стан `TimerRunInProgress` з поточною тривалістю. Нарешті, нам потрібно реалізувати обробник подій `TimerReset`. Якщо `TimerBloc` отримує подію `TimerReset`, йому потрібно скасувати поточну `_tickerSubscription`, щоб не отримувати додаткові тіки, та встановити стан `TimerInitial` з початковою тривалістю. Ось і все щодо `TimerBloc`. Тепер залишилося реалізувати інтерфейс для нашого додатку-таймера. ## UI додатку ### MyApp Ми можемо почати з видалення вмісту `main.dart` та заміни його наступним. Далі створимо наш віджет 'App' у `app.dart`, який буде кореневим елементом нашого додатку. Далі нам потрібно реалізувати наш віджет `Timer`. ### Timer Наш віджет `Timer` (`lib/timer/view/timer_page.dart`) відповідатиме за відображення часу, що залишився, а також відповідних кнопок, які дозволять користувачам запускати, призупиняти та скидати таймер. Наразі ми просто використовуємо `BlocProvider` для доступу до екземпляра нашого `TimerBloc`. Далі ми реалізуємо наш віджет `Actions`, який матиме відповідні дії (запуск, пауза та скидання). ### Barrel Для очищення наших імпортів з розділу `Timer` нам потрібно створити barrel-файл `timer/timer.dart`. ### Actions Віджет `Actions` — це ще один `StatelessWidget`, який використовує `BlocBuilder` для перебудови UI кожного разу, коли ми отримуємо новий `TimerState`. `Actions` використовує `context.read()` для доступу до екземпляра `TimerBloc` та повертає різні `FloatingActionButtons` залежно від поточного стану `TimerBloc`. Кожен з `FloatingActionButtons` додає подію у своєму зворотному виклику `onPressed` для сповіщення `TimerBloc`. Якщо ви хочете мати точний контроль над тим, коли викликається функція `builder`, можна надати необов'язковий `buildWhen` для `BlocBuilder`. `buildWhen` приймає попередній стан bloc та поточний стан bloc і повертає `boolean`. Якщо `buildWhen` повертає `true`, `builder` буде викликаний зі `state`, і віджет перебудується. Якщо `buildWhen` повертає `false`, `builder` не буде викликаний зі `state`, і перебудова не відбудеться. У цьому випадку ми не хочемо, щоб віджет `Actions` перебудовувався на кожному тіку, оскільки це було б неефективно. Замість цього ми хочемо, щоб `Actions` перебудовувався лише тоді, коли змінюється `runtimeType` `TimerState` (TimerInitial => TimerRunInProgress, TimerRunInProgress => TimerRunPause тощо). Як наслідок, якби ми випадково розфарбовували віджети при кожній перебудові, це виглядало б так: ![BlocBuilder buildWhen demo](https://cdn-images-1.medium.com/max/1600/1*YyjpH1rcZlYWxCX308l_Ew.gif) :::note Навіть хоча віджет `Text` перебудовується на кожному тіку, ми перебудовуємо `Actions` лише тоді, коли їх потрібно перебудувати. ::: ### Background Нарешті, додамо віджет фону наступним чином: ### Збираємо все разом Ось і все! На цьому етапі ми маємо досить надійний додаток-таймер, який ефективно перебудовує лише ті віджети, які потрібно перебудувати. Повний вихідний код цього прикладу можна знайти [тут](https://github.com/felangel/Bloc/tree/master/examples/flutter_timer). ================================================ FILE: docs/src/content/docs/uk/tutorials/flutter-todos.mdx ================================================ --- title: Список справ Flutter description: Детальний посібник зі створення додатку списку справ на Flutter з використанням bloc. sidebar: order: 6 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-todos/FlutterCreateSnippet.astro'; import ActivateVeryGoodCLISnippet from '~/components/tutorials/flutter-todos/ActivateVeryGoodCLISnippet.astro'; import FlutterCreatePackagesSnippet from '~/components/tutorials/flutter-todos/FlutterCreatePackagesSnippet.astro'; import ProjectStructureSnippet from '~/components/tutorials/flutter-todos/ProjectStructureSnippet.astro'; import VeryGoodPackagesGetSnippet from '~/components/tutorials/flutter-todos/VeryGoodPackagesGetSnippet.astro'; import HomePageTreeSnippet from '~/components/tutorials/flutter-todos/HomePageTreeSnippet.astro'; import TodosOverviewPageTreeSnippet from '~/components/tutorials/flutter-todos/TodosOverviewPageTreeSnippet.astro'; import StatsPageTreeSnippet from '~/components/tutorials/flutter-todos/StatsPageTreeSnippet.astro'; import EditTodosPageTreeSnippet from '~/components/tutorials/flutter-todos/EditTodosPageTreeSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) У наступному підручнику ми створимо додаток списку справ на Flutter з використанням бібліотеки Bloc. ![demo](~/assets/tutorials/flutter-todos.gif) ## Ключові теми - [Bloc та Cubit](/uk/bloc-concepts#cubit-проти-bloc) для керування різними станами функціональних модулів. - [Шарувата архітектура](/uk/architecture) для розділення відповідальності та сприяння повторному використанню. - [BlocObserver](/uk/bloc-concepts#blocobserver) для спостереження за змінами стану. - [BlocProvider](/uk/flutter-bloc-concepts#blocprovider), віджет Flutter, який надає bloc своїм дочірнім елементам. - [BlocBuilder](/uk/flutter-bloc-concepts#blocbuilder), віджет Flutter, який обробляє побудову віджета у відповідь на нові стани. - [BlocListener](/uk/flutter-bloc-concepts#bloclistener), віджет Flutter, який виконує побічні ефекти у відповідь на зміни стану. - [RepositoryProvider](/uk/flutter-bloc-concepts#repositoryprovider), віджет Flutter для надання сховища дочірнім елементам. - [Equatable](/uk/faqs#коли-використовувати-equatable) для запобігання зайвим перебудовам. - [MultiBlocListener](/uk/flutter-bloc-concepts#multibloclistener), віджет Flutter, який зменшує вкладеність при використанні кількох BlocListeners. ## Налаштування Почнемо зі створення нового Flutter проєкту за допомогою [very_good_cli](https://pub.dev/packages/very_good_cli). :::note Встановіть `very_good_cli` за допомогою наступної команди ::: Далі створимо пакети `todos_api`, `local_storage_todos_api` та `todos_repository` за допомогою `very_good_cli`: Потім ми можемо замінити вміст `pubspec.yaml` на: Нарешті, ми можемо встановити всі залежності: ## Структура проєкту Структура нашого проєкту додатку повинна виглядати так: Ми розділяємо проєкт на кілька пакетів для підтримки явних залежностей кожного пакету з чіткими межами, які забезпечують [принцип єдиної відповідальності](https://en.wikipedia.org/wiki/Single-responsibility_principle). Модуляризація нашого проєкту таким чином має багато переваг, включаючи, але не обмежуючись: - легке повторне використання пакетів у кількох проєктах - покращення CI/CD з точки зору ефективності (запуск перевірок лише для коду, який змінився) - легке підтримання пакетів ізольовано з їхніми власними наборами тестів, семантичним версіонуванням та циклом/каденцією випуску ## Архітектура ![Діаграма архітектури Todos](~/assets/tutorials/todos-architecture.png) Шарування нашого коду надзвичайно важливе та допомагає нам ітерувати швидко й з впевненістю. Кожен шар має єдину відповідальність і може використовуватися та тестуватися ізольовано. Це дозволяє нам утримувати зміни в конкретному шарі, щоб мінімізувати вплив на весь додаток. Крім того, шарування нашого додатку дозволяє нам легко повторно використовувати бібліотеки у кількох проєктах (особливо щодо шару даних). Наш додаток складається з трьох основних шарів: - шар даних - доменний шар - шар функціональності - представлення/UI (віджети) - бізнес-логіка (блоки/кубіти) **Шар даних** Цей шар є найнижчим і відповідає за отримання необроблених даних із зовнішніх джерел, таких як бази даних, API тощо. Пакети в шарі даних, як правило, не повинні залежати від будь-якого UI та можуть бути повторно використані і навіть опубліковані на [pub.dev](https://pub.dev) як окремий пакет. У цьому прикладі наш шар даних складається з пакетів `todos_api` та `local_storage_todos_api`. **Доменний шар** Цей шар поєднує одного або кількох постачальників даних та застосовує "бізнес-правила" до даних. Кожен компонент у цьому шарі називається сховищем, і кожне сховище зазвичай керує одним доменом. Пакети у шарі сховища повинні взаємодіяти лише з шаром даних. У цьому прикладі наш шар сховища складається з пакету `todos_repository`. **Шар функціональності** Цей шар містить усю функціональність та варіанти використання, специфічні для додатку. Кожен функціональний модуль зазвичай складається з деякого UI та бізнес-логіки. Модулі повинні бути незалежними один від одного, щоб їх можна було легко додавати/видаляти без впливу на решту кодової бази. У кожному модулі стан та бізнес-логіка керуються блоками. Блоки взаємодіють з нулем або більше сховищ. Блоки реагують на події та випускають стани, які ініціюють зміни в UI. Віджети в кожному модулі повинні залежати лише від відповідного bloc та відображати UI на основі поточного стану. UI може сповіщати bloc про введення користувача через події. У цьому прикладі наш додаток складатиметься з модулів `home`, `todos_overview`, `stats` та `edit_todos`. Тепер, коли ми оглянули шари на високому рівні, давайте почнемо будувати наш додаток, починаючи з шару даних! ## Шар даних Шар даних є найнижчим шаром у нашому додатку та складається з постачальників необроблених даних. Пакети в цьому шарі в першу чергу стосуються того, звідки/як надходять дані. У цьому випадку наш шар даних складатиметься з `TodosApi`, який є інтерфейсом, та `LocalStorageTodosApi`, який є реалізацією `TodosApi` на основі `shared_preferences`. ### TodosApi Пакет `todos_api` експортуватиме загальний інтерфейс для взаємодії/керування справами. Пізніше ми реалізуємо `TodosApi` з використанням `shared_preferences`. Наявність абстракції спрощує підтримку інших реалізацій без необхідності змінювати будь-яку іншу частину нашого додатку. Наприклад, ми можемо пізніше додати `FirestoreTodosApi`, що використовує `cloud_firestore` замість `shared_preferences`, з мінімальними змінами коду в решті додатку. #### Модель Todo Далі визначимо нашу модель `Todo`. Перше, що слід зазначити — модель `Todo` не живе в нашому додатку — вона є частиною пакету `todos_api`. Це тому, що `TodosApi` визначає API, які повертають/приймають об'єкти `Todo`. Модель — це Dart-представлення необробленого об'єкта Todo, який зберігатиметься/отримуватиметься. Модель `Todo` використовує [json_serializable](https://pub.dev/packages/json_serializable) для обробки json (де)серіалізації. Якщо ви слідуєте за підручником, вам потрібно буде виконати [крок генерації коду](https://pub.dev/packages/json_serializable#running-the-code-generator) для вирішення помилок компілятора. `json_map.dart` надає `typedef` для перевірки коду та лінтингу. Модель `Todo` визначена в `todos_api/models/todo.dart` та експортується через `package:todos_api/todos_api.dart`. #### Оновлення експортів Наша модель `Todo` та `TodosApi` експортуються через barrel-файли. Зверніть увагу, що ми не імпортуємо модель безпосередньо, а імпортуємо її в `lib/src/todos_api.dart` з посиланням на barrel-файл пакету: `import 'package:todos_api/todos_api.dart';`. Оновіть barrel-файли для вирішення решти помилок імпорту: #### Потоки проти Future У попередній версії цього підручника `TodosApi` був заснований на `Future` замість `Stream`. Приклад реалізації на основі `Future` дивіться у [реалізації Brian Egan в його Architecture Samples](https://github.com/brianegan/flutter_architecture_samples/tree/master/todos_repository_core). Реалізація на основі `Future` може складатися з двох методів: `loadTodos` та `saveTodos` (зверніть увагу на множину). Це означає, що повний список справ повинен надаватися методу кожного разу. - Одне обмеження цього підходу полягає в тому, що стандартна операція CRUD (Create, Read, Update та Delete) вимагає надсилання повного списку справ з кожним викликом. Наприклад, на екрані додавання справи неможливо надіслати лише додану справу. Натомість потрібно відстежувати весь список та надавати повний новий список справ при збереженні оновленого списку. - Друге обмеження полягає в тому, що `loadTodos` — це одноразова доставка даних. Додаток повинен містити логіку для періодичного запиту оновлень. У поточній реалізації `TodosApi` надає `Stream>` через `getTodos()`, який повідомлятиме про оновлення в реальному часі всіх підписників, коли список справ змінюється. Крім того, справи можна створювати, видаляти або оновлювати окремо. Наприклад, як видалення, так і збереження справи виконуються лише з `todo` як аргументом. Не потрібно надавати щоразу оновлений список справ. ### LocalStorageTodosApi Цей пакет реалізує `todos_api` з використанням пакету [`shared_preferences`](https://pub.dev/packages/shared_preferences). ## Шар сховища [Сховище](/uk/architecture#сховище) є частиною бізнес-шару. Сховище залежить від одного або кількох постачальників даних, що не мають бізнес-цінності, та поєднує їхні публічні API у API, що надають бізнес-цінність. Крім того, наявність шару сховища допомагає абстрагувати отримання даних від решти додатку, дозволяючи нам змінювати місце/спосіб зберігання даних без впливу на інші частини додатку. ### TodosRepository Створення екземпляра сховища вимагає вказівки `TodosApi`, який ми обговорювали раніше в цьому підручнику, тому ми додали його як залежність у наш `pubspec.yaml`: #### Експорти бібліотеки Крім експорту класу `TodosRepository`, ми також експортуємо модель `Todo` з пакету `todos_api`. Цей крок запобігає тісному зв'язку між додатком та постачальниками даних. Ми вирішили повторно експортувати ту саму модель `Todo` з `todos_api`, замість перевизначення окремої моделі в `todos_repository`, оскільки в цьому випадку ми повністю контролюємо модель даних. У багатьох випадках постачальник даних не буде чимось, що ви контролюєте. У таких випадках стає все важливішим підтримувати власні визначення моделей у шарі сховища для збереження повного контролю над інтерфейсом та контрактом API. ## Шар функціональності ### Точка входу Точкою входу нашого додатку є `main.dart`. У цьому випадку є три версії: Найбільш помітним є те, що конкретна реалізація `local_storage_todos_api` створюється в кожній точці входу. ### Завантаження `bootstrap.dart` завантажує наш `BlocObserver` та створює екземпляр `TodosRepository`. ### App `App` обгортає віджет `RepositoryProvider`, який надає сховище всім дочірнім елементам. Оскільки і `EditTodoPage`, і `HomePage` є нащадками, всі блоки та кубіти можуть отримати доступ до сховища. `AppView` створює `MaterialApp` та налаштовує тему та локалізації. ### Тема Тут надається визначення теми для світлого та темного режиму. ### Home Функціональний модуль home відповідає за керування станом поточно обраної вкладки та відображення правильного піддерева. #### HomeState Є лише два стани, пов'язані з двома екранами: `todos` та `stats`. :::note `EditTodo` є окремим маршрутом, тому він не є частиною `HomeState`. ::: #### HomeCubit Cubit є доречним у цьому випадку через простоту бізнес-логіки. Ми маємо один метод `setTab` для зміни вкладки. #### HomeView `view.dart` є barrel-файлом, який експортує всі відповідні компоненти UI для модуля home. `home_page.dart` містить UI для кореневої сторінки, яку побачить користувач при запуску додатку. Спрощене представлення дерева віджетів для `HomePage`: `HomePage` надає екземпляр `HomeCubit` для `HomeView`. `HomeView` використовує `context.select` для вибіркової перебудови кожного разу, коли змінюється вкладка. Це дозволяє нам легко тестувати `HomeView`, надаючи мок `HomeCubit` та підставляючи стан. `BottomAppBar` містить віджети `HomeTabButton`, які викликають `setTab` на `HomeCubit`. Екземпляр cubit знаходиться через `context.read`, і відповідний метод викликається на екземплярі cubit. :::caution `context.read` не слухає зміни, він просто використовується для доступу до `HomeCubit` та виклику `setTab`. ::: ### TodosOverview Модуль огляду справ дозволяє користувачам керувати своїми справами шляхом створення, редагування, видалення та фільтрації справ. #### TodosOverviewEvent Створимо `todos_overview/bloc/todos_overview_event.dart` та визначимо події. - `TodosOverviewSubscriptionRequested`: початкова подія. У відповідь bloc підписується на потік справ з `TodosRepository`. - `TodosOverviewTodoDeleted`: видаляє справу. - `TodosOverviewTodoCompletionToggled`: перемикає статус завершення справи. - `TodosOverviewToggleAllRequested`: перемикає завершення всіх справ. - `TodosOverviewClearCompletedRequested`: видаляє всі завершені справи. - `TodosOverviewUndoDeletionRequested`: скасовує видалення справи, наприклад, випадкове видалення. - `TodosOverviewFilterChanged`: приймає `TodosViewFilter` як аргумент та змінює відображення, застосовуючи фільтр. #### TodosOverviewState Створимо `todos_overview/bloc/todos_overview_state.dart` та визначимо стан. `TodosOverviewState` відстежуватиме список справ, активний фільтр, `lastDeletedTodo` та статус. :::note Крім стандартних гетерів та сетерів, ми маємо власний гетер `filteredTodos`. UI використовує `BlocBuilder` для доступу до `state.filteredTodos` або `state.todos`. ::: #### TodosOverviewBloc Створимо `todos_overview/bloc/todos_overview_bloc.dart`. :::note Bloc не створює екземпляр `TodosRepository` внутрішньо. Замість цього він покладається на екземпляр сховища, впроваджений через конструктор. ::: ##### onSubscriptionRequested Коли додається `TodosOverviewSubscriptionRequested`, bloc спочатку випускає стан `loading`. У відповідь UI може відобразити індикатор завантаження. Далі ми використовуємо `emit.forEach>( ... )`, який створює підписку на потік справ з `TodosRepository`. :::caution `emit.forEach()` — це не те саме `forEach()`, що використовується для списків. Цей `forEach` дозволяє bloc підписатися на `Stream` та випускати новий стан для кожного оновлення з потоку. ::: :::note `stream.listen` ніколи не викликається безпосередньо в цьому підручнику. Використання `await emit.forEach()` є новішим патерном для підписки на потік, який дозволяє bloc керувати підпискою внутрішньо. ::: Тепер, коли підписка оброблена, ми обробимо інші події, такі як додавання, модифікація та видалення справ. ##### onTodoSaved `_onTodoSaved` просто викликає `_todosRepository.saveTodo(event.todo)`. :::note `emit` ніколи не викликається з `onTodoSaved` та багатьох інших обробників подій. Замість цього вони повідомляють сховище, яке випускає оновлений список через потік справ. Дивіться розділ [потік даних](#потік-даних) для детальної інформації. ::: ##### Скасування Функція скасування дозволяє користувачам відновити останній видалений елемент. `_onTodoDeleted` робить дві речі. По-перше, він випускає новий стан зі справою, яку потрібно видалити. Потім видаляє справу через виклик сховища. `_onUndoDeletionRequested` запускається, коли подія запиту скасування видалення надходить від UI. `_onUndoDeletionRequested` виконує наступне: - Тимчасово зберігає копію останньої видаленої справи. - Оновлює стан, видаляючи `lastDeletedTodo`. - Скасовує видалення. ##### Фільтрація `_onFilterChanged` випускає новий стан з новим фільтром подій. #### Моделі Є один файл моделі, який стосується фільтрації перегляду. `todos_view_filter.dart` — це enum, який представляє три фільтри перегляду та методи для застосування фільтра. `models.dart` — це barrel-файл для експортів. Далі розглянемо `TodosOverviewPage`. #### TodosOverviewPage Спрощене представлення дерева віджетів для `TodosOverviewPage`: Як і з модулем `Home`, `TodosOverviewPage` надає екземпляр `TodosOverviewBloc` піддереву через `BlocProvider`. Це обмежує область дії `TodosOverviewBloc` лише віджетами нижче `TodosOverviewPage`. Є три віджети, які слухають зміни в `TodosOverviewBloc`. 1. Перший — `BlocListener`, який слухає помилки. `listener` буде викликаний лише тоді, коли `listenWhen` поверне `true`. Якщо статус `TodosOverviewStatus.failure`, відображається `SnackBar`. 2. Ми створили другий `BlocListener`, який слухає видалення. Коли справу видалено, відображається `SnackBar` з кнопкою скасування. Якщо користувач натисне скасувати, подія `TodosOverviewUndoDeletionRequested` буде додана до bloc. 3. Нарешті, ми використовуємо `BlocBuilder` для побудови ListView, який відображає справи. `AppBar` містить два дії, які є випадаючими списками для фільтрації та маніпулювання справами. :::note `TodosOverviewTodoCompletionToggled` та `TodosOverviewTodoDeleted` додаються до bloc через `context.read`. ::: `view.dart` — це barrel-файл, який експортує `todos_overview_page.dart`. #### Віджети `widgets.dart` — це ще один barrel-файл, який експортує всі компоненти, використовувані в модулі `todos_overview`. `todo_list_tile.dart` — це `ListTile` для кожного елемента справи. `todos_overview_options_button.dart` надає два варіанти маніпулювання справами: - `toggleAll` - `clearCompleted` `todos_overview_filter_button.dart` надає три варіанти фільтра: - `all` - `activeOnly` - `completedOnly` ### Stats Модуль статистики відображає статистику про активні та завершені справи. #### StatsState `StatsState` відстежує зведену інформацію та поточний `StatsStatus`. #### StatsEvent `StatsEvent` має лише одну подію під назвою `StatsSubscriptionRequested`: #### StatsBloc `StatsBloc` залежить від `TodosRepository` так само, як `TodosOverviewBloc`. Він підписується на потік справ через `_todosRepository.getTodos`. #### Stats View `view.dart` — це barrel-файл для `stats_page`. `stats_page.dart` містить UI для сторінки, що відображає статистику справ. Спрощене представлення дерева віджетів для `StatsPage`: :::caution `TodosOverviewBloc` та `StatsBloc` обидва взаємодіють з `TodosRepository`, але важливо зазначити, що немає прямого зв'язку між блоками. Дивіться розділ [потік даних](#потік-даних) для детальної інформації. ::: ### EditTodo Модуль `EditTodo` дозволяє користувачам редагувати існуючу справу та зберігати зміни. #### EditTodoState `EditTodoState` відстежує інформацію, необхідну при редагуванні справи. #### EditTodoEvent Різні події, на які реагуватиме bloc: - `EditTodoTitleChanged` - `EditTodoDescriptionChanged` - `EditTodoSubmitted` #### EditTodoBloc `EditTodoBloc` залежить від `TodosRepository`, так само як `TodosOverviewBloc` та `StatsBloc`. :::caution На відміну від інших блоків, `EditTodoBloc` не підписується на `_todosRepository.getTodos`. Це bloc "лише для запису", тобто йому не потрібно читати інформацію зі сховища. ::: ##### Потік даних Навіть хоча є багато модулів, які залежать від одного й того ж списку справ, немає зв'язку між блоками. Замість цього всі модулі незалежні один від одного та покладаються на `TodosRepository` для прослуховування змін у списку справ, а також для виконання оновлень списку. Наприклад, `EditTodos` нічого не знає про модулі `TodosOverview` або `Stats`. Коли UI відправляє подію `EditTodoSubmitted`: - `EditTodoBloc` обробляє бізнес-логіку для оновлення `TodosRepository`. - `TodosRepository` повідомляє `TodosOverviewBloc` та `StatsBloc`. - `TodosOverviewBloc` та `StatsBloc` повідомляють UI, який оновлюється з новим станом. #### EditTodoPage Як і з попередніми модулями, `EditTodosPage` надає екземпляр `EditTodosBloc` через `BlocProvider`. На відміну від інших модулів, `EditTodosPage` є окремим маршрутом, тому він надає `static` метод `route`. Це спрощує додавання `EditTodosPage` до стеку навігації через `Navigator.of(context).push(...)`. Спрощене представлення дерева віджетів для `EditTodosPage`: ## Підсумок Ось і все, ми завершили підручник! Повний вихідний код цього прикладу, включаючи unit та widget тести, можна знайти [тут](https://github.com/felangel/bloc/tree/master/examples/flutter_todos). ================================================ FILE: docs/src/content/docs/uk/tutorials/flutter-weather.mdx ================================================ --- title: Погода Flutter description: Детальний посібник зі створення додатку погоди на Flutter з використанням bloc. sidebar: order: 5 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-weather/FlutterCreateSnippet.astro'; import FeatureTreeSnippet from '~/components/tutorials/flutter-weather/FeatureTreeSnippet.astro'; import FlutterCreateApiClientSnippet from '~/components/tutorials/flutter-weather/FlutterCreateApiClientSnippet.astro'; import OpenMeteoModelsTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoModelsTreeSnippet.astro'; import LocationJsonSnippet from '~/components/tutorials/flutter-weather/LocationJsonSnippet.astro'; import LocationDartSnippet from '~/components/tutorials/flutter-weather/LocationDartSnippet.astro'; import WeatherJsonSnippet from '~/components/tutorials/flutter-weather/WeatherJsonSnippet.astro'; import WeatherDartSnippet from '~/components/tutorials/flutter-weather/WeatherDartSnippet.astro'; import OpenMeteoModelsBarrelTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoModelsBarrelTreeSnippet.astro'; import OpenMeteoLibrarySnippet from '~/components/tutorials/flutter-weather/OpenMeteoLibrarySnippet.astro'; import BuildRunnerBuildSnippet from '~/components/tutorials/flutter-weather/BuildRunnerBuildSnippet.astro'; import OpenMeteoApiClientTreeSnippet from '~/components/tutorials/flutter-weather/OpenMeteoApiClientTreeSnippet.astro'; import LocationSearchMethodSnippet from '~/components/tutorials/flutter-weather/LocationSearchMethodSnippet.astro'; import GetWeatherMethodSnippet from '~/components/tutorials/flutter-weather/GetWeatherMethodSnippet.astro'; import FlutterTestCoverageSnippet from '~/components/tutorials/flutter-weather/FlutterTestCoverageSnippet.astro'; import FlutterCreateRepositorySnippet from '~/components/tutorials/flutter-weather/FlutterCreateRepositorySnippet.astro'; import RepositoryModelsBarrelTreeSnippet from '~/components/tutorials/flutter-weather/RepositoryModelsBarrelTreeSnippet.astro'; import WeatherRepositoryLibrarySnippet from '~/components/tutorials/flutter-weather/WeatherRepositoryLibrarySnippet.astro'; import WeatherCubitTreeSnippet from '~/components/tutorials/flutter-weather/WeatherCubitTreeSnippet.astro'; import WeatherBarrelDartSnippet from '~/components/tutorials/flutter-weather/WeatherBarrelDartSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) У цьому підручнику ми створимо додаток погоди на Flutter, який демонструє керування кількома cubit-ами для реалізації динамічної теми, pull-to-refresh та багато іншого. Наш додаток погоди отримуватиме дані про погоду в реальному часі з публічного API OpenMeteo та демонструватиме, як розділити наш додаток на шари (дані, сховище, бізнес-логіка та представлення). ![demo](~/assets/tutorials/flutter-weather.gif) ## Вимоги до проєкту Наш додаток повинен дозволяти користувачам - Шукати місто на спеціальній сторінці пошуку - Бачити приємне відображення даних про погоду, отриманих від [Open Meteo API](https://open-meteo.com) - Змінювати одиниці вимірювання (метричні чи імперські) Додатково, - Тема додатку повинна відображати погоду для обраного міста - Стан додатку повинен зберігатися між сесіями: тобто додаток повинен пам'ятати свій стан після закриття та повторного відкриття (використовуючи [HydratedBloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc)) ## Ключові концепції - Спостереження за змінами стану за допомогою [BlocObserver](/uk/bloc-concepts#blocobserver). - [BlocProvider](/uk/flutter-bloc-concepts#blocprovider), віджет Flutter, який надає bloc своїм дочірнім елементам. - [BlocBuilder](/uk/flutter-bloc-concepts#blocbuilder), віджет Flutter, який обробляє побудову віджета у відповідь на нові стани. - Запобігання зайвим перебудовам за допомогою [Equatable](/uk/faqs#коли-використовувати-equatable). - [RepositoryProvider](/uk/flutter-bloc-concepts#repositoryprovider), віджет Flutter, який надає сховище своїм дочірнім елементам. - [BlocListener](/uk/flutter-bloc-concepts#bloclistener), віджет Flutter, який викликає код слухача у відповідь на зміни стану в bloc. - [MultiBlocProvider](/uk/flutter-bloc-concepts#multiblocprovider), віджет Flutter, який об'єднує кілька BlocProvider віджетів в один. - [BlocConsumer](/uk/flutter-bloc-concepts#blocconsumer), віджет Flutter, який надає builder та listener для реагування на нові стани. - [HydratedBloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) для керування та збереження стану. ## Налаштування Для початку створіть новий flutter проєкт ### Структура проєкту Наш додаток складатиметься з ізольованих функціональних модулів у відповідних каталогах. Це дозволяє масштабувати по мірі зростання кількості модулів та дозволяє розробникам працювати над різними модулями паралельно. Наш додаток можна розділити на чотири основні модулі: **search, settings, theme, weather**. Створимо ці каталоги. ### Архітектура Дотримуючись рекомендацій [архітектури bloc](/uk/architecture), наш додаток складатиметься з кількох шарів. У цьому підручнику ось що робитимуть ці шари: - **Дані**: отримання необроблених даних про погоду з API - **Сховище**: абстрагування шару даних та надання доменних моделей для споживання додатком - **Бізнес-логіка**: керування станом кожного модуля (інформація про одиниці, деталі міста, теми тощо) - **Представлення**: відображення інформації про погоду та збір введення від користувачів (сторінка налаштувань, сторінка пошуку тощо) ## Шар даних Для цього додатку ми будемо використовувати [Open Meteo API](https://open-meteo.com). Ми зосередимося на двох ендпоінтах: - `https://geocoding-api.open-meteo.com/v1/search?name=$city&count=1` для отримання місцезнаходження за назвою міста - `https://api.open-meteo.com/v1/forecast?latitude=$latitude&longitude=$longitude¤t_weather=true` для отримання погоди за місцезнаходженням Відкрийте [https://geocoding-api.open-meteo.com/v1/search?name=chicago&count=1](https://geocoding-api.open-meteo.com/v1/search?name=chicago&count=1) у вашому браузері, щоб побачити відповідь для міста Чикаго. Ми використаємо `latitude` та `longitude` з відповіді для запиту до ендпоінта погоди. `latitude`/`longitude` для Чикаго — `41.85003`/`-87.65005`. Перейдіть за адресою [https://api.open-meteo.com/v1/forecast?latitude=43.0389&longitude=-87.90647¤t_weather=true](https://api.open-meteo.com/v1/forecast?latitude=43.0389&longitude=-87.90647¤t_weather=true) у вашому браузері, і ви побачите відповідь для погоди в Чикаго, яка містить усі дані, необхідні для нашого додатку. ### OpenMeteo API Client OpenMeteo API Client незалежний від нашого додатку. Тому ми створимо його як внутрішній пакет (і навіть зможемо опублікувати його на [pub.dev](https://pub.dev)). Потім ми зможемо використовувати пакет, додавши його до `pubspec.yaml` для шару сховища, який оброблятиме запити даних для нашого основного додатку погоди. Створіть новий каталог на рівні проєкту під назвою `packages`. Цей каталог зберігатиме всі наші внутрішні пакети. У цьому каталозі виконайте вбудовану команду `flutter create` для створення нового пакету під назвою `open_meteo_api` для нашого API клієнта. ### Модель даних погоди Далі створимо `location.dart` та `weather.dart`, які міститимуть моделі для відповідей ендпоінтів API `location` та `weather`. #### Модель Location Модель `location.dart` повинна зберігати дані, повернуті API місцезнаходження, які виглядають наступним чином: Ось файл `location.dart` в процесі розробки, який зберігає наведену вище відповідь: #### Модель Weather Далі попрацюємо над `weather.dart`. Наша модель погоди повинна зберігати дані, повернуті API погоди, які виглядають наступним чином: Ось файл `weather.dart` в процесі розробки, який зберігає наведену вище відповідь: ### Barrel-файли Поки ми тут, створимо швидко [barrel-файл](https://adrianfaciu.dev/posts/barrel-files/) для очищення деяких наших імпортів у подальшому. Створіть barrel-файл `models.dart` та експортуйте дві моделі: Створімо також barrel-файл рівня пакету `open_meteo_api.dart` На верхньому рівні `open_meteo_api.dart` експортуємо моделі: ### Налаштування Нам потрібно мати можливість [серіалізувати та десеріалізувати](https://en.wikipedia.org/wiki/Serialization) наші моделі для роботи з даними API. Для цього ми додамо методи `toJson` та `fromJson` до наших моделей. Також нам потрібен спосіб [здійснювати HTTP-запити](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) для отримання даних з API. На щастя, є кілька популярних пакетів саме для цього. Ми використаємо пакети [json_annotation](https://pub.dev/packages/json_annotation), [json_serializable](https://pub.dev/packages/json_serializable) та [build_runner](https://pub.dev/packages/build_runner) для генерації реалізацій `toJson` та `fromJson` за нас. На наступному кроці ми також використаємо пакет [http](https://pub.dev/packages/http) для надсилання мережевих запитів до API погоди, щоб наш додаток міг відображати поточні дані про погоду. Додамо ці залежності до `pubspec.yaml`. :::note Не забудьте виконати `flutter pub get` після додавання залежностей. ::: ### (Де)серіалізація Для роботи генерації коду нам потрібно анотувати наш код наступним чином: - `@JsonSerializable` для позначення класів, які можна серіалізувати - `@JsonKey` для надання рядкових представлень імен полів - `@JsonValue` для надання рядкових представлень значень полів - Реалізувати `JSONConverter` для перетворення об'єктних представлень у JSON представлення Для кожного файлу також потрібно: - Імпортувати `json_annotation` - Включити згенерований код за допомогою ключового слова [part](https://dart.dev/tools/pub/create-packages#organizing-a-package) - Включити методи `fromJson` для десеріалізації #### Модель Location Ось наш завершений файл моделі `location.dart`: #### Модель Weather Ось наш завершений файл моделі `weather.dart`: #### Створення файлу збірки У каталозі `open_meteo_api` створіть файл `build.yaml`. Мета цього файлу — обробка невідповідностей між конвенціями іменування в іменах полів `json_serializable`. #### Генерація коду Використаємо `build_runner` для генерації коду. `build_runner` повинен згенерувати файли `location.g.dart` та `weather.g.dart`. ### OpenMeteo API Client Створимо наш API клієнт у `open_meteo_api_client.dart` в каталозі `src`. Структура нашого проєкту тепер повинна виглядати так: Тепер ми можемо використовувати пакет [http](https://pub.dev/packages/http), який ми додали раніше до файлу `pubspec.yaml`, для здійснення HTTP-запитів до API погоди та використання цієї інформації в нашому додатку. Наш API клієнт надасть два методи: - `locationSearch`, який повертає `Future` - `getWeather`, який повертає `Future` #### Location Search Метод `locationSearch` звертається до API місцезнаходження та генерує помилки `LocationRequestFailure` за потреби. Завершений метод виглядає наступним чином: #### Get Weather Аналогічно, метод `getWeather` звертається до API погоди та генерує помилки `WeatherRequestFailure` за потреби. Завершений метод виглядає наступним чином: Завершений файл виглядає так: #### Оновлення barrel-файлу Завершимо цей пакет, додавши наш API клієнт до barrel-файлу. ### Unit-тести Особливо важливо писати unit-тести для шару даних, оскільки він є фундаментом нашого додатку. Unit-тести дадуть нам впевненість, що пакет працює як очікується. #### Налаштування Раніше ми додали пакет [test](https://pub.dev/packages/test) до нашого pubspec.yaml, що дозволяє легко писати unit-тести. Ми створимо тестовий файл для API клієнта, а також для двох моделей. #### Тести Location #### Тести Weather #### Тести API Client Далі протестуємо наш API клієнт. Ми повинні перевірити, що наш API клієнт коректно обробляє обидва виклики API, включаючи граничні випадки. :::note Ми не хочемо, щоб наші тести здійснювали реальні виклики API, оскільки наша мета — тестувати логіку API клієнта (включаючи всі граничні випадки), а не сам API. Для забезпечення узгодженого, контрольованого тестового середовища ми використаємо [mocktail](https://github.com/felangel/mocktail) (який ми додали до файлу pubspec.yaml раніше) для мокування `http` клієнта. ::: #### Покриття тестами Нарешті, зберемо покриття тестами, щоб переконатися, що ми покрили кожен рядок коду хоча б одним тестовим випадком. ## Шар сховища Мета нашого шару сховища — абстрагувати шар даних та сприяти комунікації з шаром bloc. Роблячи це, решта нашої кодової бази залежить лише від функцій, наданих нашим шаром сховища, замість конкретних реалізацій постачальників даних. Це дозволяє нам змінювати постачальників даних без порушення будь-якого коду на рівні додатку. Наприклад, якщо ми вирішимо мігрувати з цього конкретного API погоди, ми зможемо створити новий API клієнт та замінити його без необхідності вносити зміни до публічного API шарів сховища або додатку. ### Налаштування У каталозі packages виконайте наступну команду: Ми використаємо ті самі пакети, що і в пакеті `open_meteo_api`, включаючи пакет `open_meteo_api` з попереднього кроку. Оновіть ваш `pubspec.yaml` та виконайте `flutter pub get`. :::note Ми використовуємо `path` для вказівки розташування `open_meteo_api`, що дозволяє обробляти його як зовнішній пакет з `pub.dev`. ::: ### Моделі Weather Repository Ми створимо новий файл `weather.dart` для надання доменно-специфічної моделі погоди. Ця модель міститиме лише дані, релевантні для наших бізнес-потреб — іншими словами, вона повинна бути повністю від'єднана від API клієнта та необробленого формату даних. Як зазвичай, ми також створимо barrel-файл `models.dart`. Цього разу наша модель погоди зберігатиме лише властивості `location, temperature, condition`. Ми продовжуватимемо анотувати наш код для серіалізації та десеріалізації. Оновіть barrel-файл, який ми створили раніше, щоб включити моделі. #### Створення файлу збірки Як і раніше, потрібно створити `build.yaml` з наступним вмістом: #### Генерація коду Як ми робили раніше, виконайте наступну команду для генерації реалізації (де)серіалізації. #### Barrel-файл Створимо також barrel-файл рівня пакету з іменем `packages/weather_repository/lib/weather_repository.dart` для експорту наших моделей: ### Weather Repository Основна мета `WeatherRepository` — надати інтерфейс, що абстрагує постачальника даних. У цьому випадку `WeatherRepository` матиме залежність від `WeatherApiClient` та надаватиме єдиний публічний метод `getWeather(String city)`. :::note Споживачі `WeatherRepository` не знають про деталі внутрішньої реалізації, такі як те, що здійснюються два мережеві запити до API погоди. Мета `WeatherRepository` — відокремити "що" від "як" — іншими словами, ми хочемо мати спосіб отримати погоду для даного міста, але не турбуємося про те, як або звідки ці дані надходять. ::: #### Налаштування Створимо файл `weather_repository.dart` у каталозі `src` нашого пакету та попрацюємо над реалізацією сховища. Основний метод, на якому ми зосередимося — `getWeather(String city)`. Ми можемо реалізувати його за допомогою двох викликів до API клієнта наступним чином: #### Barrel-файл Оновіть barrel-файл, який ми створили раніше. ### Unit-тести Як і з шаром даних, критично важливо тестувати шар сховища, щоб переконатися, що логіка доменного рівня коректна. Для тестування нашого `WeatherRepository` ми використаємо бібліотеку [mocktail](https://github.com/felangel/mocktail). Ми замокуємо базовий API клієнт для unit-тестування логіки `WeatherRepository` в ізольованому, контрольованому середовищі. ## Шар бізнес-логіки У шарі бізнес-логіки ми будемо споживати доменну модель погоди з `WeatherRepository` та надавати модель рівня функціональності, яка буде показуватися користувачу через UI. :::note Це третій різний тип моделі погоди, який ми реалізуємо. В API клієнті наша модель погоди містила всю інформацію, повернуту API. У шарі сховища наша модель погоди містила лише абстраговану модель на основі нашого бізнес-випадку. У цьому шарі наша модель погоди міститиме релевантну інформацію, необхідну саме для поточного набору функціональності. ::: ### Налаштування Оскільки наш шар бізнес-логіки знаходиться в нашому основному додатку, нам потрібно відредагувати `pubspec.yaml` для всього проєкту `flutter_weather` та включити всі пакети, які ми використовуватимемо. - Використання [equatable](https://pub.dev/packages/equatable) дозволяє порівнювати екземпляри класів стану нашого додатку за допомогою оператора рівності `==`. Під капотом bloc порівнюватиме наші стани, щоб перевірити, чи вони рівні, і якщо ні, ініціюватиме перебудову. Це гарантує, що наше дерево віджетів перебудовуватиметься лише за потреби для підтримки швидкої та чуйної продуктивності. - Ми можемо покращити наш інтерфейс за допомогою [google_fonts](https://pub.dev/packages/google_fonts). - [HydratedBloc](https://pub.dev/packages/hydrated_bloc) дозволяє нам зберігати стан додатку при закритті та повторному відкритті. - Ми включимо пакет `weather_repository`, який ми щойно створили, для отримання поточних даних про погоду! Для тестування ми включимо звичайний пакет `test` разом з `mocktail` для мокування залежностей та [bloc_test](https://pub.dev/packages/bloc_test) для зручного тестування одиниць бізнес-логіки, або блоків! Далі ми працюватимемо над шаром додатку в каталозі функціональності `weather`. ### Модель Weather Мета нашої моделі погоди — відстежувати дані про погоду, що відображаються нашим додатком, а також налаштування температури (Цельсій або Фаренгейт). Створіть `flutter_weather/lib/weather/models/weather.dart`: ### Створення файлу збірки Створіть `build.yaml` для шару бізнес-логіки. ### Генерація коду Виконайте `build_runner` для генерації реалізацій (де)серіалізації. ### Barrel-файл Експортуємо наші моделі з barrel-файлу (`flutter_weather/lib/weather/models/models.dart`): Потім створимо barrel-файл верхнього рівня для погоди (`flutter_weather/lib/weather/weather.dart`); ### Weather Ми використаємо `HydratedCubit`, щоб наш додаток запам'ятовував свій стан, навіть після закриття та повторного відкриття. :::note `HydratedCubit` є розширенням `Cubit`, яке обробляє збереження та відновлення стану між сесіями. ::: #### Weather State Використовуючи розширення [Bloc VSCode](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) або [Bloc IntelliJ](https://plugins.jetbrains.com/plugin/12129-bloc), натисніть правою кнопкою на каталог `weather` та створіть новий cubit під назвою `Weather`. Структура проєкту повинна виглядати так: Є чотири стани, в яких може перебувати наш додаток погоди: - `initial` — до завантаження будь-чого - `loading` — під час виклику API - `success` — якщо виклик API успішний - `failure` — якщо виклик API невдалий Enum `WeatherStatus` представлятиме наведені вище стани. Повний стан погоди повинен виглядати так: #### Weather Cubit Тепер, коли ми визначили `WeatherState`, напишемо `WeatherCubit`, який надасть наступні методи: - `fetchWeather(String? city)` — використовує сховище погоди для спроби отримати об'єкт погоди для даного міста - `refreshWeather()` — отримує новий об'єкт погоди, використовуючи сховище погоди на основі поточного стану погоди - `toggleUnits()` — перемикає стан між Цельсієм та Фаренгейтом - `fromJson(Map json)`, `toJson(WeatherState state)` — використовуються для збереження :::note Не забудьте згенерувати код (де)серіалізації через: ::: ### Unit-тести Аналогічно шарам даних та сховища, критично важливо тестувати шар бізнес-логіки за допомогою unit-тестів, щоб переконатися, що логіка рівня функціональності працює як очікується. Ми використовуватимемо [bloc_test](https://pub.dev/packages/bloc_test) на додаток до `mocktail` та `test`. Додамо пакети `test`, `bloc_test` та `mocktail` до `dev_dependencies`. :::note Пакет [bloc_test](https://pub.dev/packages/bloc_test) дозволяє нам легко підготувати наші блоки для тестування, обробляти зміни стану та перевіряти результати послідовним чином. ::: #### Тести Weather Cubit ## Шар представлення ### Weather Page Ми почнемо з `WeatherPage`, яка використовує `BlocProvider` для надання екземпляра `WeatherCubit` дереву віджетів. Ви помітите, що сторінка залежить від віджетів `SettingsPage` та `SearchPage`, які ми створимо далі. ### SettingsPage Сторінка налаштувань дозволяє користувачам оновлювати свої уподобання щодо одиниць температури. ### SearchPage Сторінка пошуку дозволяє користувачам вводити назву бажаного міста та передає результат пошуку попередньому маршруту через `Navigator.of(context).pop`. ### Віджети погоди Додаток відображатиме різні екрани залежно від чотирьох можливих станів `WeatherCubit`. #### WeatherEmpty Цей екран відображатиметься, коли немає даних для показу, оскільки користувач ще не вибрав місто. #### WeatherError Цей екран відображатиметься, якщо виникне помилка. #### WeatherLoading Цей екран відображатиметься під час завантаження даних додатком. #### WeatherPopulated Цей екран відображатиметься після того, як користувач обрав місто та ми отримали дані. ### Barrel-файл Додамо ці стани до barrel-файлу для очищення наших імпортів. ### Точка входу Наш `main.dart` файл повинен ініціалізувати `WeatherApp` та `BlocObserver` (для цілей налагодження), а також налаштувати `HydratedStorage` для збереження стану між сесіями. Наш віджет `app.dart` обробляє побудову представлення `WeatherPage`, яке ми створили раніше, та використовує `BlocProvider` для впровадження нашого `WeatherCubit`. ### Widget-тести Бібліотека [`bloc_test`](https://pub.dev/packages/bloc_test) також надає `MockBlocs` та `MockCubits`, які спрощують тестування UI. Ми можемо мокувати стани різних cubit-ів та переконатися, що UI реагує коректно. :::note Ми використовуємо `MockWeatherCubit` разом з API `when` з `mocktail` для підставлення стану cubit у кожному тестовому випадку. Це дозволяє нам симулювати всі стани та перевіряти, що UI працює коректно за будь-яких обставин. ::: ## Підсумок Ось і все, ми завершили підручник! Ми можемо запустити фінальний додаток за допомогою команди `flutter run`. Повний вихідний код цього прикладу, включаючи unit та widget тести, можна знайти [тут](https://github.com/felangel/bloc/tree/master/examples/flutter_weather). ================================================ FILE: docs/src/content/docs/uk/tutorials/github-search.mdx ================================================ --- title: Пошук GitHub description: Детальний посібник зі створення додатку пошуку GitHub у Flutter та AngularDart з використанням bloc. sidebar: order: 9 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import SetupSnippet from '~/components/tutorials/github-search/SetupSnippet.astro'; import DartPubGetSnippet from '~/components/tutorials/github-search/DartPubGetSnippet.astro'; import FlutterCreateSnippet from '~/components/tutorials/github-search/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; import StagehandSnippet from '~/components/tutorials/github-search/StagehandSnippet.astro'; import ActivateStagehandSnippet from '~/components/tutorials/github-search/ActivateStagehandSnippet.astro'; ![advanced](https://img.shields.io/badge/level-advanced-red.svg) У наступному посібнику ми створимо додаток пошуку GitHub у Flutter та AngularDart, щоб продемонструвати, як ми можемо спільно використовувати шари даних та бізнес-логіки між двома проєктами. ![demo](~/assets/tutorials/flutter-github-search.gif) ![demo](~/assets/tutorials/ngdart-github-search.gif) ## Ключові теми - [BlocProvider](/uk/flutter-bloc-concepts#blocprovider), Flutter віджет, який надає bloc своїм нащадкам. - [BlocBuilder](/uk/flutter-bloc-concepts#blocbuilder), Flutter віджет, який обробляє побудову віджета у відповідь на нові стани. - Використання Cubit замість Bloc. [У чому різниця?](/uk/bloc-concepts#cubit-проти-bloc) - Запобігання непотрібним перебудовам за допомогою [Equatable](/uk/faqs#коли-використовувати-equatable). - Використання користувацького `EventTransformer` з [`bloc_concurrency`](https://pub.dev/packages/bloc_concurrency). - Виконання мережевих запитів за допомогою пакету `http`. ## Спільна бібліотека пошуку GitHub Спільна бібліотека пошуку GitHub міститиме моделі, провайдер даних, сховище, а також bloc, який буде спільно використовуватися між AngularDart та Flutter. ### Налаштування Ми почнемо зі створення нової директорії для нашого додатку. :::note Директорія `common_github_search` міститиме спільну бібліотеку. ::: Нам потрібно створити `pubspec.yaml` з необхідними залежностями. Нарешті, нам потрібно встановити наші залежності. Це все для налаштування проєкту! Тепер ми можемо приступити до роботи над створенням пакету `common_github_search`. ### Github Client `GithubClient` надаватиме необроблені дані з [GitHub API](https://developer.github.com/v3/). :::note Ви можете побачити зразок даних, які ми отримаємо назад, [тут](https://api.github.com/search/repositories?q=dartlang). ::: Давайте створимо `github_client.dart`. :::note Наш `GithubClient` просто виконує мережевий запит до API пошуку репозиторіїв Github і конвертує результат або в `SearchResult`, або в `SearchResultError` як `Future`. ::: :::note Реалізація `GithubClient` залежить від `SearchResult.fromJson`, який ми ще не реалізували. ::: Далі нам потрібно визначити наші моделі `SearchResult` та `SearchResultError`. #### Модель результату пошуку Створіть `search_result.dart`, який представляє список `SearchResultItems` на основі запиту користувача: :::note Реалізація `SearchResult` залежить від `SearchResultItem.fromJson`, який ми ще не реалізували. ::: :::note Ми не включаємо властивості, які не будуть використовуватися в нашій моделі. ::: #### Модель елемента результату пошуку Далі ми створимо `search_result_item.dart`. :::note Знову ж таки, реалізація `SearchResultItem` залежить від `GithubUser.fromJson`, який ми ще не реалізували. ::: #### Модель користувача GitHub Далі ми створимо `github_user.dart`. На цьому етапі ми завершили реалізацію `SearchResult` та його залежностей. Тепер перейдемо до `SearchResultError`. #### Модель помилки результату пошуку Створіть `search_result_error.dart`. Наш `GithubClient` завершений, тому далі ми перейдемо до `GithubCache`, який відповідатиме за [мемоізацію](https://en.wikipedia.org/wiki/Memoization) як оптимізацію продуктивності. ### GitHub Cache Наш `GithubCache` відповідатиме за запам'ятовування всіх минулих запитів, щоб ми могли уникнути непотрібних мережевих запитів до GitHub API. Це також допоможе покращити продуктивність нашого додатку. Створіть `github_cache.dart`. Тепер ми готові створити наш `GithubRepository`! ### GitHub Repository Github Repository відповідає за створення абстракції між шаром даних (`GithubClient`) та шаром бізнес-логіки (`Bloc`). Тут також ми будемо використовувати наш `GithubCache`. Створіть `github_repository.dart`. :::note `GithubRepository` має залежність від `GithubCache` та `GithubClient` і абстрагує базову реалізацію. Наш додаток ніколи не повинен знати, як дані отримуються або звідки вони надходять, оскільки це його не стосується. Ми можемо змінити роботу сховища в будь-який час, і поки ми не змінюємо інтерфейс, нам не потрібно змінювати жоден клієнтський код. ::: На цьому етапі ми завершили шар провайдера даних та шар сховища, тому ми готові перейти до шару бізнес-логіки. ### GitHub Search Event Наш Bloc буде сповіщений, коли користувач введе назву репозиторію, що ми представимо як `TextChanged` `GithubSearchEvent`. Створіть `github_search_event.dart`. :::note Ми розширюємо [`Equatable`](https://pub.dev/packages/equatable), щоб мати можливість порівнювати екземпляри `GithubSearchEvent`. За замовчуванням оператор рівності повертає true тоді і тільки тоді, коли this та other є одним і тим же екземпляром. ::: ### Github Search State Наш шар представлення повинен мати кілька частин інформації, щоб правильно себе відобразити: - `SearchStateEmpty` -- повідомить шару представлення, що користувач не надав жодного введення. - `SearchStateLoading` -- повідомить шару представлення, що він повинен відобразити якийсь індикатор завантаження. - `SearchStateSuccess` -- повідомить шару представлення, що він має дані для відображення. - `items` -- буде `List`, який буде відображено. - `SearchStateError` -- повідомить шару представлення, що виникла помилка при отриманні репозиторіїв. - `error` -- буде точна помилка, яка виникла. Тепер ми можемо створити `github_search_state.dart` і реалізувати його наступним чином. :::note Ми розширюємо [`Equatable`](https://pub.dev/packages/equatable), щоб мати можливість порівнювати екземпляри `GithubSearchState`. За замовчуванням оператор рівності повертає true тоді і тільки тоді, коли this та other є одним і тим же екземпляром. ::: Тепер, коли ми реалізували наші події та стани, ми можемо створити наш `GithubSearchBloc`. ### GitHub Search Bloc Створіть `github_search_bloc.dart`: :::note Наш `GithubSearchBloc` конвертує `GithubSearchEvent` у `GithubSearchState` та має залежність від `GithubRepository`. ::: :::note Ми створюємо користувацький `EventTransformer` для [debounce](https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/debounce.html) `GithubSearchEvents`. Однією з причин, чому ми створили `Bloc` замість `Cubit`, було використання переваг трансформерів потоків. ::: Чудово! Ми завершили наш пакет `common_github_search`. Готовий продукт повинен виглядати як [цей](https://github.com/felangel/bloc/tree/master/examples/github_search/common_github_search). Далі ми працюватимемо над реалізацією на Flutter. ## Flutter GitHub Search Flutter Github Search буде додатком Flutter, який повторно використовує моделі, провайдери даних, сховища та блоки з `common_github_search` для реалізації пошуку Github. ### Налаштування Нам потрібно почати зі створення нового проєкту Flutter у нашій директорії `github_search` на тому ж рівні, що й `common_github_search`. Далі нам потрібно оновити наш `pubspec.yaml`, щоб включити всі необхідні залежності. :::note Ми включаємо нашу щойно створену бібліотеку `common_github_search` як залежність. ::: Тепер нам потрібно встановити залежності. Це все для налаштування проєкту. Оскільки пакет `common_github_search` містить наш шар даних, а також наш шар бізнес-логіки, все, що нам потрібно побудувати -- це шар представлення. ### Форма пошуку Нам потрібно створити форму з віджетами `_SearchBar` та `_SearchBody`. - `_SearchBar` відповідатиме за отримання введення від користувача. - `_SearchBody` відповідатиме за відображення результатів пошуку, індикаторів завантаження та помилок. Давайте створимо `search_form.dart`. Наш `SearchForm` буде `StatelessWidget`, який відображає віджети `_SearchBar` та `_SearchBody`. `_SearchBar` також буде `StatefulWidget`, тому що йому потрібно підтримувати свій власний `TextEditingController`, щоб ми могли відстежувати, що користувач ввів. `_SearchBody` -- це `StatelessWidget`, який відповідатиме за відображення результатів пошуку, помилок та індикаторів завантаження. Він буде споживачем `GithubSearchBloc`. Якщо наш стан `SearchStateSuccess`, ми відображаємо `_SearchResults`, який ми реалізуємо далі. `_SearchResults` -- це `StatelessWidget`, який приймає `List` і відображає їх як список `_SearchResultItems`. `_SearchResultItem` -- це `StatelessWidget`, який відповідає за відображення інформації про один результат пошуку. Він також відповідає за обробку взаємодії користувача та навігацію за URL-адресою репозиторію при натисканні користувача. :::note `_SearchBar` отримує доступ до `GitHubSearchBloc` через `context.read()` та сповіщає bloc про події `TextChanged`. ::: :::note `_SearchBody` використовує `BlocBuilder` для перебудови у відповідь на зміни стану. Оскільки параметр bloc об'єкта `BlocBuilder` було пропущено, `BlocBuilder` автоматично виконає пошук за допомогою `BlocProvider` та поточного `BuildContext`. Детальніше [тут.](/uk/flutter-bloc-concepts#blocbuilder) ::: :::note Ми використовуємо `ListView.builder` для побудови прокручуваного списку `_SearchResultItem`. ::: :::note Ми використовуємо пакет [url_launcher](https://pub.dev/packages/url_launcher) для відкриття зовнішніх URL-адрес. ::: ### Збираємо все разом Тепер залишилося лише реалізувати наш головний додаток у `main.dart`. :::note Наш `GithubRepository` створюється в `main` та впроваджується в наш `App`. Наша `SearchForm` обгорнута в `BlocProvider`, який відповідає за ініціалізацію, закриття та надання доступу до екземпляру `GithubSearchBloc` для віджета `SearchForm` та його нащадків. ::: Ось і все! Ми успішно реалізували додаток пошуку GitHub у Flutter, використовуючи пакети [bloc](https://pub.dev/packages/bloc) та [flutter_bloc](https://pub.dev/packages/flutter_bloc), і ми успішно відокремили наш шар представлення від нашої бізнес-логіки. Повний вихідний код можна знайти [тут](https://github.com/felangel/bloc/tree/master/examples/github_search/flutter_github_search). Нарешті, ми створимо наш додаток AngularDart GitHub Search. ## AngularDart GitHub Search AngularDart GitHub Search буде додатком AngularDart, який повторно використовує моделі, провайдери даних, сховища та блоки з `common_github_search` для реалізації пошуку Github. ### Налаштування Нам потрібно почати зі створення нового проєкту AngularDart у нашій директорії github_search на тому ж рівні, що й `common_github_search`. :::note Ви можете встановити `stagehand` за допомогою: ::: Потім ми можемо замінити вміст `pubspec.yaml` на: ### Форма пошуку Так само, як і в нашому додатку Flutter, нам потрібно створити `SearchForm` з компонентами `SearchBar` та `SearchBody`. Наш компонент `SearchForm` реалізовуватиме `OnInit` та `OnDestroy`, тому що йому потрібно створити та закрити `GithubSearchBloc`. - `SearchBar` відповідатиме за отримання введення від користувача. - `SearchBody` відповідатиме за відображення результатів пошуку, індикаторів завантаження та помилок. Давайте створимо `search_form_component.dart.` :::note `GithubRepository` впроваджується в `SearchFormComponent`. ::: :::note `GithubSearchBloc` створюється та закривається `SearchFormComponent`. ::: Наш шаблон (`search_form_component.html`) буде виглядати так: Далі ми реалізуємо компонент `SearchBar`. ### Панель пошуку `SearchBar` -- це компонент, який відповідатиме за отримання введення від користувача та сповіщення `GithubSearchBloc` про зміни тексту. Створіть `search_bar_component.dart`. :::note `SearchBarComponent` має залежність від `GitHubSearchBloc`, тому що він відповідає за сповіщення bloc про події `TextChanged`. ::: Далі ми можемо створити `search_bar_component.html`. Ми закінчили з `SearchBar`, тепер переходимо до `SearchBody`. ### Тіло пошуку `SearchBody` -- це компонент, який відповідатиме за відображення результатів пошуку, помилок та індикаторів завантаження. Він буде споживачем `GithubSearchBloc`. Створіть `search_body_component.dart`. :::note `SearchBodyComponent` має залежність від `GithubSearchState`, який надається `GithubSearchBloc` за допомогою пайпу `angular_bloc` bloc. ::: Створіть `search_body_component.html`. Якщо наш стан `isSuccess`, ми відображаємо `SearchResults`. Ми реалізуємо його далі. ### Результати пошуку `SearchResults` -- це компонент, який приймає `List` і відображає їх як список `SearchResultItems`. Створіть `search_results_component.dart`. Далі ми створимо `search_results_component.html`. :::note Ми використовуємо `ngFor` для побудови списку компонентів `SearchResultItem`. ::: Час реалізувати `SearchResultItem`. ### Елемент результату пошуку `SearchResultItem` -- це компонент, який відповідає за відображення інформації про один результат пошуку. Він також відповідає за обробку взаємодії користувача та навігацію за URL-адресою репозиторію при натисканні користувача. Створіть `search_result_item_component.dart`. та відповідний шаблон у `search_result_item_component.html`. ### Збираємо все разом У нас є всі наші компоненти, і тепер настав час зібрати їх усі разом в нашому `app_component.dart`. :::note Ми створюємо `GithubRepository` в `AppComponent` та впроваджуємо його в компонент `SearchForm`. ::: Ось і все! Ми успішно реалізували додаток пошуку GitHub в AngularDart, використовуючи пакети `bloc` та `angular_bloc`, і ми успішно відокремили наш шар представлення від нашої бізнес-логіки. Повний вихідний код можна знайти [тут](https://github.com/felangel/bloc/tree/master/examples/github_search/angular_github_search). ## Підсумок У цьому посібнику ми створили додаток Flutter та AngularDart, спільно використовуючи всі моделі, провайдери даних та блоки між ними. Єдине, що нам дійсно довелося написати двічі -- це шар представлення (UI), що чудово з точки зору ефективності та швидкості розробки. Крім того, досить поширено, коли веб-додатки та мобільні додатки мають різний користувацький досвід та стилі, і цей підхід дійсно демонструє, наскільки легко створити два додатки, які виглядають абсолютно по-різному, але спільно використовують ті самі шари даних та бізнес-логіки. Повний вихідний код можна знайти [тут](https://github.com/felangel/bloc/tree/master/examples/github_search). ================================================ FILE: docs/src/content/docs/uk/tutorials/ngdart-counter.mdx ================================================ --- title: Лічильник AngularDart description: Детальний посібник зі створення додатку-лічильника AngularDart з використанням bloc. sidebar: order: 8 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import ActivateStagehandSnippet from '~/components/tutorials/ngdart-counter/ActivateStagehandSnippet.astro'; import StagehandSnippet from '~/components/tutorials/ngdart-counter/StagehandSnippet.astro'; import InstallDependenciesSnippet from '~/components/tutorials/ngdart-counter/InstallDependenciesSnippet.astro'; ![beginner](https://img.shields.io/badge/level-beginner-green.svg) У наступному посібнику ми створимо лічильник в AngularDart, використовуючи бібліотеку Bloc. ![demo](~/assets/tutorials/ngdart-counter.gif) ## Налаштування Ми почнемо зі створення нового проєкту AngularDart за допомогою [stagehand](https://github.com/dart-lang/stagehand). Якщо у вас не встановлено stagehand, активуйте його за допомогою: Потім згенеруйте новий проєкт за допомогою: Потім ми можемо замінити вміст `pubspec.yaml` на: і потім встановити всі наші залежності Наш додаток лічильника матиме лише дві кнопки для збільшення/зменшення значення лічильника та елемент для відображення поточного значення. Давайте почнемо проєктувати `CounterEvents`. ## Counter Bloc Оскільки стан нашого лічильника може бути представлений цілим числом, нам не потрібно створювати користувацький клас, і ми можемо розмістити події та bloc разом. :::note Просто з оголошення класу ми можемо сказати, що наш `CounterBloc` прийматиме `CounterEvents` як вхідні дані та виводитиме цілі числа. ::: ## Counter App Тепер, коли наш `CounterBloc` повністю реалізований, ми можемо почати створювати наш компонент AngularDart App. Наш `app.component.dart` повинен виглядати так: і наш `app.component.html` повинен виглядати так: ## Counter Page Нарешті, залишилося лише побудувати наш компонент Counter Page. Наш `counter_page_component.dart` повинен виглядати так: :::note Ми можемо отримати доступ до екземпляру `CounterBloc`, використовуючи систему впровадження залежностей AngularDart. Оскільки ми зареєстрували його як `Provider`, AngularDart може правильно знайти `CounterBloc`. ::: :::note Ми закриваємо `CounterBloc` у `ngOnDestroy`. ::: :::note Ми імпортуємо `BlocPipe`, щоб ми могли використовувати його у нашому шаблоні. ::: Нарешті, наш `counter_page_component.html` повинен виглядати так: :::note Ми використовуємо `BlocPipe`, щоб ми могли відображати стан нашого `CounterBloc` по мірі його оновлення. ::: Ось і все! Ми відокремили наш шар представлення від нашого шару бізнес-логіки. Наш `CounterPageComponent` не має уявлення, що відбувається, коли користувач натискає кнопку; він просто додає подію, щоб сповістити `CounterBloc`. Крім того, наш `CounterBloc` не має уявлення, що відбувається зі станом (значенням лічильника); він просто конвертує `CounterEvents` у цілі числа. Ми можемо запустити наш додаток за допомогою `webdev serve` та переглянути його локально. Повний вихідний код цього прикладу можна знайти [тут](https://github.com/felangel/bloc/tree/master/examples/angular_counter). ================================================ FILE: docs/src/content/docs/uk/why-bloc.mdx ================================================ --- title: Чому Bloc? description: Огляд того, що робить Bloc надійним рішенням для керування станом. sidebar: order: 1 --- Bloc спрощує відокремлення представлення від бізнес-логіки, роблячи ваш код _швидким_, _легким для тестування_ та _придатним для повторного використання_. При створенні якісних додатків керування станом стає критично важливим. Як розробники ми хочемо: - знати, в якому стані перебуває наш додаток у будь-який момент часу. - легко тестувати кожний випадок, щоб переконатися, що наш додаток реагує належним чином. - записувати кожну взаємодію користувача в нашому додатку, щоб ми могли приймати рішення на основі даних. - працювати максимально ефективно та повторно використовувати компоненти як всередині нашого додатку, так і в інших додатках. - мати можливість багатьом розробникам безперешкодно працювати в одній кодовій базі, дотримуючись однакових шаблонів та угод. - розробляти швидкі та чуйні додатки. Bloc був розроблений для задоволення всіх цих потреб та багатьох інших. Існує безліч рішень для керування станом, і вибір підходящого може стати складним завданням. Не існує одного ідеального рішення для керування станом! Важливо обрати те, яке найкраще підходить для вашої команди та вашого проєкту. Bloc був розроблений з урахуванням трьох основних цінностей: - **Простий:** Легко зрозуміти та може використовуватися розробниками з різним рівнем навичок. - **Потужний:** Допомагає створювати дивовижні, складні додатки, компонуючи їх з менших компонентів. - **Тестовний:** Легко тестувати кожний аспект додатку, щоб ми могли ітерувати з впевненістю. Загалом, Bloc намагається зробити зміни стану передбачуваними, регулюючи, коли може відбутися зміна стану, та забезпечуючи єдиний спосіб зміни стану в усьому додатку. ================================================ FILE: docs/src/content/docs/why-bloc.mdx ================================================ --- title: Why Bloc? description: An overview of what makes Bloc a solid state management solution. sidebar: order: 1 --- Bloc makes it easy to separate presentation from business logic, making your code _fast_, _easy to test_, and _reusable_. When building production quality applications, managing state becomes critical. As developers we want to: - know what state our application is in at any point in time. - easily test every case to make sure our app is responding appropriately. - record every single user interaction in our application so that we can make data-driven decisions. - work as efficiently as possible and reuse components both within our application and across other applications. - have many developers seamlessly working within a single code base following the same patterns and conventions. - develop fast and reactive apps. Bloc was designed to meet all of these needs and many more. There are many state management solutions and deciding which one to use can be a daunting task. There is no one perfect state management solution! What's important is that you pick the one that works best for your team and your project. Bloc was designed with three core values in mind: - **Simple:** Easy to understand & can be used by developers with varying skill levels. - **Powerful:** Help make amazing, complex applications by composing them of smaller components. - **Testable:** Easily test every aspect of an application so that we can iterate with confidence. Overall, Bloc attempts to make state changes predictable by regulating when a state change can occur and enforcing a single way to change state throughout an entire application. ================================================ FILE: docs/src/content/docs/zh-cn/bloc-concepts.mdx ================================================ --- title: Bloc 核心概念 description: package:bloc 核心概念的概述。 sidebar: order: 1 --- import CountStreamSnippet from '~/components/concepts/bloc/CountStreamSnippet.astro'; import SumStreamSnippet from '~/components/concepts/bloc/SumStreamSnippet.astro'; import StreamsMainSnippet from '~/components/concepts/bloc/StreamsMainSnippet.astro'; import CounterCubitSnippet from '~/components/concepts/bloc/CounterCubitSnippet.astro'; import CounterCubitInitialStateSnippet from '~/components/concepts/bloc/CounterCubitInitialStateSnippet.astro'; import CounterCubitInstantiationSnippet from '~/components/concepts/bloc/CounterCubitInstantiationSnippet.astro'; import CounterCubitIncrementSnippet from '~/components/concepts/bloc/CounterCubitIncrementSnippet.astro'; import CounterCubitBasicUsageSnippet from '~/components/concepts/bloc/CounterCubitBasicUsageSnippet.astro'; import CounterCubitStreamUsageSnippet from '~/components/concepts/bloc/CounterCubitStreamUsageSnippet.astro'; import CounterCubitOnChangeSnippet from '~/components/concepts/bloc/CounterCubitOnChangeSnippet.astro'; import CounterCubitOnChangeUsageSnippet from '~/components/concepts/bloc/CounterCubitOnChangeUsageSnippet.astro'; import CounterCubitOnChangeOutputSnippet from '~/components/concepts/bloc/CounterCubitOnChangeOutputSnippet.astro'; import SimpleBlocObserverOnChangeSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeSnippet.astro'; import SimpleBlocObserverOnChangeUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeUsageSnippet.astro'; import SimpleBlocObserverOnChangeOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnChangeOutputSnippet.astro'; import CounterCubitOnErrorSnippet from '~/components/concepts/bloc/CounterCubitOnErrorSnippet.astro'; import SimpleBlocObserverOnErrorSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnErrorSnippet.astro'; import CounterCubitOnErrorOutputSnippet from '~/components/concepts/bloc/CounterCubitOnErrorOutputSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/bloc/CounterBlocSnippet.astro'; import CounterBlocEventHandlerSnippet from '~/components/concepts/bloc/CounterBlocEventHandlerSnippet.astro'; import CounterBlocIncrementSnippet from '~/components/concepts/bloc/CounterBlocIncrementSnippet.astro'; import CounterBlocUsageSnippet from '~/components/concepts/bloc/CounterBlocUsageSnippet.astro'; import CounterBlocStreamUsageSnippet from '~/components/concepts/bloc/CounterBlocStreamUsageSnippet.astro'; import CounterBlocOnChangeSnippet from '~/components/concepts/bloc/CounterBlocOnChangeSnippet.astro'; import CounterBlocOnChangeUsageSnippet from '~/components/concepts/bloc/CounterBlocOnChangeUsageSnippet.astro'; import CounterBlocOnChangeOutputSnippet from '~/components/concepts/bloc/CounterBlocOnChangeOutputSnippet.astro'; import CounterBlocOnTransitionSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionSnippet.astro'; import CounterBlocOnTransitionOutputSnippet from '~/components/concepts/bloc/CounterBlocOnTransitionOutputSnippet.astro'; import SimpleBlocObserverOnTransitionSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionSnippet.astro'; import SimpleBlocObserverOnTransitionUsageSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionUsageSnippet.astro'; import SimpleBlocObserverOnTransitionOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnTransitionOutputSnippet.astro'; import CounterBlocOnEventSnippet from '~/components/concepts/bloc/CounterBlocOnEventSnippet.astro'; import SimpleBlocObserverOnEventSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventSnippet.astro'; import SimpleBlocObserverOnEventOutputSnippet from '~/components/concepts/bloc/SimpleBlocObserverOnEventOutputSnippet.astro'; import CounterBlocOnErrorSnippet from '~/components/concepts/bloc/CounterBlocOnErrorSnippet.astro'; import CounterBlocOnErrorOutputSnippet from '~/components/concepts/bloc/CounterBlocOnErrorOutputSnippet.astro'; import CounterCubitFullSnippet from '~/components/concepts/bloc/CounterCubitFullSnippet.astro'; import CounterBlocFullSnippet from '~/components/concepts/bloc/CounterBlocFullSnippet.astro'; import AuthenticationStateSnippet from '~/components/concepts/bloc/AuthenticationStateSnippet.astro'; import AuthenticationTransitionSnippet from '~/components/concepts/bloc/AuthenticationTransitionSnippet.astro'; import AuthenticationChangeSnippet from '~/components/concepts/bloc/AuthenticationChangeSnippet.astro'; import DebounceEventTransformerSnippet from '~/components/concepts/bloc/DebounceEventTransformerSnippet.astro'; :::note 在使用 [`package:bloc`](https://pub.dev/packages/bloc) 之前请确保已仔细阅读以下内容。 ::: 有几个核心概念对于理解如何使用 bloc 包至关重要。 在接下来的部分中,我们将依次详细介绍,并研究如何将它们应用于计数器应用程序。 ## Streams (流) :::note 有关 `Streams` 的更多信息,请参阅官方 [Dart 文档](https://dart.dev/tutorials/language/streams)。 ::: 流是一系列异步数据。 要使用 bloc 库,必须对 `Streams` 及其工作原理有基本的了解。 如果您不熟悉 `Streams` ,那么可以想象一下有水流过的管道。管道是 `Stream`,水是异步数据。 我们可以通过编写 `async*`(异步生成器)函数在 Dart 中创建一个 `Stream`。 通过将函数标记为 `async*` ,我们可以使用 `yield` 关键字并返回 `Stream` 数据。在上面的例子中,我们返回一个 `Stream` 整数,它的最大值是 `max` 参数。 每次我们在 `async*` 函数中 `yield` 时,我们都会通过 `Stream` 推送该部分数据。 我们可以用多种方式使用上述 `Stream` 。如果我们想编写一个函数来返回整数 `Stream` 的总和,它看起来可能像这样: 通过将上述函数标记为 `async` ,我们可以使用 `await` 关键字并返回一个 `Future` 整数。在此示例中,我们正在等待流中的每个值并返回流中所有整数的总和。 我们可以合并上面的代码如下: 现在我们对 Dart 中 `Streams` 的原理有了一个基本的了解。我们可以学习关于 bloc 包的核心组件:`Cubit` 了。 ## Cubit `Cubit` 是扩展自 `BlocBase` 的类并可以扩展用于管理任何类型的状态。 ![Cubit 架构](~/assets/concepts/cubit_architecture_full.png) `Cubit` 可以公开可调用函数来触发状态的改变。 状态是 `Cubit` 的输出,代表应用程序状态的一部分。UI 组件可以收到状态通知,并根据当前状态进行部分重绘。 :::note 有关 `Cubit` 的起源的更多信息,请查看 [Github Issue \#69](https://github.com/felangel/cubit/issues/69)。 ::: ### 创建一个 Cubit 我们可以像这样创建一个 `CounterCubit` : 创建 `Cubit` 时,我们需要定义 `Cubit` 管理的状态类型。以上面的 `CounterCubit` 为例,状态类型是 `int` ,但在更复杂的情况下,可能需要使用 `class` 而不是值类型。 其次在创建 `Cubit` 的时候要指定初始状态。我们可以通过调用 `super` 并赋初始值来实现。在上面的代码片段中,我们在内部将初始状态设置为 `0` ,但我们也可以通过构造函数参数使 `Cubit` 更加灵活: 这样我们就可以创建具有不同初始值的 `CounterCubit` 实例,像这样: ### Cubit 状态变更 每一个 `Cubit` 都可以通过 `emit` 输出一个新的状态。 在上面的代码片段中, `CounterCubit` 公开了一个名为 `increment` 的公共方法,可以被外部调用以通知 `CounterCubit` 增加它的状态值。当调用 `increment` 时,我们可以通过 `状态` 的getter访问 `Cubit` 的当前状态,并通过在当前状态上 `+1` 来 `emit` 一个新状态。 :::caution `emit` 是保护方法,意味着它只能在 `Cubit` 内部被调用。 ::: ### 使用 Cubit 我们现在可以使用我们实现的 `CounterCubit` 了。 #### 基本用法 在上面的代码片段中,我们从创建一个 `CounterCubit` 开始。然后我们打印了当前 cubit 的初始状态(由于尚未发出任何新的状态)。接下来,我们调用 `increment` 函数来触发状态变化。最后,我们再次打印 `Cubit` 的状态,从 `0` 变为 `1` ,并在 `Cubit` 上调用 `close` 来关闭内部状态流。 #### 流的用法 `Cubit` 公开了一个 `Stream` 可以用于接收实时的状态更新: 在上面的代码片段中,我们订阅了 `CounterCubit` 并且在每次状态变化时打印出来。我们调用了 `increment` 函数来触发新的状态。最后,当我们不再需要接收时关闭了这个 `Cubit`, 并且在 `subscription` 上调用了 `cancel` 。 :::note `await Future.delayed(Duration.zero)` 在这个示例里是为了避免 `subscription` 被立即取消。 ::: :::caution 只有在 `Cubit` 上调用 `listen` 时才会收到后续状态变化。 ::: ### 观察 Cubit 当 `Cubit` 发出一个新的状态时,一个 `Change` 发生了。我们可以通过重写 `onChange` 来观察 `Cubit` 的所有变化。 然后我们可以与 `Cubit` 交互并观察输出到控制台的所有更改。 上面的示例将会输出: :::note `Change` 发生在 `Cubit` 状态更新之前。`Change` 包括 `currentState` 和 `nextState`。 ::: #### BlocObserver 使用 bloc 库的一个额外好处是我们可以在一个位置访问所有的 `Changes`。尽管在这个应用里我们只有一个 `Cubit`,但是在大型应用程序中使用多个 `Cubits` 来管理应用程序状态的不同部分是相当常见的。 如果我们想对所有的 `Changes` 做出一些反应,仅需创建我们自己的 `BlocObserver` 即可。 :::note 我们要做的仅仅是扩展 `BlocObserver` 并且重写 `onChange` 方法。 ::: 要使用 `SimpleBlocObserver`,我们只需要对 `main` 函数做少许的变更: 上面的代码将会输出: :::note 内部重写的 `onChange` 先会被调用,它再调用 `super.onChange` 通知 `BlocObserver` 的 `onChange`。 ::: :::tip 在 `BlocObserver` 中,除了 `Change` 本身之外,我们还可以访问 `Cubit` 实例。 ::: ### Cubit 的错误处理 每一个 `Cubit` 都有一个 `addError` 方法,可以用于指示发生了错误。 :::note 可以在 `Cubit` 中覆盖 `onError` 来处理特定 `Cubit` 的所有错误。 ::: 也可以在 `BlocObserver` 中重写 `onError`,以全局处理所有报告的错误。 如果我们重新运行这个程序,我们会看到下面的输出: ## Bloc 相对于函数来说,`Bloc` 是一个依赖 `事件` 触发 `状态` 变更的更高级的类。`Bloc` 同样扩展了 `BlocBase`,这意味着它和 `Cubit` 一样拥有类似的公共 API,`Blocs` 不是调用 `Bloc` 上的 `函数` 并直接发出新的 `状态`,而是接收 `事件` 并将传入的 `事件` 转换为传出的 `状态`。 ![Bloc 架构](~/assets/concepts/bloc_architecture_full.png) ### 创建一个 Bloc 创建 `Bloc` 跟创建 `Cubit` 类似,不过除了定义我们要管理的状态以外,我们还必须定义 `Bloc` 能够处理的事件。 事件是 Bloc 的输入。通常情况下这些事件用于响应用户的交互,类似按下按钮或者页面加载的生命周期事件等等。 和创建 `CounterCubit` 一样,我们必须通过基类的 `super` 来传入一个初始状态。 ### Bloc 状态变更 跟 `Cubit` 里的函数相反,`Bloc` 必须通过 `on` API注册事件处理程序。事件处理程序负责将任何传入事件转换为零个或多个传出状态。 :::tip `EventHandler` 可以访问添加的事件以及 `Emitter`,`Emitter` 可以对输入的事件发出零个或者多个状态。 ::: 然后我们可以更新 `EventHandler` 来处理 `CounterIncrementPressed` 事件: 在上面的代码片中,我们注册了一个 `EventHandler` 来管理所有的 `CounterIncrementPressed`。针对每个输入的 `CounterIncrementPressed` 事件我们都可以通过 `状态` 的 getter 来访问当前的状态并且 `emit(state + 1)`。 :::note 因为 `Bloc` 扩展了 `BlocBase`,所以我们可以像在 `Cubit` 中一样通过 `状态` 的 getter 随时访问 bloc 的当前状态。 ::: :::caution Blocs 任何时候都不应该 `emit` 新的状态。相反,每次状态变更都必须在 `EventHandler` 里响应并输出。 ::: :::caution blocs 和 cubits 都会忽略重复的状态。如果 `state == nextState` 时我们发出 `State nextState` ,不会有状态变更发生。 ::: ### 使用 Bloc 至此,我们可以创建一个我们的 `CounterBloc` 实例并使用它了! #### 基本用法 在上面的代码片段中,我们先创建了一个 `CounterBloc`。然后我们打印了 `Bloc` 的当前状态(因为还没有新的状态发出)。接下来我们添加了一个 `CounterIncrementPressed` 事件来出发状态变更。最后,我们再次打印了 `Bloc` 的状态,从 `0` 变成了 `1`,并且在 `Bloc` 上调用 `close` 关闭了内部的状态流。 :::note `await Future.delayed(Duration.zero)` 是用来确保我们等待下一个事件循环周期(以便 `EventHandler` 处理这个事件)。 ::: #### Stream的用法 和 `Cubit` 一样,`Bloc` 是一个特殊的 `Stream` 类型,这意味着我们也可以订阅 `Bloc` 以实时更新其状态: 在上面的代码片中,我们订阅了 `CounterBloc` 并且在每次状态变更时进行打印。然后我们添加了 `CounterIncrementPressed` 事件触发 `on` 这个 `EventHandler` 并且发出新的状态。最后,当我们不想再接受更新时,我们在这个订阅上调用了 `cancel` 并且 `close` 了这个 `Bloc`。 :::note 上面示例里的 await Future.delayed(Duration.zero)` 仅用于防止订阅被立即取消。 ::: ### 观察 Bloc 由于 `Bloc` 扩展了 `BlocBase`,我们可以用 `onChange` 观察 `Bloc` 的所有状态变更。 然后我们可以更新 `main.dart` 如下: 现在如果我们运行上面的代码片段,输出将会是: `Bloc` 和 `Cubit` 之间的一个关键区别因素是,由于 `Bloc` 是事件驱动的,我们还能够捕获有关触发状态变化的信息。 我们可以通过重写 `onTransition` 来实现。 从一个状态变成另一个状态称为 `过渡`。一个 `过渡` 包含了当前状态,触发事件以及下一个状态。 如果我们重新运行之前相同的 `main.dart` 代码片段,我们应该看到以下输出: :::note `onTransition` 在 `onChange` 之前被调用并且包含了触发 `currentState` 到 `nextState`的事件。 ::: #### BlocObserver 综前所述,我们可以在一个自定义的 `BlocObserver` 里重写 `onTransition` 以实现在一个位置观察所有的过渡。 我们可以像前面一样初始化 `SimpleBlocObserver`: 现在如果我们重新运行上面的代码片段,输出应该如下: :::note `onTransition` 会被先调用(先本地再全局),然后 `onChange` 被调用。 ::: 另一个 `Bloc` 实例的特有功能是:它允许我们重写 `onEvent` 方法,无论什么时候有新的事件被添加到 `Bloc`,这个方法都会被调用。和 `onChange` 和 `onTransition` 方法一样,`onEvent` 也可以在本地或者全局被重写。 我们可以像前面一样运行同样的 `main.dart` 而且应该能看到如下输出: :::note 当事件被添加时,`onEvent` 会被立即调用。本地的 `onEvent` 会在 `BlocObserver` 的全局 `onEvent` 之前被调用。 ::: ### Bloc 的错误处理 跟 `Cubit` 一样,每个 `Bloc` 都有 `addError` 和 `onError` 方法。我们可以在 `Bloc` 里的任何地方调用 `addError` 来指示发生了错误。跟 `Cubit` 里 `onError` 方法一样,我们可以重写它来响应所有发生的错误。 如果我们重新运行之前的 `main.dart`,我们可以看到当错误发生时,输出时下面这样: :::note 本地的 `onError` 方法会先被调用,然后时 `BlocObserver` 里的全局 `onError` 方法。 ::: :::note `Bloc` 和 `Cubit` 实例里的 `onError` 和 `onChange` 工作方式完全相同。 ::: :::caution `EventHandler` 里未被处理的异常同样触发 `onError`。 ::: ## Cubit 和 Bloc 对比 现在我们了解了 `Cubit` 和 `Bloc` 类的基本信息,你可能会想:什么时候应该用 `Cubit`,什么时候则应该用 `Bloc`呢? ### Cubit 的优势 #### 简单 `Cubit` 最大优势之一是简单。当创建 `Cubit` 时,我们只需要定义状态以及公开改变状态的函数。作为对比,当我们创建 `Bloc` 时,我们要定义状态,事件以及 `EventHandler` 实现。这样来看,`Cubit` 更易于理解并且需要写更少的代码。 现在咱们来看看两个计数器的实现: ##### CounterCubit ##### CounterBloc `Cubit` 的实现更加简洁,跟单独定义事件相比,函数更像事件。此外,使用 `Cubit` 时,我们可以简单的从任何地方调用 `emit` 来触发状态变更。 ### Bloc 的优势 #### 可追溯性 使用 `Bloc` 的最大优势之一是了解状态变化的顺序以及触发这些变化的确切原因。处理对应用程序功能至关重要的状态,使用更事件驱动的方法来捕获除状态变化之外的所有事件可能会非常有用。 一个常见的用例就是管理 `AuthenticationState`。为了简化,我们用 `enum` 来表示 `AuthenticationState` : 应用程序的状态从 `authenticated` 到 `unauthenticated` 的变更可能有很多种原因。比如:用户可能点击了登出来注销。再比如,用户的 access token 被收回了并且他们被强制注销了。使用 `Bloc` 时,我们可以清楚的追溯应用的状态是如何变更为特定状态的。 上面的 `过渡 ` 提供了让我们理解状态变更的所有信息。如果我们用 `Cubit` 来管理 `AuthenticationState`,我们的日志则如下: 这告诉我们用户已登出,但没有解释为什么,这使得调试和理解应用程序状态随时间的变化变得异常困难。 #### 高级事件转换 `Bloc` 优于 `Cubit` 的另一个领域是当我们需要利用响应式操作符(例如 `buffer`、`debounceTime`、`throttle` 等)时。 :::tip 查看 [`package:stream_transform`](https://pub.dev/packages/stream_transform) 和 [`package:rxdart`](https://pub.dev/packages/rxdart) 来了解 `Stream` 转换的细节。 ::: `Bloc` 有一个 event 池允许我们控制和转换输入的事件。 例如,如果我们要构建一个实时搜索,我们可能想要实现后端请求的去抖动来避免速率限制抑或是降低后端的成本/负载。 使用 `Bloc` 的话我们可以提供一个自定义的 `EventTransformer` 来改变 `Bloc` 对输入事件的处理。 通过上面的代码,我们只要添加一点点代码就可以很容易的实现对输入事件的去抖动。 :::tip 查看 [`package:bloc_concurrency`](https://pub.dev/packages/bloc_concurrency) 了解一组预定义的事件转换器。 ::: 如果你不确定应该用哪一种,先用 `Cubit`,后面根据需要你可以再重构或者升级为 `Bloc`。 ================================================ FILE: docs/src/content/docs/zh-cn/flutter-bloc-concepts.mdx ================================================ --- title: Flutter Bloc 核心概念 description: package:flutter_bloc 的核心概念概览 sidebar: order: 2 --- import BlocBuilderSnippet from '~/components/concepts/flutter-bloc/BlocBuilderSnippet.astro'; import BlocBuilderExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocBuilderExplicitBlocSnippet.astro'; import BlocBuilderConditionSnippet from '~/components/concepts/flutter-bloc/BlocBuilderConditionSnippet.astro'; import BlocSelectorSnippet from '~/components/concepts/flutter-bloc/BlocSelectorSnippet.astro'; import BlocProviderSnippet from '~/components/concepts/flutter-bloc/BlocProviderSnippet.astro'; import BlocProviderEagerSnippet from '~/components/concepts/flutter-bloc/BlocProviderEagerSnippet.astro'; import BlocProviderValueSnippet from '~/components/concepts/flutter-bloc/BlocProviderValueSnippet.astro'; import BlocProviderLookupSnippet from '~/components/concepts/flutter-bloc/BlocProviderLookupSnippet.astro'; import NestedBlocProviderSnippet from '~/components/concepts/flutter-bloc/NestedBlocProviderSnippet.astro'; import MultiBlocProviderSnippet from '~/components/concepts/flutter-bloc/MultiBlocProviderSnippet.astro'; import BlocListenerSnippet from '~/components/concepts/flutter-bloc/BlocListenerSnippet.astro'; import BlocListenerExplicitBlocSnippet from '~/components/concepts/flutter-bloc/BlocListenerExplicitBlocSnippet.astro'; import BlocListenerConditionSnippet from '~/components/concepts/flutter-bloc/BlocListenerConditionSnippet.astro'; import NestedBlocListenerSnippet from '~/components/concepts/flutter-bloc/NestedBlocListenerSnippet.astro'; import MultiBlocListenerSnippet from '~/components/concepts/flutter-bloc/MultiBlocListenerSnippet.astro'; import BlocConsumerSnippet from '~/components/concepts/flutter-bloc/BlocConsumerSnippet.astro'; import BlocConsumerConditionSnippet from '~/components/concepts/flutter-bloc/BlocConsumerConditionSnippet.astro'; import RepositoryProviderSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderSnippet.astro'; import RepositoryProviderLookupSnippet from '~/components/concepts/flutter-bloc/RepositoryProviderLookupSnippet.astro'; import NestedRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/NestedRepositoryProviderSnippet.astro'; import MultiRepositoryProviderSnippet from '~/components/concepts/flutter-bloc/MultiRepositoryProviderSnippet.astro'; import CounterBlocSnippet from '~/components/concepts/flutter-bloc/CounterBlocSnippet.astro'; import CounterMainSnippet from '~/components/concepts/flutter-bloc/CounterMainSnippet.astro'; import CounterPageSnippet from '~/components/concepts/flutter-bloc/CounterPageSnippet.astro'; import WeatherRepositorySnippet from '~/components/concepts/flutter-bloc/WeatherRepositorySnippet.astro'; import WeatherMainSnippet from '~/components/concepts/flutter-bloc/WeatherMainSnippet.astro'; import WeatherAppSnippet from '~/components/concepts/flutter-bloc/WeatherAppSnippet.astro'; import WeatherPageSnippet from '~/components/concepts/flutter-bloc/WeatherPageSnippet.astro'; :::note 在开始 [`package:flutter_bloc`](https://pub.dev/packages/flutter_bloc) 请详细阅读以下文档 ::: :::note 所有 `flutter_bloc` 套件导出的Widget都与 Cubit 和 Bloc 实例集成。 ::: ## Bloc 组件 ### BlocBuilder **BlocBuilder** 是一个 Flutter 的 Widget,它需要一个 `Bloc` 和一个 `builder` 函数。`BlocBuilder` 处理于构建响应新状态时构建的 Widget 。`BlocBuilder` 与 `StreamBuilder` 非常相似,但是 `StreamBuilder`具有更简单的 API,以减少所需的样板代码量。`builder` 函数可能会被多次调用,并且必须是一个根据状态返回一个widget的[纯函数](https://en.wikipedia.org/wiki/Pure_function)。 如果你希望在状态变化时执行一些操作,比如导航、显示对话框等...,请查看 `BlocListener` 在BlocBuilder中,如果省略了 `bloc` 参数,则 `BlocBuilder` 会自动通过 `BlocProvider` 和当前的 `BuildContext` 进行查找。 只有在希望提供一个仅作用于单个 widget 且无法透过父级 `BlocProvider` 和当前的 `BuildContext` 访问的 `bloc` 时,才需要明确指定 `bloc`。 在使用 BlocBuilder 时,如果要达到更细緻地控制 widget 的更新,可以使用 `buildWhen` 参数来实现何时调用 `builder` 函数。`buildWhen` 接受前一个 bloc 状态和当前 bloc 状态,并返回一个布尔值。如果 `buildWhen` 返回 true,则 `builder` 函数将被调用并使用当前 `state` 进行widget重建。如果 buildWhen 返回 false,则 `builder` 将不会被使用当前 `state` 调用,且不会进行重建。 ### BlocSelector **BlocSelector** 是一个 Flutter Widget,类似于 BlocBuilder,但允许开发者通过根据当前 bloc 状态选择新值来过滤更新。如果选择的值不变,则防止不必要的构建。选择的值必须是 immutable 的,以便 BlocSelector 可以准确地确定是否应该再次调用 builder。 如果`bloc` 被省略没有传入, `BlocSelector` 将自动使用 `BlocProvider` 和当前的 `BuildContext` 执行查找。 ### BlocProvider **BlocProvider** 是一个 Flutter widget ,透过 `BlocProvider.of(context)` 将一个 bloc 提供给它的children。`BlocProvider` 用作于依赖注入(DI)widget ,以便在子树 (subtree) 中可以提供单个 bloc 实例给多个widget 使用。 通常情况下,应该使用 `BlocProvider` 来创建新的 bloc,这样可以使该 bloc 对于子树中的其他 widget 使用。在这种情况下,由于 `BlocProvider` 负责创建 bloc,它将自动处理关闭该 bloc。 默认情况下,`BlocProvider` 将延迟创建 bloc 实例,这意味着当通过 `BlocProvider.of(context)` 查找该 bloc 时,`create` 方法才会被执行。 为了复盖这种行为并强制立即运行 `create` 方法,可以将 `lazy` 设置为 `false`。 在某些情况下,可以使用 `BlocProvider` 来将一个已存在的 Bloc 提供给 Widget 树中的新部分。这通常发生在需要将一个已存在的 Bloc 提供给新的路由(route)时。在这种情况下,`BlocProvider` 不会自动关闭 Bloc,因为它并非 Bloc 的创建者。 如此,从 `ChildA` 或者 `ScreenA` 中,我们可以获取 `BlocA` ### MultiBlocProvider **MultiBlocProvider** 是一个 Flutter Widget 用来将多个 `BlocProvider` Widgets 合併为一个。 `MultiBlocProvider` 可以提高代码的可读性,消除了需要嵌套多个 `BlocProvider` 的情况。 通过使用了 `MultiBlocProvider` 我们可以从 转为 ### BlocListener **BlocListener** 是一个 Flutter Widget,它接受一个 `BlocWidgetListener` 和一个可选的 `Bloc`,并在 bloc 状态发生变化时调用 `listener`。它应该用于需要每个状态变化仅执行一次的功能,例如导航、显示 `SnackBar`、显示 `Dialog` 等等。 与 `BlocBuilder` 中的 `builder` 不同的是,`listener` 对于每个状态变化(初始状态除外)只会被调用一次,并且是一个 `void` 函数。 如果`bloc` 被省略没有传入, `BlocListener` 将自动使用 `BlocProvider` 和当前的 `BuildContext` 执行查找。 只有在希望提供一个通过 `BlocProvider` 和当前 `BuildContext` 否则无法访问的 bloc 的情况时,才指定 bloc。 要对 `listener` 函数何时调用进行更细緻地控制,可以提供一个可选的 `listenWhen`。`listenWhen` 接受先前的 bloc 状态和当前的 bloc 状态,并返回一个布尔值。如果 `listenWhen` 返回 true,则会用 `state` 调用 `listener`。如果 `listenWhen` 返回 false,则不会用 `state` 调用 `listener`。 ### MultiBlocListener **MultiBlocListener** 是 Flutter 中的一个 Widget,用来将多个 `BlocListener` Widgets 合併为一个。`MultiBlocListener` 可以提高代码的可读性,消除了需要嵌套多个 BlocListener 的情况。 通过使用 `MultiBlocListener`,我们可以从以下代码转变为: 转为 ### BlocConsumer **BlocConsumer** 提供了 `builder` 和 `listener`,以便对新的状态做出反应。`BlocConsumer` 类似于嵌套的 `BlocListener` 和 `BlocBuilder`,但可以减少所需的样板代码量。只有在需要重建 UI 并执行其他对 `bloc` 状态变化做出反应时,才应该使用 `BlocConsumer`。`BlocConsumer` 接受`BlocWidgetBuilder` (必传参数) 和 `BlocWidgetListener` (必传参数) ,以及 `bloc` (可选参数)、`BlocBuilderCondition` (可选参数) 和 `BlocListenerCondition` (可选参数)。 如果`bloc` 被省略没有传入, `BlocConsumer` 将自动使用 `BlocProvider` 和当前的 `BuildContext` 进行查找。 可选参数 `listenWhen` 和 `buildWhen` 可以用来更精细地控制 `listener` 和 `builder` 何时被调用。`listenWhen` 和 `buildWhen` 将在每个 `bloc` 的 `state` 更改时被调用。它们都接受先前的 `state` 和当前的 `state`,且必须返回一个布尔值,用于确定是否调用 `builder` 和/或 `listener` 函数。当 `BlocConsumer` 初始化时,先前的 `state` 将初始化为 `bloc` 的 `state`。由于 `listenWhen` 和 `buildWhen` 是可选的,因此不实现它们时,默认为 `true`。 ### RepositoryProvider **MultiBlocListener** 是 Flutter 中的一个 Widget,透过 `RepositoryProvider.of(context)` 将 repository 提供给其子节点。它被用作依赖注入(DI)小部件,以便在子树中提供一个仓库的单个实例给多个 Widgets。`BlocProvider` 应该用于提供 bloc,而 `RepositoryProvider` 应该只用于提供 repositories。 那么从 `ChildA`,我们可以通过以下代码获取 `Repository` 实例: ### MultiRepositoryProvider **MultiRepositoryProvider** 是一个 Flutter Widget 用来将多个 `RepositoryProvider` Widgets 合併为一个。 `MultiRepositoryProvider` 可以提高代码的可读性,消除了需要嵌套多个 `RepositoryProvider` 的情况。 通过使用了 `MultiRepositoryProvider` 我们可以从 转为 ## BlocProvider 用法 接着来看看如何使用 `BlocProvider` 来提供一个 `CounterBloc` 给一个 `CounterPage` 并且使用`BlocBuilder` 来对状态变化做出反应. 到目前为止,我们成功地将我们的表示层 ( presentational layer ) 与我们的业务逻辑层 (business logic layer) 分开了。注意,`CounterPage` widget 不知道当用户点击按钮时会发生什么。该 widget 只是告诉 `CounterBloc` 用户按下了增加或减少按钮。 ## RepositoryProvider 用法 接下来,我们将利用 flutter_weather 范例,来查看如何使用 RepositoryProvider。 由于应用程式明确依赖于 `WeatherRepository`,我们通过建构函式注入一个实例。这使我们能够根据构建版本或环境注入不同的 `WeatherRepository` 实例。 由于在这个例子中,我们的应用程序只有一个 repository,我们将通过 `RepositoryProvider.value` 将其注入到我们的 widget tree 中。如果您有多个 repository,则可以使用 `MultiRepositoryProvider` 将多个 repository 实例提供给 subtree。 在大多数情况下,应用程序的顶层 widgets 将透过 `RepositoryProvider` 向 subtree 公开一个或多个repository。 在实例化一个 bloc 时,我们可以通过 `context.read` 访问存储库的实例,并通过构造函数将存储库注入到 bloc 中。 [flutter_weather_link]: https://github.com/felangel/bloc/blob/master/examples/flutter_weather ## 扩展方法 [ Extension methods](https://dart.dev/guides/language/extension-methods),在 Dart 2.7 中引入,是一种向现有库添加功能的方法。在本节中,我们将看一下 `package:flutter_bloc` 中包含的扩展方法以及它们的使用方式。 `flutter_bloc` 依赖于 [package:provider](https://pub.dev/packages/provider),它简化了对 [`InheritedWidget`](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html) 的使用。 在内部,`package:flutter_bloc` 使用 `package:provider` 实现了 `BlocProvider`、`MultiBlocProvider`、`RepositoryProvider` 和 `MultiRepositoryProvider` widgets。 `package:flutter_bloc` 从 `package:provider` 中导出了 `ReadContext`、`WatchContext` 和 `SelectContext` 扩展。 :::note 查询更多关于 [`package:provider`](https://pub.dev/packages/provider). ::: ### context.read `context.read()` 查找最接近的类型为 T 的祖先实例,并在功能上等同于 `BlocProvider.of(context)`。`context.read` 最常用于在 `onPressed` 回调中查找 bloc 实例以添加事件。 :::note `context.read()` 不会监听 `T` —— 如果提供的类型为 `T` 的对象发生变化,`context.read` 不会触发 widget重建。 ::: #### Usage ✅ **建议** 在回调函数中使用`context.read` 方法来添加事件。 ```dart onPressed() { context.read().add(CounterIncrementPressed()), } ``` ❌ **避免** 在 `build` 方法当中使用`context.read` 查找状态 ```dart @override Widget build(BuildContext context) { final state = context.read().state; return Text('$state'); } ``` 以上的使用可能会导致UI 不会反应最新的状态变化的错误,因为即使状态改变了 `Text` widget 并不会被重新建立(rebuild),。 :::caution 使用 `BlocBuilder` 或者 `context.watch` 以确保当状态改变时,UI 可以被正确地重新构建。 ::: ### context.watch 如同 `context.watch()` , `context.watch()` 提供最接近祖先类型为 `T` 的实例,并且同时还会监听该实例的变化。它的功能等同于 `BlocProvider.of(context, listen: true)`。 如果提供类型 T 的对象发生变化,context.watch 将会触发重新构建(rebuild)。 :::caution `context.watch` 只能在 `StatelessWidget` 或 `State` 类的 build 方法内部被访问。 ::: #### Usage ✅ **建议** 使用 `BlocBuilder` 而不是 `context.watch` 来明确地范围化重新构建。 ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocBuilder( builder: (context, state) { // 当状态变化时,只会重新构建 Text。 return Text(state.value); }, ), ), ); } ``` 作为选择,建议使用 `Builder`来范围化(或限定)重新构建的范围。 ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // 当状态变化时,只会重新构建 Text。 final state = context.watch().state; return Text(state.value); }, ), ), ); } ``` ✅ **建议** 将 `Builder` 和 `context.watch` 一起使用,类似于 `MultiBlocBuilder` 的效果. ```dart Builder( builder: (context) { final stateA = context.watch().state; final stateB = context.watch().state; final stateC = context.watch().state; // 返回一个依赖于 BlocA 、BlocB, 和 BlocC 状态的 Widget。 } ); ``` ❌ **避免** 使用 `context.watch` 当父 Widget 的 build 方法不依赖于状态 ```dart @override Widget build(BuildContext context) { // 无论状态如何变化,都会重新构建 MaterialApp, // 即使它只用于 Text Widget 中。 final state = context.watch().state; return MaterialApp( home: Scaffold( body: Text(state.value), ), ); } ``` :::caution 如果在 `build` 方法的根部使用 `context.watch` ,当 bloc 的状态发生变化时,整个 Widget 都将被重新构建。 ::: ### context.select 如同 `context.watch()` , `context.select(R function(T value))` 提供最接近祖先类型为 `T` 的实例 且同时监听该实例的变化。不同于 `context.watch` 的是,`context.select` 允许你监听状态对象中的特定部分 ```dart Widget build(BuildContext context) { final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); } ``` 以上实例将在属性 `ProfileBloc` 内中的 `name` 属性变化时重新构建 Widget。 #### Usage ✅ **建议** 使用 BlocSelector 而不是 context.select,以明确地限定重新构建的范围。 ```dart Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocSelector( selector: (state) => state.name, builder: (context, name) { // 当 state.name 变化时,只有 Text 会重新构建。 return Text(name); }, ), ), ); } ``` 作为选择,建议使用 `Builder`来范围化(或限定)重新构建的范围。 ```dart @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Builder( builder: (context) { // 当 state.name 变化时,只有 Text 会重新构建。 final name = context.select((ProfileBloc bloc) => bloc.state.name); return Text(name); }, ), ), ); } ``` ❌ **避免** 在父 Widget 的 build 方法中使用 context.select,但父 Widget 不依赖于状态。 ```dart @override Widget build(BuildContext context) { // 当 state.value 变化时,即使它只在 Text widget 中使用,也会重新构建 MaterialApp。 final name = context.select((ProfileBloc bloc) => bloc.state.name); return MaterialApp( home: Scaffold( body: Text(name), ), ); } ``` :::caution 在 `build` 方法的根部使用 `context.select` 将导致在选择范围发生变化时重新构建整个 Widget。 ::: ================================================ FILE: docs/src/content/docs/zh-cn/getting-started.mdx ================================================ --- title: 快速入门 description: 用 Bloc 开始构建的前提 --- import InstallationTabs from '~/components/getting-started/InstallationTabs.astro'; import ImportTabs from '~/components/getting-started/ImportTabs.astro'; ## Bloc 包 Bloc 生态包括了以下几个包: | Package | Description | Link | | ------------------------------------------------------------------------------------------ | ------------------ | -------------------------------------------------------------------------------------------------------------- | | [angular_bloc](https://github.com/felangel/bloc/tree/master/packages/angular_bloc) | AngularDart 组件 | [![pub package](https://img.shields.io/pub/v/angular_bloc.svg)](https://pub.dev/packages/angular_bloc) | | [bloc](https://github.com/felangel/bloc/tree/master/packages/bloc) | 核心 Dart API | [![pub package](https://img.shields.io/pub/v/bloc.svg)](https://pub.dev/packages/bloc) | | [bloc_concurrency](https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency) | 事件转换器 | [![pub package](https://img.shields.io/pub/v/bloc_concurrency.svg)](https://pub.dev/packages/bloc_concurrency) | | [bloc_lint](https://github.com/felangel/bloc/tree/master/packages/bloc_lint) | Custom Linter | [![pub package](https://img.shields.io/pub/v/bloc_lint.svg)](https://pub.dev/packages/bloc_lint) | | [bloc_test](https://github.com/felangel/bloc/tree/master/packages/bloc_test) | 测试 API | [![pub package](https://img.shields.io/pub/v/bloc_test.svg)](https://pub.dev/packages/bloc_test) | | [bloc_tools](https://github.com/felangel/bloc/tree/master/packages/bloc_tools) | Command-line Tools | [![pub package](https://img.shields.io/pub/v/bloc_tools.svg)](https://pub.dev/packages/bloc_tools) | | [flutter_bloc](https://github.com/felangel/bloc/tree/master/packages/flutter_bloc) | Flutter 组件 | [![pub package](https://img.shields.io/pub/v/flutter_bloc.svg)](https://pub.dev/packages/flutter_bloc) | | [hydrated_bloc](https://github.com/felangel/bloc/tree/master/packages/hydrated_bloc) | 缓存/持久化 支持库 | [![pub package](https://img.shields.io/pub/v/hydrated_bloc.svg)](https://pub.dev/packages/hydrated_bloc) | | [replay_bloc](https://github.com/felangel/bloc/tree/master/packages/replay_bloc) | 撤销/重做 支持库 | [![pub package](https://img.shields.io/pub/v/replay_bloc.svg)](https://pub.dev/packages/replay_bloc) | ## 安装 :::note 在开始使用 bloc 之前请确保你在机器上已经安装了 [Dart SDK](https://dart.dev/get-dart)。 ::: ## 导入 现在我们已经成功安装了 bloc,我们可以创建 `main.dart` 并且导入对应的 `bloc` 包了。 ================================================ FILE: docs/src/content/docs/zh-cn/index.mdx ================================================ --- template: splash title: Bloc State Management Library description: Official documentation for the bloc state management library. Support for Dart, Flutter, and AngularDart. Includes examples and tutorials. banner: content: | ✨ 访问 Bloc 商店 ✨ editUrl: false lastUpdated: false hero: title: Bloc v9.2.0 tagline: 基于 Dart 的可预测的状态管理库。 image: alt: Bloc logo file: ~/assets/bloc.svg actions: - text: 快速入门 link: /zh-cn/getting-started/ variant: primary icon: rocket - text: GitHub link: https://github.com/felangel/bloc icon: github variant: secondary --- import { CardGrid } from '@astrojs/starlight/components'; import SponsorsGrid from '~/components/landing/SponsorsGrid.astro'; import Card from '~/components/landing/Card.astro'; import ListCard from '~/components/landing/ListCard.astro'; import SplitCard from '~/components/landing/SplitCard.astro'; import Discord from '~/components/landing/Discord.astro';
```sh # 将bloc添加到你的项目。 dart pub add bloc ``` 我们的 [快速入门](/zh-cn/getting-started) 提供了如何在几分钟内开始使用 Bloc 的详细指引。 完成 [官方向导](/zh-cn/tutorials/flutter-counter) 来了解最佳实践以及构建基于 Bloc 的各种不同应用。 浏览优质并充分测试过的 [示例应用](https://github.com/felangel/bloc/tree/master/examples) 例如 计数器,定时器,无限列表,天气,待办事项以及更多其他示例! - [为什么用 Bloc?](/zh-cn/why-bloc) - [核心概念](/zh-cn/bloc-concepts) - [架构](/zh-cn/architecture) - [测试](/zh-cn/testing) - [命名约定](/zh-cn/naming-conventions) - [FAQs](/zh-cn/faqs) - [VSCode 集成](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) - [IntelliJ 集成](https://plugins.jetbrains.com/plugin/12129-bloc) - [Neovim 集成](https://github.com/wa11breaker/flutter-bloc.nvim) - [Mason CLI 集成](https://github.com/felangel/bloc/blob/master/bricks/README.md) - [自定义模板](https://brickhub.dev/search?q=bloc) - [开发者工具](https://github.com/felangel/bloc/blob/master/packages/bloc_tools/README.md) ================================================ FILE: docs/src/content/docs/zh-cn/tutorials/flutter-counter.mdx ================================================ --- title: Flutter 计数器 description: 关于如何使用 bloc 构建 Flutter 计数器应用程序的深入指南。 sidebar: order: 1 --- import RemoteCode from '~/components/code/RemoteCode.astro'; import FlutterCreateSnippet from '~/components/tutorials/flutter-counter/FlutterCreateSnippet.astro'; import FlutterPubGetSnippet from '~/components/tutorials/FlutterPubGetSnippet.astro'; ![初级](https://img.shields.io/badge/level-beginner-green.svg) 在下面的教程中,我们将使用 Bloc 库在 Flutter 中构建一个计数器。 ![demo](~/assets/tutorials/flutter-counter.gif) ## 关键主题 - 使用 [BlocObserver](/zh-cn/bloc-concepts#blocobserver) 观察状态变更。 - [BlocProvider](/zh-cn/flutter-bloc-concepts#blocprovider),为子组件提供 bloc 的 Flutter 部件。 - [BlocBuilder](/zh-cn/flutter-bloc-concepts#blocbuilder),用于响应新状态来构建小部件的 Flutter 部件。 - 用 Cubit 替代 Bloc。[有什么不同?](/zh-cn/bloc-concepts#cubit-和-bloc-对比)。 - 使用 [context.read](/zh-cn/flutter-bloc-concepts#contextread) 添加事件。 ## 设置 我们从创建一个全新的 Flutter 项目开始 用下面的代码替换 `pubspec.yaml` 的内容: 然后安装所有的依赖 ## 项目结构 ``` ├── lib │ ├── app.dart │ ├── counter │ │ ├── counter.dart │ │ ├── cubit │ │ │ └── counter_cubit.dart │ │ └── view │ │ ├── counter_page.dart │ │ ├── counter_view.dart │ │ └── view.dart │ ├── counter_observer.dart │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ``` 这个应用采用了功能驱动的目录结构。这种项目结构以便于我们按照独立的功能对项目进行扩展。这个示例项目里只有一个功能(计数器),但是在更加复杂的应用里我们可以包含数以百计的功能。 ## BlocObserver 我们要做的第一件事是看看如何创建一个 `BlocObserver` 来观察应用里所有的状态变更。 我们先创建一个 `lib/counter_observer.dart`: 目前,我们仅重写了 `onChange` 来查看所有发生的状态变更。 :::note `Bloc` 和 `Cubit` 的 `onChange` 工作方式是相同的。 ::: ## main.dart 下一步,我们替换 `lib/main.dart` 的代码如下: 我们初始化了我们创建的 `CounterObserver` 并且在 `runApp` 里添加了 `CounterApp` 组件。 ## Counter App 创建一个 `lib/app.dart`: `CounterApp` 是一个 `MaterialApp` 并且指定了 `CounterPage` 作为主页。 :::note 我们扩展了 `MaterialApp` 因为 `CounterApp` _是_ 一个 `MaterialApp`。在大多数情况下,我们会创建 `StatelessWidget` 或者 `StatefulWidget` 实例并且在 `build` 里进行组合。但是在这里没有部件需要组合,所以最简单的方式就是直接扩展 `MaterialApp`。 ::: 接下来咱们看看 `CounterPage` ! ## Counter Page 创建 `lib/counter/view/counter_page.dart` 如下: `CounterPage` 部件负责创建 `CounterCubit` (下面会讲到)并且提供给 `CounterView`。 :::note 将 `Cubit` 的创建和消费分开并解耦非常重要,这样可以让代码更加的易于测试和重用。 ::: ## Counter Cubit 创建 `lib/counter/cubit/counter_cubit.dart` 如下: `CounterCubit` 会公开以下方法: - `increment`: 当前状态 +1 - `decrement`: 当前状态 -1 `CounterCubit` 管理的是 `int` 型的状态,初始值为 `0`。 :::tip 使用 [VSCode 插件](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) 或者 [IntelliJ 插件](https://plugins.jetbrains.com/plugin/12129-bloc) 可以自动创建新的 cubits。::: 接下来,我们看看负责消费并且与 `CounterCubit` 进行交互的 `CounterView`。 ## Counter View 创建 `lib/counter/view/counter_view.dart` 如下: `CounterView` 负责渲染当前的数值,以及两个 FloatingActionButtons 来负责增/减计数。 `BlocBuilder` 用来包装 `Text` 部件,这样 `CounterCubit` 任何的状态变化都会更新文本。此外, `context.read()` 用于查找最近的 `CounterCubit` 实例。 :::note 只有 `Text` 部件被包装在 `BlocBuilder` 里,因为它是 `CounterCubit` 状态变更时需要更新的唯一部件。避免包装不需要根据状态变更重新构建的部件。 ::: ## Barrel 创建 `lib/counter/view/view.dart`: 添加 `view.dart` 来导出所有 counter 视图的公共部分。 创建 `lib/counter/counter.dart`: 添加 `counter.dart` 来导出 counter 功能的所有公共部分。 以上就是全部!我们将业务逻辑层从展现层种分离了出来。 `CounterView` 不知道用户按下按钮以后会发生什么;它只是通知 `CounterCubit`。此外,`CounterCubit` 也不知道状态(计数器的值)发生了什么;它只是响应调用的方法发出新状态。 我们可以运行 `flutter run` 来在设备或者模拟器上查看运行的效果。 可以在 [这里](https://github.com/felangel/Bloc/tree/master/examples/flutter_counter) 找到这个例子的完整代码(包括单元和部件测试)。 ================================================ FILE: docs/src/content/docs/zh-cn/why-bloc.mdx ================================================ --- title: 为什么用 Bloc? description: 关于 Bloc 如何成为杰出的状态管理解决方案的概述。 sidebar: order: 1 --- Bloc 让展现与业务逻辑分离变得更加容易,使你的代码 _更快_, _易于测试_, 并且 _易于复用_。 当构建产品级别应用时,管理状态变得至关重要。 作为开发者,我们想要: - 在任何时间点都能知道我们的应用处于什么状态。 - 方便的测试任何场景以确保我们的应用保持正确的响应。 - 记录我们程序中的每个用户交互,以便我们做出基于数据驱动的决策。 - 尽可能高效地工作,并在我们的应用程序和其他应用程序内重复使用组件。 - 让许多开发人员在遵循相同模式和约定的单一代码库内无缝地工作。 - 开发快速且反应灵敏的应用程序。 Bloc 的设计初衷就是满足所有这些需求,甚至更多。 状态管理解决方案有很多,决定使用哪一种可能是一项艰巨的任务。没有一个完美的状态管理解决方案!重要的是,您要选择最适合您的团队和项目的解决方案。 Bloc 的设计秉承了以下三个核心价值观: - **简单**:易于理解,不同技能水平的开发人员都能轻松上手。 - **强大**:通过将应用程序组合成更小的组件,帮助创建令人惊叹的复杂应用程序。 - **易于测试**:轻松测试应用程序的各个方面,以便我们可以放心地进行迭代。 总体而言,Bloc 尝试通过规范状态变更发生的时间以及在整个应用程序中强制采用单一方式来改变状态,从而使状态变更变得可预测。 ================================================ FILE: docs/src/env.d.ts ================================================ /// /// ================================================ FILE: docs/src/styles/landing.css ================================================ :root { --green-hsl: 200, 60%, 60%; --overlay-green: hsla(var(--green-hsl), 0.5); } [data-has-hero] .page { background: linear-gradient( to right, #c3fcfc, #c6fbf6, #cafaf0, #cef9eb, #d2f7e6, #d1f6e1, #d1f5dc, #d1f4d7, #ccf3d0, #c8f2c9, #c4f1c1, #c1f0b9 ); } [data-theme='dark'][data-has-hero] .page { background: linear-gradient(215deg, var(--overlay-green), transparent 40%), radial-gradient(var(--overlay-green), transparent 65%) no-repeat 50% calc(100% + 20rem) / 60rem 30rem; } [data-has-hero] header { border-bottom: 1px solid transparent; background-color: transparent; -webkit-backdrop-filter: blur(16px); backdrop-filter: blur(16px); } [data-has-hero] .card { border-radius: 25px; background-color: transparent; border: none; } @media screen and (min-width: 50rem) { [data-has-hero] .hero { padding-block: clamp(2.5rem, calc(1rem + 10vmin), 2.5rem); } [data-has-hero] .hero > img { filter: drop-shadow(0 0 5rem #3982bc); } [data-theme='dark'][data-has-hero] .hero > img { filter: drop-shadow(0 0 5rem #81d9ef); } } /* Secondary Button Styles */ .sl-link-button.secondary { border: 1px solid var(--sl-color-white) !important; } ================================================ FILE: docs/src/tailwind.css ================================================ @layer base, starlight, theme, components, utilities; @import '@astrojs/starlight-tailwind'; @import 'tailwindcss/theme.css' layer(theme); @import 'tailwindcss/utilities.css' layer(utilities); @import '@fontsource-variable/figtree'; @theme { --font-sans: 'Figtree Variable', sans-serif; --color-accent-200: #a8d3c9; --color-accent-600: #007d6d; --color-accent-900: #003c33; --color-accent-950: #002b25; --color-gray-100: #f5f6f8; --color-gray-200: #eceef2; --color-gray-300: #c0c2c7; --color-gray-400: #888b96; --color-gray-500: #545861; --color-gray-700: #353841; --color-gray-800: #24272f; --color-gray-900: #17181c; } .badges { margin-bottom: 1.5em; display: flex; flex-direction: row; gap: 0.5em; } .warning .highlight { background: none; } .info .highlight { background: none; } .warning .mark .code { border-left-color: #bda036; } .warning .highlight > .code > span:not(.indent) { border-bottom: 2px #bda036 dashed; padding: 0; } .info .mark .code { border-left-color: #4c81d7; } .info .highlight > .code > span:not(.indent) { border-bottom: 2px #4c81d7 dashed; padding: 0; } ================================================ FILE: docs/tsconfig.json ================================================ { "extends": "astro/tsconfigs/strict", "compilerOptions": { "baseUrl": ".", "paths": { "~/*": ["src/*"] } } } ================================================ FILE: examples/angular_counter/.gitignore ================================================ # Files and directories created by pub .dart_tool/ .packages # Remove the following pattern if you wish to check in your lock file pubspec.lock # Conventional directory for build outputs build/ # Directory created by dartdoc doc/api/ ================================================ FILE: examples/angular_counter/README.md ================================================ [![build](https://github.com/felangel/bloc/actions/workflows/main.yaml/badge.svg)](https://github.com/felangel/bloc/actions) # example A web app that uses [AngularDart](https://angulardart.dev/). Created from templates made available by Stagehand under a BSD-style [license](https://github.com/dart-lang/stagehand/blob/master/LICENSE). ================================================ FILE: examples/angular_counter/analysis_options.yaml ================================================ include: ../../analysis_options.yaml analyzer: exclude: [build/**] errors: uri_has_not_been_generated: ignore ================================================ FILE: examples/angular_counter/lib/app_component.css ================================================ :host { /* This is equivalent of the 'body' selector of a page. */ } ================================================ FILE: examples/angular_counter/lib/app_component.dart ================================================ import 'package:angular_counter/src/counter_page/counter_page_component.dart'; import 'package:ngdart/angular.dart'; /// Top level application component. @Component( selector: 'my-app', templateUrl: 'app_component.html', directives: [CounterPageComponent], ) class AppComponent {} ================================================ FILE: examples/angular_counter/lib/app_component.html ================================================ ================================================ FILE: examples/angular_counter/lib/src/counter_page/counter_bloc.dart ================================================ import 'package:bloc/bloc.dart'; /// Base counter event. sealed class CounterEvent {} /// Notifies bloc to increment state. final class CounterIncrementPressed extends CounterEvent {} /// Notifies bloc to decrement state. final class CounterDecrementPressed extends CounterEvent {} /// {@template counter_bloc} /// A simple [Bloc] that manages an `int` as its state. /// {@endtemplate} class CounterBloc extends Bloc { /// {@macro counter_bloc} CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); on((event, emit) => emit(state - 1)); } } ================================================ FILE: examples/angular_counter/lib/src/counter_page/counter_page_component.css ================================================ .counter-page-container { text-align: center; } .counter-button { background: lightskyblue; color: black; padding: 24px; border-radius: 50%; font-size: 24px; } ================================================ FILE: examples/angular_counter/lib/src/counter_page/counter_page_component.dart ================================================ import 'package:angular_bloc/angular_bloc.dart'; import 'package:angular_counter/src/counter_page/counter_bloc.dart'; import 'package:ngdart/angular.dart'; /// {@template counter_page} /// Counter page component which renders a counter /// and allows users to increment/decrement the counter. /// {@endtemplate} @Component( selector: 'counter-page', templateUrl: 'counter_page_component.html', styleUrls: ['counter_page_component.css'], providers: [ClassProvider(CounterBloc)], pipes: [BlocPipe], ) class CounterPageComponent implements OnDestroy { /// {@macro counter_page} const CounterPageComponent(this.counterBloc); /// The associated [CounterBloc] which manages the count. final CounterBloc counterBloc; @override void ngOnDestroy() { counterBloc.close(); } /// Increment the count. void increment() => counterBloc.add(CounterIncrementPressed()); /// Decrement the count. void decrement() => counterBloc.add(CounterDecrementPressed()); } ================================================ FILE: examples/angular_counter/lib/src/counter_page/counter_page_component.html ================================================

Counter App

Current Count: {{ $pipe.bloc(counterBloc) }}

================================================ FILE: examples/angular_counter/pubspec.yaml ================================================ name: angular_counter description: A web app that uses angular_bloc environment: sdk: ">=3.10.0 <4.0.0" dependencies: angular_bloc: ^10.0.0-dev.5 bloc: ^9.0.0 ngdart: ^8.0.0-dev.4 dev_dependencies: build_daemon: ^4.0.0 build_runner: ^2.0.0 build_web_compilers: ^4.0.0 ================================================ FILE: examples/angular_counter/pubspec_overrides.yaml ================================================ dependency_overrides: angular_bloc: path: ../../packages/angular_bloc build_modules: ^5.0.0 build_web_compilers: ^4.0.0 ================================================ FILE: examples/angular_counter/web/index.html ================================================ example Loading... ================================================ FILE: examples/angular_counter/web/main.dart ================================================ // ignore_for_file: avoid_print import 'package:angular_counter/app_component.template.dart' as ng; import 'package:bloc/bloc.dart'; import 'package:ngdart/angular.dart'; class SimpleBlocObserver extends BlocObserver { const SimpleBlocObserver(); @override void onEvent(Bloc bloc, Object? event) { print(event); super.onEvent(bloc, event); } @override void onTransition( Bloc bloc, Transition transition, ) { print(transition); super.onTransition(bloc, transition); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { print(error); super.onError(bloc, error, stackTrace); } } void main() { Bloc.observer = const SimpleBlocObserver(); runApp(ng.AppComponentNgFactory); } ================================================ FILE: examples/angular_counter/web/styles.css ================================================ @import url(https://fonts.googleapis.com/css?family=Roboto); @import url(https://fonts.googleapis.com/css?family=Material+Icons); body { max-width: 600px; margin: 0 auto; padding: 5vw; } * { font-family: Roboto, Helvetica, Arial, sans-serif; } ================================================ FILE: examples/bloc_concurrency_visualizer/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: examples/bloc_concurrency_visualizer/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "edada7c56edf4a183c1735310e123c7f923584f1" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: edada7c56edf4a183c1735310e123c7f923584f1 base_revision: edada7c56edf4a183c1735310e123c7f923584f1 - platform: android create_revision: edada7c56edf4a183c1735310e123c7f923584f1 base_revision: edada7c56edf4a183c1735310e123c7f923584f1 - platform: ios create_revision: edada7c56edf4a183c1735310e123c7f923584f1 base_revision: edada7c56edf4a183c1735310e123c7f923584f1 - platform: linux create_revision: edada7c56edf4a183c1735310e123c7f923584f1 base_revision: edada7c56edf4a183c1735310e123c7f923584f1 - platform: macos create_revision: edada7c56edf4a183c1735310e123c7f923584f1 base_revision: edada7c56edf4a183c1735310e123c7f923584f1 - platform: web create_revision: edada7c56edf4a183c1735310e123c7f923584f1 base_revision: edada7c56edf4a183c1735310e123c7f923584f1 - platform: windows create_revision: edada7c56edf4a183c1735310e123c7f923584f1 base_revision: edada7c56edf4a183c1735310e123c7f923584f1 # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/bloc_concurrency_visualizer/README.md ================================================ # bloc_concurrency visualizer A Flutter app to help visualize `bloc_concurrency` transformers ![Demo](https://raw.githubusercontent.com/felangel/bloc/master/examples/bloc_concurrency_visualizer/assets/demo.gif) ================================================ FILE: examples/bloc_concurrency_visualizer/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml linter: rules: public_member_api_docs: false ================================================ FILE: examples/bloc_concurrency_visualizer/lib/main.dart ================================================ import 'package:bloc_concurrency_visualizer/timeline/timeline.dart'; import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Bloc Concurrency Visualizer', theme: ThemeData.from( colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal), ), home: const TimelinePage(), ); } } ================================================ FILE: examples/bloc_concurrency_visualizer/lib/timeline/bloc/timeline_bloc.dart ================================================ import 'dart:math'; import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:bloc_concurrency_visualizer/timeline/timeline.dart'; import 'package:equatable/equatable.dart'; part 'timeline_event.dart'; part 'timeline_state.dart'; class TimelineBloc extends Bloc { TimelineBloc({ required Transformer transformer, required double Function() now, }) : _now = now, _transformer = transformer, super(const TimelineState()) { on(_onTaskQueued); on<_TimelineTaskAdded>(_onTaskAdded, transformer: transformer.create()); on((event, emit) => emit(const TimelineState())); on((event, emit) => emit(const TimelineState())); } final Transformer _transformer; final double Function() _now; Future _onTaskQueued( TimelineTaskQueued event, Emitter emit, ) async { final cancel = _transformer.isDroppable && state.tasks.values.any((task) => task.isRunning); final duration = Duration(seconds: Random().nextInt(1) + 1); final now = _now(); final task = cancel ? Task.canceled(start: now, end: now, duration: duration) : Task.queued(start: now, duration: duration); emit(TimelineState(tasks: {...state.tasks, task.id: task})); add(_TimelineTaskAdded(task: task)); } Future _onTaskAdded( _TimelineTaskAdded event, Emitter emit, ) async { emit( TimelineState( tasks: { ...state.tasks.map((id, task) { if (id == event.task.id) return MapEntry(id, task.run()); return MapEntry( id, _transformer.isRestartable && task.isRunning ? task.cancel(end: _now()) : task, ); }), }, ), ); await Future.delayed(event.task.duration); if (!state.tasks.containsKey(event.task.id)) return; emit( TimelineState( tasks: {...state.tasks} ..update( event.task.id, (task) => emit.isDone ? task.cancel(end: _now()) : task.finish(end: _now()), ), ), ); } } extension on Transformer { bool get isDroppable => this == Transformer.droppable; bool get isRestartable => this == Transformer.restartable; EventTransformer<_TimelineTaskAdded> create() { return switch (this) { Transformer.concurrent => concurrent(), Transformer.sequential => sequential(), Transformer.droppable => droppable(), Transformer.restartable => restartable(), }; } } ================================================ FILE: examples/bloc_concurrency_visualizer/lib/timeline/bloc/timeline_event.dart ================================================ part of 'timeline_bloc.dart'; sealed class TimelineEvent { const TimelineEvent(); } class TimelineTaskQueued extends TimelineEvent { const TimelineTaskQueued(); } class _TimelineTaskAdded extends TimelineEvent { const _TimelineTaskAdded({required this.task}); final Task task; } class TimelineCompleted extends TimelineEvent { const TimelineCompleted(); } class TimelineReset extends TimelineEvent { const TimelineReset(); } ================================================ FILE: examples/bloc_concurrency_visualizer/lib/timeline/bloc/timeline_state.dart ================================================ part of 'timeline_bloc.dart'; class TimelineState extends Equatable { const TimelineState({this.tasks = const {}}); final Map tasks; @override List get props => [tasks]; } ================================================ FILE: examples/bloc_concurrency_visualizer/lib/timeline/models/models.dart ================================================ export './task.dart'; export './transformer.dart'; ================================================ FILE: examples/bloc_concurrency_visualizer/lib/timeline/models/task.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:uuid/uuid.dart'; enum TaskStatus { queued, running, finished, canceled } class Task extends Equatable { Task.queued({required double start, required Duration duration}) : this._(start: start, duration: duration); Task.canceled({ required double start, required double end, required Duration duration, }) : this._( start: start, end: end, duration: duration, status: TaskStatus.canceled, ); Task._({ required this.start, required this.duration, this.status = TaskStatus.queued, this.end, String? id, }) : id = id ?? const Uuid().v4(); final double start; final Duration duration; final TaskStatus status; final double? end; final String id; bool get isRunning => status == TaskStatus.running; bool get isCanceled => status == TaskStatus.canceled; Task run() => _copyWith(status: TaskStatus.running); Task finish({required double end}) { return _copyWith(status: TaskStatus.finished, end: end); } Task cancel({required double end}) { return _copyWith(status: TaskStatus.canceled, end: end); } Task _copyWith({TaskStatus? status, double? end}) { return Task._( id: id, duration: duration, start: start, end: end ?? this.end, status: status ?? this.status, ); } @override List get props => [start, end, status]; } ================================================ FILE: examples/bloc_concurrency_visualizer/lib/timeline/models/transformer.dart ================================================ enum Transformer { concurrent, sequential, droppable, restartable } ================================================ FILE: examples/bloc_concurrency_visualizer/lib/timeline/timeline.dart ================================================ export './bloc/timeline_bloc.dart'; export './models/models.dart'; export './view/timeline_page.dart'; ================================================ FILE: examples/bloc_concurrency_visualizer/lib/timeline/view/timeline_page.dart ================================================ import 'package:bloc_concurrency_visualizer/timeline/timeline.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class TimelinePage extends StatelessWidget { const TimelinePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Bloc Concurrency Visualizer'), forceMaterialTransparency: true, ), body: ListView.builder( itemCount: Transformer.values.length, itemBuilder: (_, i) => TimelineView(transformer: Transformer.values[i]), ), ); } } class TimelineView extends StatefulWidget { const TimelineView({required this.transformer, super.key}); final Transformer transformer; @override State createState() => _TimelineViewState(); } class _TimelineViewState extends State with SingleTickerProviderStateMixin { static const _duration = Duration(seconds: 10); static const _spacing = 8.0; late final TimelineBloc _bloc; late final AnimationController _timelineController; @override void initState() { super.initState(); _timelineController = AnimationController(vsync: this, duration: _duration) ..addListener(_onAnimation); _bloc = TimelineBloc( transformer: widget.transformer, now: () => _timelineController.value, ); } void _onAnimation() { if (_timelineController.isCompleted) { _bloc.add(const TimelineCompleted()); _timelineController.reset(); } setState(() {}); } @override void dispose() { _timelineController.dispose(); _bloc.close(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final buttonStyle = OutlinedButton.styleFrom( shape: const BeveledRectangleBorder(), side: const BorderSide(width: 0.5), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ); final title = Text( widget.transformer.title, style: theme.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold), ); final description = Text( widget.transformer.description, style: theme.textTheme.bodyMedium, ); final addTaskButton = OutlinedButton( style: buttonStyle, onPressed: () { _bloc.add(const TimelineTaskQueued()); if (!_timelineController.isAnimating) { _timelineController.forward(); } }, child: const Text('add task'), ); final resetButton = OutlinedButton( style: buttonStyle, onPressed: () { _bloc.add(const TimelineReset()); _timelineController.reset(); }, child: const Text('reset'), ); final actions = Row( spacing: _spacing, children: [addTaskButton, resetButton], ); final timeline = BlocBuilder( bloc: _bloc, builder: (context, state) => Timeline( cursorPosition: _timelineController.value, tasks: [...state.tasks.values], ), ); const gap = SizedBox(height: _spacing); return Padding( padding: const EdgeInsets.all(_spacing), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [title, description, gap, actions, gap, timeline], ), ); } } class Timeline extends StatelessWidget { const Timeline({ required this.tasks, required this.cursorPosition, super.key, }); final double cursorPosition; final List tasks; static const _height = 100.0; static const _barCount = 4; static const _barSpacing = 6.5; static const _barHeight = _height / 5; @override Widget build(BuildContext context) { final theme = Theme.of(context); final width = MediaQuery.of(context).size.width; final background = Positioned.fill( child: SizedBox( width: width, height: _height, child: DecoratedBox(decoration: BoxDecoration(border: Border.all())), ), ); final cursor = Positioned( left: width * cursorPosition, child: SizedBox( width: 1, height: _height, child: DecoratedBox( decoration: BoxDecoration(border: Border.all(), color: Colors.black), ), ), ); final instructions = Align( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'timeline is empty', style: theme.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, ), ), const Text('tap "add task" to enqueue tasks'), ], ), ); return SizedBox( width: width, height: _height, child: Stack( children: [ background, cursor, if (tasks.isEmpty) instructions, for (final (index, task) in tasks.indexed) ...[ _BarBackground( left: width * task.start, top: (index % _barCount) * (_barHeight + _barSpacing), height: _barHeight, value: cursorPosition, task: task, ), _BarForeground( left: width * task.start, top: (index % _barCount) * (_barHeight + _barSpacing), height: _barHeight, width: width, task: task, text: Text( '#${index + 1} ${task.status.text}', style: theme.textTheme.bodyMedium?.copyWith( color: task.status.color, decoration: task.isCanceled ? TextDecoration.lineThrough : null, ), ), ), ], ], ), ); } } class _BarBackground extends StatelessWidget { const _BarBackground({ required this.left, required this.top, required this.height, required this.value, required this.task, }); final double left; final double top; final double height; final double value; final Task task; @override Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width; return Positioned( left: left, top: top, child: SizedBox( width: (((task.end ?? value) - task.start) * width).abs(), height: height, child: DecoratedBox( decoration: BoxDecoration( border: task.isCanceled ? Border(left: BorderSide(color: task.status.borderColor)) : Border.all(color: task.status.borderColor), color: task.status.fillColor, ), ), ), ); } } class _BarForeground extends StatelessWidget { const _BarForeground({ required this.left, required this.top, required this.height, required this.width, required this.text, required this.task, }); final double left; final double top; final double height; final double width; final Text text; final Task task; @override Widget build(BuildContext context) { return Positioned( left: left, top: top, child: SizedBox( width: 100, height: height, child: Padding(padding: const EdgeInsets.only(left: 4), child: text), ), ); } } extension on TaskStatus { String get text { return switch (this) { TaskStatus.queued => 'Queued', TaskStatus.running => 'Running', TaskStatus.finished => 'Finished', TaskStatus.canceled => 'Canceled', }; } Color get borderColor { return switch (this) { TaskStatus.queued => Colors.orange.shade800, TaskStatus.running => Colors.lightBlue.shade800, TaskStatus.finished => Colors.greenAccent.shade700, TaskStatus.canceled => Colors.grey.shade800, }; } Color get fillColor { return switch (this) { TaskStatus.queued => Colors.orange.shade300, TaskStatus.running => Colors.lightBlue.shade300, TaskStatus.finished => Colors.greenAccent.shade200, TaskStatus.canceled => Colors.transparent, }; } Color get color { return switch (this) { TaskStatus.queued => Colors.black, TaskStatus.running => Colors.black, TaskStatus.finished => Colors.black, TaskStatus.canceled => Colors.grey.shade700, }; } } extension on Transformer { String get title { return switch (this) { Transformer.concurrent => 'Concurrent', Transformer.sequential => 'Sequential', Transformer.droppable => 'Droppable', Transformer.restartable => 'Restartable', }; } String get description { return switch (this) { Transformer.concurrent => 'Process all tasks in parallel.', Transformer.sequential => 'Process tasks in order, one at a time.', Transformer.restartable => 'Only process the most recent task.', Transformer.droppable => 'Ignore new tasks if a task is processing.', }; } } ================================================ FILE: examples/bloc_concurrency_visualizer/pubspec.yaml ================================================ name: bloc_concurrency_visualizer description: A Flutter app to help visualize bloc_concurrency transformers version: 1.0.0+1 publish_to: "none" environment: sdk: ">=3.10.0 <4.0.0" dependencies: bloc: ^9.0.0 bloc_concurrency: ^0.3.0 equatable: ^2.0.0 flutter: sdk: flutter flutter_bloc: ^9.0.0 uuid: ^4.0.0 dev_dependencies: bloc_lint: ^0.3.0 flutter: uses-material-design: true ================================================ FILE: examples/bloc_concurrency_visualizer/web/index.html ================================================ Bloc Concurrency Visualizer ================================================ FILE: examples/bloc_concurrency_visualizer/web/manifest.json ================================================ { "name": "example", "short_name": "example", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: examples/flutter_bloc_with_stream/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: examples/flutter_bloc_with_stream/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: web create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/flutter_bloc_with_stream/README.md ================================================ [![build](https://github.com/felangel/bloc/actions/workflows/main.yaml/badge.svg)](https://github.com/felangel/bloc/actions) # flutter_bloc_with_stream A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: examples/flutter_bloc_with_stream/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml ================================================ FILE: examples/flutter_bloc_with_stream/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: examples/flutter_bloc_with_stream/lib/bloc/ticker_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc_with_stream/ticker/ticker.dart'; part 'ticker_event.dart'; part 'ticker_state.dart'; /// {@template ticker_bloc} /// Bloc which manages the current [TickerState] /// and depends on a [Ticker] instance. /// {@endtemplate} class TickerBloc extends Bloc { /// {@macro ticker_bloc} TickerBloc(Ticker ticker) : super(TickerInitial()) { on( (event, emit) async { await emit.onEach( ticker.tick(), onData: (tick) => add(_TickerTicked(tick)), ); emit(const TickerComplete()); }, transformer: restartable(), ); on<_TickerTicked>((event, emit) => emit(TickerTickSuccess(event.tick))); } } ================================================ FILE: examples/flutter_bloc_with_stream/lib/bloc/ticker_event.dart ================================================ part of 'ticker_bloc.dart'; /// {@template ticker_event} /// Base class for all [TickerEvent]s which are /// handled by the [TickerBloc]. /// {@endtemplate} sealed class TickerEvent extends Equatable { /// {@macro ticker_event} const TickerEvent(); @override List get props => []; } /// {@template ticker_started} /// Signifies to the [TickerBloc] that the user /// has requested to start the [Ticker]. /// {@endtemplate} final class TickerStarted extends TickerEvent { /// {@macro ticker_started} const TickerStarted(); } final class _TickerTicked extends TickerEvent { const _TickerTicked(this.tick); /// The current tick count. final int tick; @override List get props => [tick]; } ================================================ FILE: examples/flutter_bloc_with_stream/lib/bloc/ticker_state.dart ================================================ part of 'ticker_bloc.dart'; /// {@template ticker_state} /// Base class for all [TickerState]s which are /// managed by the [TickerBloc]. /// {@endtemplate} sealed class TickerState extends Equatable { /// {@macro ticker_state} const TickerState(); @override List get props => []; } /// The initial state of the [TickerBloc]. final class TickerInitial extends TickerState {} /// {@template ticker_tick_success} /// The state of the [TickerBloc] after a [Ticker] /// has been started and includes the current tick count. /// {@endtemplate} final class TickerTickSuccess extends TickerState { /// {@macro ticker_tick_success} const TickerTickSuccess(this.count); /// The current tick count. final int count; @override List get props => [count]; } /// {@template ticker_complete} /// The state of the [TickerBloc] after the [Ticker] has completed. /// {@endtemplate} final class TickerComplete extends TickerState { /// {@macro ticker_complete} const TickerComplete(); } ================================================ FILE: examples/flutter_bloc_with_stream/lib/main.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc_with_stream/bloc/ticker_bloc.dart'; import 'package:flutter_bloc_with_stream/ticker/ticker.dart'; void main() => runApp(TickerApp()); /// {@template ticker_app} /// [MaterialApp] which sets the [TickerPage] as the `home`. /// [TickerApp] also provides an instance of [TickerBloc] to /// the [TickerPage]. /// {@endtemplate} class TickerApp extends MaterialApp { /// {@macro ticker_app} TickerApp({super.key}) : super( home: BlocProvider( create: (_) => TickerBloc(Ticker()), child: const TickerPage(), ), ); } /// {@template ticker_page} /// [StatelessWidget] which consumes a [TickerBloc] /// and responds to changes in the [TickerState]. /// [TickerPage] also notifies the [TickerBloc] when /// the user taps on the start button. /// {@endtemplate} class TickerPage extends StatelessWidget { /// {@macro ticker_page} const TickerPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: BlocBuilder( builder: (context, state) { return switch (state) { TickerInitial() => const Text( 'Press the floating button to start.', ), TickerTickSuccess() => Text('Tick #${state.count}'), TickerComplete() => const Text( 'Complete! Press the floating button to restart.', ), }; }, ), ), floatingActionButton: FloatingActionButton( onPressed: () => context.read().add(const TickerStarted()), tooltip: 'Start', child: const Icon(Icons.timer), ), ); } } ================================================ FILE: examples/flutter_bloc_with_stream/lib/ticker/ticker.dart ================================================ import 'dart:async'; /// Class which exposes a `tick` method to emit values periodically. class Ticker { /// Emits a new `int` up to 10 every second. Stream tick() { return Stream.periodic(const Duration(seconds: 1), (x) => x).take(10); } } ================================================ FILE: examples/flutter_bloc_with_stream/pubspec.yaml ================================================ name: flutter_bloc_with_stream description: A new Flutter project. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: bloc: ^9.0.0 bloc_concurrency: ^0.3.0 equatable: ^2.0.0 flutter: sdk: flutter flutter_bloc: ^9.1.0 dev_dependencies: bloc_lint: ^0.3.0 bloc_test: ^10.0.0 flutter_test: sdk: flutter mocktail: ^1.0.0 flutter: uses-material-design: true ================================================ FILE: examples/flutter_bloc_with_stream/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../../packages/bloc bloc_concurrency: path: ../../packages/bloc_concurrency bloc_lint: path: ../../packages/bloc_lint bloc_test: path: ../../packages/bloc_test flutter_bloc: path: ../../packages/flutter_bloc ================================================ FILE: examples/flutter_bloc_with_stream/test/app_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc_with_stream/main.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group(TickerApp, () { testWidgets('is a MaterialApp', (tester) async { expect(TickerApp(), isA()); }); testWidgets('renders $TickerPage', (tester) async { await tester.pumpWidget(TickerApp()); expect(find.byType(TickerPage), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_bloc_with_stream/test/bloc/ticker_bloc_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_bloc_with_stream/bloc/ticker_bloc.dart'; import 'package:flutter_bloc_with_stream/ticker/ticker.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class _MockTicker extends Mock implements Ticker {} void main() { group(TickerBloc, () { late Ticker ticker; setUp(() { ticker = _MockTicker(); when(ticker.tick).thenAnswer( (_) => Stream.fromIterable([1, 2, 3]), ); }); test('initial state is $TickerInitial', () { expect(TickerBloc(ticker).state, TickerInitial()); }); blocTest( 'emits [] when ticker has not started', build: () => TickerBloc(ticker), expect: () => [], ); blocTest( 'emits $TickerTickSuccess from 1 to 3', build: () => TickerBloc(ticker), act: (bloc) => bloc.add(TickerStarted()), expect: () => [ TickerTickSuccess(1), TickerTickSuccess(2), TickerTickSuccess(3), TickerComplete(), ], ); blocTest( 'emits $TickerTickSuccess ' 'from 1 to 3 and cancels previous subscription', build: () => TickerBloc(ticker), act: (bloc) => bloc ..add(TickerStarted()) ..add(TickerStarted()), expect: () => [ TickerTickSuccess(1), TickerTickSuccess(2), TickerTickSuccess(3), TickerComplete(), ], ); }); } ================================================ FILE: examples/flutter_bloc_with_stream/test/bloc/ticker_event_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_bloc_with_stream/bloc/ticker_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group(TickerEvent, () { group(TickerStarted, () { test('supports value comparison', () { expect(TickerStarted(), equals(TickerStarted())); }); }); }); } ================================================ FILE: examples/flutter_bloc_with_stream/test/bloc/ticker_state_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_bloc_with_stream/bloc/ticker_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group(TickerState, () { group(TickerInitial, () { test('supports value comparison', () { expect(TickerInitial(), TickerInitial()); }); }); group(TickerTickSuccess, () { test('supports value comparison', () { expect(TickerTickSuccess(1), TickerTickSuccess(1)); expect( TickerTickSuccess(1), isNot(equals(TickerTickSuccess(2))), ); }); }); group(TickerComplete, () { test('supports value comparison', () { expect(TickerComplete(), equals(TickerComplete())); }); }); }); } ================================================ FILE: examples/flutter_bloc_with_stream/test/ticker_page_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc_with_stream/bloc/ticker_bloc.dart'; import 'package:flutter_bloc_with_stream/main.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class _MockTickerBloc extends MockBloc implements TickerBloc {} extension on WidgetTester { Future pumpTickerPage(TickerBloc tickerBloc) { return pumpWidget( MaterialApp( home: BlocProvider.value(value: tickerBloc, child: TickerPage()), ), ); } } void main() { late TickerBloc tickerBloc; setUp(() { tickerBloc = _MockTickerBloc(); }); tearDown(() => reset(tickerBloc)); group(TickerPage, () { testWidgets('renders initial state', (tester) async { when(() => tickerBloc.state).thenReturn(TickerInitial()); await tester.pumpTickerPage(tickerBloc); expect(find.text('Press the floating button to start.'), findsOneWidget); }); testWidgets('renders tick count ', (tester) async { const tickCount = 5; when(() => tickerBloc.state).thenReturn(TickerTickSuccess(tickCount)); await tester.pumpTickerPage(tickerBloc); expect(find.text('Tick #$tickCount'), findsOneWidget); }); testWidgets('adds ticker started ' 'when start ticker floating action button is pressed', (tester) async { when(() => tickerBloc.state).thenReturn(TickerInitial()); await tester.pumpTickerPage(tickerBloc); await tester.tap(find.byType(FloatingActionButton)); verify(() => tickerBloc.add(TickerStarted())).called(1); }); testWidgets('tick count periodically increments ' 'every 1 second', (tester) async { whenListen( tickerBloc, Stream.periodic(Duration(seconds: 1), TickerTickSuccess.new).take(3), initialState: TickerInitial(), ); await tester.pumpTickerPage(tickerBloc..add(TickerStarted())); await tester.pump(Duration(seconds: 1)); expect(find.text('Tick #0'), findsOneWidget); await tester.pump(Duration(seconds: 1)); expect(find.text('Tick #1'), findsOneWidget); await tester.pump(Duration(seconds: 1)); expect(find.text('Tick #2'), findsOneWidget); }); testWidgets('renders complete state', (tester) async { when(() => tickerBloc.state).thenReturn(TickerComplete()); await tester.pumpTickerPage(tickerBloc); await tester.pumpAndSettle(); expect( find.text('Complete! Press the floating button to restart.'), findsOneWidget, ); }); }); } ================================================ FILE: examples/flutter_bloc_with_stream/web/index.html ================================================ flutter_bloc_with_stream ================================================ FILE: examples/flutter_bloc_with_stream/web/manifest.json ================================================ { "name": "flutter_bloc_with_stream", "short_name": "flutter_bloc_with_stream", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: examples/flutter_complex_list/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: examples/flutter_complex_list/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: web create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/flutter_complex_list/README.md ================================================ [![build](https://github.com/felangel/bloc/actions/workflows/main.yaml/badge.svg)](https://github.com/felangel/bloc/actions) # flutter_complex_list A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: examples/flutter_complex_list/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml linter: rules: public_member_api_docs: false ================================================ FILE: examples/flutter_complex_list/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: examples/flutter_complex_list/lib/app.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_complex_list/complex_list/complex_list.dart'; import 'package:flutter_complex_list/repository.dart'; class App extends MaterialApp { App({required Repository repository, super.key}) : super( home: RepositoryProvider.value( value: repository, child: const ComplexListPage(), ), ); } ================================================ FILE: examples/flutter_complex_list/lib/complex_list/complex_list.dart ================================================ export 'cubit/complex_list_cubit.dart'; export 'models/models.dart'; export 'view/view.dart'; ================================================ FILE: examples/flutter_complex_list/lib/complex_list/cubit/complex_list_cubit.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_complex_list/complex_list/complex_list.dart'; import 'package:flutter_complex_list/repository.dart'; part 'complex_list_state.dart'; class ComplexListCubit extends Cubit { ComplexListCubit({required Repository repository}) : _repository = repository, super(const ComplexListState.loading()); final Repository _repository; Future fetchList() async { try { final items = await _repository.fetchItems(); emit(ComplexListState.success(items)); } on Exception { emit(const ComplexListState.failure()); } } Future deleteItem(String id) async { final deleteInProgress = state.items.map((item) { return item.id == id ? item.copyWith(isDeleting: true) : item; }).toList(); emit(ComplexListState.success(deleteInProgress)); unawaited( _repository.deleteItem(id).then((_) { final deleteSuccess = List.of(state.items) ..removeWhere((element) => element.id == id); emit(ComplexListState.success(deleteSuccess)); }), ); } } ================================================ FILE: examples/flutter_complex_list/lib/complex_list/cubit/complex_list_state.dart ================================================ part of 'complex_list_cubit.dart'; enum ListStatus { loading, success, failure } final class ComplexListState extends Equatable { const ComplexListState._({ this.status = ListStatus.loading, this.items = const [], }); const ComplexListState.loading() : this._(); const ComplexListState.success(List items) : this._(status: ListStatus.success, items: items); const ComplexListState.failure() : this._(status: ListStatus.failure); final ListStatus status; final List items; @override List get props => [status, items]; } ================================================ FILE: examples/flutter_complex_list/lib/complex_list/models/item.dart ================================================ import 'package:equatable/equatable.dart'; class Item extends Equatable { const Item({ required this.id, required this.value, this.isDeleting = false, }); final String id; final String value; final bool isDeleting; Item copyWith({String? id, String? value, bool? isDeleting}) { return Item( id: id ?? this.id, value: value ?? this.value, isDeleting: isDeleting ?? this.isDeleting, ); } @override List get props => [id, value, isDeleting]; } ================================================ FILE: examples/flutter_complex_list/lib/complex_list/models/models.dart ================================================ export 'item.dart'; ================================================ FILE: examples/flutter_complex_list/lib/complex_list/view/complex_list_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_complex_list/complex_list/complex_list.dart'; import 'package:flutter_complex_list/repository.dart'; class ComplexListPage extends StatelessWidget { const ComplexListPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: BlocProvider( create: (_) => ComplexListCubit( repository: context.read(), )..fetchList(), child: const ComplexListView(), ), ); } } class ComplexListView extends StatelessWidget { const ComplexListView({super.key}); @override Widget build(BuildContext context) { final state = context.watch().state; switch (state.status) { case ListStatus.failure: return const Center(child: Text('Oops something went wrong!')); case ListStatus.success: return ItemView(items: state.items); case ListStatus.loading: return const Center(child: CircularProgressIndicator()); } } } class ItemView extends StatelessWidget { const ItemView({required this.items, super.key}); final List items; @override Widget build(BuildContext context) { return items.isEmpty ? const Center(child: Text('No Content')) : ListView.builder( itemBuilder: (BuildContext context, int index) { return ItemTile( item: items[index], onDeletePressed: (id) { context.read().deleteItem(id); }, ); }, itemCount: items.length, ); } } class ItemTile extends StatelessWidget { const ItemTile({ required this.item, required this.onDeletePressed, super.key, }); final Item item; final ValueSetter onDeletePressed; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Material( child: ListTile( title: Text(item.value), trailing: item.isDeleting ? const CircularProgressIndicator() : IconButton( icon: Icon(Icons.delete, color: theme.colorScheme.error), onPressed: () => onDeletePressed(item.id), ), ), ); } } ================================================ FILE: examples/flutter_complex_list/lib/complex_list/view/view.dart ================================================ export 'complex_list_page.dart'; ================================================ FILE: examples/flutter_complex_list/lib/main.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_complex_list/app.dart'; import 'package:flutter_complex_list/repository.dart'; import 'package:flutter_complex_list/simple_bloc_observer.dart'; void main() { Bloc.observer = const SimpleBlocObserver(); runApp(App(repository: Repository())); } ================================================ FILE: examples/flutter_complex_list/lib/repository.dart ================================================ import 'dart:async'; import 'dart:math'; import 'package:flutter_complex_list/complex_list/complex_list.dart'; class Repository { final _random = Random(); int _randomRange(int min, int max) => min + _random.nextInt(max - min); Future> fetchItems() async { await Future.delayed(Duration(seconds: _randomRange(1, 5))); return List.of(_generateItemsList(10)); } List _generateItemsList(int length) { return List.generate( length, (index) => Item(id: '$index', value: 'Item $index'), ); } Future deleteItem(String id) async { await Future.delayed(Duration(seconds: _randomRange(1, 5))); } } ================================================ FILE: examples/flutter_complex_list/lib/simple_bloc_observer.dart ================================================ import 'package:bloc/bloc.dart'; class SimpleBlocObserver extends BlocObserver { const SimpleBlocObserver(); @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { // ignore: avoid_print print(error); super.onError(bloc, error, stackTrace); } @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); // ignore: avoid_print print(change); } } ================================================ FILE: examples/flutter_complex_list/pubspec.yaml ================================================ name: flutter_complex_list description: A new Flutter project. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: bloc: ^9.0.0 equatable: ^2.0.0 flutter: sdk: flutter flutter_bloc: ^9.1.0 dev_dependencies: bloc_lint: ^0.3.0 bloc_test: ^10.0.0 flutter_test: sdk: flutter mocktail: ^1.0.0 flutter: uses-material-design: true ================================================ FILE: examples/flutter_complex_list/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../../packages/bloc bloc_lint: path: ../../packages/bloc_lint bloc_test: path: ../../packages/bloc_test flutter_bloc: path: ../../packages/flutter_bloc ================================================ FILE: examples/flutter_complex_list/test/app_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_complex_list/app.dart'; import 'package:flutter_complex_list/complex_list/complex_list.dart'; import 'package:flutter_complex_list/repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockRepository extends Mock implements Repository {} void main() { late Repository repository; setUp(() { repository = MockRepository(); }); group(App, () { testWidgets('is a $MaterialApp', (tester) async { expect(App(repository: repository), isA()); }); testWidgets('renders $ComplexListPage', (tester) async { when(repository.fetchItems).thenAnswer((_) async => []); await tester.pumpWidget(App(repository: repository)); expect(find.byType(ComplexListPage), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_complex_list/test/complex_list/cubit/complex_list_cubit_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_complex_list/complex_list/complex_list.dart'; import 'package:flutter_complex_list/repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class _MockRepository extends Mock implements Repository {} void main() { const mockItems = [ Item(id: '1', value: '1'), Item(id: '2', value: '2'), Item(id: '3', value: '3'), ]; group(ComplexListCubit, () { late Repository repository; setUp(() { repository = _MockRepository(); }); test('initial state is ${ComplexListState.loading}', () { expect( ComplexListCubit(repository: repository).state, const ComplexListState.loading(), ); }); group('fetchList', () { blocTest( 'emits ${ComplexListState.success} after fetching list', setUp: () { when(repository.fetchItems).thenAnswer((_) async => mockItems); }, build: () => ComplexListCubit(repository: repository), act: (cubit) => cubit.fetchList(), expect: () => [ const ComplexListState.success(mockItems), ], verify: (_) => verify(repository.fetchItems).called(1), ); blocTest( 'emits ${ComplexListState.failure} after failing to fetch list', setUp: () { when(repository.fetchItems).thenThrow(Exception('Error')); }, build: () => ComplexListCubit(repository: repository), act: (cubit) => cubit.fetchList(), expect: () => [ const ComplexListState.failure(), ], verify: (_) => verify(repository.fetchItems).called(1), ); }); group('deleteItem', () { blocTest( 'emits corrects states when deleting an item', setUp: () { when(() => repository.deleteItem('2')).thenAnswer((_) async {}); }, build: () => ComplexListCubit(repository: repository), seed: () => const ComplexListState.success(mockItems), act: (cubit) => cubit.deleteItem('2'), expect: () => [ const ComplexListState.success([ Item(id: '1', value: '1'), Item(id: '2', value: '2', isDeleting: true), Item(id: '3', value: '3'), ]), const ComplexListState.success([ Item(id: '1', value: '1'), Item(id: '3', value: '3'), ]), ], verify: (_) => verify(() => repository.deleteItem('2')).called(1), ); }); }); } ================================================ FILE: examples/flutter_complex_list/test/complex_list/cubit/complex_list_state_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_complex_list/complex_list/complex_list.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group(ComplexListState, () { const mockItems = [Item(id: '1', value: '1')]; test('support value comparisons', () { expect(ComplexListState.loading(), ComplexListState.loading()); expect(ComplexListState.failure(), ComplexListState.failure()); expect( ComplexListState.success(mockItems), ComplexListState.success(mockItems), ); }); }); } ================================================ FILE: examples/flutter_complex_list/test/complex_list/models/item_test.dart ================================================ import 'package:flutter_complex_list/complex_list/complex_list.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group(Item, () { const mockItem = Item(id: '1', value: 'Item 1'); test('supports value comparisons', () { expect(mockItem, mockItem); }); test('supports copyWith comparisons', () { expect(mockItem.copyWith(), mockItem); expect( mockItem.copyWith(id: '2', value: 'Item 2'), isNot(equals(mockItem)), ); }); }); } ================================================ FILE: examples/flutter_complex_list/test/complex_list/view/complex_list_page_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_complex_list/complex_list/complex_list.dart'; import 'package:flutter_complex_list/repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class _MockRepository extends Mock implements Repository {} class _MockComplexListCubit extends MockCubit implements ComplexListCubit {} extension on WidgetTester { Future pumpListPage(Repository repository) { return pumpWidget( MaterialApp( home: RepositoryProvider.value( value: repository, child: const ComplexListPage(), ), ), ); } Future pumpListView(ComplexListCubit listCubit) { return pumpWidget( MaterialApp( home: BlocProvider.value( value: listCubit, child: const ComplexListView(), ), ), ); } } void main() { const mockItems = [ Item(id: '1', value: 'Item 1'), Item(id: '2', value: 'Item 2'), Item(id: '3', value: 'Item 3'), ]; late Repository repository; late ComplexListCubit listCubit; setUp(() { repository = _MockRepository(); listCubit = _MockComplexListCubit(); }); group(ComplexListPage, () { testWidgets('renders $ComplexListView', (tester) async { when(() => repository.fetchItems()).thenAnswer((_) async => []); await tester.pumpListPage(repository); expect(find.byType(ComplexListView), findsOneWidget); }); }); group(ComplexListView, () { testWidgets('renders $CircularProgressIndicator while ' 'waiting for items to load', (tester) async { when(() => listCubit.state).thenReturn(const ComplexListState.loading()); await tester.pumpListView(listCubit); expect(find.byType(CircularProgressIndicator), findsOneWidget); }); testWidgets('renders error text ' 'when items fail to load', (tester) async { when(() => listCubit.state).thenReturn(const ComplexListState.failure()); await tester.pumpListView(listCubit); expect(find.text('Oops something went wrong!'), findsOneWidget); }); testWidgets('renders $ComplexListView after items ' 'are finished loading', (tester) async { when(() => listCubit.state).thenReturn( const ComplexListState.success(mockItems), ); await tester.pumpListView(listCubit); expect(find.byType(ComplexListView), findsOneWidget); }); testWidgets('renders "No Content" text when ' 'no items are present', (tester) async { when(() => listCubit.state).thenReturn( const ComplexListState.success([]), ); await tester.pumpListView(listCubit); expect(find.text('No Content'), findsOneWidget); }); testWidgets('renders three ${ItemTile}s', (tester) async { when(() => listCubit.state).thenReturn( const ComplexListState.success(mockItems), ); await tester.pumpListView(listCubit); expect(find.byType(ItemTile), findsNWidgets(3)); }); testWidgets('deletes first item', (tester) async { when(() => listCubit.state).thenReturn( const ComplexListState.success(mockItems), ); when(() => listCubit.deleteItem('1')).thenAnswer((_) async {}); await tester.pumpListView(listCubit); await tester.tap(find.byIcon(Icons.delete).first); verify(() => listCubit.deleteItem('1')).called(1); }); }); group(ItemTile, () { testWidgets('renders value text', (tester) async { const mockItem = Item(id: '1', value: 'Item 1'); when(() => listCubit.state).thenReturn( const ComplexListState.success([mockItem]), ); await tester.pumpListView(listCubit); expect(find.text('Item 1'), findsOneWidget); }); testWidgets('renders delete icon button ' 'when item is not being deleted', (tester) async { const mockItem = Item(id: '1', value: 'Item 1'); when(() => listCubit.state).thenReturn( const ComplexListState.success([mockItem]), ); await tester.pumpListView(listCubit); expect(find.byIcon(Icons.delete), findsOneWidget); }); testWidgets('renders $CircularProgressIndicator ' 'when item is being deleting', (tester) async { const mockItem = Item(id: '1', value: 'Item 1', isDeleting: true); when(() => listCubit.state).thenReturn( const ComplexListState.success([mockItem]), ); await tester.pumpListView(listCubit); expect(find.byType(CircularProgressIndicator), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_complex_list/test/repository_test.dart ================================================ import 'package:flutter_complex_list/complex_list/complex_list.dart'; import 'package:flutter_complex_list/repository.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group(Repository, () { late Repository repository; setUp(() { repository = Repository(); }); group('fetchItems', () { test('returns list of items', () { final items = List.generate( 10, (index) => Item(id: '$index', value: 'Item $index'), ); expect( repository.fetchItems(), completion(equals(items)), ); }); }); group('deleteItem', () { test('return null when deleting item', () { expect( repository.deleteItem('2'), completion(equals(null)), ); }); }); }); } ================================================ FILE: examples/flutter_complex_list/web/index.html ================================================ flutter_complex_list ================================================ FILE: examples/flutter_complex_list/web/manifest.json ================================================ { "name": "flutter_complex_list", "short_name": "flutter_complex_list", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: examples/flutter_counter/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: examples/flutter_counter/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: android create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/flutter_counter/README.md ================================================ # flutter_counter A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: examples/flutter_counter/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml ================================================ FILE: examples/flutter_counter/integration_test/app_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_counter/main.dart' as app; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('CounterApp', () { testWidgets('renders correct initial count', (tester) async { await tester.pumpApp(); expect(find.text('0'), findsOneWidget); }); testWidgets('tapping increment button updates the count', (tester) async { await tester.pumpApp(); await tester.incrementCounter(); expect(find.text('1'), findsOneWidget); await tester.incrementCounter(); expect(find.text('2'), findsOneWidget); await tester.incrementCounter(); expect(find.text('3'), findsOneWidget); }); testWidgets('tapping decrement button updates the count', (tester) async { await tester.pumpApp(); await tester.decrementCounter(); expect(find.text('-1'), findsOneWidget); await tester.decrementCounter(); expect(find.text('-2'), findsOneWidget); await tester.decrementCounter(); expect(find.text('-3'), findsOneWidget); }); }); } extension on WidgetTester { Future pumpApp() async { app.main(); await pumpAndSettle(); } Future incrementCounter() async { await tap( find.byKey(const Key('counterView_increment_floatingActionButton')), ); await pump(); } Future decrementCounter() async { await tap( find.byKey(const Key('counterView_decrement_floatingActionButton')), ); await pump(); } } ================================================ FILE: examples/flutter_counter/lib/app.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_counter/counter/counter.dart'; /// {@template counter_app} /// A [MaterialApp] which sets the `home` to [CounterPage]. /// {@endtemplate} class CounterApp extends MaterialApp { /// {@macro counter_app} const CounterApp({super.key}) : super(home: const CounterPage()); } ================================================ FILE: examples/flutter_counter/lib/counter/counter.dart ================================================ export 'cubit/counter_cubit.dart'; export 'view/view.dart'; ================================================ FILE: examples/flutter_counter/lib/counter/cubit/counter_cubit.dart ================================================ import 'package:bloc/bloc.dart'; /// {@template counter_cubit} /// A [Cubit] which manages an [int] as its state. /// {@endtemplate} class CounterCubit extends Cubit { /// {@macro counter_cubit} CounterCubit() : super(0); /// Add 1 to the current state. void increment() => emit(state + 1); /// Subtract 1 from the current state. void decrement() => emit(state - 1); } ================================================ FILE: examples/flutter_counter/lib/counter/view/counter_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_counter/counter/counter.dart'; /// {@template counter_page} /// A [StatelessWidget] which is responsible for providing a /// [CounterCubit] instance to the [CounterView]. /// {@endtemplate} class CounterPage extends StatelessWidget { /// {@macro counter_page} const CounterPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => CounterCubit(), child: const CounterView(), ); } } ================================================ FILE: examples/flutter_counter/lib/counter/view/counter_view.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_counter/counter/counter.dart'; /// {@template counter_view} /// A [StatelessWidget] which reacts to the provided /// [CounterCubit] state and notifies it in response to user input. /// {@endtemplate} class CounterView extends StatelessWidget { /// {@macro counter_view} const CounterView({super.key}); @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; return Scaffold( body: Center( child: BlocBuilder( builder: (context, state) { return Text('$state', style: textTheme.displayMedium); }, ), ), floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, children: [ FloatingActionButton( key: const Key('counterView_increment_floatingActionButton'), child: const Icon(Icons.add), onPressed: () => context.read().increment(), ), const SizedBox(height: 8), FloatingActionButton( key: const Key('counterView_decrement_floatingActionButton'), child: const Icon(Icons.remove), onPressed: () => context.read().decrement(), ), ], ), ); } } ================================================ FILE: examples/flutter_counter/lib/counter/view/view.dart ================================================ export 'counter_page.dart'; export 'counter_view.dart'; ================================================ FILE: examples/flutter_counter/lib/counter_observer.dart ================================================ import 'package:bloc/bloc.dart'; /// {@template counter_observer} /// [BlocObserver] for the counter application which /// observes all state changes. /// {@endtemplate} class CounterObserver extends BlocObserver { /// {@macro counter_observer} const CounterObserver(); @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); // ignore: avoid_print print('${bloc.runtimeType} $change'); } } ================================================ FILE: examples/flutter_counter/lib/main.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_counter/app.dart'; import 'package:flutter_counter/counter_observer.dart'; void main() { Bloc.observer = const CounterObserver(); runApp(const CounterApp()); } ================================================ FILE: examples/flutter_counter/pubspec.yaml ================================================ name: flutter_counter description: A new Flutter project. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: bloc: ^9.0.0 flutter: sdk: flutter flutter_bloc: ^9.1.0 dev_dependencies: bloc_lint: ^0.3.0 bloc_test: ^10.0.0 flutter_test: sdk: flutter integration_test: sdk: flutter mocktail: ^1.0.0 flutter: uses-material-design: true ================================================ FILE: examples/flutter_counter/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../../packages/bloc bloc_lint: path: ../../packages/bloc_lint bloc_test: path: ../../packages/bloc_test flutter_bloc: path: ../../packages/flutter_bloc ================================================ FILE: examples/flutter_counter/test/app_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter/material.dart'; import 'package:flutter_counter/app.dart'; import 'package:flutter_counter/counter/counter.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group(CounterApp, () { testWidgets('is a $MaterialApp', (tester) async { expect(CounterApp(), isA()); }); testWidgets('home is $CounterPage', (tester) async { expect(CounterApp().home, isA()); }); testWidgets('renders $CounterPage', (tester) async { await tester.pumpWidget(CounterApp()); expect(find.byType(CounterPage), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_counter/test/counter/cubit/counter_cubit_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_counter/counter/counter.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group(CounterCubit, () { test('initial state is 0', () { expect(CounterCubit().state, 0); }); group('increment', () { blocTest( 'emits [1] when state is 0', build: CounterCubit.new, act: (cubit) => cubit.increment(), expect: () => const [1], ); blocTest( 'emits [1, 2] when state is 0 and invoked twice', build: CounterCubit.new, act: (cubit) => cubit ..increment() ..increment(), expect: () => const [1, 2], ); blocTest( 'emits [42] when state is 41', build: CounterCubit.new, seed: () => 41, act: (cubit) => cubit.increment(), expect: () => const [42], ); }); group('decrement', () { blocTest( 'emits [-1] when state is 0', build: CounterCubit.new, act: (cubit) => cubit.decrement(), expect: () => const [-1], ); blocTest( 'emits [-1, -2] when state is 0 and invoked twice', build: CounterCubit.new, act: (cubit) => cubit ..decrement() ..decrement(), expect: () => const [-1, -2], ); blocTest( 'emits [42] when state is 43', build: CounterCubit.new, seed: () => 43, act: (cubit) => cubit.decrement(), expect: () => const [42], ); }); }); } ================================================ FILE: examples/flutter_counter/test/counter/view/counter_page_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter/material.dart'; import 'package:flutter_counter/counter/counter.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group(CounterPage, () { testWidgets('renders $CounterView', (tester) async { await tester.pumpWidget(MaterialApp(home: CounterPage())); expect(find.byType(CounterView), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_counter/test/counter/view/counter_view_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_counter/counter/counter.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class _MockCounterCubit extends MockCubit implements CounterCubit {} const _incrementButtonKey = Key('counterView_increment_floatingActionButton'); const _decrementButtonKey = Key('counterView_decrement_floatingActionButton'); void main() { late CounterCubit counterCubit; setUp(() { counterCubit = _MockCounterCubit(); }); group(CounterView, () { testWidgets('renders current $CounterCubit state', (tester) async { when(() => counterCubit.state).thenReturn(42); await tester.pumpWidget( MaterialApp( home: BlocProvider.value( value: counterCubit, child: CounterView(), ), ), ); expect(find.text('42'), findsOneWidget); }); testWidgets('tapping increment button invokes increment', (tester) async { when(() => counterCubit.state).thenReturn(0); when(() => counterCubit.increment()).thenReturn(null); await tester.pumpWidget( MaterialApp( home: BlocProvider.value( value: counterCubit, child: CounterView(), ), ), ); await tester.tap(find.byKey(_incrementButtonKey)); verify(() => counterCubit.increment()).called(1); }); testWidgets('tapping decrement button invokes decrement', (tester) async { when(() => counterCubit.state).thenReturn(0); when(() => counterCubit.decrement()).thenReturn(null); await tester.pumpWidget( MaterialApp( home: BlocProvider.value( value: counterCubit, child: CounterView(), ), ), ); final decrementFinder = find.byKey(_decrementButtonKey); await tester.ensureVisible(decrementFinder); await tester.tap(decrementFinder); verify(() => counterCubit.decrement()).called(1); }); }); } ================================================ FILE: examples/flutter_counter/test_driver/integration_test.dart ================================================ import 'package:integration_test/integration_test_driver.dart'; Future main() => integrationDriver(); ================================================ FILE: examples/flutter_counter/web/index.html ================================================ flutter_counter ================================================ FILE: examples/flutter_counter/web/manifest.json ================================================ { "name": "flutter_counter", "short_name": "flutter_counter", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: examples/flutter_dynamic_form/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: examples/flutter_dynamic_form/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: web create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/flutter_dynamic_form/README.md ================================================ [![build](https://github.com/felangel/bloc/actions/workflows/main.yaml/badge.svg)](https://github.com/felangel/bloc/actions) # flutter_dynamic_form A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: examples/flutter_dynamic_form/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml linter: rules: public_member_api_docs: false ================================================ FILE: examples/flutter_dynamic_form/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: examples/flutter_dynamic_form/lib/app.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dynamic_form/new_car/new_car.dart'; import 'package:flutter_dynamic_form/new_car_repository.dart'; class MyApp extends StatelessWidget { const MyApp({required this.newCarRepository, super.key}); final NewCarRepository newCarRepository; @override Widget build(BuildContext context) { return RepositoryProvider.value( value: newCarRepository, child: const MaterialApp(home: NewCarPage()), ); } } ================================================ FILE: examples/flutter_dynamic_form/lib/main.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_dynamic_form/app.dart'; import 'package:flutter_dynamic_form/new_car_repository.dart'; void main() => runApp(MyApp(newCarRepository: NewCarRepository())); ================================================ FILE: examples/flutter_dynamic_form/lib/new_car/bloc/new_car_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_dynamic_form/new_car_repository.dart'; part 'new_car_event.dart'; part 'new_car_state.dart'; class NewCarBloc extends Bloc { NewCarBloc({required NewCarRepository newCarRepository}) : _newCarRepository = newCarRepository, super(const NewCarState.initial()) { on(_onEvent, transformer: sequential()); } final NewCarRepository _newCarRepository; Future _onEvent(NewCarEvent event, Emitter emit) async { return switch (event) { final NewCarFormLoaded e => _onNewCarFormLoaded(e, emit), final NewCarBrandChanged e => _onNewCarBrandChanged(e, emit), final NewCarModelChanged e => _onNewCarModelChanged(e, emit), final NewCarYearChanged e => _onNewCarYearChanged(e, emit), }; } Future _onNewCarFormLoaded( NewCarFormLoaded event, Emitter emit, ) async { emit(const NewCarState.brandsLoadInProgress()); final brands = await _newCarRepository.fetchBrands(); emit(NewCarState.brandsLoadSuccess(brands: brands)); } Future _onNewCarBrandChanged( NewCarBrandChanged event, Emitter emit, ) async { emit( NewCarState.modelsLoadInProgress( brands: state.brands, brand: event.brand, ), ); final models = await _newCarRepository.fetchModels(brand: event.brand); emit( NewCarState.modelsLoadSuccess( brands: state.brands, brand: event.brand, models: models, ), ); } Future _onNewCarModelChanged( NewCarModelChanged event, Emitter emit, ) async { emit( NewCarState.yearsLoadInProgress( brands: state.brands, brand: state.brand, models: state.models, model: event.model, ), ); final years = await _newCarRepository.fetchYears( brand: state.brand, model: event.model, ); emit( NewCarState.yearsLoadSuccess( brands: state.brands, brand: state.brand, models: state.models, model: event.model, years: years, ), ); } Future _onNewCarYearChanged( NewCarYearChanged event, Emitter emit, ) async { emit(state.copyWith(year: event.year)); } } ================================================ FILE: examples/flutter_dynamic_form/lib/new_car/bloc/new_car_event.dart ================================================ part of 'new_car_bloc.dart'; sealed class NewCarEvent extends Equatable { const NewCarEvent(); @override List get props => []; } final class NewCarFormLoaded extends NewCarEvent { const NewCarFormLoaded(); } final class NewCarBrandChanged extends NewCarEvent { const NewCarBrandChanged({this.brand}); final String? brand; @override List get props => [brand]; } final class NewCarModelChanged extends NewCarEvent { const NewCarModelChanged({this.model}); final String? model; @override List get props => [model]; } final class NewCarYearChanged extends NewCarEvent { const NewCarYearChanged({this.year}); final String? year; @override List get props => [year]; } ================================================ FILE: examples/flutter_dynamic_form/lib/new_car/bloc/new_car_state.dart ================================================ part of 'new_car_bloc.dart'; final class NewCarState extends Equatable { const NewCarState._({ this.brands = const [], this.brand, this.models = const [], this.model, this.years = const [], this.year, }); const NewCarState.initial() : this._(); const NewCarState.brandsLoadInProgress() : this._(); const NewCarState.brandsLoadSuccess({required List brands}) : this._(brands: brands); const NewCarState.modelsLoadInProgress({ required List brands, String? brand, }) : this._(brands: brands, brand: brand); const NewCarState.modelsLoadSuccess({ required List brands, required String? brand, required List models, }) : this._(brands: brands, brand: brand, models: models); const NewCarState.yearsLoadInProgress({ required List brands, required String? brand, required List models, required String? model, }) : this._(brands: brands, brand: brand, models: models, model: model); const NewCarState.yearsLoadSuccess({ required List brands, required String? brand, required List models, required String? model, required List years, }) : this._( brands: brands, brand: brand, models: models, model: model, years: years, ); NewCarState copyWith({ List? brands, String? brand, List? models, String? model, List? years, String? year, }) { return NewCarState._( brands: brands ?? this.brands, brand: brand ?? this.brand, models: models ?? this.models, model: model ?? this.model, years: years ?? this.years, year: year ?? this.year, ); } final List brands; final String? brand; final List models; final String? model; final List years; final String? year; bool get isComplete => brand != null && model != null && year != null; @override List get props => [brands, brand, models, model, years, year]; } ================================================ FILE: examples/flutter_dynamic_form/lib/new_car/new_car.dart ================================================ export 'bloc/new_car_bloc.dart'; export 'view/new_car_page.dart'; ================================================ FILE: examples/flutter_dynamic_form/lib/new_car/view/new_car_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dynamic_form/new_car/new_car.dart'; import 'package:flutter_dynamic_form/new_car_repository.dart'; class NewCarPage extends StatelessWidget { const NewCarPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: BlocProvider( create: (_) => NewCarBloc( newCarRepository: context.read(), )..add(const NewCarFormLoaded()), child: const NewCarForm(), ), ); } } class NewCarForm extends StatelessWidget { const NewCarForm({super.key}); @override Widget build(BuildContext context) { return const Align( alignment: Alignment(0, -3 / 4), child: Padding( padding: EdgeInsets.all(8), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ _BrandDropdownButton(), _ModelDropdownButton(), _YearDropdownButton(), _FormSubmitButton(), ], ), ), ); } } class _BrandDropdownButton extends StatelessWidget { const _BrandDropdownButton(); @override Widget build(BuildContext context) { final brands = context.select((NewCarBloc bloc) => bloc.state.brands); final brand = context.select((NewCarBloc bloc) => bloc.state.brand); return Material( child: DropdownButton( key: const Key('newCarForm_brand_dropdownButton'), items: brands.isNotEmpty ? brands.map((brand) { return DropdownMenuItem(value: brand, child: Text(brand)); }).toList() : const [], value: brand, hint: const Text('Select a Brand'), onChanged: (brand) { context.read().add(NewCarBrandChanged(brand: brand)); }, ), ); } } class _ModelDropdownButton extends StatelessWidget { const _ModelDropdownButton(); @override Widget build(BuildContext context) { final models = context.select((NewCarBloc bloc) => bloc.state.models); final model = context.select((NewCarBloc bloc) => bloc.state.model); return Material( child: DropdownButton( key: const Key('newCarForm_model_dropdownButton'), items: models.isNotEmpty ? models.map((model) { return DropdownMenuItem(value: model, child: Text(model)); }).toList() : const [], value: model, hint: const Text('Select a Model'), onChanged: (model) { context.read().add(NewCarModelChanged(model: model)); }, ), ); } } class _YearDropdownButton extends StatelessWidget { const _YearDropdownButton(); @override Widget build(BuildContext context) { final years = context.select((NewCarBloc bloc) => bloc.state.years); final year = context.select((NewCarBloc bloc) => bloc.state.year); return Material( child: DropdownButton( key: const Key('newCarForm_year_dropdownButton'), items: years.isNotEmpty ? years.map((year) { return DropdownMenuItem(value: year, child: Text(year)); }).toList() : const [], value: year, hint: const Text('Select a Year'), onChanged: (year) { context.read().add(NewCarYearChanged(year: year)); }, ), ); } } class _FormSubmitButton extends StatelessWidget { const _FormSubmitButton(); @override Widget build(BuildContext context) { final state = context.watch().state; void onFormSubmitted() { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( content: Text( 'Submitted ${state.brand} ${state.model} ${state.year}', ), ), ); } return ElevatedButton( onPressed: state.isComplete ? onFormSubmitted : null, child: const Text('Submit'), ); } } ================================================ FILE: examples/flutter_dynamic_form/lib/new_car_repository.dart ================================================ const _delay = Duration(milliseconds: 300); Future wait() => Future.delayed(_delay); class NewCarRepository { Future> fetchBrands() async { await wait(); return ['Chevy', 'Toyota', 'Honda']; } Future> fetchModels({String? brand}) async { await wait(); switch (brand) { case 'Chevy': return ['Malibu', 'Impala']; case 'Toyota': return ['Corolla', 'Supra']; case 'Honda': return ['Civic', 'Accord']; default: return []; } } Future> fetchYears({String? brand, String? model}) async { await wait(); switch (brand) { case 'Chevy': switch (model) { case 'Malibu': return ['2019', '2018']; case 'Impala': return ['2017', '2016']; default: return []; } case 'Toyota': switch (model) { case 'Corolla': return ['2015', '2014']; case 'Supra': return ['2013', '2012']; default: return []; } case 'Honda': switch (model) { case 'Civic': return ['2011', '2010']; case 'Accord': return ['2009', '2008']; default: return []; } default: return []; } } } ================================================ FILE: examples/flutter_dynamic_form/pubspec.yaml ================================================ name: flutter_dynamic_form description: A new Flutter project. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: bloc: ^9.0.0 bloc_concurrency: ^0.3.0 equatable: ^2.0.0 flutter: sdk: flutter flutter_bloc: ^9.1.0 dev_dependencies: bloc_lint: ^0.3.0 bloc_test: ^10.0.0 flutter_test: sdk: flutter mocktail: ^1.0.0 flutter: uses-material-design: true ================================================ FILE: examples/flutter_dynamic_form/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../../packages/bloc bloc_lint: path: ../../packages/bloc_lint bloc_test: path: ../../packages/bloc_test flutter_bloc: path: ../../packages/flutter_bloc ================================================ FILE: examples/flutter_dynamic_form/test/app_test.dart ================================================ import 'package:flutter_dynamic_form/app.dart'; import 'package:flutter_dynamic_form/new_car/new_car.dart'; import 'package:flutter_dynamic_form/new_car_repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockNewCarRepository extends Mock implements NewCarRepository {} void main() { group('MyApp', () { late NewCarRepository newCarRepository; setUp(() { newCarRepository = MockNewCarRepository(); }); testWidgets('renders NewCarPage', (tester) async { when(() => newCarRepository.fetchBrands()).thenAnswer( (_) async => ['honda'], ); await tester.pumpWidget(MyApp(newCarRepository: newCarRepository)); expect(find.byType(NewCarPage), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_dynamic_form/test/new_car/bloc/new_car_bloc_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_dynamic_form/new_car/new_car.dart'; import 'package:flutter_dynamic_form/new_car_repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockNewCarRepository extends Mock implements NewCarRepository {} void main() { const mockBrands = ['Chevy', 'Toyota', 'Honda']; final mockBrand = mockBrands[0]; const mockModels = ['Malibu', 'Impala']; final mockModel = mockModels[0]; const mockYears = ['2008', '2020']; final mockYear = mockYears[0]; group('NewCarBloc', () { late NewCarRepository newCarRepository; setUp(() { newCarRepository = MockNewCarRepository(); }); test('initial state is NewCarState.initial', () { expect( NewCarBloc(newCarRepository: newCarRepository).state, const NewCarState.initial(), ); }); blocTest( 'emits brands loading in progress and brands load success', setUp: () { when(newCarRepository.fetchBrands).thenAnswer((_) async => mockBrands); }, build: () => NewCarBloc(newCarRepository: newCarRepository), act: (bloc) => bloc.add(const NewCarFormLoaded()), expect: () => [ const NewCarState.brandsLoadInProgress(), const NewCarState.brandsLoadSuccess(brands: mockBrands), ], verify: (_) => verify(newCarRepository.fetchBrands).called(1), ); blocTest( 'emits models loading in progress and models load success', setUp: () { when( () => newCarRepository.fetchModels(brand: mockBrand), ).thenAnswer((_) async => mockModels); }, build: () => NewCarBloc(newCarRepository: newCarRepository), act: (bloc) => bloc.add(NewCarBrandChanged(brand: mockBrand)), expect: () => [ NewCarState.modelsLoadInProgress(brands: const [], brand: mockBrand), NewCarState.modelsLoadSuccess( brands: const [], brand: mockBrand, models: mockModels, ), ], verify: (_) { verify(() => newCarRepository.fetchModels(brand: mockBrand)).called(1); }, ); blocTest( 'emits years loading in progress and year load success', setUp: () { when( () => newCarRepository.fetchYears(model: mockModel), ).thenAnswer((_) async => mockYears); }, build: () => NewCarBloc(newCarRepository: newCarRepository), act: (bloc) => bloc.add(NewCarModelChanged(model: mockModel)), expect: () => [ NewCarState.yearsLoadInProgress( brands: const [], brand: null, models: const [], model: mockModel, ), NewCarState.yearsLoadSuccess( brands: const [], brand: null, models: const [], model: mockModel, years: mockYears, ), ], verify: (_) { verify( () => newCarRepository.fetchYears(model: mockModel), ).called(1); }, ); blocTest( 'changes year when NewCarYearChanged is added', build: () => NewCarBloc(newCarRepository: newCarRepository), act: (bloc) => bloc.add(NewCarYearChanged(year: mockYear)), expect: () => [const NewCarState.initial().copyWith(year: mockYear)], ); blocTest( 'emits correct states when complete flow is executed', setUp: () { when( newCarRepository.fetchBrands, ).thenAnswer((_) => Future.value(mockBrands)); when( () => newCarRepository.fetchModels(brand: mockBrand), ).thenAnswer((_) => Future.value(mockModels)); when( () => newCarRepository.fetchYears(brand: mockBrand, model: mockModel), ).thenAnswer((_) => Future.value(mockYears)); }, build: () => NewCarBloc(newCarRepository: newCarRepository), act: (bloc) => bloc ..add(const NewCarFormLoaded()) ..add(NewCarBrandChanged(brand: mockBrand)) ..add(NewCarModelChanged(model: mockModel)) ..add(NewCarYearChanged(year: mockYear)), expect: () => [ const NewCarState.brandsLoadInProgress(), const NewCarState.brandsLoadSuccess(brands: mockBrands), NewCarState.modelsLoadInProgress(brands: mockBrands, brand: mockBrand), NewCarState.modelsLoadSuccess( brands: mockBrands, brand: mockBrand, models: mockModels, ), NewCarState.yearsLoadInProgress( brands: mockBrands, brand: mockBrand, models: mockModels, model: mockModel, ), NewCarState.yearsLoadSuccess( brands: mockBrands, brand: mockBrand, models: mockModels, model: mockModel, years: mockYears, ), NewCarState.yearsLoadSuccess( brands: mockBrands, brand: mockBrand, models: mockModels, model: mockModel, years: mockYears, ).copyWith(year: mockYear), ], verify: (_) => verifyInOrder([ newCarRepository.fetchBrands, () => newCarRepository.fetchModels(brand: mockBrand), () => newCarRepository.fetchYears(brand: mockBrand, model: mockModel), ]), ); }); } ================================================ FILE: examples/flutter_dynamic_form/test/new_car/bloc/new_car_event_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_dynamic_form/new_car/new_car.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('NewCarEvent', () { group('NewCarFormLoaded', () { test('supports value comparison', () { expect(NewCarFormLoaded(), NewCarFormLoaded()); }); }); group('NewCarBrandChanged', () { const mockCarBrand = 'Chevy'; test('supports value comparison', () { expect(NewCarBrandChanged(), NewCarBrandChanged()); expect( NewCarBrandChanged(brand: mockCarBrand), NewCarBrandChanged(brand: mockCarBrand), ); }); }); group('NewCarModelChanged', () { const mockCarModel = 'Malibu'; test('supports value comparison', () { expect(NewCarModelChanged(), NewCarModelChanged()); expect( NewCarModelChanged(model: mockCarModel), NewCarModelChanged(model: mockCarModel), ); }); }); group('NewCarYearChanged', () { const mockYear = '2021'; test('supports value comparison', () { expect(NewCarYearChanged(), NewCarYearChanged()); expect( NewCarYearChanged(year: mockYear), NewCarYearChanged(year: mockYear), ); }); }); }); } ================================================ FILE: examples/flutter_dynamic_form/test/new_car/bloc/new_car_state_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_dynamic_form/new_car/new_car.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('NewCarState', () { const mockBrands = ['Chevy', 'Toyota', 'Honda']; final mockBrand = mockBrands[0]; const mockModels = ['Malibu', 'Impala']; final mockModel = mockModels[0]; const mockYears = ['2008', '2020']; group('NewCarState', () { test('supports value comparison', () { expect(NewCarState.initial(), NewCarState.initial()); expect( NewCarState.brandsLoadInProgress(), NewCarState.brandsLoadInProgress(), ); expect( NewCarState.brandsLoadSuccess(brands: mockBrands), NewCarState.brandsLoadSuccess(brands: mockBrands), ); expect( NewCarState.modelsLoadInProgress(brands: mockBrands), NewCarState.modelsLoadInProgress(brands: mockBrands), ); expect( NewCarState.modelsLoadSuccess( brands: mockBrands, brand: mockBrand, models: mockModels, ), NewCarState.modelsLoadSuccess( brands: mockBrands, brand: mockBrand, models: mockModels, ), ); expect( NewCarState.yearsLoadInProgress( brands: mockBrands, brand: mockBrand, models: mockModels, model: mockModel, ), NewCarState.yearsLoadInProgress( brands: mockBrands, brand: mockBrand, models: mockModels, model: mockModel, ), ); expect( NewCarState.yearsLoadSuccess( brands: mockBrands, brand: mockBrand, models: mockModels, model: mockModel, years: mockYears, ), NewCarState.yearsLoadSuccess( brands: mockBrands, brand: mockBrand, models: mockModels, model: mockModel, years: mockYears, ), ); }); }); }); } ================================================ FILE: examples/flutter_dynamic_form/test/new_car/view/new_car_page_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dynamic_form/new_car/new_car.dart'; import 'package:flutter_dynamic_form/new_car_repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockNewCarRepository extends Mock implements NewCarRepository {} class MockNewCarBloc extends MockBloc implements NewCarBloc {} extension on WidgetTester { Future pumpNewCarPage(NewCarRepository newCarRepository) { return pumpWidget( MaterialApp( home: Scaffold( body: RepositoryProvider.value( value: newCarRepository, child: const NewCarPage(), ), ), ), ); } Future pumpNewCarForm(NewCarBloc newCarBloc) { return pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: newCarBloc, child: const NewCarForm(), ), ), ), ); } } void main() { const brandDropdownButtonKey = Key('newCarForm_brand_dropdownButton'); const modelDropdownButtonKey = Key('newCarForm_model_dropdownButton'); const yearDropdownButtonKey = Key('newCarForm_year_dropdownButton'); late NewCarRepository newCarRepository; late NewCarBloc newCarBloc; const mockBrands = ['Chevy', 'Toyota', 'Honda']; final mockBrand = mockBrands[0]; const mockModels = ['Malibu', 'Impala']; final mockModel = mockModels[0]; const mockYears = ['2008', '2020']; final mockYear = mockYears[0]; setUp(() { newCarRepository = MockNewCarRepository(); newCarBloc = MockNewCarBloc(); }); tearDown(resetMocktailState); group('NewCarPage', () { testWidgets('renders NewCarForm', (tester) async { when(() => newCarRepository.fetchBrands()).thenAnswer( (_) async => ['honda'], ); await tester.pumpNewCarPage(newCarRepository); expect(find.byType(NewCarForm), findsOneWidget); verify(() => newCarRepository.fetchBrands()).called(1); }); }); group('NewCarForm', () { testWidgets('displays SnackBar after a submission', (tester) async { when(() => newCarBloc.state).thenReturn( const NewCarState.initial().copyWith( brand: mockBrand, model: mockModel, year: mockYear, ), ); await tester.pumpNewCarForm(newCarBloc); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.byType(SnackBar), findsOneWidget); expect( find.text('Submitted $mockBrand $mockModel $mockYear'), findsOneWidget, ); }); testWidgets('cannot submit blank form', (tester) async { when(() => newCarBloc.state).thenReturn(const NewCarState.initial()); await tester.pumpNewCarForm(newCarBloc); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.byType(SnackBar), findsNothing); }); testWidgets('can select a brand via DropdownButton', (tester) async { when(() => newCarBloc.state).thenReturn( const NewCarState.initial().copyWith(brands: mockBrands), ); await tester.pumpNewCarForm(newCarBloc..add(const NewCarFormLoaded())); await tester.tap(find.byKey(brandDropdownButtonKey)); await tester.pumpAndSettle(); await tester.tap(find.text(mockBrand).last); verify( () => newCarBloc.add(NewCarBrandChanged(brand: mockBrand)), ).called(1); }); testWidgets('can select a model via DropdownButton', (tester) async { when(() => newCarBloc.state).thenReturn( const NewCarState.initial().copyWith(models: mockModels), ); await tester.pumpNewCarForm(newCarBloc); await tester.tap(find.byKey(modelDropdownButtonKey)); await tester.pumpAndSettle(); await tester.tap(find.text(mockModel).last); verify( () => newCarBloc.add(NewCarModelChanged(model: mockModel)), ).called(1); }); testWidgets('can select a year via DropdownButton', (tester) async { when(() => newCarBloc.state).thenReturn( const NewCarState.initial().copyWith(years: mockYears), ); await tester.pumpNewCarForm(newCarBloc); await tester.tap(find.byKey(yearDropdownButtonKey)); await tester.pumpAndSettle(); await tester.tap(find.text(mockYear).last); verify(() => newCarBloc.add(NewCarYearChanged(year: mockYear))).called(1); }); }); } ================================================ FILE: examples/flutter_dynamic_form/test/new_car_repository_test.dart ================================================ import 'package:flutter_dynamic_form/new_car_repository.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('NewCarRepository', () { late NewCarRepository newCarRepository; setUp(() { newCarRepository = NewCarRepository(); }); group('fetchBrands', () { test('returns car brands', () async { expect( newCarRepository.fetchBrands(), completion( equals(['Chevy', 'Toyota', 'Honda']), ), ); }); }); group('fetchModels', () { test('returns Chevy models', () async { expect( newCarRepository.fetchModels(brand: 'Chevy'), completion(equals(['Malibu', 'Impala'])), ); }); test('returns Toyota models', () async { expect( newCarRepository.fetchModels(brand: 'Toyota'), completion(equals(['Corolla', 'Supra'])), ); }); test('returns Honda models', () async { expect( newCarRepository.fetchModels(brand: 'Honda'), completion(equals(['Civic', 'Accord'])), ); }); test('returns no models', () async { expect( newCarRepository.fetchModels(brand: 'Fake-Brand'), completion(equals([])), ); }); }); group('fetchYears', () { group('Chevy brand', () { const brand = 'Chevy'; test('returns years on Malibu model', () { expect( newCarRepository.fetchYears(brand: brand, model: 'Malibu'), completion(equals(['2019', '2018'])), ); }); test('returns years on Impala model', () { expect( newCarRepository.fetchYears(brand: brand, model: 'Impala'), completion(equals(['2017', '2016'])), ); }); test('returns no years on non-existent model', () { expect( newCarRepository.fetchYears(brand: brand, model: 'Fake-Model'), completion(equals([])), ); }); }); group('Toyota brand', () { const brand = 'Toyota'; test('returns years on Corolla model', () { expect( newCarRepository.fetchYears(brand: brand, model: 'Corolla'), completion(equals(['2015', '2014'])), ); }); test('returns years on Supra model', () { expect( newCarRepository.fetchYears(brand: brand, model: 'Supra'), completion(equals(['2013', '2012'])), ); }); test('returns no years on non-existent model', () { expect( newCarRepository.fetchYears(brand: brand, model: 'Fake-Model'), completion(equals([])), ); }); }); group('Honda brand', () { const brand = 'Honda'; test('returns years on Civic model', () { expect( newCarRepository.fetchYears(brand: brand, model: 'Civic'), completion(equals(['2011', '2010'])), ); }); test('returns years on Accord model', () { expect( newCarRepository.fetchYears(brand: brand, model: 'Accord'), completion(equals(['2009', '2008'])), ); }); test('returns no years on non-existent model', () { expect( newCarRepository.fetchYears(brand: brand, model: 'Fake-Model'), completion(equals([])), ); }); }); group('No brand', () { const brand = 'Fake-Brand'; test('returns no years on non-existent model', () { expect( newCarRepository.fetchYears(brand: brand, model: 'Fake-Model'), completion(equals([])), ); }); }); }); }); } ================================================ FILE: examples/flutter_dynamic_form/web/index.html ================================================ flutter_dynamic_form ================================================ FILE: examples/flutter_dynamic_form/web/manifest.json ================================================ { "name": "flutter_dynamic_form", "short_name": "flutter_dynamic_form", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: examples/flutter_firebase_login/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: examples/flutter_firebase_login/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "d693b4b9dbac2acd4477aea4555ca6dcbea44ba2" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2 base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2 - platform: android create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2 base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2 # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/flutter_firebase_login/README.md ================================================ [![build](https://github.com/felangel/bloc/actions/workflows/main.yaml/badge.svg)](https://github.com/felangel/bloc/actions) # flutter_firebase_login Example Flutter app built with `flutter_bloc` to implement login using Firebase. ## Features - Sign in with Google - Sign up with email and password - Sign in with email and password ## Getting Started ### Firebase 1. Create your project 2. Enable desired authentication options ### iOS 1. Replace `./ios/Runner/GoogleService-Info.plist` with your own 2. Update `./ios/Runner/info.plist` - Paste the `REVERSED_CLIENT_ID` from `GoogleService-Info.plist` to key `CFBundleURLSchemes` in `info.plist` ### Android 1. Replace `./android/app/google-services.json` with your own 2. Update `./android/app/build.gradle` - Replace `"com.example.flutter_firebase_login"` with the `package_name` from `google-services.json` ### Run the project 1. `flutter run` ================================================ FILE: examples/flutter_firebase_login/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml linter: rules: public_member_api_docs: false ================================================ FILE: examples/flutter_firebase_login/android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/to/reference-keystore key.properties **/*.keystore **/*.jks ================================================ FILE: examples/flutter_firebase_login/android/app/build.gradle.kts ================================================ plugins { id("com.android.application") id("com.google.gms.google-services") id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") } android { namespace = "com.example.flutter_firebase_login" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "com.example.flutter_firebase_login" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig = signingConfigs.getByName("debug") } } } flutter { source = "../.." } ================================================ FILE: examples/flutter_firebase_login/android/app/google-services.json ================================================ { "project_info": { "project_number": "979633879366", "firebase_url": "https://flutter-firebase-auth-2b1c3.firebaseio.com", "project_id": "flutter-firebase-auth-2b1c3", "storage_bucket": "flutter-firebase-auth-2b1c3.appspot.com" }, "client": [ { "client_info": { "mobilesdk_app_id": "1:979633879366:android:91dc73f56b484d46d98d47", "android_client_info": { "package_name": "com.example.flutter_firebase_login" } }, "oauth_client": [ { "client_id": "979633879366-5f9f4h5o9bf741b22ahggarebl9iiktq.apps.googleusercontent.com", "client_type": 1, "android_info": { "package_name": "com.example.flutter_firebase_login", "certificate_hash": "d16ccecc133cd0d68b93ed0d06b0ff98cf49de9a" } }, { "client_id": "979633879366-lucrjde7auulec5l2pouo6aa34u4576d.apps.googleusercontent.com", "client_type": 3 } ], "api_key": [ { "current_key": "AIzaSyCUqu6Uggf0FRSPljkCT8GNlgPO9OzSa2g" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [ { "client_id": "979633879366-lucrjde7auulec5l2pouo6aa34u4576d.apps.googleusercontent.com", "client_type": 3 }, { "client_id": "979633879366-7fh9utsod6e5pfjh2l01fh6dk2chjc8d.apps.googleusercontent.com", "client_type": 2, "ios_info": { "bundle_id": "com.bloclibrary.flutterFirebaseLogin" } } ] } } }, { "client_info": { "mobilesdk_app_id": "1:979633879366:android:cd6c9969cbe46460", "android_client_info": { "package_name": "com.example.flutterfirebaselogin" } }, "oauth_client": [ { "client_id": "979633879366-lucrjde7auulec5l2pouo6aa34u4576d.apps.googleusercontent.com", "client_type": 3 } ], "api_key": [ { "current_key": "AIzaSyCUqu6Uggf0FRSPljkCT8GNlgPO9OzSa2g" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [ { "client_id": "979633879366-lucrjde7auulec5l2pouo6aa34u4576d.apps.googleusercontent.com", "client_type": 3 }, { "client_id": "979633879366-7fh9utsod6e5pfjh2l01fh6dk2chjc8d.apps.googleusercontent.com", "client_type": 2, "ios_info": { "bundle_id": "com.bloclibrary.flutterFirebaseLogin" } } ] } } } ], "configuration_version": "1" } ================================================ FILE: examples/flutter_firebase_login/android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: examples/flutter_firebase_login/android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: examples/flutter_firebase_login/android/app/src/main/kotlin/com/example/flutter_firebase_login/MainActivity.kt ================================================ package com.example.flutter_firebase_login import io.flutter.embedding.android.FlutterActivity class MainActivity: FlutterActivity() ================================================ FILE: examples/flutter_firebase_login/android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: examples/flutter_firebase_login/android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: examples/flutter_firebase_login/android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: examples/flutter_firebase_login/android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: examples/flutter_firebase_login/android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: examples/flutter_firebase_login/android/build.gradle.kts ================================================ allprojects { repositories { google() mavenCentral() } } val newBuildDir: Directory = rootProject.layout.buildDirectory .dir("../../build") .get() rootProject.layout.buildDirectory.value(newBuildDir) subprojects { val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) project.layout.buildDirectory.value(newSubprojectBuildDir) } subprojects { project.evaluationDependsOn(":app") } tasks.register("clean") { delete(rootProject.layout.buildDirectory) } ================================================ FILE: examples/flutter_firebase_login/android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip ================================================ FILE: examples/flutter_firebase_login/android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true android.enableJetifier=true ================================================ FILE: examples/flutter_firebase_login/android/settings.gradle.kts ================================================ pluginManagement { val flutterSdkPath = run { val properties = java.util.Properties() file("local.properties").inputStream().use { properties.load(it) } val flutterSdkPath = properties.getProperty("flutter.sdk") require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } flutterSdkPath } includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") repositories { google() mavenCentral() gradlePluginPortal() } } plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.9.1" apply false id ("com.google.gms.google-services") version "4.3.15" apply false id("org.jetbrains.kotlin.android") version "2.1.0" apply false } include(":app") ================================================ FILE: examples/flutter_firebase_login/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: examples/flutter_firebase_login/ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 13.0 ================================================ FILE: examples/flutter_firebase_login/ios/Flutter/Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: examples/flutter_firebase_login/ios/Flutter/Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: examples/flutter_firebase_login/ios/Podfile ================================================ # Uncomment this line to define a global platform for your project # platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end ================================================ FILE: examples/flutter_firebase_login/ios/Runner/AppDelegate.swift ================================================ import UIKit import Flutter @main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } ================================================ FILE: examples/flutter_firebase_login/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "Icon-App-1024x1024@1x.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: examples/flutter_firebase_login/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "LaunchImage.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "LaunchImage@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "LaunchImage@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: examples/flutter_firebase_login/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. ================================================ FILE: examples/flutter_firebase_login/ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: examples/flutter_firebase_login/ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: examples/flutter_firebase_login/ios/Runner/GoogleService-Info.plist ================================================ CLIENT_ID 979633879366-ctkb3hh1kse9ljd4c3lhs9djigaukmu2.apps.googleusercontent.com REVERSED_CLIENT_ID com.googleusercontent.apps.979633879366-ctkb3hh1kse9ljd4c3lhs9djigaukmu2 API_KEY AIzaSyDVqdUKRfDx2AJbcbGp9MDMq_GdYqlOBXA GCM_SENDER_ID 979633879366 PLIST_VERSION 1 BUNDLE_ID com.example.flutterFirebaseLogin PROJECT_ID flutter-firebase-auth-2b1c3 STORAGE_BUCKET flutter-firebase-auth-2b1c3.appspot.com IS_ADS_ENABLED IS_ANALYTICS_ENABLED IS_APPINVITE_ENABLED IS_GCM_ENABLED IS_SIGNIN_ENABLED GOOGLE_APP_ID 1:979633879366:ios:45c728c386ca2b9bd98d47 DATABASE_URL https://flutter-firebase-auth-2b1c3.firebaseio.com ================================================ FILE: examples/flutter_firebase_login/ios/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName Flutter Firebase Login CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName flutter_firebase_login CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLSchemes com.googleusercontent.apps.979633879366-ctkb3hh1kse9ljd4c3lhs9djigaukmu2 CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents ================================================ FILE: examples/flutter_firebase_login/ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: examples/flutter_firebase_login/ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 101895F97E16CE730BB23D30 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7D9CF32C2591DC1A9362B866 /* GoogleService-Info.plist */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 6B35C96F2FDC4A10A2082240 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F907645AC58FE1557878117 /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; F583EFC4D4CF51F4978597AC /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15F570C26B5F42E37708B7BD /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; proxyType = 1; remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 15F570C26B5F42E37708B7BD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 272CEB1E0934A2ADD5D44C56 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 36211D31DD83A06C480B7933 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 3810D845A293D96D307AF84D /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 47257ACFBA77E1966ABB77F1 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7D9CF32C2591DC1A9362B866 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9F907645AC58FE1557878117 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B2D2646CC85C5457B8D0C536 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; E6B39746540F83CD989EDC9B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 7BF2C52BF829726895EFB865 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 6B35C96F2FDC4A10A2082240 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F583EFC4D4CF51F4978597AC /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( 331C807B294A618700263BE5 /* RunnerTests.swift */, ); path = RunnerTests; sourceTree = ""; }; 92053946832FBC8B5CE45542 /* Pods */ = { isa = PBXGroup; children = ( 272CEB1E0934A2ADD5D44C56 /* Pods-Runner.debug.xcconfig */, B2D2646CC85C5457B8D0C536 /* Pods-Runner.release.xcconfig */, 47257ACFBA77E1966ABB77F1 /* Pods-Runner.profile.xcconfig */, 36211D31DD83A06C480B7933 /* Pods-RunnerTests.debug.xcconfig */, E6B39746540F83CD989EDC9B /* Pods-RunnerTests.release.xcconfig */, 3810D845A293D96D307AF84D /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 7D9CF32C2591DC1A9362B866 /* GoogleService-Info.plist */, 92053946832FBC8B5CE45542 /* Pods */, BB0F7C07A7CAC845A63B396E /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; BB0F7C07A7CAC845A63B396E /* Frameworks */ = { isa = PBXGroup; children = ( 15F570C26B5F42E37708B7BD /* Pods_Runner.framework */, 9F907645AC58FE1557878117 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 331C8080294A63A400263BE5 /* RunnerTests */ = { isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( 496B4F410984E7F8DE7E9A59 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, 7BF2C52BF829726895EFB865 /* Frameworks */, ); buildRules = ( ); dependencies = ( 331C8086294A63A400263BE5 /* PBXTargetDependency */, ); name = RunnerTests; productName = RunnerTests; productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( 5317527F9B4101BFF11BB5E3 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, FCB1BED7BFC4D59CA29B954A /* [CP] Embed Pods Frameworks */, 43A609F4BA8157E3189042DA /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { CreatedOnToolsVersion = 14.0; TestTargetID = 97C146ED1CF9000F007C117D; }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 331C807F294A63A400263BE5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 101895F97E16CE730BB23D30 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 43A609F4BA8157E3189042DA /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; 496B4F410984E7F8DE7E9A59 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 5317527F9B4101BFF11BB5E3 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; FCB1BED7BFC4D59CA29B954A /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 331C807D294A63A400263BE5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = HNHCWRWB4P; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterFirebaseLogin; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 36211D31DD83A06C480B7933 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterFirebaseLogin.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Debug; }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = E6B39746540F83CD989EDC9B /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterFirebaseLogin.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Release; }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 3810D845A293D96D307AF84D /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterFirebaseLogin.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = HNHCWRWB4P; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterFirebaseLogin; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = HNHCWRWB4P; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterFirebaseLogin; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 331C8088294A63A400263BE5 /* Debug */, 331C8089294A63A400263BE5 /* Release */, 331C808A294A63A400263BE5 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: examples/flutter_firebase_login/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: examples/flutter_firebase_login/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: examples/flutter_firebase_login/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: examples/flutter_firebase_login/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: examples/flutter_firebase_login/ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: examples/flutter_firebase_login/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: examples/flutter_firebase_login/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: examples/flutter_firebase_login/ios/RunnerTests/RunnerTests.swift ================================================ import Flutter import UIKit import XCTest class RunnerTests: XCTestCase { func testExample() { // If you add code to the Runner application, consider adding tests here. // See https://developer.apple.com/documentation/xctest for more information about using XCTest. } } ================================================ FILE: examples/flutter_firebase_login/lib/app/app.dart ================================================ export 'bloc/app_bloc.dart'; export 'bloc_observer.dart'; export 'routes/routes.dart'; export 'view/app.dart'; ================================================ FILE: examples/flutter_firebase_login/lib/app/bloc/app_bloc.dart ================================================ import 'dart:async'; import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; part 'app_event.dart'; part 'app_state.dart'; class AppBloc extends Bloc { AppBloc({required AuthenticationRepository authenticationRepository}) : _authenticationRepository = authenticationRepository, super(AppState(user: authenticationRepository.currentUser)) { on(_onUserSubscriptionRequested); on(_onLogoutPressed); } final AuthenticationRepository _authenticationRepository; Future _onUserSubscriptionRequested( AppUserSubscriptionRequested event, Emitter emit, ) { return emit.onEach( _authenticationRepository.user, onData: (user) => emit(AppState(user: user)), onError: addError, ); } void _onLogoutPressed( AppLogoutPressed event, Emitter emit, ) { _authenticationRepository.logOut(); } } ================================================ FILE: examples/flutter_firebase_login/lib/app/bloc/app_event.dart ================================================ part of 'app_bloc.dart'; sealed class AppEvent { const AppEvent(); } final class AppUserSubscriptionRequested extends AppEvent { const AppUserSubscriptionRequested(); } final class AppLogoutPressed extends AppEvent { const AppLogoutPressed(); } ================================================ FILE: examples/flutter_firebase_login/lib/app/bloc/app_state.dart ================================================ part of 'app_bloc.dart'; enum AppStatus { authenticated, unauthenticated } final class AppState extends Equatable { const AppState({User user = User.empty}) : this._( status: user == User.empty ? AppStatus.unauthenticated : AppStatus.authenticated, user: user, ); const AppState._({required this.status, this.user = User.empty}); final AppStatus status; final User user; @override List get props => [status, user]; } ================================================ FILE: examples/flutter_firebase_login/lib/app/bloc_observer.dart ================================================ // ignore_for_file: avoid_print import 'package:bloc/bloc.dart'; class AppBlocObserver extends BlocObserver { const AppBlocObserver(); @override void onEvent(Bloc bloc, Object? event) { super.onEvent(bloc, event); print(event); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { print(error); super.onError(bloc, error, stackTrace); } @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); print(change); } @override void onTransition( Bloc bloc, Transition transition, ) { super.onTransition(bloc, transition); print(transition); } } ================================================ FILE: examples/flutter_firebase_login/lib/app/routes/routes.dart ================================================ import 'package:flutter/widgets.dart'; import 'package:flutter_firebase_login/app/app.dart'; import 'package:flutter_firebase_login/home/home.dart'; import 'package:flutter_firebase_login/login/login.dart'; List> onGenerateAppViewPages( AppStatus state, List> pages, ) { switch (state) { case AppStatus.authenticated: return [HomePage.page()]; case AppStatus.unauthenticated: return [LoginPage.page()]; } } ================================================ FILE: examples/flutter_firebase_login/lib/app/view/app.dart ================================================ import 'package:authentication_repository/authentication_repository.dart'; import 'package:flow_builder/flow_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_firebase_login/app/app.dart'; import 'package:flutter_firebase_login/theme.dart'; class App extends StatelessWidget { const App({ required AuthenticationRepository authenticationRepository, super.key, }) : _authenticationRepository = authenticationRepository; final AuthenticationRepository _authenticationRepository; @override Widget build(BuildContext context) { return RepositoryProvider.value( value: _authenticationRepository, child: BlocProvider( lazy: false, create: (_) => AppBloc( authenticationRepository: _authenticationRepository, )..add(const AppUserSubscriptionRequested()), child: const AppView(), ), ); } } class AppView extends StatelessWidget { const AppView({super.key}); @override Widget build(BuildContext context) { return MaterialApp( theme: theme, home: FlowBuilder( state: context.select((AppBloc bloc) => bloc.state.status), onGeneratePages: onGenerateAppViewPages, ), ); } } ================================================ FILE: examples/flutter_firebase_login/lib/home/home.dart ================================================ export 'view/home_page.dart'; export 'widgets/widgets.dart'; ================================================ FILE: examples/flutter_firebase_login/lib/home/view/home_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_firebase_login/app/app.dart'; import 'package:flutter_firebase_login/home/home.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); static Page page() => const MaterialPage(child: HomePage()); @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; final user = context.select((AppBloc bloc) => bloc.state.user); return Scaffold( appBar: AppBar( title: const Text('Home'), actions: [ IconButton( key: const Key('homePage_logout_iconButton'), icon: const Icon(Icons.exit_to_app), onPressed: () { context.read().add(const AppLogoutPressed()); }, ), ], ), body: Align( alignment: const Alignment(0, -1 / 3), child: Column( mainAxisSize: MainAxisSize.min, children: [ Avatar(photo: user.photo), const SizedBox(height: 4), Text(user.email ?? '', style: textTheme.titleLarge), const SizedBox(height: 4), Text(user.name ?? '', style: textTheme.headlineSmall), ], ), ), ); } } ================================================ FILE: examples/flutter_firebase_login/lib/home/widgets/avatar.dart ================================================ import 'package:flutter/material.dart'; const _avatarSize = 48.0; class Avatar extends StatelessWidget { const Avatar({super.key, this.photo}); final String? photo; @override Widget build(BuildContext context) { final photo = this.photo; return CircleAvatar( radius: _avatarSize, backgroundImage: photo != null ? NetworkImage(photo) : null, child: photo == null ? const Icon(Icons.person_outline, size: _avatarSize) : null, ); } } ================================================ FILE: examples/flutter_firebase_login/lib/home/widgets/widgets.dart ================================================ export 'avatar.dart'; ================================================ FILE: examples/flutter_firebase_login/lib/login/cubit/login_cubit.dart ================================================ import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:form_inputs/form_inputs.dart'; import 'package:formz/formz.dart'; part 'login_state.dart'; class LoginCubit extends Cubit { LoginCubit(this._authenticationRepository) : super(const LoginState()); final AuthenticationRepository _authenticationRepository; void emailChanged(String email) => emit(state.withEmail(email)); void passwordChanged(String password) => emit(state.withPassword(password)); Future logInWithCredentials() async { if (!state.isValid) return; emit(state.withSubmissionInProgress()); try { await _authenticationRepository.logInWithEmailAndPassword( email: state.email.value, password: state.password.value, ); emit(state.withSubmissionSuccess()); } on LogInWithEmailAndPasswordFailure catch (e) { emit(state.withSubmissionFailure(e.message)); } catch (_) { emit(state.withSubmissionFailure()); } } Future logInWithGoogle() async { emit(state.withSubmissionInProgress()); try { await _authenticationRepository.logInWithGoogle(); emit(state.withSubmissionSuccess()); } on LogInWithGoogleFailure catch (e) { emit(state.withSubmissionFailure(e.message)); } catch (_) { emit(state.withSubmissionFailure()); } } } ================================================ FILE: examples/flutter_firebase_login/lib/login/cubit/login_state.dart ================================================ part of 'login_cubit.dart'; final class LoginState extends Equatable { const LoginState() : this._(); const LoginState._({ this.email = const Email.pure(), this.password = const Password.pure(), this.status = FormzSubmissionStatus.initial, this.errorMessage, }); LoginState withEmail(String email) { return LoginState._(email: Email.dirty(email), password: password); } LoginState withPassword(String password) { return LoginState._(email: email, password: Password.dirty(password)); } LoginState withSubmissionInProgress() { return LoginState._( email: email, password: password, status: FormzSubmissionStatus.inProgress, ); } LoginState withSubmissionSuccess() { return LoginState._( email: email, password: password, status: FormzSubmissionStatus.success, ); } LoginState withSubmissionFailure([String? error]) { return LoginState._( email: email, password: password, status: FormzSubmissionStatus.failure, errorMessage: error, ); } final Email email; final Password password; final FormzSubmissionStatus status; final String? errorMessage; bool get isValid => Formz.validate([email, password]); @override List get props => [email, password, status, errorMessage]; } ================================================ FILE: examples/flutter_firebase_login/lib/login/login.dart ================================================ export 'cubit/login_cubit.dart'; export 'view/view.dart'; ================================================ FILE: examples/flutter_firebase_login/lib/login/view/login_form.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_firebase_login/login/login.dart'; import 'package:flutter_firebase_login/sign_up/sign_up.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:formz/formz.dart'; class LoginForm extends StatelessWidget { const LoginForm({super.key}); @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { if (state.status.isFailure) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( content: Text(state.errorMessage ?? 'Authentication Failure'), ), ); } }, child: Align( alignment: const Alignment(0, -1 / 3), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ Image.asset( 'assets/bloc_logo_small.png', height: 120, ), const SizedBox(height: 16), _EmailInput(), const SizedBox(height: 8), _PasswordInput(), const SizedBox(height: 8), _LoginButton(), const SizedBox(height: 8), _GoogleLoginButton(), const SizedBox(height: 4), _SignUpButton(), ], ), ), ), ); } } class _EmailInput extends StatelessWidget { @override Widget build(BuildContext context) { final displayError = context.select( (LoginCubit cubit) => cubit.state.email.displayError, ); return TextField( key: const Key('loginForm_emailInput_textField'), onChanged: (email) => context.read().emailChanged(email), keyboardType: TextInputType.emailAddress, decoration: InputDecoration( labelText: 'email', helperText: '', errorText: displayError != null ? 'invalid email' : null, ), ); } } class _PasswordInput extends StatelessWidget { @override Widget build(BuildContext context) { final displayError = context.select( (LoginCubit cubit) => cubit.state.password.displayError, ); return TextField( key: const Key('loginForm_passwordInput_textField'), onChanged: (password) => context.read().passwordChanged(password), obscureText: true, decoration: InputDecoration( labelText: 'password', helperText: '', errorText: displayError != null ? 'invalid password' : null, ), ); } } class _LoginButton extends StatelessWidget { @override Widget build(BuildContext context) { final isInProgress = context.select( (LoginCubit cubit) => cubit.state.status.isInProgress, ); if (isInProgress) return const CircularProgressIndicator(); final isValid = context.select( (LoginCubit cubit) => cubit.state.isValid, ); return ElevatedButton( key: const Key('loginForm_continue_raisedButton'), style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ), backgroundColor: const Color(0xFFFFD600), ), onPressed: isValid ? () => context.read().logInWithCredentials() : null, child: const Text('LOGIN'), ); } } class _GoogleLoginButton extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); return ElevatedButton.icon( key: const Key('loginForm_googleLogin_raisedButton'), label: const Text( 'SIGN IN WITH GOOGLE', style: TextStyle(color: Colors.white), ), style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ), backgroundColor: theme.colorScheme.secondary, ), icon: const Icon(FontAwesomeIcons.google, color: Colors.white), onPressed: () => context.read().logInWithGoogle(), ); } } class _SignUpButton extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); return TextButton( key: const Key('loginForm_createAccount_flatButton'), onPressed: () => Navigator.of(context).push(SignUpPage.route()), child: Text( 'CREATE ACCOUNT', style: TextStyle(color: theme.primaryColor), ), ); } } ================================================ FILE: examples/flutter_firebase_login/lib/login/view/login_page.dart ================================================ import 'package:authentication_repository/authentication_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_firebase_login/login/login.dart'; class LoginPage extends StatelessWidget { const LoginPage({super.key}); static Page page() => const MaterialPage(child: LoginPage()); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Login')), body: Padding( padding: const EdgeInsets.all(8), child: BlocProvider( create: (_) => LoginCubit(context.read()), child: const LoginForm(), ), ), ); } } ================================================ FILE: examples/flutter_firebase_login/lib/login/view/view.dart ================================================ export 'login_form.dart'; export 'login_page.dart'; ================================================ FILE: examples/flutter_firebase_login/lib/main.dart ================================================ import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc/bloc.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_firebase_login/app/app.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); Bloc.observer = const AppBlocObserver(); await Firebase.initializeApp(); final authenticationRepository = AuthenticationRepository(); await authenticationRepository.user.first; runApp(App(authenticationRepository: authenticationRepository)); } ================================================ FILE: examples/flutter_firebase_login/lib/sign_up/cubit/sign_up_cubit.dart ================================================ import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:form_inputs/form_inputs.dart'; import 'package:formz/formz.dart'; part 'sign_up_state.dart'; class SignUpCubit extends Cubit { SignUpCubit(this._authenticationRepository) : super(const SignUpState()); final AuthenticationRepository _authenticationRepository; void emailChanged(String email) => emit(state.withEmail(email)); void passwordChanged(String password) => emit(state.withPassword(password)); void confirmedPasswordChanged(String confirmedPassword) { emit(state.withConfirmedPassword(confirmedPassword)); } Future signUpFormSubmitted() async { if (!state.isValid) return; emit(state.withSubmissionInProgress()); try { await _authenticationRepository.signUp( email: state.email.value, password: state.password.value, ); emit(state.withSubmissionSuccess()); } on SignUpWithEmailAndPasswordFailure catch (e) { emit(state.withSubmissionFailure(e.message)); } catch (_) { emit(state.withSubmissionFailure()); } } } ================================================ FILE: examples/flutter_firebase_login/lib/sign_up/cubit/sign_up_state.dart ================================================ part of 'sign_up_cubit.dart'; final class SignUpState extends Equatable { const SignUpState() : this._(); const SignUpState._({ this.email = const Email.pure(), this.password = const Password.pure(), this.confirmedPassword = const ConfirmedPassword.pure(), this.status = FormzSubmissionStatus.initial, this.errorMessage, }); final Email email; final Password password; final ConfirmedPassword confirmedPassword; final FormzSubmissionStatus status; final String? errorMessage; SignUpState withEmail(String email) { return SignUpState._( email: Email.dirty(email), password: password, confirmedPassword: confirmedPassword, ); } SignUpState withPassword(String password) { return SignUpState._( email: email, password: Password.dirty(password), confirmedPassword: ConfirmedPassword.dirty( password: password, value: confirmedPassword.value, ), ); } SignUpState withConfirmedPassword(String confirmedPassword) { return SignUpState._( email: email, password: password, confirmedPassword: ConfirmedPassword.dirty( password: password.value, value: confirmedPassword, ), ); } SignUpState withSubmissionInProgress() { return SignUpState._( email: email, password: password, confirmedPassword: confirmedPassword, status: FormzSubmissionStatus.inProgress, ); } SignUpState withSubmissionSuccess() { return SignUpState._( email: email, password: password, confirmedPassword: confirmedPassword, status: FormzSubmissionStatus.success, ); } SignUpState withSubmissionFailure([String? error]) { return SignUpState._( email: email, password: password, confirmedPassword: confirmedPassword, status: FormzSubmissionStatus.failure, errorMessage: error, ); } bool get isValid => Formz.validate([email, password, confirmedPassword]); @override List get props => [ email, password, confirmedPassword, status, errorMessage, ]; } ================================================ FILE: examples/flutter_firebase_login/lib/sign_up/sign_up.dart ================================================ export 'cubit/sign_up_cubit.dart'; export 'view/view.dart'; ================================================ FILE: examples/flutter_firebase_login/lib/sign_up/view/sign_up_form.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_firebase_login/sign_up/sign_up.dart'; import 'package:formz/formz.dart'; class SignUpForm extends StatelessWidget { const SignUpForm({super.key}); @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { if (state.status.isSuccess) { Navigator.of(context).pop(); } else if (state.status.isFailure) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar(content: Text(state.errorMessage ?? 'Sign Up Failure')), ); } }, child: Align( alignment: const Alignment(0, -1 / 3), child: Column( mainAxisSize: MainAxisSize.min, children: [ _EmailInput(), const SizedBox(height: 8), _PasswordInput(), const SizedBox(height: 8), _ConfirmPasswordInput(), const SizedBox(height: 8), _SignUpButton(), ], ), ), ); } } class _EmailInput extends StatelessWidget { @override Widget build(BuildContext context) { final displayError = context.select( (SignUpCubit cubit) => cubit.state.email.displayError, ); return TextField( key: const Key('signUpForm_emailInput_textField'), onChanged: (email) => context.read().emailChanged(email), keyboardType: TextInputType.emailAddress, decoration: InputDecoration( labelText: 'email', helperText: '', errorText: displayError != null ? 'invalid email' : null, ), ); } } class _PasswordInput extends StatelessWidget { @override Widget build(BuildContext context) { final displayError = context.select( (SignUpCubit cubit) => cubit.state.password.displayError, ); return TextField( key: const Key('signUpForm_passwordInput_textField'), onChanged: (password) => context.read().passwordChanged(password), obscureText: true, decoration: InputDecoration( labelText: 'password', helperText: '', errorText: displayError != null ? 'invalid password' : null, ), ); } } class _ConfirmPasswordInput extends StatelessWidget { @override Widget build(BuildContext context) { final displayError = context.select( (SignUpCubit cubit) => cubit.state.confirmedPassword.displayError, ); return TextField( key: const Key('signUpForm_confirmedPasswordInput_textField'), onChanged: (confirmPassword) => context.read().confirmedPasswordChanged(confirmPassword), obscureText: true, decoration: InputDecoration( labelText: 'confirm password', helperText: '', errorText: displayError != null ? 'passwords do not match' : null, ), ); } } class _SignUpButton extends StatelessWidget { @override Widget build(BuildContext context) { final isInProgress = context.select( (SignUpCubit cubit) => cubit.state.status.isInProgress, ); if (isInProgress) return const CircularProgressIndicator(); final isValid = context.select( (SignUpCubit cubit) => cubit.state.isValid, ); return ElevatedButton( key: const Key('signUpForm_continue_raisedButton'), style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ), backgroundColor: Colors.orangeAccent, ), onPressed: isValid ? () => context.read().signUpFormSubmitted() : null, child: const Text('SIGN UP'), ); } } ================================================ FILE: examples/flutter_firebase_login/lib/sign_up/view/sign_up_page.dart ================================================ import 'package:authentication_repository/authentication_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_firebase_login/sign_up/sign_up.dart'; class SignUpPage extends StatelessWidget { const SignUpPage({super.key}); static Route route() { return MaterialPageRoute(builder: (_) => const SignUpPage()); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Sign Up')), body: Padding( padding: const EdgeInsets.all(8), child: BlocProvider( create: (_) => SignUpCubit(context.read()), child: const SignUpForm(), ), ), ); } } ================================================ FILE: examples/flutter_firebase_login/lib/sign_up/view/view.dart ================================================ export 'sign_up_form.dart'; export 'sign_up_page.dart'; ================================================ FILE: examples/flutter_firebase_login/lib/theme.dart ================================================ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; final theme = ThemeData( textTheme: GoogleFonts.openSansTextTheme(), appBarTheme: const AppBarTheme( backgroundColor: Color.fromARGB(255, 113, 243, 230), elevation: 4, ), colorScheme: const ColorScheme.light( primary: Color(0xFF0097A7), secondary: Color(0xFF009688), surface: Color(0xFFE0F2F1), ), inputDecorationTheme: InputDecorationTheme( border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), ); ================================================ FILE: examples/flutter_firebase_login/packages/authentication_repository/analysis_options.yaml ================================================ include: ../../../../analysis_options.yaml ================================================ FILE: examples/flutter_firebase_login/packages/authentication_repository/lib/authentication_repository.dart ================================================ export 'src/authentication_repository.dart'; export 'src/models/models.dart'; ================================================ FILE: examples/flutter_firebase_login/packages/authentication_repository/lib/src/authentication_repository.dart ================================================ import 'dart:async'; import 'package:authentication_repository/authentication_repository.dart'; import 'package:cache/cache.dart'; import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:google_sign_in/google_sign_in.dart'; import 'package:meta/meta.dart'; /// {@template sign_up_with_email_and_password_failure} /// Thrown during the sign up process if a failure occurs. /// {@endtemplate} class SignUpWithEmailAndPasswordFailure implements Exception { /// {@macro sign_up_with_email_and_password_failure} const SignUpWithEmailAndPasswordFailure([ this.message = 'An unknown exception occurred.', ]); /// Create an authentication message /// from a firebase authentication exception code. /// https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/createUserWithEmailAndPassword.html factory SignUpWithEmailAndPasswordFailure.fromCode(String code) { switch (code) { case 'invalid-email': return const SignUpWithEmailAndPasswordFailure( 'Email is not valid or badly formatted.', ); case 'user-disabled': return const SignUpWithEmailAndPasswordFailure( 'This user has been disabled. Please contact support for help.', ); case 'email-already-in-use': return const SignUpWithEmailAndPasswordFailure( 'An account already exists for that email.', ); case 'operation-not-allowed': return const SignUpWithEmailAndPasswordFailure( 'Operation is not allowed. Please contact support.', ); case 'weak-password': return const SignUpWithEmailAndPasswordFailure( 'Please enter a stronger password.', ); default: return const SignUpWithEmailAndPasswordFailure(); } } /// The associated error message. final String message; } /// {@template log_in_with_email_and_password_failure} /// Thrown during the login process if a failure occurs. /// https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/signInWithEmailAndPassword.html /// {@endtemplate} class LogInWithEmailAndPasswordFailure implements Exception { /// {@macro log_in_with_email_and_password_failure} const LogInWithEmailAndPasswordFailure([ this.message = 'An unknown exception occurred.', ]); /// Create an authentication message /// from a firebase authentication exception code. factory LogInWithEmailAndPasswordFailure.fromCode(String code) { switch (code) { case 'invalid-email': return const LogInWithEmailAndPasswordFailure( 'Email is not valid or badly formatted.', ); case 'user-disabled': return const LogInWithEmailAndPasswordFailure( 'This user has been disabled. Please contact support for help.', ); case 'user-not-found': return const LogInWithEmailAndPasswordFailure( 'Email is not found, please create an account.', ); case 'wrong-password': return const LogInWithEmailAndPasswordFailure( 'Incorrect password, please try again.', ); default: return const LogInWithEmailAndPasswordFailure(); } } /// The associated error message. final String message; } /// {@template log_in_with_google_failure} /// Thrown during the sign in with google process if a failure occurs. /// https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/signInWithCredential.html /// {@endtemplate} class LogInWithGoogleFailure implements Exception { /// {@macro log_in_with_google_failure} const LogInWithGoogleFailure([ this.message = 'An unknown exception occurred.', ]); /// Create an authentication message /// from a firebase authentication exception code. factory LogInWithGoogleFailure.fromCode(String code) { switch (code) { case 'account-exists-with-different-credential': return const LogInWithGoogleFailure( 'Account exists with different credentials.', ); case 'invalid-credential': return const LogInWithGoogleFailure( 'The credential received is malformed or has expired.', ); case 'operation-not-allowed': return const LogInWithGoogleFailure( 'Operation is not allowed. Please contact support.', ); case 'user-disabled': return const LogInWithGoogleFailure( 'This user has been disabled. Please contact support for help.', ); case 'user-not-found': return const LogInWithGoogleFailure( 'Email is not found, please create an account.', ); case 'wrong-password': return const LogInWithGoogleFailure( 'Incorrect password, please try again.', ); case 'invalid-verification-code': return const LogInWithGoogleFailure( 'The credential verification code received is invalid.', ); case 'invalid-verification-id': return const LogInWithGoogleFailure( 'The credential verification ID received is invalid.', ); default: return const LogInWithGoogleFailure(); } } /// The associated error message. final String message; } /// Thrown during the logout process if a failure occurs. class LogOutFailure implements Exception {} /// {@template authentication_repository} /// Repository which manages user authentication. /// {@endtemplate} class AuthenticationRepository { /// {@macro authentication_repository} AuthenticationRepository({ CacheClient? cache, firebase_auth.FirebaseAuth? firebaseAuth, GoogleSignIn? googleSignIn, }) : _cache = cache ?? CacheClient(), _firebaseAuth = firebaseAuth ?? firebase_auth.FirebaseAuth.instance, _googleSignIn = googleSignIn ?? GoogleSignIn.standard(); final CacheClient _cache; final firebase_auth.FirebaseAuth _firebaseAuth; final GoogleSignIn _googleSignIn; /// Whether or not the current environment is web /// Should only be overridden for testing purposes. Otherwise, /// defaults to [kIsWeb] @visibleForTesting bool isWeb = kIsWeb; /// User cache key. /// Should only be used for testing purposes. @visibleForTesting static const userCacheKey = '__user_cache_key__'; /// Stream of [User] which will emit the current user when /// the authentication state changes. /// /// Emits [User.empty] if the user is not authenticated. Stream get user { return _firebaseAuth.authStateChanges().map((firebaseUser) { final user = firebaseUser == null ? User.empty : firebaseUser.toUser; _cache.write(key: userCacheKey, value: user); return user; }); } /// Returns the current cached user. /// Defaults to [User.empty] if there is no cached user. User get currentUser { return _cache.read(key: userCacheKey) ?? User.empty; } /// Creates a new user with the provided [email] and [password]. /// /// Throws a [SignUpWithEmailAndPasswordFailure] if an exception occurs. Future signUp({required String email, required String password}) async { try { await _firebaseAuth.createUserWithEmailAndPassword( email: email, password: password, ); } on firebase_auth.FirebaseAuthException catch (e) { throw SignUpWithEmailAndPasswordFailure.fromCode(e.code); } catch (_) { throw const SignUpWithEmailAndPasswordFailure(); } } /// Starts the Sign In with Google Flow. /// /// Throws a [LogInWithGoogleFailure] if an exception occurs. Future logInWithGoogle() async { try { late final firebase_auth.AuthCredential credential; if (isWeb) { final googleProvider = firebase_auth.GoogleAuthProvider(); final userCredential = await _firebaseAuth.signInWithPopup( googleProvider, ); credential = userCredential.credential!; } else { final googleUser = await _googleSignIn.signIn(); final googleAuth = await googleUser!.authentication; credential = firebase_auth.GoogleAuthProvider.credential( accessToken: googleAuth.accessToken, idToken: googleAuth.idToken, ); } await _firebaseAuth.signInWithCredential(credential); } on firebase_auth.FirebaseAuthException catch (e) { throw LogInWithGoogleFailure.fromCode(e.code); } catch (_) { throw const LogInWithGoogleFailure(); } } /// Signs in with the provided [email] and [password]. /// /// Throws a [LogInWithEmailAndPasswordFailure] if an exception occurs. Future logInWithEmailAndPassword({ required String email, required String password, }) async { try { await _firebaseAuth.signInWithEmailAndPassword( email: email, password: password, ); } on firebase_auth.FirebaseAuthException catch (e) { throw LogInWithEmailAndPasswordFailure.fromCode(e.code); } catch (_) { throw const LogInWithEmailAndPasswordFailure(); } } /// Signs out the current user which will emit /// [User.empty] from the [user] Stream. /// /// Throws a [LogOutFailure] if an exception occurs. Future logOut() async { try { await Future.wait([ _firebaseAuth.signOut(), _googleSignIn.signOut(), ]); } catch (_) { throw LogOutFailure(); } } } extension on firebase_auth.User { /// Maps a [firebase_auth.User] into a [User]. User get toUser { return User(id: uid, email: email, name: displayName, photo: photoURL); } } ================================================ FILE: examples/flutter_firebase_login/packages/authentication_repository/lib/src/models/models.dart ================================================ export 'user.dart'; ================================================ FILE: examples/flutter_firebase_login/packages/authentication_repository/lib/src/models/user.dart ================================================ import 'package:equatable/equatable.dart'; /// {@template user} /// User model /// /// [User.empty] represents an unauthenticated user. /// {@endtemplate} class User extends Equatable { /// {@macro user} const User({ required this.id, this.email, this.name, this.photo, }); /// The current user's email address. final String? email; /// The current user's id. final String id; /// The current user's name (display name). final String? name; /// Url for the current user's photo. final String? photo; /// Empty user which represents an unauthenticated user. static const empty = User(id: ''); @override List get props => [email, id, name, photo]; } ================================================ FILE: examples/flutter_firebase_login/packages/authentication_repository/pubspec.yaml ================================================ name: authentication_repository description: Dart package which manages the authentication domain. version: 1.0.0 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: cache: path: ../cache equatable: ^2.0.0 firebase_auth: ^5.0.0 firebase_core: ^3.0.0 flutter: sdk: flutter google_sign_in: ^6.1.0 meta: ^1.8.0 dev_dependencies: firebase_auth_platform_interface: ^7.0.5 firebase_core_platform_interface: ^5.0.0 flutter_test: sdk: flutter mocktail: ^1.0.0 plugin_platform_interface: ^2.1.7 ================================================ FILE: examples/flutter_firebase_login/packages/authentication_repository/test/authentication_repository_test.dart ================================================ import 'package:authentication_repository/authentication_repository.dart'; import 'package:cache/cache.dart'; import 'package:firebase_auth/firebase_auth.dart' as firebase_auth; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:mocktail/mocktail.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; const _mockFirebaseUserUid = 'mock-uid'; const _mockFirebaseUserEmail = 'mock-email'; class MockCacheClient extends Mock implements CacheClient {} class MockFirebaseAuth extends Mock implements firebase_auth.FirebaseAuth {} class MockFirebaseCore extends Mock with MockPlatformInterfaceMixin implements FirebasePlatform {} class MockFirebaseUser extends Mock implements firebase_auth.User {} class MockGoogleSignIn extends Mock implements GoogleSignIn {} class MockGoogleSignInAccount extends Mock implements GoogleSignInAccount {} class MockGoogleSignInAuthentication extends Mock implements GoogleSignInAuthentication {} class MockUserCredential extends Mock implements firebase_auth.UserCredential {} class FakeAuthCredential extends Fake implements firebase_auth.AuthCredential {} class FakeAuthProvider extends Fake implements AuthProvider {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); const email = 'test@gmail.com'; const password = 't0ps3cret42'; const user = User( id: _mockFirebaseUserUid, email: _mockFirebaseUserEmail, ); group('AuthenticationRepository', () { late CacheClient cache; late firebase_auth.FirebaseAuth firebaseAuth; late GoogleSignIn googleSignIn; late AuthenticationRepository authenticationRepository; setUpAll(() { registerFallbackValue(FakeAuthCredential()); registerFallbackValue(FakeAuthProvider()); }); setUp(() { const options = FirebaseOptions( apiKey: 'apiKey', appId: 'appId', messagingSenderId: 'messagingSenderId', projectId: 'projectId', ); final platformApp = FirebaseAppPlatform(defaultFirebaseAppName, options); final firebaseCore = MockFirebaseCore(); when(() => firebaseCore.apps).thenReturn([platformApp]); when(firebaseCore.app).thenReturn(platformApp); when( () => firebaseCore.initializeApp( name: defaultFirebaseAppName, options: options, ), ).thenAnswer((_) async => platformApp); Firebase.delegatePackingProperty = firebaseCore; cache = MockCacheClient(); firebaseAuth = MockFirebaseAuth(); googleSignIn = MockGoogleSignIn(); authenticationRepository = AuthenticationRepository( cache: cache, firebaseAuth: firebaseAuth, googleSignIn: googleSignIn, ); }); test('creates FirebaseAuth instance internally when not injected', () { expect(AuthenticationRepository.new, isNot(throwsException)); }); group('signUp', () { setUp(() { when( () => firebaseAuth.createUserWithEmailAndPassword( email: any(named: 'email'), password: any(named: 'password'), ), ).thenAnswer((_) => Future.value(MockUserCredential())); }); test('calls createUserWithEmailAndPassword', () async { await authenticationRepository.signUp(email: email, password: password); verify( () => firebaseAuth.createUserWithEmailAndPassword( email: email, password: password, ), ).called(1); }); test('succeeds when createUserWithEmailAndPassword succeeds', () async { expect( authenticationRepository.signUp(email: email, password: password), completes, ); }); test('throws SignUpWithEmailAndPasswordFailure ' 'when createUserWithEmailAndPassword throws', () async { when( () => firebaseAuth.createUserWithEmailAndPassword( email: any(named: 'email'), password: any(named: 'password'), ), ).thenThrow(Exception()); expect( authenticationRepository.signUp(email: email, password: password), throwsA(isA()), ); }); }); group('loginWithGoogle', () { const accessToken = 'access-token'; const idToken = 'id-token'; setUp(() { final googleSignInAuthentication = MockGoogleSignInAuthentication(); final googleSignInAccount = MockGoogleSignInAccount(); when( () => googleSignInAuthentication.accessToken, ).thenReturn(accessToken); when(() => googleSignInAuthentication.idToken).thenReturn(idToken); when( () => googleSignInAccount.authentication, ).thenAnswer((_) async => googleSignInAuthentication); when( () => googleSignIn.signIn(), ).thenAnswer((_) async => googleSignInAccount); when( () => firebaseAuth.signInWithCredential(any()), ).thenAnswer((_) => Future.value(MockUserCredential())); when( () => firebaseAuth.signInWithPopup(any()), ).thenAnswer((_) => Future.value(MockUserCredential())); }); test('calls signIn authentication, and signInWithCredential', () async { await authenticationRepository.logInWithGoogle(); verify(() => googleSignIn.signIn()).called(1); verify(() => firebaseAuth.signInWithCredential(any())).called(1); }); test( 'throws LogInWithGoogleFailure and calls signIn authentication, and ' 'signInWithPopup when authCredential is null and kIsWeb is true', () async { authenticationRepository.isWeb = true; await expectLater( () => authenticationRepository.logInWithGoogle(), throwsA(isA()), ); verifyNever(() => googleSignIn.signIn()); verify(() => firebaseAuth.signInWithPopup(any())).called(1); }, ); test( 'successfully calls signIn authentication, and ' 'signInWithPopup when authCredential is not null and kIsWeb is true', () async { final credential = MockUserCredential(); when( () => firebaseAuth.signInWithPopup(any()), ).thenAnswer((_) async => credential); when(() => credential.credential).thenReturn(FakeAuthCredential()); authenticationRepository.isWeb = true; await expectLater( authenticationRepository.logInWithGoogle(), completes, ); verifyNever(() => googleSignIn.signIn()); verify(() => firebaseAuth.signInWithPopup(any())).called(1); }, ); test('succeeds when signIn succeeds', () { expect(authenticationRepository.logInWithGoogle(), completes); }); test('throws LogInWithGoogleFailure when exception occurs', () async { when( () => firebaseAuth.signInWithCredential(any()), ).thenThrow(Exception()); expect( authenticationRepository.logInWithGoogle(), throwsA(isA()), ); }); }); group('logInWithEmailAndPassword', () { setUp(() { when( () => firebaseAuth.signInWithEmailAndPassword( email: any(named: 'email'), password: any(named: 'password'), ), ).thenAnswer((_) => Future.value(MockUserCredential())); }); test('calls signInWithEmailAndPassword', () async { await authenticationRepository.logInWithEmailAndPassword( email: email, password: password, ); verify( () => firebaseAuth.signInWithEmailAndPassword( email: email, password: password, ), ).called(1); }); test('succeeds when signInWithEmailAndPassword succeeds', () async { expect( authenticationRepository.logInWithEmailAndPassword( email: email, password: password, ), completes, ); }); test('throws LogInWithEmailAndPasswordFailure ' 'when signInWithEmailAndPassword throws', () async { when( () => firebaseAuth.signInWithEmailAndPassword( email: any(named: 'email'), password: any(named: 'password'), ), ).thenThrow(Exception()); expect( authenticationRepository.logInWithEmailAndPassword( email: email, password: password, ), throwsA(isA()), ); }); }); group('logOut', () { test('calls signOut', () async { when(() => firebaseAuth.signOut()).thenAnswer((_) async {}); when(() => googleSignIn.signOut()).thenAnswer((_) async => null); await authenticationRepository.logOut(); verify(() => firebaseAuth.signOut()).called(1); verify(() => googleSignIn.signOut()).called(1); }); test('throws LogOutFailure when signOut throws', () async { when(() => firebaseAuth.signOut()).thenThrow(Exception()); expect( authenticationRepository.logOut(), throwsA(isA()), ); }); }); group('user', () { test('emits User.empty when firebase user is null', () async { when( () => firebaseAuth.authStateChanges(), ).thenAnswer((_) => Stream.value(null)); await expectLater( authenticationRepository.user, emitsInOrder(const [User.empty]), ); }); test('emits User when firebase user is not null', () async { final firebaseUser = MockFirebaseUser(); when(() => firebaseUser.uid).thenReturn(_mockFirebaseUserUid); when(() => firebaseUser.email).thenReturn(_mockFirebaseUserEmail); when(() => firebaseUser.photoURL).thenReturn(null); when( () => firebaseAuth.authStateChanges(), ).thenAnswer((_) => Stream.value(firebaseUser)); await expectLater( authenticationRepository.user, emitsInOrder(const [user]), ); verify( () => cache.write( key: AuthenticationRepository.userCacheKey, value: user, ), ).called(1); }); }); group('currentUser', () { test('returns User.empty when cached user is null', () { when( () => cache.read(key: AuthenticationRepository.userCacheKey), ).thenReturn(null); expect( authenticationRepository.currentUser, equals(User.empty), ); }); test('returns User when cached user is not null', () async { when( () => cache.read(key: AuthenticationRepository.userCacheKey), ).thenReturn(user); expect(authenticationRepository.currentUser, equals(user)); }); }); }); } ================================================ FILE: examples/flutter_firebase_login/packages/authentication_repository/test/models/user_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:authentication_repository/authentication_repository.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('User', () { const id = 'mock-id'; const email = 'mock-email'; test('uses value equality', () { expect( User(email: email, id: id), equals(User(email: email, id: id)), ); }); }); } ================================================ FILE: examples/flutter_firebase_login/packages/cache/analysis_options.yaml ================================================ include: ../../../../analysis_options.yaml ================================================ FILE: examples/flutter_firebase_login/packages/cache/lib/cache.dart ================================================ /// {@template cache_client} /// An in-memory cache client. /// {@endtemplate} class CacheClient { /// {@macro cache_client} CacheClient() : _cache = {}; final Map _cache; /// Writes the provide [key], [value] pair to the in-memory cache. void write({required String key, required T value}) { _cache[key] = value; } /// Looks up the value for the provided [key]. /// Defaults to `null` if no value exists for the provided key. T? read({required String key}) { final value = _cache[key]; if (value is T) return value; return null; } } ================================================ FILE: examples/flutter_firebase_login/packages/cache/pubspec.yaml ================================================ name: cache description: A simple in memory cache made for dart version: 1.0.0 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dev_dependencies: test: ^1.17.11 ================================================ FILE: examples/flutter_firebase_login/packages/cache/test/cache_test.dart ================================================ import 'package:cache/cache.dart'; import 'package:test/test.dart'; void main() { group('CacheClient', () { test('can write and read a value for a given key', () { final cache = CacheClient(); const key = '__key__'; const value = '__value__'; expect(cache.read(key: key), isNull); cache.write(key: key, value: value); expect(cache.read(key: key), equals(value)); }); }); } ================================================ FILE: examples/flutter_firebase_login/packages/form_inputs/analysis_options.yaml ================================================ include: ../../../../analysis_options.yaml ================================================ FILE: examples/flutter_firebase_login/packages/form_inputs/lib/form_inputs.dart ================================================ export './src/confirmed_password.dart'; export './src/email.dart'; export './src/password.dart'; ================================================ FILE: examples/flutter_firebase_login/packages/form_inputs/lib/src/confirmed_password.dart ================================================ import 'package:formz/formz.dart'; /// Validation errors for the [ConfirmedPassword] [FormzInput]. enum ConfirmedPasswordValidationError { /// Generic invalid error. invalid, } /// {@template confirmed_password} /// Form input for a confirmed password input. /// {@endtemplate} class ConfirmedPassword extends FormzInput { /// {@macro confirmed_password} const ConfirmedPassword.pure({this.password = ''}) : super.pure(''); /// {@macro confirmed_password} const ConfirmedPassword.dirty({required this.password, String value = ''}) : super.dirty(value); /// The original password. final String password; @override ConfirmedPasswordValidationError? validator(String? value) { return password == value ? null : ConfirmedPasswordValidationError.invalid; } } ================================================ FILE: examples/flutter_firebase_login/packages/form_inputs/lib/src/email.dart ================================================ import 'package:formz/formz.dart'; /// Validation errors for the [Email] [FormzInput]. enum EmailValidationError { /// Generic invalid error. invalid, } /// {@template email} /// Form input for an email input. /// {@endtemplate} class Email extends FormzInput { /// {@macro email} const Email.pure() : super.pure(''); /// {@macro email} const Email.dirty([super.value = '']) : super.dirty(); static final RegExp _emailRegExp = RegExp( r'^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$', ); @override EmailValidationError? validator(String? value) { return _emailRegExp.hasMatch(value ?? '') ? null : EmailValidationError.invalid; } } ================================================ FILE: examples/flutter_firebase_login/packages/form_inputs/lib/src/password.dart ================================================ import 'package:formz/formz.dart'; /// Validation errors for the [Password] [FormzInput]. enum PasswordValidationError { /// Generic invalid error. invalid, } /// {@template password} /// Form input for an password input. /// {@endtemplate} class Password extends FormzInput { /// {@macro password} const Password.pure() : super.pure(''); /// {@macro password} const Password.dirty([super.value = '']) : super.dirty(); static final _passwordRegExp = RegExp( r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$', ); @override PasswordValidationError? validator(String? value) { return _passwordRegExp.hasMatch(value ?? '') ? null : PasswordValidationError.invalid; } } ================================================ FILE: examples/flutter_firebase_login/packages/form_inputs/pubspec.yaml ================================================ name: form_inputs version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: formz: ^0.8.0 ================================================ FILE: examples/flutter_firebase_login/pubspec.yaml ================================================ name: flutter_firebase_login description: A new Flutter project. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: authentication_repository: path: packages/authentication_repository bloc: ^9.0.0 equatable: ^2.0.0 firebase_core: ^3.0.0 flow_builder: ^0.1.0 flutter: sdk: flutter flutter_bloc: ^9.1.0 font_awesome_flutter: ^10.1.0 form_inputs: path: packages/form_inputs formz: ^0.8.0 google_fonts: ^6.0.0 meta: ^1.7.0 dev_dependencies: bloc_lint: ^0.3.0 bloc_test: ^10.0.0 flutter_test: sdk: flutter mocktail: ^1.0.0 flutter: uses-material-design: true assets: - assets/ ================================================ FILE: examples/flutter_firebase_login/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../../packages/bloc bloc_lint: path: ../../packages/bloc_lint bloc_test: path: ../../packages/bloc_test flutter_bloc: path: ../../packages/flutter_bloc ================================================ FILE: examples/flutter_firebase_login/test/app/bloc/app_bloc_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_firebase_login/app/app.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockAuthenticationRepository extends Mock implements AuthenticationRepository {} class MockUser extends Mock implements User {} void main() { group(AppBloc, () { final user = MockUser(); late AuthenticationRepository authenticationRepository; setUp(() { authenticationRepository = MockAuthenticationRepository(); when(() => authenticationRepository.user).thenAnswer( (_) => const Stream.empty(), ); when( () => authenticationRepository.currentUser, ).thenReturn(user); }); AppBloc buildBloc() { return AppBloc( authenticationRepository: authenticationRepository, ); } test('initial state is $AppState', () { expect(buildBloc().state, equals(AppState(user: user))); }); group(AppUserSubscriptionRequested, () { final error = Exception('oops'); blocTest( 'emits $AppState when user stream emits a new value', setUp: () { when(() => authenticationRepository.user).thenAnswer( (_) => Stream.value(user), ); }, build: buildBloc, act: (bloc) => bloc.add(AppUserSubscriptionRequested()), expect: () => [AppState(user: user)], ); blocTest( 'adds error when user stream emits an error', setUp: () { when( () => authenticationRepository.user, ).thenAnswer((_) => Stream.error(error)); }, build: buildBloc, act: (bloc) => bloc.add(AppUserSubscriptionRequested()), errors: () => [error], ); }); group(AppLogoutPressed, () { blocTest( 'invokes logOut', setUp: () { when( () => authenticationRepository.logOut(), ).thenAnswer((_) async {}); }, build: buildBloc, act: (bloc) => bloc.add(AppLogoutPressed()), verify: (_) { verify(() => authenticationRepository.logOut()).called(1); }, ); }); }); } ================================================ FILE: examples/flutter_firebase_login/test/app/bloc/app_state_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:authentication_repository/authentication_repository.dart'; import 'package:flutter_firebase_login/app/app.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockUser extends Mock implements User {} void main() { group(AppState, () { test('returns state with status unauthenticated ' 'when user is empty', () { expect(AppState().status, equals(AppStatus.unauthenticated)); }); test('returns state with status authenticated and user ' 'when user is not empty', () { final user = MockUser(); final state = AppState(user: user); expect(state.status, equals(AppStatus.authenticated)); expect(state.user, equals(user)); }); }); } ================================================ FILE: examples/flutter_firebase_login/test/app/bloc_observer_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:flutter_firebase_login/app/app.dart'; import 'package:flutter_test/flutter_test.dart'; class FakeBloc extends Fake implements Bloc {} class FakeCubit extends Fake implements Cubit {} class FakeEvent extends Fake implements Object {} class FakeStackTrace extends Fake implements StackTrace {} class FakeChange extends Fake implements Change {} class FakeTransition extends Fake implements Transition {} void main() { group('AppBlocObserver', () { setUp(logs.clear); test( 'onEvent prints event', overridePrint(() { final bloc = FakeBloc(); final event = FakeEvent(); AppBlocObserver().onEvent(bloc, event); expect(logs, equals(['$event'])); }), ); test( 'onError prints error', overridePrint(() { final bloc = FakeBloc(); final error = Object(); final stackTrace = FakeStackTrace(); AppBlocObserver().onError(bloc, error, stackTrace); expect(logs, equals(['$error'])); }), ); test( 'onChange prints change', overridePrint(() { final cubit = FakeCubit(); final change = FakeChange(); AppBlocObserver().onChange(cubit, change); expect(logs, equals(['$change'])); }), ); test( 'onTransition prints transition', overridePrint(() { final bloc = FakeBloc(); final transition = FakeTransition(); AppBlocObserver().onTransition(bloc, transition); expect(logs, equals(['$transition'])); }), ); }); } final logs = []; void Function() overridePrint(void Function() testFn) { return () { final spec = ZoneSpecification( print: (_, _, _, String msg) => logs.add(msg), ); return Zone.current.fork(specification: spec).run(testFn); }; } ================================================ FILE: examples/flutter_firebase_login/test/app/routes/routes_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_firebase_login/app/app.dart'; import 'package:flutter_firebase_login/home/home.dart'; import 'package:flutter_firebase_login/login/login.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('onGenerateAppViewPages', () { test('returns [HomePage] when authenticated', () { expect( onGenerateAppViewPages(AppStatus.authenticated, []), [ isA>().having( (p) => p.child, 'child', isA(), ), ], ); }); test('returns [LoginPage] when unauthenticated', () { expect( onGenerateAppViewPages(AppStatus.unauthenticated, []), [ isA>().having( (p) => p.child, 'child', isA(), ), ], ); }); }); } ================================================ FILE: examples/flutter_firebase_login/test/app/view/app_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_firebase_login/app/app.dart'; import 'package:flutter_firebase_login/home/home.dart'; import 'package:flutter_firebase_login/login/login.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockUser extends Mock implements User {} class MockAuthenticationRepository extends Mock implements AuthenticationRepository {} class MockAppBloc extends MockBloc implements AppBloc {} void main() { group('App', () { late AuthenticationRepository authenticationRepository; late User user; setUp(() { authenticationRepository = MockAuthenticationRepository(); user = MockUser(); when(() => authenticationRepository.user).thenAnswer( (_) => const Stream.empty(), ); when(() => authenticationRepository.currentUser).thenReturn(user); when(() => user.email).thenReturn('test@gmail.com'); }); testWidgets('renders AppView', (tester) async { await tester.pumpWidget( App(authenticationRepository: authenticationRepository), ); await tester.pump(); expect(find.byType(AppView), findsOneWidget); }); }); group('AppView', () { late AuthenticationRepository authenticationRepository; late AppBloc appBloc; setUp(() { authenticationRepository = MockAuthenticationRepository(); appBloc = MockAppBloc(); }); testWidgets('navigates to LoginPage when unauthenticated', (tester) async { when(() => appBloc.state).thenReturn(AppState()); await tester.pumpWidget( RepositoryProvider.value( value: authenticationRepository, child: MaterialApp( home: BlocProvider.value(value: appBloc, child: const AppView()), ), ), ); await tester.pumpAndSettle(); expect(find.byType(LoginPage), findsOneWidget); }); testWidgets('navigates to HomePage when authenticated', (tester) async { final user = MockUser(); when(() => user.email).thenReturn('test@gmail.com'); when(() => appBloc.state).thenReturn(AppState(user: user)); await tester.pumpWidget( RepositoryProvider.value( value: authenticationRepository, child: MaterialApp( home: BlocProvider.value(value: appBloc, child: const AppView()), ), ), ); await tester.pumpAndSettle(); expect(find.byType(HomePage), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_firebase_login/test/home/view/home_page_test.dart ================================================ import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_firebase_login/app/app.dart'; import 'package:flutter_firebase_login/home/home.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockAppBloc extends MockBloc implements AppBloc {} class MockUser extends Mock implements User {} void main() { const logoutButtonKey = Key('homePage_logout_iconButton'); group('HomePage', () { late AppBloc appBloc; late User user; setUp(() { appBloc = MockAppBloc(); user = MockUser(); when(() => user.email).thenReturn('test@gmail.com'); when(() => appBloc.state).thenReturn(AppState(user: user)); }); group('calls', () { testWidgets('AppLogoutPressed when logout is pressed', (tester) async { await tester.pumpWidget( BlocProvider.value( value: appBloc, child: const MaterialApp(home: HomePage()), ), ); await tester.tap(find.byKey(logoutButtonKey)); verify(() => appBloc.add(const AppLogoutPressed())).called(1); }); }); group('renders', () { testWidgets('avatar widget', (tester) async { await tester.pumpWidget( BlocProvider.value( value: appBloc, child: const MaterialApp(home: HomePage()), ), ); expect(find.byType(Avatar), findsOneWidget); }); testWidgets('email address', (tester) async { await tester.pumpWidget( BlocProvider.value( value: appBloc, child: const MaterialApp(home: HomePage()), ), ); expect(find.text('test@gmail.com'), findsOneWidget); }); testWidgets('name', (tester) async { when(() => user.name).thenReturn('Joe'); await tester.pumpWidget( BlocProvider.value( value: appBloc, child: const MaterialApp(home: HomePage()), ), ); expect(find.text('Joe'), findsOneWidget); }); }); }); } ================================================ FILE: examples/flutter_firebase_login/test/home/widgets/avatar_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_firebase_login/home/home.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { const imageUrl = 'https://www.fnordware.com/superpng/pngtest16rgba.png'; group('Avatar', () { setUpAll(() => HttpOverrides.global = null); testWidgets('renders CircleAvatar', (tester) async { await tester.pumpWidget(MaterialApp(home: Avatar())); expect(find.byType(CircleAvatar), findsOneWidget); }); testWidgets('has correct radius', (tester) async { await tester.pumpWidget(MaterialApp(home: Avatar())); final avatar = tester.widget(find.byType(CircleAvatar)); expect(avatar.radius, 48); }); testWidgets('renders backgroundImage if photo is not null', (tester) async { await tester.pumpWidget(MaterialApp(home: Avatar(photo: imageUrl))); final avatar = tester.widget(find.byType(CircleAvatar)); expect(avatar.backgroundImage, isNotNull); await tester.pumpAndSettle(); }); testWidgets('renders icon if photo is null', (tester) async { await tester.pumpWidget(MaterialApp(home: Avatar())); final avatar = tester.widget(find.byType(CircleAvatar)); expect(avatar.backgroundImage, isNull); final icon = avatar.child! as Icon; expect(icon.icon, Icons.person_outline); expect(icon.size, 48); }); }); } ================================================ FILE: examples/flutter_firebase_login/test/login/cubit/login_cubit_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_firebase_login/login/login.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockAuthenticationRepository extends Mock implements AuthenticationRepository {} void main() { const invalidEmail = 'invalid'; const validEmail = 'test@gmail.com'; const invalidPassword = 'invalid'; const validPassword = 't0pS3cret1234'; group('LoginCubit', () { late AuthenticationRepository authenticationRepository; setUp(() { authenticationRepository = MockAuthenticationRepository(); when( () => authenticationRepository.logInWithGoogle(), ).thenAnswer((_) async {}); when( () => authenticationRepository.logInWithEmailAndPassword( email: any(named: 'email'), password: any(named: 'password'), ), ).thenAnswer((_) async {}); }); test('initial state is LoginState', () { expect(LoginCubit(authenticationRepository).state, LoginState()); }); group('emailChanged', () { blocTest( 'emits [invalid] when email/password are invalid', build: () => LoginCubit(authenticationRepository), act: (cubit) => cubit.emailChanged(invalidEmail), expect: () => [LoginState().withEmail(invalidEmail)], ); blocTest( 'emits [valid] when email/password are valid', build: () => LoginCubit(authenticationRepository), seed: () => LoginState().withPassword(validPassword), act: (cubit) => cubit.emailChanged(validEmail), expect: () => [ LoginState().withEmail(validEmail).withPassword(validPassword), ], ); }); group('passwordChanged', () { blocTest( 'emits [invalid] when email/password are invalid', build: () => LoginCubit(authenticationRepository), act: (cubit) => cubit.passwordChanged(invalidPassword), expect: () => [LoginState().withPassword(invalidPassword)], ); blocTest( 'emits [valid] when email/password are valid', build: () => LoginCubit(authenticationRepository), seed: () => LoginState().withEmail(validEmail), act: (cubit) => cubit.passwordChanged(validPassword), expect: () => [ LoginState().withEmail(validEmail).withPassword(validPassword), ], ); }); group('logInWithCredentials', () { blocTest( 'does nothing when status is not validated', build: () => LoginCubit(authenticationRepository), act: (cubit) => cubit.logInWithCredentials(), expect: () => const [], ); blocTest( 'calls logInWithEmailAndPassword with correct email/password', build: () => LoginCubit(authenticationRepository), seed: () { return LoginState().withEmail(validEmail).withPassword(validPassword); }, act: (cubit) => cubit.logInWithCredentials(), verify: (_) { verify( () => authenticationRepository.logInWithEmailAndPassword( email: validEmail, password: validPassword, ), ).called(1); }, ); blocTest( 'emits [submissionInProgress, submissionSuccess] ' 'when logInWithEmailAndPassword succeeds', build: () => LoginCubit(authenticationRepository), seed: () { return LoginState().withEmail(validEmail).withPassword(validPassword); }, act: (cubit) => cubit.logInWithCredentials(), expect: () => [ LoginState() .withEmail(validEmail) .withPassword(validPassword) .withSubmissionInProgress(), LoginState() .withEmail(validEmail) .withPassword(validPassword) .withSubmissionSuccess(), ], ); blocTest( 'emits [submissionInProgress, submissionFailure] ' 'when logInWithEmailAndPassword fails ' 'due to LogInWithEmailAndPasswordFailure', setUp: () { when( () => authenticationRepository.logInWithEmailAndPassword( email: any(named: 'email'), password: any(named: 'password'), ), ).thenThrow(LogInWithEmailAndPasswordFailure('oops')); }, build: () => LoginCubit(authenticationRepository), seed: () { return LoginState().withEmail(validEmail).withPassword(validPassword); }, act: (cubit) => cubit.logInWithCredentials(), expect: () => [ LoginState() .withEmail(validEmail) .withPassword(validPassword) .withSubmissionInProgress(), LoginState() .withEmail(validEmail) .withPassword(validPassword) .withSubmissionFailure('oops'), ], ); blocTest( 'emits [submissionInProgress, submissionFailure] ' 'when logInWithEmailAndPassword fails due to generic exception', setUp: () { when( () => authenticationRepository.logInWithEmailAndPassword( email: any(named: 'email'), password: any(named: 'password'), ), ).thenThrow(Exception('oops')); }, build: () => LoginCubit(authenticationRepository), seed: () { return LoginState().withEmail(validEmail).withPassword(validPassword); }, act: (cubit) => cubit.logInWithCredentials(), expect: () => [ LoginState() .withEmail(validEmail) .withPassword(validPassword) .withSubmissionInProgress(), LoginState() .withEmail(validEmail) .withPassword(validPassword) .withSubmissionFailure(), ], ); }); group('logInWithGoogle', () { blocTest( 'calls logInWithGoogle', build: () => LoginCubit(authenticationRepository), act: (cubit) => cubit.logInWithGoogle(), verify: (_) { verify(() => authenticationRepository.logInWithGoogle()).called(1); }, ); blocTest( 'emits [inProgress, success] ' 'when logInWithGoogle succeeds', build: () => LoginCubit(authenticationRepository), act: (cubit) => cubit.logInWithGoogle(), expect: () => [ LoginState().withSubmissionInProgress(), LoginState().withSubmissionSuccess(), ], ); blocTest( 'emits [inProgress, failure] ' 'when logInWithGoogle fails due to LogInWithGoogleFailure', setUp: () { when( () => authenticationRepository.logInWithGoogle(), ).thenThrow(LogInWithGoogleFailure('oops')); }, build: () => LoginCubit(authenticationRepository), act: (cubit) => cubit.logInWithGoogle(), expect: () => [ LoginState().withSubmissionInProgress(), LoginState().withSubmissionFailure('oops'), ], ); blocTest( 'emits [inProgress, failure] ' 'when logInWithGoogle fails due to generic exception', setUp: () { when( () => authenticationRepository.logInWithGoogle(), ).thenThrow(Exception('oops')); }, build: () => LoginCubit(authenticationRepository), act: (cubit) => cubit.logInWithGoogle(), expect: () => [ LoginState().withSubmissionInProgress(), LoginState().withSubmissionFailure(), ], ); }); }); } ================================================ FILE: examples/flutter_firebase_login/test/login/cubit/login_state_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_firebase_login/login/login.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { const email = 'test@gmail.com'; const password = 'Test1234'; group('LoginState', () { test('supports value comparisons', () { expect(LoginState(), LoginState()); }); group('isValid', () { test('is false for initial state', () { expect(LoginState().isValid, isFalse); }); test('is true when validation succeeds', () { expect( LoginState().withEmail(email).withPassword(password).isValid, isTrue, ); }); }); }); } ================================================ FILE: examples/flutter_firebase_login/test/login/view/login_form_test.dart ================================================ import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_firebase_login/login/login.dart'; import 'package:flutter_firebase_login/sign_up/sign_up.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockAuthenticationRepository extends Mock implements AuthenticationRepository {} class MockLoginCubit extends MockCubit implements LoginCubit {} void main() { const loginButtonKey = Key('loginForm_continue_raisedButton'); const signInWithGoogleButtonKey = Key('loginForm_googleLogin_raisedButton'); const emailInputKey = Key('loginForm_emailInput_textField'); const passwordInputKey = Key('loginForm_passwordInput_textField'); const createAccountButtonKey = Key('loginForm_createAccount_flatButton'); const validEmail = 'test@gmail.com'; const validPassword = 'Test1234'; const invalidEmail = 'invalid'; const invalidPassword = 'invalid'; group('LoginForm', () { late LoginCubit loginCubit; setUp(() { loginCubit = MockLoginCubit(); when(() => loginCubit.state).thenReturn(const LoginState()); when(() => loginCubit.logInWithGoogle()).thenAnswer((_) async {}); when(() => loginCubit.logInWithCredentials()).thenAnswer((_) async {}); }); group('calls', () { testWidgets('emailChanged when email changes', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginCubit, child: const LoginForm(), ), ), ), ); await tester.enterText(find.byKey(emailInputKey), validEmail); verify(() => loginCubit.emailChanged(validEmail)).called(1); }); testWidgets('passwordChanged when password changes', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginCubit, child: const LoginForm(), ), ), ), ); await tester.enterText(find.byKey(passwordInputKey), validPassword); verify(() => loginCubit.passwordChanged(validPassword)).called(1); }); testWidgets('logInWithCredentials when login button is pressed', ( tester, ) async { when(() => loginCubit.state).thenReturn( const LoginState().withEmail(validEmail).withPassword(validPassword), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginCubit, child: const LoginForm(), ), ), ), ); await tester.tap(find.byKey(loginButtonKey)); verify(() => loginCubit.logInWithCredentials()).called(1); }); testWidgets( 'logInWithGoogle when sign in with google button is pressed', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginCubit, child: const LoginForm(), ), ), ), ); await tester.tap(find.byKey(signInWithGoogleButtonKey)); verify(() => loginCubit.logInWithGoogle()).called(1); }, ); }); group('renders', () { testWidgets('AuthenticationFailure SnackBar when submission fails', ( tester, ) async { whenListen( loginCubit, Stream.fromIterable([ const LoginState().withSubmissionInProgress(), const LoginState().withSubmissionFailure(), ]), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginCubit, child: const LoginForm(), ), ), ), ); await tester.pump(); expect(find.text('Authentication Failure'), findsOneWidget); }); testWidgets('invalid email error text when email is invalid', ( tester, ) async { when(() => loginCubit.state).thenReturn( const LoginState().withEmail(invalidEmail), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginCubit, child: const LoginForm(), ), ), ), ); expect(find.text('invalid email'), findsOneWidget); }); testWidgets('invalid password error text when password is invalid', ( tester, ) async { when(() => loginCubit.state).thenReturn( const LoginState().withPassword(invalidPassword), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginCubit, child: const LoginForm(), ), ), ), ); expect(find.text('invalid password'), findsOneWidget); }); testWidgets('disabled login button when status is not validated', ( tester, ) async { when(() => loginCubit.state).thenReturn(const LoginState()); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginCubit, child: const LoginForm(), ), ), ), ); final loginButton = tester.widget( find.byKey(loginButtonKey), ); expect(loginButton.enabled, isFalse); }); testWidgets('enabled login button when status is validated', ( tester, ) async { when(() => loginCubit.state).thenReturn( const LoginState().withEmail(validEmail).withPassword(validPassword), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginCubit, child: const LoginForm(), ), ), ), ); final loginButton = tester.widget( find.byKey(loginButtonKey), ); expect(loginButton.enabled, isTrue); }); testWidgets('Sign in with Google Button', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginCubit, child: const LoginForm(), ), ), ), ); expect(find.byKey(signInWithGoogleButtonKey), findsOneWidget); }); }); group('navigates', () { testWidgets('to SignUpPage when Create Account is pressed', ( tester, ) async { await tester.pumpWidget( RepositoryProvider( create: (_) => MockAuthenticationRepository(), child: MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginCubit, child: const LoginForm(), ), ), ), ), ); await tester.tap(find.byKey(createAccountButtonKey)); await tester.pumpAndSettle(); expect(find.byType(SignUpPage), findsOneWidget); }); }); }); } ================================================ FILE: examples/flutter_firebase_login/test/login/view/login_page_test.dart ================================================ import 'package:authentication_repository/authentication_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_firebase_login/login/login.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockAuthenticationRepository extends Mock implements AuthenticationRepository {} void main() { group('LoginPage', () { test('has a page', () { expect(LoginPage.page(), isA>()); }); testWidgets('renders a LoginForm', (tester) async { await tester.pumpWidget( RepositoryProvider( create: (_) => MockAuthenticationRepository(), child: const MaterialApp(home: LoginPage()), ), ); expect(find.byType(LoginForm), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_firebase_login/test/sign_up/cubit/sign_up_cubit_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_firebase_login/sign_up/sign_up.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockAuthenticationRepository extends Mock implements AuthenticationRepository {} void main() { const invalidEmail = 'invalid'; const validEmail = 'test@gmail.com'; const invalidPassword = 'invalid'; const validPassword = 't0pS3cret1234'; const invalidConfirmedPassword = 'invalid'; const validConfirmedPassword = 't0pS3cret1234'; group('SignUpCubit', () { late AuthenticationRepository authenticationRepository; setUp(() { authenticationRepository = MockAuthenticationRepository(); when( () => authenticationRepository.signUp( email: any(named: 'email'), password: any(named: 'password'), ), ).thenAnswer((_) async {}); }); test('initial state is SignUpState', () { expect( SignUpCubit(authenticationRepository).state, SignUpState(), ); }); group('emailChanged', () { blocTest( 'emits [invalid] when email/password/confirmedPassword are invalid', build: () => SignUpCubit(authenticationRepository), act: (cubit) => cubit.emailChanged(invalidEmail), expect: () => [SignUpState().withEmail(invalidEmail)], ); blocTest( 'emits [valid] when email/password/confirmedPassword are valid', build: () => SignUpCubit(authenticationRepository), seed: () => SignUpState() .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword), act: (cubit) => cubit.emailChanged(validEmail), expect: () => [ SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword), ], ); }); group('passwordChanged', () { blocTest( 'emits [invalid] when email/password/confirmedPassword are invalid', build: () => SignUpCubit(authenticationRepository), act: (cubit) => cubit.passwordChanged(invalidPassword), expect: () => [ SignUpState().withPassword(invalidPassword), ], ); blocTest( 'emits [valid] when email/password/confirmedPassword are valid', build: () => SignUpCubit(authenticationRepository), seed: () => SignUpState() .withEmail(validEmail) .withConfirmedPassword(validConfirmedPassword), act: (cubit) => cubit.passwordChanged(validPassword), expect: () => [ SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword), ], ); blocTest( 'emits [valid] when confirmedPasswordChanged is called first and then ' 'passwordChanged is called', build: () => SignUpCubit(authenticationRepository), seed: () => SignUpState().withEmail(validEmail), act: (cubit) => cubit ..confirmedPasswordChanged(validConfirmedPassword) ..passwordChanged(validPassword), expect: () => [ SignUpState() .withEmail(validEmail) .withConfirmedPassword(validConfirmedPassword), SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword), ], ); }); group('confirmedPasswordChanged', () { blocTest( 'emits [invalid] when email/password/confirmedPassword are invalid', build: () => SignUpCubit(authenticationRepository), act: (cubit) { cubit.confirmedPasswordChanged(invalidConfirmedPassword); }, expect: () => [ SignUpState().withConfirmedPassword(invalidConfirmedPassword), ], ); blocTest( 'emits [valid] when email/password/confirmedPassword are valid', build: () => SignUpCubit(authenticationRepository), seed: () { return SignUpState() .withEmail(validEmail) .withPassword(validPassword); }, act: (cubit) => cubit.confirmedPasswordChanged(validConfirmedPassword), expect: () => [ SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword), ], ); blocTest( 'emits [valid] when passwordChanged is called first and then ' 'confirmedPasswordChanged is called', build: () => SignUpCubit(authenticationRepository), seed: () => SignUpState().withEmail(validEmail), act: (cubit) => cubit ..passwordChanged(validPassword) ..confirmedPasswordChanged(validConfirmedPassword), expect: () => [ SignUpState().withEmail(validEmail).withPassword(validPassword), SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword), ], ); }); group('signUpFormSubmitted', () { blocTest( 'does nothing when status is not validated', build: () => SignUpCubit(authenticationRepository), act: (cubit) => cubit.signUpFormSubmitted(), expect: () => const [], ); blocTest( 'calls signUp with correct email/password/confirmedPassword', build: () => SignUpCubit(authenticationRepository), seed: () => SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword), act: (cubit) => cubit.signUpFormSubmitted(), verify: (_) { verify( () => authenticationRepository.signUp( email: validEmail, password: validPassword, ), ).called(1); }, ); blocTest( 'emits [inProgress, success] ' 'when signUp succeeds', build: () => SignUpCubit(authenticationRepository), seed: () => SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword), act: (cubit) => cubit.signUpFormSubmitted(), expect: () => [ SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword) .withSubmissionInProgress(), SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword) .withSubmissionSuccess(), ], ); blocTest( 'emits [inProgress, failure] ' 'when signUp fails due to SignUpWithEmailAndPasswordFailure', setUp: () { when( () => authenticationRepository.signUp( email: any(named: 'email'), password: any(named: 'password'), ), ).thenThrow(SignUpWithEmailAndPasswordFailure('oops')); }, build: () => SignUpCubit(authenticationRepository), seed: () => SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword), act: (cubit) => cubit.signUpFormSubmitted(), expect: () => [ SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword) .withSubmissionInProgress(), SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword) .withSubmissionFailure('oops'), ], ); blocTest( 'emits [inProgress, failure] ' 'when signUp fails due to generic exception', setUp: () { when( () => authenticationRepository.signUp( email: any(named: 'email'), password: any(named: 'password'), ), ).thenThrow(Exception('oops')); }, build: () => SignUpCubit(authenticationRepository), seed: () => SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword), act: (cubit) => cubit.signUpFormSubmitted(), expect: () => [ SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword) .withSubmissionInProgress(), SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validConfirmedPassword) .withSubmissionFailure(), ], ); }); }); } ================================================ FILE: examples/flutter_firebase_login/test/sign_up/cubit/sign_up_state_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_firebase_login/sign_up/sign_up.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { const email = 'test@gmail.com'; const password = 'Test1234'; group('SignUpState', () { test('supports value comparisons', () { expect(SignUpState(), SignUpState()); }); group('isValid', () { test('is false for initial state', () { expect(SignUpState().isValid, isFalse); }); test('is true when validation succeeds', () { expect( SignUpState() .withEmail(email) .withPassword(password) .withConfirmedPassword(password) .isValid, isTrue, ); }); }); }); } ================================================ FILE: examples/flutter_firebase_login/test/sign_up/view/sign_up_form_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_firebase_login/sign_up/sign_up.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockSignUpCubit extends MockCubit implements SignUpCubit {} void main() { const signUpButtonKey = Key('signUpForm_continue_raisedButton'); const emailInputKey = Key('signUpForm_emailInput_textField'); const passwordInputKey = Key('signUpForm_passwordInput_textField'); const confirmedPasswordInputKey = Key( 'signUpForm_confirmedPasswordInput_textField', ); const validEmail = 'test@gmail.com'; const validPassword = 'Test1234'; const invalidEmail = 'invalid'; const invalidPassword = 'invalid'; group('SignUpForm', () { late SignUpCubit signUpCubit; setUp(() { signUpCubit = MockSignUpCubit(); when(() => signUpCubit.state).thenReturn(const SignUpState()); when(() => signUpCubit.signUpFormSubmitted()).thenAnswer((_) async {}); }); group('calls', () { testWidgets('emailChanged when email changes', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: signUpCubit, child: const SignUpForm(), ), ), ), ); await tester.enterText(find.byKey(emailInputKey), validEmail); verify(() => signUpCubit.emailChanged(validEmail)).called(1); }); testWidgets('passwordChanged when password changes', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: signUpCubit, child: const SignUpForm(), ), ), ), ); await tester.enterText(find.byKey(passwordInputKey), validPassword); verify(() => signUpCubit.passwordChanged(validPassword)).called(1); }); testWidgets('confirmedPasswordChanged when confirmedPassword changes', ( tester, ) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: signUpCubit, child: const SignUpForm(), ), ), ), ); await tester.enterText( find.byKey(confirmedPasswordInputKey), validPassword, ); verify( () => signUpCubit.confirmedPasswordChanged(validPassword), ).called(1); }); testWidgets('signUpFormSubmitted when sign up button is pressed', ( tester, ) async { when(() => signUpCubit.state).thenReturn( const SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validPassword), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: signUpCubit, child: const SignUpForm(), ), ), ), ); await tester.tap(find.byKey(signUpButtonKey)); verify(() => signUpCubit.signUpFormSubmitted()).called(1); }); }); group('renders', () { testWidgets('Sign Up Failure SnackBar when submission fails', ( tester, ) async { whenListen( signUpCubit, Stream.fromIterable([ const SignUpState(), const SignUpState().withSubmissionFailure(), ]), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: signUpCubit, child: const SignUpForm(), ), ), ), ); await tester.pump(); expect(find.text('Sign Up Failure'), findsOneWidget); }); testWidgets('invalid email error text when email is invalid', ( tester, ) async { when(() => signUpCubit.state).thenReturn( const SignUpState() .withEmail(invalidEmail) .withPassword(validPassword) .withConfirmedPassword(validPassword), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: signUpCubit, child: const SignUpForm(), ), ), ), ); expect(find.text('invalid email'), findsOneWidget); }); testWidgets('invalid password error text when password is invalid', ( tester, ) async { when(() => signUpCubit.state).thenReturn( const SignUpState() .withEmail(validEmail) .withPassword(invalidPassword), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: signUpCubit, child: const SignUpForm(), ), ), ), ); expect(find.text('invalid password'), findsOneWidget); }); testWidgets('invalid confirmedPassword error text' ' when confirmedPassword is invalid', (tester) async { when(() => signUpCubit.state).thenReturn( const SignUpState().withConfirmedPassword(invalidPassword), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: signUpCubit, child: const SignUpForm(), ), ), ), ); expect(find.text('passwords do not match'), findsOneWidget); }); testWidgets('disabled sign up button when status is not validated', ( tester, ) async { when(() => signUpCubit.state).thenReturn(const SignUpState()); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: signUpCubit, child: const SignUpForm(), ), ), ), ); final signUpButton = tester.widget( find.byKey(signUpButtonKey), ); expect(signUpButton.enabled, isFalse); }); testWidgets('enabled sign up button when status is validated', ( tester, ) async { when(() => signUpCubit.state).thenReturn( const SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validPassword), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: signUpCubit, child: const SignUpForm(), ), ), ), ); final signUpButton = tester.widget( find.byKey(signUpButtonKey), ); expect(signUpButton.enabled, isTrue); }); }); group('navigates', () { testWidgets('back to previous page when submission status is success', ( tester, ) async { whenListen( signUpCubit, Stream.fromIterable([ const SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validPassword) .withSubmissionInProgress(), const SignUpState() .withEmail(validEmail) .withPassword(validPassword) .withConfirmedPassword(validPassword) .withSubmissionSuccess(), ]), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: signUpCubit, child: const SignUpForm(), ), ), ), ); expect(find.byType(SignUpForm), findsOneWidget); await tester.pumpAndSettle(); expect(find.byType(SignUpForm), findsNothing); }); }); }); } ================================================ FILE: examples/flutter_firebase_login/test/sign_up/view/sign_up_page_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:authentication_repository/authentication_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_firebase_login/sign_up/sign_up.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockAuthenticationRepository extends Mock implements AuthenticationRepository {} void main() { group('SignUpPage', () { test('has a route', () { expect(SignUpPage.route(), isA>()); }); testWidgets('renders a SignUpForm', (tester) async { await tester.pumpWidget( RepositoryProvider( create: (_) => MockAuthenticationRepository(), child: MaterialApp(home: SignUpPage()), ), ); expect(find.byType(SignUpForm), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_form_validation/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: examples/flutter_form_validation/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: web create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/flutter_form_validation/README.md ================================================ [![build](https://github.com/felangel/bloc/actions/workflows/main.yaml/badge.svg)](https://github.com/felangel/bloc/actions) # flutter_form_validation A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: examples/flutter_form_validation/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml linter: rules: public_member_api_docs: false ================================================ FILE: examples/flutter_form_validation/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: examples/flutter_form_validation/lib/bloc/my_form_bloc.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_form_validation/models/models.dart'; import 'package:formz/formz.dart'; part 'my_form_event.dart'; part 'my_form_state.dart'; class MyFormBloc extends Bloc { MyFormBloc() : super(const MyFormState()) { on(_onEmailChanged); on(_onPasswordChanged); on(_onEmailUnfocused); on(_onPasswordUnfocused); on(_onFormSubmitted); } void _onEmailChanged(EmailChanged event, Emitter emit) { final email = Email.dirty(event.email); emit( state.copyWith( email: email.isValid ? email : Email.pure(event.email), isValid: Formz.validate([email, state.password]), status: FormzSubmissionStatus.initial, ), ); } void _onPasswordChanged(PasswordChanged event, Emitter emit) { final password = Password.dirty(event.password); emit( state.copyWith( password: password.isValid ? password : Password.pure(event.password), isValid: Formz.validate([state.email, password]), status: FormzSubmissionStatus.initial, ), ); } void _onEmailUnfocused(EmailUnfocused event, Emitter emit) { final email = Email.dirty(state.email.value); emit( state.copyWith( email: email, isValid: Formz.validate([email, state.password]), status: FormzSubmissionStatus.initial, ), ); } void _onPasswordUnfocused( PasswordUnfocused event, Emitter emit, ) { final password = Password.dirty(state.password.value); emit( state.copyWith( password: password, isValid: Formz.validate([state.email, password]), status: FormzSubmissionStatus.initial, ), ); } Future _onFormSubmitted( FormSubmitted event, Emitter emit, ) async { final email = Email.dirty(state.email.value); final password = Password.dirty(state.password.value); emit( state.copyWith( email: email, password: password, isValid: Formz.validate([email, password]), status: FormzSubmissionStatus.initial, ), ); if (state.isValid) { emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); await Future.delayed(const Duration(seconds: 1)); emit(state.copyWith(status: FormzSubmissionStatus.success)); } } } ================================================ FILE: examples/flutter_form_validation/lib/bloc/my_form_event.dart ================================================ part of 'my_form_bloc.dart'; sealed class MyFormEvent extends Equatable { const MyFormEvent(); @override List get props => []; } final class EmailChanged extends MyFormEvent { const EmailChanged({required this.email}); final String email; @override List get props => [email]; } final class EmailUnfocused extends MyFormEvent {} final class PasswordChanged extends MyFormEvent { const PasswordChanged({required this.password}); final String password; @override List get props => [password]; } final class PasswordUnfocused extends MyFormEvent {} final class FormSubmitted extends MyFormEvent {} ================================================ FILE: examples/flutter_form_validation/lib/bloc/my_form_state.dart ================================================ part of 'my_form_bloc.dart'; final class MyFormState extends Equatable { const MyFormState({ this.email = const Email.pure(), this.password = const Password.pure(), this.isValid = false, this.status = FormzSubmissionStatus.initial, }); final Email email; final Password password; final bool isValid; final FormzSubmissionStatus status; MyFormState copyWith({ Email? email, Password? password, bool? isValid, FormzSubmissionStatus? status, }) { return MyFormState( email: email ?? this.email, password: password ?? this.password, isValid: isValid ?? this.isValid, status: status ?? this.status, ); } @override List get props => [email, password, status]; } ================================================ FILE: examples/flutter_form_validation/lib/main.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_validation/bloc/my_form_bloc.dart'; import 'package:formz/formz.dart'; void main() => runApp(const App()); class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: BlocProvider( create: (_) => MyFormBloc(), child: const MyForm(), ), ), ); } } class MyForm extends StatefulWidget { const MyForm({super.key}); @override State createState() => _MyFormState(); } class _MyFormState extends State { final _emailFocusNode = FocusNode(); final _passwordFocusNode = FocusNode(); @override void initState() { super.initState(); _emailFocusNode.addListener(() { if (!_emailFocusNode.hasFocus) { context.read().add(EmailUnfocused()); FocusScope.of(context).requestFocus(_passwordFocusNode); } }); _passwordFocusNode.addListener(() { if (!_passwordFocusNode.hasFocus) { context.read().add(PasswordUnfocused()); } }); } @override void dispose() { _emailFocusNode.dispose(); _passwordFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { if (state.status.isSuccess) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); showDialog( context: context, builder: (_) => const SuccessDialog(), ); } if (state.status.isInProgress) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( const SnackBar(content: Text('Submitting...')), ); } }, child: Padding( padding: const EdgeInsets.all(16), child: Align( alignment: const Alignment(0, -3 / 4), child: Column( mainAxisSize: MainAxisSize.min, children: [ EmailInput(focusNode: _emailFocusNode), PasswordInput(focusNode: _passwordFocusNode), const SubmitButton(), ], ), ), ), ); } } class EmailInput extends StatelessWidget { const EmailInput({required this.focusNode, super.key}); final FocusNode focusNode; @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { return TextFormField( initialValue: state.email.value, focusNode: focusNode, decoration: InputDecoration( icon: const Icon(Icons.email), labelText: 'Email', helperText: 'A complete, valid email e.g. joe@gmail.com', errorText: state.email.displayError != null ? 'Please ensure the email entered is valid' : null, ), keyboardType: TextInputType.emailAddress, onChanged: (value) { context.read().add(EmailChanged(email: value)); }, textInputAction: TextInputAction.next, ); }, ); } } class PasswordInput extends StatelessWidget { const PasswordInput({required this.focusNode, super.key}); final FocusNode focusNode; @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { return TextFormField( initialValue: state.password.value, focusNode: focusNode, decoration: InputDecoration( icon: const Icon(Icons.lock), helperText: '''Password should be at least 8 characters with at least one letter and number''', helperMaxLines: 2, labelText: 'Password', errorMaxLines: 2, errorText: state.password.displayError != null ? '''Password must be at least 8 characters and contain at least one letter and number''' : null, ), obscureText: true, onChanged: (value) { context.read().add(PasswordChanged(password: value)); }, textInputAction: TextInputAction.done, ); }, ); } } class SubmitButton extends StatelessWidget { const SubmitButton({super.key}); @override Widget build(BuildContext context) { final isValid = context.select((MyFormBloc bloc) => bloc.state.isValid); return ElevatedButton( onPressed: isValid ? () => context.read().add(FormSubmitted()) : null, child: const Text('Submit'), ); } } class SuccessDialog extends StatelessWidget { const SuccessDialog({super.key}); @override Widget build(BuildContext context) { return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), child: Padding( padding: const EdgeInsets.all(8), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Row( children: [ Icon(Icons.info), Flexible( child: Padding( padding: EdgeInsets.all(10), child: Text( 'Form Submitted Successfully!', softWrap: true, ), ), ), ], ), ElevatedButton( child: const Text('OK'), onPressed: () => Navigator.of(context).pop(), ), ], ), ), ); } } ================================================ FILE: examples/flutter_form_validation/lib/models/email.dart ================================================ import 'package:formz/formz.dart'; enum EmailValidationError { invalid } final class Email extends FormzInput { const Email.pure([super.value = '']) : super.pure(); const Email.dirty([super.value = '']) : super.dirty(); static final _emailRegex = RegExp( r'^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$', ); @override EmailValidationError? validator(String? value) { return _emailRegex.hasMatch(value ?? '') ? null : EmailValidationError.invalid; } } ================================================ FILE: examples/flutter_form_validation/lib/models/models.dart ================================================ export 'email.dart'; export 'password.dart'; ================================================ FILE: examples/flutter_form_validation/lib/models/password.dart ================================================ import 'package:formz/formz.dart'; enum PasswordValidationError { invalid } final class Password extends FormzInput { const Password.pure([super.value = '']) : super.pure(); const Password.dirty([super.value = '']) : super.dirty(); static final _passwordRegex = RegExp( r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$', ); @override PasswordValidationError? validator(String? value) { return _passwordRegex.hasMatch(value ?? '') ? null : PasswordValidationError.invalid; } } ================================================ FILE: examples/flutter_form_validation/pubspec.yaml ================================================ name: flutter_form_validation description: A new Flutter project. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: bloc: ^9.0.0 equatable: ^2.0.0 flutter: sdk: flutter flutter_bloc: ^9.1.0 formz: ^0.8.0 flutter: uses-material-design: true dev_dependencies: bloc_lint: ^0.3.0 ================================================ FILE: examples/flutter_form_validation/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../../packages/bloc bloc_lint: path: ../../packages/bloc_lint flutter_bloc: path: ../../packages/flutter_bloc ================================================ FILE: examples/flutter_form_validation/web/index.html ================================================ flutter_form_validation ================================================ FILE: examples/flutter_form_validation/web/manifest.json ================================================ { "name": "flutter_form_validation", "short_name": "flutter_form_validation", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: examples/flutter_infinite_list/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: examples/flutter_infinite_list/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: web create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/flutter_infinite_list/README.md ================================================ [![build](https://github.com/felangel/bloc/actions/workflows/main.yaml/badge.svg)](https://github.com/felangel/bloc/actions) # flutter_infinite_list A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: examples/flutter_infinite_list/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml linter: rules: public_member_api_docs: false ================================================ FILE: examples/flutter_infinite_list/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: examples/flutter_infinite_list/lib/app.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_infinite_list/posts/posts.dart'; class App extends MaterialApp { const App({super.key}) : super(home: const PostsPage()); } ================================================ FILE: examples/flutter_infinite_list/lib/main.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_infinite_list/app.dart'; import 'package:flutter_infinite_list/simple_bloc_observer.dart'; void main() { Bloc.observer = const SimpleBlocObserver(); runApp(const App()); } ================================================ FILE: examples/flutter_infinite_list/lib/posts/bloc/post_bloc.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_infinite_list/posts/posts.dart'; import 'package:http/http.dart' as http; import 'package:stream_transform/stream_transform.dart'; part 'post_event.dart'; part 'post_state.dart'; const _postLimit = 20; const throttleDuration = Duration(milliseconds: 100); EventTransformer throttleDroppable(Duration duration) { return (events, mapper) { return droppable().call(events.throttle(duration), mapper); }; } class PostBloc extends Bloc { PostBloc({required http.Client httpClient}) : _httpClient = httpClient, super(const PostState()) { on( _onFetched, transformer: throttleDroppable(throttleDuration), ); } final http.Client _httpClient; Future _onFetched( PostFetched event, Emitter emit, ) async { if (state.hasReachedMax) return; try { final posts = await _fetchPosts(startIndex: state.posts.length); if (posts.isEmpty) { return emit(state.copyWith(hasReachedMax: true)); } emit( state.copyWith( status: PostStatus.success, posts: [...state.posts, ...posts], ), ); } catch (_) { emit(state.copyWith(status: PostStatus.failure)); } } Future> _fetchPosts({required int startIndex}) async { final response = await _httpClient.get( Uri.https( 'jsonplaceholder.typicode.com', '/posts', {'_start': '$startIndex', '_limit': '$_postLimit'}, ), ); if (response.statusCode == 200) { final body = json.decode(response.body) as List; return body.map((dynamic json) { final map = json as Map; return Post( id: map['id'] as int, title: map['title'] as String, body: map['body'] as String, ); }).toList(); } throw Exception('error fetching posts'); } } ================================================ FILE: examples/flutter_infinite_list/lib/posts/bloc/post_event.dart ================================================ part of 'post_bloc.dart'; sealed class PostEvent extends Equatable { @override List get props => []; } final class PostFetched extends PostEvent {} ================================================ FILE: examples/flutter_infinite_list/lib/posts/bloc/post_state.dart ================================================ part of 'post_bloc.dart'; enum PostStatus { initial, success, failure } final class PostState extends Equatable { const PostState({ this.status = PostStatus.initial, this.posts = const [], this.hasReachedMax = false, }); final PostStatus status; final List posts; final bool hasReachedMax; PostState copyWith({ PostStatus? status, List? posts, bool? hasReachedMax, }) { return PostState( status: status ?? this.status, posts: posts ?? this.posts, hasReachedMax: hasReachedMax ?? this.hasReachedMax, ); } @override String toString() { return '''PostState { status: $status, hasReachedMax: $hasReachedMax, posts: ${posts.length} }'''; } @override List get props => [status, posts, hasReachedMax]; } ================================================ FILE: examples/flutter_infinite_list/lib/posts/models/models.dart ================================================ export './post.dart'; ================================================ FILE: examples/flutter_infinite_list/lib/posts/models/post.dart ================================================ import 'package:equatable/equatable.dart'; final class Post extends Equatable { const Post({required this.id, required this.title, required this.body}); final int id; final String title; final String body; @override List get props => [id, title, body]; } ================================================ FILE: examples/flutter_infinite_list/lib/posts/posts.dart ================================================ export 'bloc/post_bloc.dart'; export 'models/models.dart'; export 'view/view.dart'; export 'widgets/widgets.dart'; ================================================ FILE: examples/flutter_infinite_list/lib/posts/view/posts_list.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_infinite_list/posts/posts.dart'; class PostsList extends StatefulWidget { const PostsList({super.key}); @override State createState() => _PostsListState(); } class _PostsListState extends State { final _scrollController = ScrollController(); @override void initState() { super.initState(); _scrollController.addListener(_onScroll); } @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { switch (state.status) { case PostStatus.failure: return const Center(child: Text('failed to fetch posts')); case PostStatus.success: if (state.posts.isEmpty) { return const Center(child: Text('no posts')); } return ListView.builder( itemBuilder: (BuildContext context, int index) { return index >= state.posts.length ? const BottomLoader() : PostListItem(post: state.posts[index]); }, itemCount: state.hasReachedMax ? state.posts.length : state.posts.length + 1, controller: _scrollController, ); case PostStatus.initial: return const Center(child: CircularProgressIndicator()); } }, ); } @override void dispose() { _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_isBottom) context.read().add(PostFetched()); } bool get _isBottom { if (!_scrollController.hasClients) return false; final maxScroll = _scrollController.position.maxScrollExtent; final currentScroll = _scrollController.offset; return currentScroll >= (maxScroll * 0.9); } } ================================================ FILE: examples/flutter_infinite_list/lib/posts/view/posts_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_infinite_list/posts/posts.dart'; import 'package:http/http.dart' as http; class PostsPage extends StatelessWidget { const PostsPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: BlocProvider( create: (_) => PostBloc(httpClient: http.Client())..add(PostFetched()), child: const PostsList(), ), ); } } ================================================ FILE: examples/flutter_infinite_list/lib/posts/view/view.dart ================================================ export 'posts_list.dart'; export 'posts_page.dart'; ================================================ FILE: examples/flutter_infinite_list/lib/posts/widgets/bottom_loader.dart ================================================ import 'package:flutter/material.dart'; class BottomLoader extends StatelessWidget { const BottomLoader({super.key}); @override Widget build(BuildContext context) { return const Center( child: SizedBox( height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 1.5), ), ); } } ================================================ FILE: examples/flutter_infinite_list/lib/posts/widgets/post_list_item.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_infinite_list/posts/posts.dart'; class PostListItem extends StatelessWidget { const PostListItem({required this.post, super.key}); final Post post; @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; return ListTile( leading: Text('${post.id}', style: textTheme.bodySmall), title: Text(post.title), isThreeLine: true, subtitle: Text(post.body), dense: true, ); } } ================================================ FILE: examples/flutter_infinite_list/lib/posts/widgets/widgets.dart ================================================ export 'bottom_loader.dart'; export 'post_list_item.dart'; ================================================ FILE: examples/flutter_infinite_list/lib/simple_bloc_observer.dart ================================================ // ignore_for_file: avoid_print import 'package:bloc/bloc.dart'; class SimpleBlocObserver extends BlocObserver { const SimpleBlocObserver(); @override void onTransition( Bloc bloc, Transition transition, ) { super.onTransition(bloc, transition); print(transition); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { print(error); super.onError(bloc, error, stackTrace); } } ================================================ FILE: examples/flutter_infinite_list/macos/.gitignore ================================================ # Flutter-related **/Flutter/ephemeral/ **/Pods/ # Xcode-related **/dgph **/xcuserdata/ ================================================ FILE: examples/flutter_infinite_list/macos/Flutter/GeneratedPluginRegistrant.swift ================================================ // // Generated file. Do not edit. // import FlutterMacOS import Foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { } ================================================ FILE: examples/flutter_infinite_list/pubspec.yaml ================================================ name: flutter_infinite_list description: A new Flutter project. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: bloc: ^9.0.0 bloc_concurrency: ^0.3.0 equatable: ^2.0.0 flutter: sdk: flutter flutter_bloc: ^9.1.0 http: ^1.0.0 stream_transform: ^2.0.0 dev_dependencies: bloc_lint: ^0.3.0 bloc_test: ^10.0.0 flutter_test: sdk: flutter mocktail: ^1.0.0 flutter: uses-material-design: true ================================================ FILE: examples/flutter_infinite_list/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../../packages/bloc bloc_lint: path: ../../packages/bloc_lint bloc_test: path: ../../packages/bloc_test flutter_bloc: path: ../../packages/flutter_bloc ================================================ FILE: examples/flutter_infinite_list/test/app_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_infinite_list/app.dart'; import 'package:flutter_infinite_list/posts/posts.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('App', () { testWidgets('renders PostsPage', (tester) async { await tester.pumpWidget(App()); await tester.pumpAndSettle(); expect(find.byType(PostsPage), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_infinite_list/test/posts/bloc/post_bloc_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_infinite_list/posts/bloc/post_bloc.dart'; import 'package:flutter_infinite_list/posts/models/post.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:mocktail/mocktail.dart'; class MockClient extends Mock implements http.Client {} Uri _postsUrl({required int start}) { return Uri.https( 'jsonplaceholder.typicode.com', '/posts', {'_start': '$start', '_limit': '20'}, ); } void main() { group('PostBloc', () { const mockPosts = [Post(id: 1, title: 'post title', body: 'post body')]; const extraMockPosts = [ Post(id: 2, title: 'post title', body: 'post body'), ]; late http.Client httpClient; setUpAll(() { registerFallbackValue(Uri()); }); setUp(() { httpClient = MockClient(); }); test('initial state is PostState()', () { expect(PostBloc(httpClient: httpClient).state, const PostState()); }); group('PostFetched', () { blocTest( 'emits nothing when posts has reached maximum amount', build: () => PostBloc(httpClient: httpClient), seed: () => const PostState(hasReachedMax: true), act: (bloc) => bloc.add(PostFetched()), expect: () => [], ); blocTest( 'emits successful status when http fetches initial posts', setUp: () { when(() => httpClient.get(any())).thenAnswer((_) async { return http.Response( '[{ "id": 1, "title": "post title", "body": "post body" }]', 200, ); }); }, build: () => PostBloc(httpClient: httpClient), act: (bloc) => bloc.add(PostFetched()), expect: () => const [ PostState(status: PostStatus.success, posts: mockPosts), ], verify: (_) { verify(() => httpClient.get(_postsUrl(start: 0))).called(1); }, ); blocTest( 'drops new events when processing current event', setUp: () { when(() => httpClient.get(any())).thenAnswer((_) async { return http.Response( '[{ "id": 1, "title": "post title", "body": "post body" }]', 200, ); }); }, build: () => PostBloc(httpClient: httpClient), act: (bloc) => bloc ..add(PostFetched()) ..add(PostFetched()), expect: () => const [ PostState(status: PostStatus.success, posts: mockPosts), ], verify: (_) { verify(() => httpClient.get(any())).called(1); }, ); blocTest( 'throttles events', setUp: () { when(() => httpClient.get(any())).thenAnswer((_) async { return http.Response( '[{ "id": 1, "title": "post title", "body": "post body" }]', 200, ); }); }, build: () => PostBloc(httpClient: httpClient), act: (bloc) async { bloc.add(PostFetched()); await Future.delayed(Duration.zero); bloc.add(PostFetched()); }, expect: () => const [ PostState(status: PostStatus.success, posts: mockPosts), ], verify: (_) { verify(() => httpClient.get(any())).called(1); }, ); blocTest( 'emits failure status when http fetches posts and throw exception', setUp: () { when(() => httpClient.get(any())).thenAnswer( (_) async => http.Response('', 500), ); }, build: () => PostBloc(httpClient: httpClient), act: (bloc) => bloc.add(PostFetched()), expect: () => [const PostState(status: PostStatus.failure)], verify: (_) { verify(() => httpClient.get(_postsUrl(start: 0))).called(1); }, ); blocTest( 'emits successful status and reaches max posts when ' '0 additional posts are fetched', setUp: () { when(() => httpClient.get(any())).thenAnswer( (_) async => http.Response('[]', 200), ); }, build: () => PostBloc(httpClient: httpClient), seed: () => const PostState( status: PostStatus.success, posts: mockPosts, ), act: (bloc) => bloc.add(PostFetched()), expect: () => const [ PostState( status: PostStatus.success, posts: mockPosts, hasReachedMax: true, ), ], verify: (_) { verify(() => httpClient.get(_postsUrl(start: 1))).called(1); }, ); blocTest( 'emits successful status and does not reach max posts ' 'when additional posts are fetched', setUp: () { when(() => httpClient.get(any())).thenAnswer((_) async { return http.Response( '[{ "id": 2, "title": "post title", "body": "post body" }]', 200, ); }); }, build: () => PostBloc(httpClient: httpClient), seed: () => const PostState( status: PostStatus.success, posts: mockPosts, ), act: (bloc) => bloc.add(PostFetched()), expect: () => const [ PostState( status: PostStatus.success, posts: [...mockPosts, ...extraMockPosts], ), ], verify: (_) { verify(() => httpClient.get(_postsUrl(start: 1))).called(1); }, ); }); }); } ================================================ FILE: examples/flutter_infinite_list/test/posts/bloc/post_event_test.dart ================================================ import 'package:flutter_infinite_list/posts/posts.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('PostEvent', () { group('PostFetched', () { test('supports value comparison', () { expect(PostFetched(), PostFetched()); }); }); }); } ================================================ FILE: examples/flutter_infinite_list/test/posts/bloc/post_state_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_infinite_list/posts/posts.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('PostState', () { test('supports value comparison', () { expect(PostState(), PostState()); expect( PostState().toString(), PostState().toString(), ); }); }); } ================================================ FILE: examples/flutter_infinite_list/test/posts/models/post_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_infinite_list/posts/models/models.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('Post', () { test('supports value comparison', () { expect( Post(id: 1, title: 'post title', body: 'post body'), Post(id: 1, title: 'post title', body: 'post body'), ); }); }); } ================================================ FILE: examples/flutter_infinite_list/test/posts/view/posts_list_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_infinite_list/posts/posts.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockPostBloc extends MockBloc implements PostBloc {} extension on WidgetTester { Future pumpPostsList(PostBloc postBloc) { return pumpWidget( MaterialApp( home: BlocProvider.value( value: postBloc, child: Scaffold(body: PostsList()), ), ), ); } } void main() { final mockPosts = List.generate( 5, (i) => Post(id: i, title: 'post title', body: 'post body'), ); late PostBloc postBloc; setUp(() { postBloc = MockPostBloc(); }); group('PostsList', () { testWidgets('renders CircularProgressIndicator ' 'when post status is initial', (tester) async { when(() => postBloc.state).thenReturn(const PostState()); await tester.pumpPostsList(postBloc); expect(find.byType(CircularProgressIndicator), findsOneWidget); }); testWidgets('renders no posts text ' 'when post status is success but with 0 posts', (tester) async { when(() => postBloc.state).thenReturn( const PostState(status: PostStatus.success, hasReachedMax: true), ); await tester.pumpPostsList(postBloc); expect(find.text('no posts'), findsOneWidget); }); testWidgets( 'renders 5 posts and a bottom loader when post max is not reached yet', (tester) async { when(() => postBloc.state).thenReturn( PostState( status: PostStatus.success, posts: mockPosts, ), ); await tester.pumpPostsList(postBloc); expect(find.byType(PostListItem), findsNWidgets(5)); expect(find.byType(BottomLoader), findsOneWidget); }, ); testWidgets('does not render bottom loader when post max is reached', ( tester, ) async { when(() => postBloc.state).thenReturn( PostState( status: PostStatus.success, posts: mockPosts, hasReachedMax: true, ), ); await tester.pumpPostsList(postBloc); expect(find.byType(BottomLoader), findsNothing); }); testWidgets('fetches more posts when scrolled to the bottom', ( tester, ) async { when(() => postBloc.state).thenReturn( PostState( status: PostStatus.success, posts: List.generate( 10, (i) => Post(id: i, title: 'post title', body: 'post body'), ), ), ); await tester.pumpPostsList(postBloc); await tester.drag(find.byType(PostsList), const Offset(0, -500)); verify(() => postBloc.add(PostFetched())).called(1); }); }); } ================================================ FILE: examples/flutter_infinite_list/test/posts/view/posts_page_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter/material.dart'; import 'package:flutter_infinite_list/posts/posts.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('PostsPage', () { testWidgets('renders PostList', (tester) async { await tester.pumpWidget(MaterialApp(home: PostsPage())); await tester.pumpAndSettle(); expect(find.byType(PostsList), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_infinite_list/web/index.html ================================================ flutter_infinite_list ================================================ FILE: examples/flutter_infinite_list/web/manifest.json ================================================ { "name": "flutter_infinite_list", "short_name": "flutter_infinite_list", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: examples/flutter_login/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: examples/flutter_login/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: web create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/flutter_login/README.md ================================================ # flutter_login A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: examples/flutter_login/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml linter: rules: public_member_api_docs: false ================================================ FILE: examples/flutter_login/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: examples/flutter_login/lib/app.dart ================================================ import 'package:authentication_repository/authentication_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_login/authentication/authentication.dart'; import 'package:flutter_login/home/home.dart'; import 'package:flutter_login/login/login.dart'; import 'package:flutter_login/splash/splash.dart'; import 'package:user_repository/user_repository.dart'; class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (_) => AuthenticationRepository(), dispose: (repository) => repository.dispose(), ), RepositoryProvider(create: (_) => UserRepository()), ], child: BlocProvider( lazy: false, create: (context) => AuthenticationBloc( authenticationRepository: context.read(), userRepository: context.read(), )..add(AuthenticationSubscriptionRequested()), child: const AppView(), ), ); } } class AppView extends StatefulWidget { const AppView({super.key}); @override State createState() => _AppViewState(); } class _AppViewState extends State { final _navigatorKey = GlobalKey(); NavigatorState get _navigator => _navigatorKey.currentState!; @override Widget build(BuildContext context) { return MaterialApp( navigatorKey: _navigatorKey, builder: (context, child) { return BlocListener( listener: (context, state) { switch (state.status) { case AuthenticationStatus.authenticated: _navigator.pushAndRemoveUntil( HomePage.route(), (route) => false, ); case AuthenticationStatus.unauthenticated: _navigator.pushAndRemoveUntil( LoginPage.route(), (route) => false, ); case AuthenticationStatus.unknown: break; } }, child: child, ); }, onGenerateRoute: (_) => SplashPage.route(), ); } } ================================================ FILE: examples/flutter_login/lib/authentication/authentication.dart ================================================ export 'bloc/authentication_bloc.dart'; ================================================ FILE: examples/flutter_login/lib/authentication/bloc/authentication_bloc.dart ================================================ import 'dart:async'; import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:user_repository/user_repository.dart'; part 'authentication_event.dart'; part 'authentication_state.dart'; class AuthenticationBloc extends Bloc { AuthenticationBloc({ required AuthenticationRepository authenticationRepository, required UserRepository userRepository, }) : _authenticationRepository = authenticationRepository, _userRepository = userRepository, super(const AuthenticationState.unknown()) { on(_onSubscriptionRequested); on(_onLogoutPressed); } final AuthenticationRepository _authenticationRepository; final UserRepository _userRepository; Future _onSubscriptionRequested( AuthenticationSubscriptionRequested event, Emitter emit, ) { return emit.onEach( _authenticationRepository.status, onData: (status) async { switch (status) { case AuthenticationStatus.unauthenticated: return emit(const AuthenticationState.unauthenticated()); case AuthenticationStatus.authenticated: final user = await _tryGetUser(); return emit( user != null ? AuthenticationState.authenticated(user) : const AuthenticationState.unauthenticated(), ); case AuthenticationStatus.unknown: return emit(const AuthenticationState.unknown()); } }, onError: addError, ); } void _onLogoutPressed( AuthenticationLogoutPressed event, Emitter emit, ) { _authenticationRepository.logOut(); } Future _tryGetUser() async { try { final user = await _userRepository.getUser(); return user; } catch (_) { return null; } } } ================================================ FILE: examples/flutter_login/lib/authentication/bloc/authentication_event.dart ================================================ part of 'authentication_bloc.dart'; sealed class AuthenticationEvent { const AuthenticationEvent(); } final class AuthenticationSubscriptionRequested extends AuthenticationEvent {} final class AuthenticationLogoutPressed extends AuthenticationEvent {} ================================================ FILE: examples/flutter_login/lib/authentication/bloc/authentication_state.dart ================================================ part of 'authentication_bloc.dart'; class AuthenticationState extends Equatable { const AuthenticationState._({ this.status = AuthenticationStatus.unknown, this.user = User.empty, }); const AuthenticationState.unknown() : this._(); const AuthenticationState.authenticated(User user) : this._(status: AuthenticationStatus.authenticated, user: user); const AuthenticationState.unauthenticated() : this._(status: AuthenticationStatus.unauthenticated); final AuthenticationStatus status; final User user; @override List get props => [status, user]; } ================================================ FILE: examples/flutter_login/lib/home/home.dart ================================================ export 'view/home_page.dart'; ================================================ FILE: examples/flutter_login/lib/home/view/home_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_login/authentication/authentication.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); static Route route() { return MaterialPageRoute(builder: (_) => const HomePage()); } @override Widget build(BuildContext context) { return const Scaffold( body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [_UserId(), _LogoutButton()], ), ), ); } } class _LogoutButton extends StatelessWidget { const _LogoutButton(); @override Widget build(BuildContext context) { return ElevatedButton( child: const Text('Logout'), onPressed: () { context.read().add(AuthenticationLogoutPressed()); }, ); } } class _UserId extends StatelessWidget { const _UserId(); @override Widget build(BuildContext context) { final userId = context.select( (AuthenticationBloc bloc) => bloc.state.user.id, ); return Text('UserID: $userId'); } } ================================================ FILE: examples/flutter_login/lib/login/bloc/login_bloc.dart ================================================ import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_login/login/login.dart'; import 'package:formz/formz.dart'; part 'login_event.dart'; part 'login_state.dart'; class LoginBloc extends Bloc { LoginBloc({ required AuthenticationRepository authenticationRepository, }) : _authenticationRepository = authenticationRepository, super(const LoginState()) { on(_onUsernameChanged); on(_onPasswordChanged); on(_onSubmitted); } final AuthenticationRepository _authenticationRepository; void _onUsernameChanged( LoginUsernameChanged event, Emitter emit, ) { final username = Username.dirty(event.username); emit( state.copyWith( username: username, isValid: Formz.validate([state.password, username]), ), ); } void _onPasswordChanged( LoginPasswordChanged event, Emitter emit, ) { final password = Password.dirty(event.password); emit( state.copyWith( password: password, isValid: Formz.validate([password, state.username]), ), ); } Future _onSubmitted( LoginSubmitted event, Emitter emit, ) async { if (state.isValid) { emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); try { await _authenticationRepository.logIn( username: state.username.value, password: state.password.value, ); emit(state.copyWith(status: FormzSubmissionStatus.success)); } catch (_) { emit(state.copyWith(status: FormzSubmissionStatus.failure)); } } } } ================================================ FILE: examples/flutter_login/lib/login/bloc/login_event.dart ================================================ part of 'login_bloc.dart'; sealed class LoginEvent extends Equatable { const LoginEvent(); @override List get props => []; } final class LoginUsernameChanged extends LoginEvent { const LoginUsernameChanged(this.username); final String username; @override List get props => [username]; } final class LoginPasswordChanged extends LoginEvent { const LoginPasswordChanged(this.password); final String password; @override List get props => [password]; } final class LoginSubmitted extends LoginEvent { const LoginSubmitted(); } ================================================ FILE: examples/flutter_login/lib/login/bloc/login_state.dart ================================================ part of 'login_bloc.dart'; final class LoginState extends Equatable { const LoginState({ this.status = FormzSubmissionStatus.initial, this.username = const Username.pure(), this.password = const Password.pure(), this.isValid = false, }); final FormzSubmissionStatus status; final Username username; final Password password; final bool isValid; LoginState copyWith({ FormzSubmissionStatus? status, Username? username, Password? password, bool? isValid, }) { return LoginState( status: status ?? this.status, username: username ?? this.username, password: password ?? this.password, isValid: isValid ?? this.isValid, ); } @override List get props => [status, username, password]; } ================================================ FILE: examples/flutter_login/lib/login/login.dart ================================================ export 'bloc/login_bloc.dart'; export 'models/models.dart'; export 'view/view.dart'; ================================================ FILE: examples/flutter_login/lib/login/models/models.dart ================================================ export 'password.dart'; export 'username.dart'; ================================================ FILE: examples/flutter_login/lib/login/models/password.dart ================================================ import 'package:formz/formz.dart'; enum PasswordValidationError { empty } class Password extends FormzInput { const Password.pure() : super.pure(''); const Password.dirty([super.value = '']) : super.dirty(); @override PasswordValidationError? validator(String value) { if (value.isEmpty) return PasswordValidationError.empty; return null; } } ================================================ FILE: examples/flutter_login/lib/login/models/username.dart ================================================ import 'package:formz/formz.dart'; enum UsernameValidationError { empty } class Username extends FormzInput { const Username.pure() : super.pure(''); const Username.dirty([super.value = '']) : super.dirty(); @override UsernameValidationError? validator(String value) { if (value.isEmpty) return UsernameValidationError.empty; return null; } } ================================================ FILE: examples/flutter_login/lib/login/view/login_form.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_login/login/login.dart'; import 'package:formz/formz.dart'; class LoginForm extends StatelessWidget { const LoginForm({super.key}); @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { if (state.status.isFailure) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( const SnackBar(content: Text('Authentication Failure')), ); } }, child: Align( alignment: const Alignment(0, -1 / 3), child: Column( mainAxisSize: MainAxisSize.min, children: [ _UsernameInput(), const Padding(padding: EdgeInsets.all(12)), _PasswordInput(), const Padding(padding: EdgeInsets.all(12)), _LoginButton(), ], ), ), ); } } class _UsernameInput extends StatelessWidget { @override Widget build(BuildContext context) { final displayError = context.select( (LoginBloc bloc) => bloc.state.username.displayError, ); return TextField( key: const Key('loginForm_usernameInput_textField'), onChanged: (username) { context.read().add(LoginUsernameChanged(username)); }, decoration: InputDecoration( labelText: 'username', errorText: displayError != null ? 'invalid username' : null, ), ); } } class _PasswordInput extends StatelessWidget { @override Widget build(BuildContext context) { final displayError = context.select( (LoginBloc bloc) => bloc.state.password.displayError, ); return TextField( key: const Key('loginForm_passwordInput_textField'), onChanged: (password) { context.read().add(LoginPasswordChanged(password)); }, obscureText: true, decoration: InputDecoration( labelText: 'password', errorText: displayError != null ? 'invalid password' : null, ), ); } } class _LoginButton extends StatelessWidget { @override Widget build(BuildContext context) { final isInProgressOrSuccess = context.select( (LoginBloc bloc) => bloc.state.status.isInProgressOrSuccess, ); if (isInProgressOrSuccess) return const CircularProgressIndicator(); final isValid = context.select((LoginBloc bloc) => bloc.state.isValid); return ElevatedButton( key: const Key('loginForm_continue_raisedButton'), onPressed: isValid ? () => context.read().add(const LoginSubmitted()) : null, child: const Text('Login'), ); } } ================================================ FILE: examples/flutter_login/lib/login/view/login_page.dart ================================================ import 'package:authentication_repository/authentication_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_login/login/login.dart'; class LoginPage extends StatelessWidget { const LoginPage({super.key}); static Route route() { return MaterialPageRoute(builder: (_) => const LoginPage()); } @override Widget build(BuildContext context) { return Scaffold( body: Padding( padding: const EdgeInsets.all(12), child: BlocProvider( create: (context) => LoginBloc( authenticationRepository: context.read(), ), child: const LoginForm(), ), ), ); } } ================================================ FILE: examples/flutter_login/lib/login/view/view.dart ================================================ export 'login_form.dart'; export 'login_page.dart'; ================================================ FILE: examples/flutter_login/lib/main.dart ================================================ import 'package:flutter/widgets.dart'; import 'package:flutter_login/app.dart'; void main() => runApp(const App()); ================================================ FILE: examples/flutter_login/lib/splash/splash.dart ================================================ export 'view/splash_page.dart'; ================================================ FILE: examples/flutter_login/lib/splash/view/splash_page.dart ================================================ import 'package:flutter/material.dart'; class SplashPage extends StatelessWidget { const SplashPage({super.key}); static Route route() { return MaterialPageRoute(builder: (_) => const SplashPage()); } @override Widget build(BuildContext context) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } } ================================================ FILE: examples/flutter_login/packages/authentication_repository/analysis_options.yaml ================================================ include: ../../../../analysis_options.yaml linter: rules: public_member_api_docs: false ================================================ FILE: examples/flutter_login/packages/authentication_repository/lib/authentication_repository.dart ================================================ export 'src/authentication_repository.dart'; ================================================ FILE: examples/flutter_login/packages/authentication_repository/lib/src/authentication_repository.dart ================================================ import 'dart:async'; enum AuthenticationStatus { unknown, authenticated, unauthenticated } class AuthenticationRepository { final _controller = StreamController(); Stream get status async* { await Future.delayed(const Duration(seconds: 1)); yield AuthenticationStatus.unauthenticated; yield* _controller.stream; } Future logIn({ required String username, required String password, }) async { await Future.delayed( const Duration(milliseconds: 300), () => _controller.add(AuthenticationStatus.authenticated), ); } void logOut() { _controller.add(AuthenticationStatus.unauthenticated); } void dispose() => _controller.close(); } ================================================ FILE: examples/flutter_login/packages/authentication_repository/pubspec.yaml ================================================ name: authentication_repository description: Dart package which manages the authentication domain. publish_to: none environment: sdk: ">=3.10.0 <4.0.0" ================================================ FILE: examples/flutter_login/packages/user_repository/analysis_options.yaml ================================================ include: ../../../../analysis_options.yaml linter: rules: public_member_api_docs: false ================================================ FILE: examples/flutter_login/packages/user_repository/lib/src/models/models.dart ================================================ export 'user.dart'; ================================================ FILE: examples/flutter_login/packages/user_repository/lib/src/models/user.dart ================================================ import 'package:equatable/equatable.dart'; class User extends Equatable { const User(this.id); final String id; @override List get props => [id]; static const empty = User('-'); } ================================================ FILE: examples/flutter_login/packages/user_repository/lib/src/user_repository.dart ================================================ import 'dart:async'; import 'package:user_repository/src/models/models.dart'; import 'package:uuid/uuid.dart'; class UserRepository { User? _user; Future getUser() async { if (_user != null) return _user; return Future.delayed( const Duration(milliseconds: 300), () => _user = User(const Uuid().v4()), ); } } ================================================ FILE: examples/flutter_login/packages/user_repository/lib/user_repository.dart ================================================ export 'src/models/models.dart'; export 'src/user_repository.dart'; ================================================ FILE: examples/flutter_login/packages/user_repository/pubspec.yaml ================================================ name: user_repository description: Dart package which manages the user domain. publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: equatable: ^2.0.0 uuid: ^3.0.0 ================================================ FILE: examples/flutter_login/pubspec.yaml ================================================ name: flutter_login description: A new Flutter project. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: authentication_repository: path: packages/authentication_repository bloc: ^9.0.0 equatable: ^2.0.0 flutter: sdk: flutter flutter_bloc: ^9.1.0 formz: ^0.8.0 user_repository: path: packages/user_repository dev_dependencies: bloc_lint: ^0.3.0 bloc_test: ^10.0.0 flutter_test: sdk: flutter mocktail: ^1.0.0 flutter: uses-material-design: true ================================================ FILE: examples/flutter_login/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../../packages/bloc bloc_lint: path: ../../packages/bloc_lint bloc_test: path: ../../packages/bloc_test flutter_bloc: path: ../../packages/flutter_bloc ================================================ FILE: examples/flutter_login/test/authentication/authentication_bloc_test.dart ================================================ import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_login/authentication/authentication.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:user_repository/user_repository.dart'; class _MockAuthenticationRepository extends Mock implements AuthenticationRepository {} class _MockUserRepository extends Mock implements UserRepository {} void main() { const user = User('id'); late AuthenticationRepository authenticationRepository; late UserRepository userRepository; setUp(() { authenticationRepository = _MockAuthenticationRepository(); when( () => authenticationRepository.status, ).thenAnswer((_) => const Stream.empty()); userRepository = _MockUserRepository(); }); AuthenticationBloc buildBloc() { return AuthenticationBloc( authenticationRepository: authenticationRepository, userRepository: userRepository, ); } group('AuthenticationBloc', () { test('initial state is AuthenticationState.unknown', () { final authenticationBloc = buildBloc(); expect(authenticationBloc.state, const AuthenticationState.unknown()); authenticationBloc.close(); }); group('AuthenticationSubscriptionRequested', () { final error = Exception('oops'); blocTest( 'emits [unauthenticated] when status is unauthenticated', setUp: () { when(() => authenticationRepository.status).thenAnswer( (_) => Stream.value(AuthenticationStatus.unauthenticated), ); }, build: buildBloc, act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), expect: () => const [AuthenticationState.unauthenticated()], ); blocTest( 'emits [authenticated] when status is authenticated', setUp: () { when(() => authenticationRepository.status).thenAnswer( (_) => Stream.value(AuthenticationStatus.authenticated), ); when(() => userRepository.getUser()).thenAnswer((_) async => user); }, build: buildBloc, act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), expect: () => const [AuthenticationState.authenticated(user)], ); blocTest( 'emits [authenticated] when status is authenticated', setUp: () { when( () => authenticationRepository.status, ).thenAnswer((_) => Stream.value(AuthenticationStatus.authenticated)); when(() => userRepository.getUser()).thenAnswer((_) async => user); }, build: buildBloc, act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), expect: () => const [AuthenticationState.authenticated(user)], ); blocTest( 'emits [unauthenticated] when status is unauthenticated', setUp: () { when(() => authenticationRepository.status).thenAnswer( (_) => Stream.value(AuthenticationStatus.unauthenticated), ); }, build: buildBloc, act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), expect: () => const [AuthenticationState.unauthenticated()], ); blocTest( 'emits [unauthenticated] when status is authenticated ' 'but getUser fails', setUp: () { when( () => authenticationRepository.status, ).thenAnswer((_) => Stream.value(AuthenticationStatus.authenticated)); when(() => userRepository.getUser()).thenThrow(Exception('oops')); }, build: buildBloc, act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), expect: () => const [AuthenticationState.unauthenticated()], ); blocTest( 'emits [unauthenticated] when status is authenticated ' 'but getUser returns null', setUp: () { when( () => authenticationRepository.status, ).thenAnswer((_) => Stream.value(AuthenticationStatus.authenticated)); when(() => userRepository.getUser()).thenAnswer((_) async => null); }, build: buildBloc, act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), expect: () => const [AuthenticationState.unauthenticated()], ); blocTest( 'emits [unknown] when status is unknown', setUp: () { when( () => authenticationRepository.status, ).thenAnswer((_) => Stream.value(AuthenticationStatus.unknown)); }, build: buildBloc, act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), expect: () => const [AuthenticationState.unknown()], ); blocTest( 'adds error when status stream emits an error', setUp: () { when( () => authenticationRepository.status, ).thenAnswer((_) => Stream.error(error)); }, build: buildBloc, act: (bloc) => bloc.add(AuthenticationSubscriptionRequested()), errors: () => [error], ); }); }); group('AuthenticationLogoutPressed', () { blocTest( 'calls logOut on authenticationRepository ', build: buildBloc, act: (bloc) => bloc.add(AuthenticationLogoutPressed()), verify: (_) { verify(() => authenticationRepository.logOut()).called(1); }, ); }); } ================================================ FILE: examples/flutter_login/test/authentication/authentication_state_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_login/authentication/authentication.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:user_repository/user_repository.dart'; class MockUser extends Mock implements User {} void main() { group('AuthenticationState', () { group('AuthenticationState.unknown', () { test('supports value comparisons', () { expect( AuthenticationState.unknown(), AuthenticationState.unknown(), ); }); }); group('AuthenticationState.authenticated', () { test('supports value comparisons', () { final user = MockUser(); expect( AuthenticationState.authenticated(user), AuthenticationState.authenticated(user), ); }); }); group('AuthenticationState.unauthenticated', () { test('supports value comparisons', () { expect( AuthenticationState.unauthenticated(), AuthenticationState.unauthenticated(), ); }); }); }); } ================================================ FILE: examples/flutter_login/test/login/bloc/login_bloc_test.dart ================================================ import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_login/login/login.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:formz/formz.dart'; import 'package:mocktail/mocktail.dart'; class MockAuthenticationRepository extends Mock implements AuthenticationRepository {} void main() { late AuthenticationRepository authenticationRepository; setUp(() { authenticationRepository = MockAuthenticationRepository(); }); group('LoginBloc', () { test('initial state is LoginState', () { final loginBloc = LoginBloc( authenticationRepository: authenticationRepository, ); expect(loginBloc.state, const LoginState()); }); group('LoginSubmitted', () { blocTest( 'emits [submissionInProgress, submissionSuccess] ' 'when login succeeds', setUp: () { when( () => authenticationRepository.logIn( username: 'username', password: 'password', ), ).thenAnswer((_) => Future.value('user')); }, build: () => LoginBloc( authenticationRepository: authenticationRepository, ), act: (bloc) { bloc ..add(const LoginUsernameChanged('username')) ..add(const LoginPasswordChanged('password')) ..add(const LoginSubmitted()); }, expect: () => const [ LoginState(username: Username.dirty('username')), LoginState( username: Username.dirty('username'), password: Password.dirty('password'), isValid: true, ), LoginState( username: Username.dirty('username'), password: Password.dirty('password'), isValid: true, status: FormzSubmissionStatus.inProgress, ), LoginState( username: Username.dirty('username'), password: Password.dirty('password'), isValid: true, status: FormzSubmissionStatus.success, ), ], ); blocTest( 'emits [LoginInProgress, LoginFailure] when logIn fails', setUp: () { when( () => authenticationRepository.logIn( username: 'username', password: 'password', ), ).thenThrow(Exception('oops')); }, build: () => LoginBloc( authenticationRepository: authenticationRepository, ), act: (bloc) { bloc ..add(const LoginUsernameChanged('username')) ..add(const LoginPasswordChanged('password')) ..add(const LoginSubmitted()); }, expect: () => const [ LoginState( username: Username.dirty('username'), ), LoginState( username: Username.dirty('username'), password: Password.dirty('password'), isValid: true, ), LoginState( username: Username.dirty('username'), password: Password.dirty('password'), isValid: true, status: FormzSubmissionStatus.inProgress, ), LoginState( username: Username.dirty('username'), password: Password.dirty('password'), isValid: true, status: FormzSubmissionStatus.failure, ), ], ); }); }); } ================================================ FILE: examples/flutter_login/test/login/bloc/login_event_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_login/login/login.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { const username = 'mock-username'; const password = 'mock-password'; group('LoginEvent', () { group('LoginUsernameChanged', () { test('supports value comparisons', () { expect(LoginUsernameChanged(username), LoginUsernameChanged(username)); }); }); group('LoginPasswordChanged', () { test('supports value comparisons', () { expect(LoginPasswordChanged(password), LoginPasswordChanged(password)); }); }); group('LoginSubmitted', () { test('supports value comparisons', () { expect(LoginSubmitted(), LoginSubmitted()); }); }); }); } ================================================ FILE: examples/flutter_login/test/login/bloc/login_state_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_login/login/login.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:formz/formz.dart'; void main() { const username = Username.dirty('username'); const password = Password.dirty('password'); group('LoginState', () { test('supports value comparisons', () { expect(LoginState(), LoginState()); }); test('returns same object when no properties are passed', () { expect(LoginState().copyWith(), LoginState()); }); test('returns object with updated status when status is passed', () { expect( LoginState().copyWith(status: FormzSubmissionStatus.initial), LoginState(), ); }); test('returns object with updated username when username is passed', () { expect( LoginState().copyWith(username: username), LoginState(username: username), ); }); test('returns object with updated password when password is passed', () { expect( LoginState().copyWith(password: password), LoginState(password: password), ); }); }); } ================================================ FILE: examples/flutter_login/test/login/models/password_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_login/login/login.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { const passwordString = 'mock-password'; group('Password', () { group('constructors', () { test('pure creates correct instance', () { final password = Password.pure(); expect(password.value, ''); expect(password.isPure, isTrue); }); test('dirty creates correct instance', () { final password = Password.dirty(passwordString); expect(password.value, passwordString); expect(password.isPure, isFalse); }); }); group('validator', () { test('returns empty error when password is empty', () { expect( Password.dirty().error, PasswordValidationError.empty, ); }); test('is valid when password is not empty', () { expect( Password.dirty(passwordString).error, isNull, ); }); }); }); } ================================================ FILE: examples/flutter_login/test/login/models/username_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_login/login/login.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { const usernameString = 'mock-username'; group('Username', () { group('constructors', () { test('pure creates correct instance', () { final username = Username.pure(); expect(username.value, ''); expect(username.isPure, isTrue); }); test('dirty creates correct instance', () { final username = Username.dirty(usernameString); expect(username.value, usernameString); expect(username.isPure, isFalse); }); }); group('validator', () { test('returns empty error when username is empty', () { expect( Username.dirty().error, UsernameValidationError.empty, ); }); test('is valid when username is not empty', () { expect( Username.dirty(usernameString).error, isNull, ); }); }); }); } ================================================ FILE: examples/flutter_login/test/login/view/login_form_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_login/login/login.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:formz/formz.dart'; import 'package:mocktail/mocktail.dart'; class MockLoginBloc extends MockBloc implements LoginBloc {} void main() { group('LoginForm', () { late LoginBloc loginBloc; setUp(() { loginBloc = MockLoginBloc(); }); testWidgets( 'adds LoginUsernameChanged to LoginBloc when username is updated', (tester) async { const username = 'username'; when(() => loginBloc.state).thenReturn(const LoginState()); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginBloc, child: LoginForm(), ), ), ), ); await tester.enterText( find.byKey(const Key('loginForm_usernameInput_textField')), username, ); verify( () => loginBloc.add(const LoginUsernameChanged(username)), ).called(1); }, ); testWidgets( 'adds LoginPasswordChanged to LoginBloc when password is updated', (tester) async { const password = 'password'; when(() => loginBloc.state).thenReturn(const LoginState()); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginBloc, child: LoginForm(), ), ), ), ); await tester.enterText( find.byKey(const Key('loginForm_passwordInput_textField')), password, ); verify( () => loginBloc.add(const LoginPasswordChanged(password)), ).called(1); }, ); testWidgets('continue button is disabled by default', (tester) async { when(() => loginBloc.state).thenReturn(const LoginState()); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginBloc, child: LoginForm(), ), ), ), ); final button = tester.widget(find.byType(ElevatedButton)); expect(button.enabled, isFalse); }); testWidgets( 'loading indicator is shown when status is submission in progress', (tester) async { when(() => loginBloc.state).thenReturn( const LoginState(status: FormzSubmissionStatus.inProgress), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginBloc, child: LoginForm(), ), ), ), ); expect(find.byType(ElevatedButton), findsNothing); expect(find.byType(CircularProgressIndicator), findsOneWidget); }, ); testWidgets( 'loading indicator is shown when status is submission success', (tester) async { when(() => loginBloc.state).thenReturn( const LoginState(status: FormzSubmissionStatus.success), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginBloc, child: LoginForm(), ), ), ), ); expect(find.byType(ElevatedButton), findsNothing); expect(find.byType(CircularProgressIndicator), findsOneWidget); }, ); testWidgets('continue button is enabled when status is validated', ( tester, ) async { when(() => loginBloc.state).thenReturn(const LoginState(isValid: true)); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginBloc, child: LoginForm(), ), ), ), ); final button = tester.widget(find.byType(ElevatedButton)); expect(button.enabled, isTrue); }); testWidgets( 'LoginSubmitted is added to LoginBloc when continue is tapped', (tester) async { when(() => loginBloc.state).thenReturn(const LoginState(isValid: true)); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginBloc, child: LoginForm(), ), ), ), ); await tester.tap(find.byType(ElevatedButton)); verify(() => loginBloc.add(const LoginSubmitted())).called(1); }, ); testWidgets('shows SnackBar when status is submission failure', ( tester, ) async { whenListen( loginBloc, Stream.fromIterable([ const LoginState(status: FormzSubmissionStatus.inProgress), const LoginState(status: FormzSubmissionStatus.failure), ]), initialState: const LoginState(status: FormzSubmissionStatus.failure), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: BlocProvider.value( value: loginBloc, child: LoginForm(), ), ), ), ); await tester.pump(); expect(find.byType(SnackBar), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_login/test/login/view/login_page_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:authentication_repository/authentication_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_login/login/login.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockAuthenticationRepository extends Mock implements AuthenticationRepository {} void main() { group('LoginPage', () { late AuthenticationRepository authenticationRepository; setUp(() { authenticationRepository = MockAuthenticationRepository(); }); test('is routable', () { expect(LoginPage.route(), isA>()); }); testWidgets('renders a LoginForm', (tester) async { await tester.pumpWidget( RepositoryProvider.value( value: authenticationRepository, child: MaterialApp( home: Scaffold(body: LoginPage()), ), ), ); expect(find.byType(LoginForm), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_login/web/index.html ================================================ flutter_login ================================================ FILE: examples/flutter_login/web/manifest.json ================================================ { "name": "flutter_login", "short_name": "flutter_login", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: examples/flutter_shopping_cart/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: examples/flutter_shopping_cart/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: web create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/flutter_shopping_cart/README.md ================================================ [![build](https://github.com/felangel/bloc/actions/workflows/main.yaml/badge.svg)](https://github.com/felangel/bloc/actions) # flutter_shopping_cart A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: examples/flutter_shopping_cart/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml linter: rules: public_member_api_docs: false ================================================ FILE: examples/flutter_shopping_cart/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: examples/flutter_shopping_cart/lib/app.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_shopping_cart/cart/cart.dart'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_shopping_cart/shopping_repository.dart'; class App extends StatelessWidget { const App({required this.shoppingRepository, super.key}); final ShoppingRepository shoppingRepository; @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider( create: (_) => CatalogBloc( shoppingRepository: shoppingRepository, )..add(CatalogStarted()), ), BlocProvider( create: (_) => CartBloc( shoppingRepository: shoppingRepository, )..add(CartStarted()), ), ], child: MaterialApp( title: 'Flutter Bloc Shopping Cart', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.white), appBarTheme: const AppBarTheme( backgroundColor: Colors.transparent, elevation: 0, ), ), initialRoute: '/', routes: { '/': (_) => const CatalogPage(), '/cart': (_) => const CartPage(), }, ), ); } } ================================================ FILE: examples/flutter_shopping_cart/lib/cart/bloc/cart_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_shopping_cart/cart/cart.dart'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_shopping_cart/shopping_repository.dart'; import 'package:meta/meta.dart'; part 'cart_event.dart'; part 'cart_state.dart'; class CartBloc extends Bloc { CartBloc({required ShoppingRepository shoppingRepository}) : _shoppingRepository = shoppingRepository, super(CartLoading()) { on(_onStarted); on(_onItemAdded); on(_onItemRemoved); } final ShoppingRepository _shoppingRepository; Future _onStarted(CartStarted event, Emitter emit) async { emit(CartLoading()); try { final items = await _shoppingRepository.loadCartItems(); emit(CartLoaded(cart: Cart(items: [...items]))); } catch (_) { emit(CartError()); } } Future _onItemAdded( CartItemAdded event, Emitter emit, ) async { final state = this.state; if (state is CartLoaded) { try { _shoppingRepository.addItemToCart(event.item); emit(CartLoaded(cart: Cart(items: [...state.cart.items, event.item]))); } catch (_) { emit(CartError()); } } } void _onItemRemoved(CartItemRemoved event, Emitter emit) { final state = this.state; if (state is CartLoaded) { try { _shoppingRepository.removeItemFromCart(event.item); emit( CartLoaded( cart: Cart( items: [...state.cart.items]..remove(event.item), ), ), ); } catch (_) { emit(CartError()); } } } } ================================================ FILE: examples/flutter_shopping_cart/lib/cart/bloc/cart_event.dart ================================================ part of 'cart_bloc.dart'; @immutable sealed class CartEvent extends Equatable { const CartEvent(); } final class CartStarted extends CartEvent { @override List get props => []; } final class CartItemAdded extends CartEvent { const CartItemAdded(this.item); final Item item; @override List get props => [item]; } final class CartItemRemoved extends CartEvent { const CartItemRemoved(this.item); final Item item; @override List get props => [item]; } ================================================ FILE: examples/flutter_shopping_cart/lib/cart/bloc/cart_state.dart ================================================ part of 'cart_bloc.dart'; @immutable sealed class CartState extends Equatable { const CartState(); } final class CartLoading extends CartState { @override List get props => []; } final class CartLoaded extends CartState { const CartLoaded({this.cart = const Cart()}); final Cart cart; @override List get props => [cart]; } final class CartError extends CartState { @override List get props => []; } ================================================ FILE: examples/flutter_shopping_cart/lib/cart/cart.dart ================================================ export 'bloc/cart_bloc.dart'; export 'models/models.dart'; export 'view/cart_page.dart'; ================================================ FILE: examples/flutter_shopping_cart/lib/cart/models/cart.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; class Cart extends Equatable { const Cart({this.items = const []}); final List items; int get totalPrice { return items.fold(0, (total, current) => total + current.price); } @override List get props => [items]; } ================================================ FILE: examples/flutter_shopping_cart/lib/cart/models/models.dart ================================================ export 'cart.dart'; ================================================ FILE: examples/flutter_shopping_cart/lib/cart/view/cart_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_shopping_cart/cart/cart.dart'; class CartPage extends StatelessWidget { const CartPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( extendBodyBehindAppBar: true, appBar: AppBar(title: const Text('Cart')), body: const ColoredBox( color: Colors.yellow, child: Column( children: [ Expanded( child: Padding( padding: EdgeInsets.all(32), child: CartList(), ), ), Divider(height: 4, color: Colors.black), CartTotal(), ], ), ), ); } } class CartList extends StatelessWidget { const CartList({super.key}); @override Widget build(BuildContext context) { final itemNameStyle = Theme.of(context).textTheme.titleLarge; return BlocBuilder( builder: (context, state) { return switch (state) { CartLoading() => const CircularProgressIndicator(), CartError() => const Text('Something went wrong!'), CartLoaded() => ListView.separated( itemCount: state.cart.items.length, separatorBuilder: (_, _) => const SizedBox(height: 4), itemBuilder: (context, index) { final item = state.cart.items[index]; return Material( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), clipBehavior: Clip.hardEdge, child: ListTile( leading: const Icon(Icons.done), title: Text(item.name, style: itemNameStyle), onLongPress: () { context.read().add(CartItemRemoved(item)); }, ), ); }, ), }; }, ); } } class CartTotal extends StatelessWidget { const CartTotal({super.key}); @override Widget build(BuildContext context) { final hugeStyle = Theme.of( context, ).textTheme.displayLarge?.copyWith(fontSize: 48); return SizedBox( height: 200, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ BlocBuilder( builder: (context, state) { return switch (state) { CartLoading() => const CircularProgressIndicator(), CartError() => const Text('Something went wrong!'), CartLoaded() => Text( '\$${state.cart.totalPrice}', style: hugeStyle, ), }; }, ), const SizedBox(width: 24), ElevatedButton( onPressed: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Buying not supported yet.')), ); }, child: const Text('BUY'), ), ], ), ), ); } } ================================================ FILE: examples/flutter_shopping_cart/lib/catalog/bloc/catalog_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_shopping_cart/shopping_repository.dart'; part 'catalog_event.dart'; part 'catalog_state.dart'; class CatalogBloc extends Bloc { CatalogBloc({required ShoppingRepository shoppingRepository}) : _shoppingRepository = shoppingRepository, super(CatalogLoading()) { on(_onStarted); } final ShoppingRepository _shoppingRepository; Future _onStarted( CatalogStarted event, Emitter emit, ) async { emit(CatalogLoading()); try { final catalog = await _shoppingRepository.loadCatalog(); emit(CatalogLoaded(Catalog(itemNames: catalog))); } catch (_) { emit(CatalogError()); } } } ================================================ FILE: examples/flutter_shopping_cart/lib/catalog/bloc/catalog_event.dart ================================================ part of 'catalog_bloc.dart'; sealed class CatalogEvent extends Equatable { const CatalogEvent(); } final class CatalogStarted extends CatalogEvent { @override List get props => []; } ================================================ FILE: examples/flutter_shopping_cart/lib/catalog/bloc/catalog_state.dart ================================================ part of 'catalog_bloc.dart'; sealed class CatalogState extends Equatable { const CatalogState(); @override List get props => []; } final class CatalogLoading extends CatalogState {} final class CatalogLoaded extends CatalogState { const CatalogLoaded(this.catalog); final Catalog catalog; @override List get props => [catalog]; } final class CatalogError extends CatalogState {} ================================================ FILE: examples/flutter_shopping_cart/lib/catalog/catalog.dart ================================================ export 'bloc/catalog_bloc.dart'; export 'models/models.dart'; export 'view/catalog_page.dart'; ================================================ FILE: examples/flutter_shopping_cart/lib/catalog/models/catalog.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; class Catalog extends Equatable { const Catalog({required this.itemNames}); final List itemNames; Item getById(int id) => Item(id, itemNames[id % itemNames.length]); Item getByPosition(int position) => getById(position); @override List get props => [itemNames]; } ================================================ FILE: examples/flutter_shopping_cart/lib/catalog/models/item.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; class Item extends Equatable { Item(this.id, this.name) : color = Colors.primaries[id % Colors.primaries.length]; final int id; final String name; final Color color; final int price = 42; @override List get props => [id, name, color, price]; } ================================================ FILE: examples/flutter_shopping_cart/lib/catalog/models/models.dart ================================================ export 'catalog.dart'; export 'item.dart'; ================================================ FILE: examples/flutter_shopping_cart/lib/catalog/view/catalog_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_shopping_cart/cart/cart.dart'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; class CatalogPage extends StatelessWidget { const CatalogPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( slivers: [ const CatalogAppBar(), const SliverToBoxAdapter(child: SizedBox(height: 12)), BlocBuilder( builder: (context, state) { return switch (state) { CatalogLoading() => const SliverFillRemaining( child: Center(child: CircularProgressIndicator()), ), CatalogError() => const SliverFillRemaining( child: Text('Something went wrong!'), ), CatalogLoaded() => SliverList( delegate: SliverChildBuilderDelegate( (context, index) => CatalogListItem( state.catalog.getByPosition(index), ), childCount: state.catalog.itemNames.length, ), ), }; }, ), ], ), ); } } class AddButton extends StatelessWidget { const AddButton({required this.item, super.key}); final Item item; @override Widget build(BuildContext context) { final theme = Theme.of(context); return BlocBuilder( builder: (context, state) { return switch (state) { CartLoading() => const CircularProgressIndicator(), CartError() => const Text('Something went wrong!'), CartLoaded() => Builder( builder: (context) { final isInCart = state.cart.items.contains(item); return TextButton( style: TextButton.styleFrom( disabledForegroundColor: theme.primaryColor, ), onPressed: isInCart ? null : () => context.read().add(CartItemAdded(item)), child: isInCart ? const Icon(Icons.check, semanticLabel: 'ADDED') : const Text('ADD'), ); }, ), }; }, ); } } class CatalogAppBar extends StatelessWidget { const CatalogAppBar({super.key}); @override Widget build(BuildContext context) { return SliverAppBar( title: const Text('Catalog'), floating: true, actions: [ IconButton( icon: const Icon(Icons.shopping_cart), onPressed: () => Navigator.of(context).pushNamed('/cart'), ), ], ); } } class CatalogListItem extends StatelessWidget { const CatalogListItem(this.item, {super.key}); final Item item; @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme.titleLarge; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: LimitedBox( maxHeight: 48, child: Row( children: [ AspectRatio(aspectRatio: 1, child: ColoredBox(color: item.color)), const SizedBox(width: 24), Expanded(child: Text(item.name, style: textTheme)), const SizedBox(width: 24), AddButton(item: item), ], ), ), ); } } ================================================ FILE: examples/flutter_shopping_cart/lib/main.dart ================================================ import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_shopping_cart/app.dart'; import 'package:flutter_shopping_cart/shopping_repository.dart'; import 'package:flutter_shopping_cart/simple_bloc_observer.dart'; void main() { Bloc.observer = const SimpleBlocObserver(); runApp(App(shoppingRepository: ShoppingRepository())); } ================================================ FILE: examples/flutter_shopping_cart/lib/shopping_repository.dart ================================================ import 'dart:async'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; const _delay = Duration(milliseconds: 800); const _catalog = [ 'Code Smell', 'Control Flow', 'Interpreter', 'Recursion', 'Sprint', 'Heisenbug', 'Spaghetti', 'Hydra Code', 'Off-By-One', 'Scope', 'Callback', 'Closure', 'Automata', 'Bit Shift', 'Currying', ]; class ShoppingRepository { final _items = []; Future> loadCatalog() => Future.delayed(_delay, () => _catalog); Future> loadCartItems() => Future.delayed(_delay, () => _items); void addItemToCart(Item item) => _items.add(item); void removeItemFromCart(Item item) => _items.remove(item); } ================================================ FILE: examples/flutter_shopping_cart/lib/simple_bloc_observer.dart ================================================ import 'dart:developer'; import 'package:bloc/bloc.dart'; class SimpleBlocObserver extends BlocObserver { const SimpleBlocObserver(); @override void onEvent(Bloc bloc, Object? event) { super.onEvent(bloc, event); log('${bloc.runtimeType} $event'); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { log('${bloc.runtimeType} $error'); super.onError(bloc, error, stackTrace); } @override void onTransition( Bloc bloc, Transition transition, ) { super.onTransition(bloc, transition); log('$transition'); } } ================================================ FILE: examples/flutter_shopping_cart/macos/.gitignore ================================================ # Flutter-related **/Flutter/ephemeral/ **/Pods/ # Xcode-related **/dgph **/xcuserdata/ ================================================ FILE: examples/flutter_shopping_cart/macos/Flutter/GeneratedPluginRegistrant.swift ================================================ // // Generated file. Do not edit. // import FlutterMacOS import Foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { } ================================================ FILE: examples/flutter_shopping_cart/pubspec.yaml ================================================ name: flutter_shopping_cart description: A new Flutter project. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: bloc: ^9.0.0 equatable: ^2.0.0 flutter: sdk: flutter flutter_bloc: ^9.1.0 meta: ^1.0.0 dev_dependencies: bloc_lint: ^0.3.0 bloc_test: ^10.0.0 flutter_test: sdk: flutter mocktail: ^1.0.0 flutter: uses-material-design: true ================================================ FILE: examples/flutter_shopping_cart/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../../packages/bloc bloc_lint: path: ../../packages/bloc_lint bloc_test: path: ../../packages/bloc_test flutter_bloc: path: ../../packages/flutter_bloc ================================================ FILE: examples/flutter_shopping_cart/test/app_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_shopping_cart/app.dart'; import 'package:flutter_shopping_cart/cart/cart.dart'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_shopping_cart/shopping_repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockShoppingRepository extends Mock implements ShoppingRepository {} void main() { group('App', () { late ShoppingRepository shoppingRepository; setUp(() { shoppingRepository = MockShoppingRepository(); when(shoppingRepository.loadCatalog).thenAnswer( (_) async => ['Orange Juice', 'Milk'], ); }); testWidgets('renders CatalogPage', (tester) async { await tester.pumpWidget(App(shoppingRepository: shoppingRepository)); expect(find.byType(CatalogPage), findsOneWidget); }); testWidgets('renders CatalogPage (initial route)', (tester) async { await tester.pumpWidget(App(shoppingRepository: shoppingRepository)); expect(find.byType(CatalogPage), findsOneWidget); }); testWidgets('can navigate back and forth ' 'between CartPage and CatalogPage', (tester) async { await tester.pumpWidget(App(shoppingRepository: shoppingRepository)); await tester.tap(find.byIcon(Icons.shopping_cart)); await tester.pumpAndSettle(); expect(find.byType(CartPage), findsOneWidget); expect(find.byType(CatalogPage), findsNothing); await tester.tap(find.byIcon(Icons.arrow_back)); await tester.pumpAndSettle(); expect(find.byType(CartPage), findsNothing); expect(find.byType(CatalogPage), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_shopping_cart/test/cart/bloc/cart_bloc_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_shopping_cart/cart/cart.dart'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_shopping_cart/shopping_repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockShoppingRepository extends Mock implements ShoppingRepository {} void main() { group('CartBloc', () { final mockItems = [ Item(1, 'item #1'), Item(2, 'item #2'), Item(3, 'item #3'), ]; final mockItemToAdd = Item(4, 'item #4'); final mockItemToRemove = Item(2, 'item #2'); late ShoppingRepository shoppingRepository; setUp(() { shoppingRepository = MockShoppingRepository(); }); test('initial state is CartLoading', () { expect( CartBloc(shoppingRepository: shoppingRepository).state, CartLoading(), ); }); blocTest( 'emits [CartLoading, CartLoaded] when cart is loaded successfully', setUp: () { when(shoppingRepository.loadCartItems).thenAnswer((_) async => []); }, build: () => CartBloc(shoppingRepository: shoppingRepository), act: (bloc) => bloc.add(CartStarted()), expect: () => [CartLoading(), const CartLoaded()], verify: (_) => verify(shoppingRepository.loadCartItems).called(1), ); blocTest( 'emits [CartLoading, CartError] when loading the cart throws an error', setUp: () { when(shoppingRepository.loadCartItems).thenThrow(Exception('Error')); }, build: () => CartBloc(shoppingRepository: shoppingRepository), act: (bloc) => bloc..add(CartStarted()), expect: () => [CartLoading(), CartError()], verify: (_) => verify(shoppingRepository.loadCartItems).called(1), ); blocTest( 'emits [] when cart is not finished loading and item is added', setUp: () { when( () => shoppingRepository.addItemToCart(mockItemToAdd), ).thenAnswer((_) async {}); }, build: () => CartBloc(shoppingRepository: shoppingRepository), act: (bloc) => bloc.add(CartItemAdded(mockItemToAdd)), expect: () => [], ); blocTest( 'emits [CartLoaded] when item is added successfully', setUp: () { when( () => shoppingRepository.addItemToCart(mockItemToAdd), ).thenAnswer((_) async {}); }, build: () => CartBloc(shoppingRepository: shoppingRepository), seed: () => CartLoaded(cart: Cart(items: mockItems)), act: (bloc) => bloc.add(CartItemAdded(mockItemToAdd)), expect: () => [ CartLoaded(cart: Cart(items: [...mockItems, mockItemToAdd])), ], verify: (_) { verify(() => shoppingRepository.addItemToCart(mockItemToAdd)).called(1); }, ); blocTest( 'emits [CartError] when item is not added successfully', setUp: () { when( () => shoppingRepository.addItemToCart(mockItemToAdd), ).thenThrow(Exception('Error')); }, build: () => CartBloc(shoppingRepository: shoppingRepository), seed: () => CartLoaded(cart: Cart(items: mockItems)), act: (bloc) => bloc.add(CartItemAdded(mockItemToAdd)), expect: () => [CartError()], verify: (_) { verify( () => shoppingRepository.addItemToCart(mockItemToAdd), ).called(1); }, ); blocTest( 'emits [CartLoaded] when item is removed successfully', setUp: () { when( () => shoppingRepository.removeItemFromCart(mockItemToRemove), ).thenAnswer((_) async {}); }, build: () => CartBloc(shoppingRepository: shoppingRepository), seed: () => CartLoaded(cart: Cart(items: mockItems)), act: (bloc) => bloc.add(CartItemRemoved(mockItemToRemove)), expect: () => [ CartLoaded(cart: Cart(items: [...mockItems]..remove(mockItemToRemove))), ], verify: (_) { verify( () => shoppingRepository.removeItemFromCart(mockItemToRemove), ).called(1); }, ); blocTest( 'emits [CartError] when item is not removed successfully', setUp: () { when( () => shoppingRepository.removeItemFromCart(mockItemToRemove), ).thenThrow(Exception('Error')); }, build: () => CartBloc(shoppingRepository: shoppingRepository), seed: () => CartLoaded(cart: Cart(items: mockItems)), act: (bloc) => bloc.add(CartItemRemoved(mockItemToRemove)), expect: () => [CartError()], verify: (_) { verify( () => shoppingRepository.removeItemFromCart(mockItemToRemove), ).called(1); }, ); }); } ================================================ FILE: examples/flutter_shopping_cart/test/cart/bloc/cart_event_test.dart ================================================ import 'package:flutter_shopping_cart/cart/cart.dart'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_test/flutter_test.dart'; class FakeItem extends Fake implements Item {} void main() { group('CartEvent', () { group('CartStarted', () { test('supports value comparison', () { expect(CartStarted(), CartStarted()); }); }); group('CartItemAdded', () { final item = FakeItem(); test('supports value comparison', () { expect(CartItemAdded(item), CartItemAdded(item)); }); }); group('CartItemRemoved', () { final item = FakeItem(); test('supports value comparison', () { expect(CartItemRemoved(item), CartItemRemoved(item)); }); }); }); } ================================================ FILE: examples/flutter_shopping_cart/test/cart/bloc/cart_state_test.dart ================================================ import 'package:flutter_shopping_cart/cart/cart.dart'; import 'package:flutter_test/flutter_test.dart'; class FakeCart extends Fake implements Cart {} void main() { group('CartState', () { group('CartLoading', () { test('supports value comparison', () { expect(CartLoading(), CartLoading()); }); }); group('CartLoaded', () { final cart = FakeCart(); test('supports value comparison', () { expect(CartLoaded(cart: cart), CartLoaded(cart: cart)); }); }); group('CartError', () { test('supports value comparison', () { expect(CartError(), CartError()); }); }); }); } ================================================ FILE: examples/flutter_shopping_cart/test/cart/models/cart_test.dart ================================================ import 'package:flutter_shopping_cart/cart/cart.dart'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('Cart', () { final mockItems = [ Item(1, 'item #1'), Item(2, 'item #2'), Item(3, 'item #3'), ]; test('supports value comparison', () async { expect(Cart(items: mockItems), Cart(items: mockItems)); }); test('gets correct total price for 3 items', () async { expect(Cart(items: mockItems).totalPrice, 42 * 3); }); }); } ================================================ FILE: examples/flutter_shopping_cart/test/cart/view/cart_page_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter/material.dart'; import 'package:flutter_shopping_cart/cart/cart.dart'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import '../../helper.dart'; void main() { late CartBloc cartBloc; final mockItems = [ Item(1, 'item #1'), Item(2, 'item #2'), Item(3, 'item #3'), ]; setUp(() { cartBloc = MockCartBloc(); }); group('CartPage', () { testWidgets('renders CartList and CartTotal', (tester) async { when(() => cartBloc.state).thenReturn(CartLoading()); await tester.pumpApp( cartBloc: cartBloc, child: CartPage(), ); expect(find.byType(CartList), findsOneWidget); expect(find.byType(CartTotal), findsOneWidget); }); }); group('CartList', () { testWidgets('renders CircularProgressIndicator ' 'when cart is loading', (tester) async { when(() => cartBloc.state).thenReturn(CartLoading()); await tester.pumpApp( cartBloc: cartBloc, child: CartList(), ); expect(find.byType(CircularProgressIndicator), findsOneWidget); }); testWidgets('renders 3 ListTile ' 'when cart is loaded with three items', (tester) async { when( () => cartBloc.state, ).thenReturn(CartLoaded(cart: Cart(items: mockItems))); await tester.pumpApp( cartBloc: cartBloc, child: CartList(), ); expect(find.byType(ListTile), findsNWidgets(3)); }); testWidgets('renders error text ' 'when cart fails to load', (tester) async { when(() => cartBloc.state).thenReturn(CartError()); await tester.pumpApp( cartBloc: cartBloc, child: CartList(), ); expect(find.text('Something went wrong!'), findsOneWidget); }); }); group('CartTotal', () { testWidgets('renders CircularProgressIndicator ' 'when cart is loading', (tester) async { when(() => cartBloc.state).thenReturn(CartLoading()); await tester.pumpApp( cartBloc: cartBloc, child: CartTotal(), ); expect(find.byType(CircularProgressIndicator), findsOneWidget); }); testWidgets('renders total price ' 'when cart is loaded with three items', (tester) async { when( () => cartBloc.state, ).thenReturn(CartLoaded(cart: Cart(items: mockItems))); await tester.pumpApp( cartBloc: cartBloc, child: CartTotal(), ); expect(find.text('\$${42 * 3}'), findsOneWidget); }); testWidgets('renders error text ' 'when cart fails to load', (tester) async { when(() => cartBloc.state).thenReturn(CartError()); await tester.pumpApp( cartBloc: cartBloc, child: CartTotal(), ); expect(find.text('Something went wrong!'), findsOneWidget); }); testWidgets('renders SnackBar after ' "tapping the 'BUY' button", (tester) async { when( () => cartBloc.state, ).thenReturn(CartLoaded(cart: Cart(items: mockItems))); await tester.pumpApp( cartBloc: cartBloc, child: Scaffold(body: CartTotal()), ); await tester.tap(find.text('BUY')); await tester.pumpAndSettle(); expect(find.byType(SnackBar), findsOneWidget); expect(find.text('Buying not supported yet.'), findsOneWidget); }); testWidgets('adds CartItemRemoved on long press', (tester) async { when(() => cartBloc.state).thenReturn( CartLoaded(cart: Cart(items: mockItems)), ); final mockItemToRemove = mockItems.last; await tester.pumpApp( cartBloc: cartBloc, child: Scaffold(body: CartList()), ); await tester.longPress(find.text(mockItemToRemove.name)); verify(() => cartBloc.add(CartItemRemoved(mockItemToRemove))).called(1); }); }); } ================================================ FILE: examples/flutter_shopping_cart/test/catalog/bloc/catalog_bloc_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_shopping_cart/shopping_repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; class MockShoppingRepository extends Mock implements ShoppingRepository {} void main() { group('CatalogBloc', () { const mockItemNames = ['Orange Juice', 'Milk', 'Macaroons', 'Cookies']; late ShoppingRepository shoppingRepository; setUp(() { shoppingRepository = MockShoppingRepository(); }); test('initial state is CatalogLoading', () { expect( CatalogBloc(shoppingRepository: shoppingRepository).state, CatalogLoading(), ); }); blocTest( 'emits [CatalogLoading, CatalogLoaded] ' 'when catalog is loaded successfully', setUp: () { when(shoppingRepository.loadCatalog).thenAnswer( (_) async => mockItemNames, ); }, build: () => CatalogBloc(shoppingRepository: shoppingRepository), act: (bloc) => bloc.add(CatalogStarted()), expect: () => [ CatalogLoading(), CatalogLoaded(Catalog(itemNames: mockItemNames)), ], verify: (_) => verify(shoppingRepository.loadCatalog).called(1), ); blocTest( 'emits [CatalogLoading, CatalogError] ' 'when loading the catalog throws an exception', setUp: () { when(shoppingRepository.loadCatalog).thenThrow(Exception('Error')); }, build: () => CatalogBloc(shoppingRepository: shoppingRepository), act: (bloc) => bloc.add(CatalogStarted()), expect: () => [ CatalogLoading(), CatalogError(), ], verify: (_) => verify(shoppingRepository.loadCatalog).called(1), ); }); } ================================================ FILE: examples/flutter_shopping_cart/test/catalog/bloc/catalog_event_test.dart ================================================ import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('CatalogEvent', () { group('CatalogStarted', () { test('supports value comparison', () { expect(CatalogStarted(), CatalogStarted()); }); }); }); } ================================================ FILE: examples/flutter_shopping_cart/test/catalog/bloc/catalog_state_test.dart ================================================ // ignore_for_file: prefer_const_constructors, import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('CatalogState', () { group('CatalogLoading', () { test('supports value comparison', () { expect(CatalogLoading(), CatalogLoading()); }); }); group('CatalogLoaded', () { test('supports value comparison', () { final catalog = Catalog(itemNames: const ['item #1', 'item #2']); expect(CatalogLoaded(catalog), CatalogLoaded(catalog)); }); }); }); } ================================================ FILE: examples/flutter_shopping_cart/test/catalog/models/catalog_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('Catalog', () { const mockItemNames = ['Orange Juice', 'Milk', 'Macaroons', 'Cookies']; test('supports value comparison', () async { expect( Catalog(itemNames: mockItemNames), Catalog(itemNames: mockItemNames), ); }); test('gets correct item by id', () async { expect( Catalog(itemNames: mockItemNames).getById(1), Item(1, 'Milk'), ); }); test('gets correct item by id', () async { expect( Catalog(itemNames: mockItemNames).getByPosition(2), Item(2, 'Macaroons'), ); }); }); } ================================================ FILE: examples/flutter_shopping_cart/test/catalog/models/item_test.dart ================================================ import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('Item', () { test('supports value comparison', () async { expect(Item(1, 'item #1'), Item(1, 'item #1')); }); }); } ================================================ FILE: examples/flutter_shopping_cart/test/catalog/view/catalog_page_test.dart ================================================ // ignore_for_file: prefer_const_constructors, import 'package:flutter/material.dart'; import 'package:flutter_shopping_cart/cart/cart.dart'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import '../../helper.dart'; void main() { late CartBloc cartBloc; late CatalogBloc catalogBloc; setUp(() { catalogBloc = MockCatalogBloc(); cartBloc = MockCartBloc(); }); group('CatalogPage', () { testWidgets('renders SliverFillRemaining with loading indicator ' 'when catalog is loading', (tester) async { when(() => catalogBloc.state).thenReturn(CatalogLoading()); await tester.pumpApp( catalogBloc: catalogBloc, child: CatalogPage(), ); expect( find.descendant( of: find.byType(SliverFillRemaining), matching: find.byType(CircularProgressIndicator), ), findsOneWidget, ); }); testWidgets('renders SliverList with two items ' 'when catalog is loaded', (tester) async { final catalog = Catalog(itemNames: const ['item #1', 'item #2']); when(() => catalogBloc.state).thenReturn(CatalogLoaded(catalog)); when(() => cartBloc.state).thenReturn(CartLoading()); await tester.pumpApp( cartBloc: cartBloc, catalogBloc: catalogBloc, child: CatalogPage(), ); expect(find.byType(SliverList), findsOneWidget); expect(find.byType(CatalogListItem), findsNWidgets(2)); }); testWidgets('renders error text ' 'when catalog fails to load', (tester) async { when(() => catalogBloc.state).thenReturn(CatalogError()); await tester.pumpApp( catalogBloc: catalogBloc, child: CatalogPage(), ); expect(find.text('Something went wrong!'), findsOneWidget); }); }); group('AddButton', () { final mockItem = Item(1, 'item #1'); testWidgets('renders CircularProgressIndicator when ' 'cart is loading', (tester) async { when(() => cartBloc.state).thenReturn(CartLoading()); await tester.pumpApp( cartBloc: cartBloc, child: AddButton(item: mockItem), ); expect(find.byType(CircularProgressIndicator), findsOneWidget); }); testWidgets("renders 'Add' text button " 'when item is not in the cart', (tester) async { when(() => cartBloc.state).thenReturn(const CartLoaded()); await tester.pumpApp( cartBloc: cartBloc, child: AddButton(item: mockItem), ); expect(find.text('ADD'), findsOneWidget); expect(find.byIcon(Icons.check), findsNothing); }); testWidgets('renders check icon ' 'when item is already added to cart', (tester) async { when(() => cartBloc.state).thenReturn( CartLoaded(cart: Cart(items: [mockItem])), ); await tester.pumpApp( cartBloc: cartBloc, child: AddButton(item: mockItem), ); expect(find.byIcon(Icons.check), findsOneWidget); expect(find.text('ADD'), findsNothing); }); testWidgets('adds item to the cart', (tester) async { when(() => cartBloc.state).thenReturn(const CartLoaded()); await tester.pumpApp( cartBloc: cartBloc, child: AddButton(item: mockItem), ); await tester.tap(find.text('ADD')); verify(() => cartBloc.add(CartItemAdded(mockItem))).called(1); }); }); } ================================================ FILE: examples/flutter_shopping_cart/test/helper.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_shopping_cart/cart/cart.dart'; import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_test/flutter_test.dart'; class MockCartBloc extends MockBloc implements CartBloc {} class MockCatalogBloc extends MockBloc implements CatalogBloc {} extension PumpApp on WidgetTester { Future pumpApp({ required Widget child, CartBloc? cartBloc, CatalogBloc? catalogBloc, }) { return pumpWidget( MaterialApp( home: MultiBlocProvider( providers: [ if (cartBloc != null) BlocProvider.value(value: cartBloc) else BlocProvider(create: (_) => MockCartBloc()), if (catalogBloc != null) BlocProvider.value(value: catalogBloc) else BlocProvider(create: (_) => MockCatalogBloc()), ], child: child, ), ), ); } } ================================================ FILE: examples/flutter_shopping_cart/test/shopping_repository_test.dart ================================================ import 'package:flutter_shopping_cart/catalog/catalog.dart'; import 'package:flutter_shopping_cart/shopping_repository.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('ShoppingRepository', () { late ShoppingRepository shoppingRepository; setUp(() { shoppingRepository = ShoppingRepository(); }); group('loadCatalog', () { test('returns list of item names', () { const items = [ 'Code Smell', 'Control Flow', 'Interpreter', 'Recursion', 'Sprint', 'Heisenbug', 'Spaghetti', 'Hydra Code', 'Off-By-One', 'Scope', 'Callback', 'Closure', 'Automata', 'Bit Shift', 'Currying', ]; expect( shoppingRepository.loadCatalog(), completion(equals(items)), ); }); }); group('loadCartItems', () { test('return empty list after loading cart items', () { expect( shoppingRepository.loadCartItems(), completion(equals([])), ); }); }); group('addItemToCart', () { test('returns newly added item after adding item to cart', () { final item = Item(1, 'item #1'); shoppingRepository.addItemToCart(item); expect( shoppingRepository.loadCartItems(), completion(equals([item])), ); }); }); group('removeItemFromCart', () { test('removes item from cart', () { final item = Item(1, 'item #1'); shoppingRepository ..addItemToCart(item) ..removeItemFromCart(item); expect( shoppingRepository.loadCartItems(), completion(equals([])), ); }); }); }); } ================================================ FILE: examples/flutter_shopping_cart/web/index.html ================================================ flutter_shopping_cart ================================================ FILE: examples/flutter_shopping_cart/web/manifest.json ================================================ { "name": "flutter_shopping_cart", "short_name": "flutter_shopping_cart", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: examples/flutter_timer/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: examples/flutter_timer/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: web create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/flutter_timer/README.md ================================================ [![build](https://github.com/felangel/bloc/actions/workflows/main.yaml/badge.svg)](https://github.com/felangel/bloc/actions) # flutter_timer A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: examples/flutter_timer/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml linter: rules: public_member_api_docs: false ================================================ FILE: examples/flutter_timer/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: examples/flutter_timer/lib/app.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_timer/timer/timer.dart'; class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Timer', theme: ThemeData( colorScheme: const ColorScheme.light( primary: Color.fromRGBO(72, 74, 126, 1), ), ), home: const TimerPage(), ); } } ================================================ FILE: examples/flutter_timer/lib/main.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_timer/app.dart'; void main() => runApp(const App()); ================================================ FILE: examples/flutter_timer/lib/ticker.dart ================================================ class Ticker { const Ticker(); Stream tick({required int ticks}) { return Stream.periodic( const Duration(seconds: 1), (x) => ticks - x - 1, ).take(ticks); } } ================================================ FILE: examples/flutter_timer/lib/timer/bloc/timer_bloc.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_timer/ticker.dart'; part 'timer_event.dart'; part 'timer_state.dart'; class TimerBloc extends Bloc { TimerBloc({required Ticker ticker}) : _ticker = ticker, super(const TimerInitial(_duration)) { on(_onStarted); on(_onPaused); on(_onResumed); on(_onReset); on<_TimerTicked>(_onTicked); } final Ticker _ticker; static const int _duration = 60; StreamSubscription? _tickerSubscription; @override Future close() { _tickerSubscription?.cancel(); return super.close(); } void _onStarted(TimerStarted event, Emitter emit) { emit(TimerRunInProgress(event.duration)); _tickerSubscription?.cancel(); _tickerSubscription = _ticker .tick(ticks: event.duration) .listen((duration) => add(_TimerTicked(duration: duration))); } void _onPaused(TimerPaused event, Emitter emit) { if (state is TimerRunInProgress) { _tickerSubscription?.pause(); emit(TimerRunPause(state.duration)); } } void _onResumed(TimerResumed resume, Emitter emit) { if (state is TimerRunPause) { _tickerSubscription?.resume(); emit(TimerRunInProgress(state.duration)); } } void _onReset(TimerReset event, Emitter emit) { _tickerSubscription?.cancel(); emit(const TimerInitial(_duration)); } void _onTicked(_TimerTicked event, Emitter emit) { emit( event.duration > 0 ? TimerRunInProgress(event.duration) : const TimerRunComplete(), ); } } ================================================ FILE: examples/flutter_timer/lib/timer/bloc/timer_event.dart ================================================ part of 'timer_bloc.dart'; sealed class TimerEvent { const TimerEvent(); } final class TimerStarted extends TimerEvent { const TimerStarted({required this.duration}); final int duration; } final class TimerPaused extends TimerEvent { const TimerPaused(); } final class TimerResumed extends TimerEvent { const TimerResumed(); } class TimerReset extends TimerEvent { const TimerReset(); } class _TimerTicked extends TimerEvent { const _TimerTicked({required this.duration}); final int duration; } ================================================ FILE: examples/flutter_timer/lib/timer/bloc/timer_state.dart ================================================ part of 'timer_bloc.dart'; sealed class TimerState extends Equatable { const TimerState(this.duration); final int duration; @override List get props => [duration]; } final class TimerInitial extends TimerState { const TimerInitial(super.duration); @override String toString() => 'TimerInitial { duration: $duration }'; } final class TimerRunPause extends TimerState { const TimerRunPause(super.duration); @override String toString() => 'TimerRunPause { duration: $duration }'; } final class TimerRunInProgress extends TimerState { const TimerRunInProgress(super.duration); @override String toString() => 'TimerRunInProgress { duration: $duration }'; } final class TimerRunComplete extends TimerState { const TimerRunComplete() : super(0); } ================================================ FILE: examples/flutter_timer/lib/timer/timer.dart ================================================ export 'bloc/timer_bloc.dart'; export 'view/timer_page.dart'; ================================================ FILE: examples/flutter_timer/lib/timer/view/timer_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_timer/ticker.dart'; import 'package:flutter_timer/timer/timer.dart'; class TimerPage extends StatelessWidget { const TimerPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => TimerBloc(ticker: const Ticker()), child: const TimerView(), ); } } class TimerView extends StatelessWidget { const TimerView({super.key}); @override Widget build(BuildContext context) { return const Scaffold( body: Stack( children: [ Background(), Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( padding: EdgeInsets.symmetric(vertical: 100), child: Center(child: TimerText()), ), Actions(), ], ), ], ), ); } } class TimerText extends StatelessWidget { const TimerText({super.key}); @override Widget build(BuildContext context) { final duration = context.select((TimerBloc bloc) => bloc.state.duration); final minutesStr = ((duration / 60) % 60).floor().toString().padLeft( 2, '0', ); final secondsStr = (duration % 60).toString().padLeft(2, '0'); return Text( '$minutesStr:$secondsStr', style: Theme.of( context, ).textTheme.displayLarge?.copyWith(fontWeight: FontWeight.w500), ); } } class Actions extends StatelessWidget { const Actions({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (prev, state) => prev.runtimeType != state.runtimeType, builder: (context, state) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ...switch (state) { TimerInitial() => [ FloatingActionButton( child: const Icon(Icons.play_arrow), onPressed: () => context.read().add( TimerStarted(duration: state.duration), ), ), ], TimerRunInProgress() => [ FloatingActionButton( child: const Icon(Icons.pause), onPressed: () { context.read().add(const TimerPaused()); }, ), FloatingActionButton( child: const Icon(Icons.replay), onPressed: () { context.read().add(const TimerReset()); }, ), ], TimerRunPause() => [ FloatingActionButton( child: const Icon(Icons.play_arrow), onPressed: () { context.read().add(const TimerResumed()); }, ), FloatingActionButton( child: const Icon(Icons.replay), onPressed: () { context.read().add(const TimerReset()); }, ), ], TimerRunComplete() => [ FloatingActionButton( child: const Icon(Icons.replay), onPressed: () { context.read().add(const TimerReset()); }, ), ], }, ], ); }, ); } } class Background extends StatelessWidget { const Background({super.key}); @override Widget build(BuildContext context) { return SizedBox.expand( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.blue.shade50, Colors.blue.shade500, ], ), ), ), ); } } ================================================ FILE: examples/flutter_timer/macos/.gitignore ================================================ # Flutter-related **/Flutter/ephemeral/ **/Pods/ # Xcode-related **/dgph **/xcuserdata/ ================================================ FILE: examples/flutter_timer/macos/Flutter/GeneratedPluginRegistrant.swift ================================================ // // Generated file. Do not edit. // import FlutterMacOS import Foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { } ================================================ FILE: examples/flutter_timer/pubspec.yaml ================================================ name: flutter_timer description: A new Flutter project. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: bloc: ^9.0.0 equatable: ^2.0.0 flutter: sdk: flutter flutter_bloc: ^9.1.0 dev_dependencies: bloc_lint: ^0.3.0 bloc_test: ^10.0.0 flutter_test: sdk: flutter mocktail: ^1.0.0 flutter: uses-material-design: true ================================================ FILE: examples/flutter_timer/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../../packages/bloc bloc_lint: path: ../../packages/bloc_lint bloc_test: path: ../../packages/bloc_test flutter_bloc: path: ../../packages/flutter_bloc ================================================ FILE: examples/flutter_timer/test/app_test.dart ================================================ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_timer/app.dart'; import 'package:flutter_timer/timer/timer.dart'; void main() { group('App', () { testWidgets('renders TimerPage', (tester) async { await tester.pumpWidget(const App()); expect(find.byType(TimerPage), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_timer/test/ticker_test.dart ================================================ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_timer/ticker.dart'; void main() { group('Ticker', () { const ticker = Ticker(); test('ticker emits 3 ticks from 2-0 every second', () { expectLater(ticker.tick(ticks: 3), emitsInOrder([2, 1, 0])); }); }); } ================================================ FILE: examples/flutter_timer/test/timer/bloc/timer_bloc_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_timer/ticker.dart'; import 'package:flutter_timer/timer/timer.dart'; import 'package:mocktail/mocktail.dart'; class _MockTicker extends Mock implements Ticker {} void main() { group('TimerBloc', () { late Ticker ticker; setUp(() { ticker = _MockTicker(); when(() => ticker.tick(ticks: 5)).thenAnswer( (_) => Stream.fromIterable([5, 4, 3, 2, 1]), ); }); test('initial state is TimerInitial(60)', () { expect( TimerBloc(ticker: ticker).state, TimerInitial(60), ); }); blocTest( 'emits TickerRunInProgress 5 times after timer started', build: () => TimerBloc(ticker: ticker), act: (bloc) => bloc.add(const TimerStarted(duration: 5)), expect: () => [ TimerRunInProgress(5), TimerRunInProgress(4), TimerRunInProgress(3), TimerRunInProgress(2), TimerRunInProgress(1), ], verify: (_) => verify(() => ticker.tick(ticks: 5)).called(1), ); blocTest( 'emits [TickerRunPause(2)] when ticker is paused at 2', build: () => TimerBloc(ticker: ticker), seed: () => TimerRunInProgress(2), act: (bloc) => bloc.add(TimerPaused()), expect: () => [TimerRunPause(2)], ); blocTest( 'emits [TickerRunInProgress(5)] when ticker is resumed at 5', build: () => TimerBloc(ticker: ticker), seed: () => TimerRunPause(5), act: (bloc) => bloc.add(TimerResumed()), expect: () => [TimerRunInProgress(5)], ); blocTest( 'emits [TickerInitial(60)] when timer is restarted', build: () => TimerBloc(ticker: ticker), act: (bloc) => bloc.add(TimerReset()), expect: () => [TimerInitial(60)], ); blocTest( 'emits [TimerRunInProgress(3)] when timer ticks to 3', setUp: () { when(() => ticker.tick(ticks: 3)).thenAnswer( (_) => Stream.value(3), ); }, build: () => TimerBloc(ticker: ticker), act: (bloc) => bloc.add(TimerStarted(duration: 3)), expect: () => [TimerRunInProgress(3)], ); blocTest( 'emits [TimerRunInProgress(1), TimerRunComplete()] when timer ticks to 0', setUp: () { when(() => ticker.tick(ticks: 1)).thenAnswer( (_) => Stream.fromIterable([1, 0]), ); }, build: () => TimerBloc(ticker: ticker), act: (bloc) => bloc.add(TimerStarted(duration: 1)), expect: () => [TimerRunInProgress(1), TimerRunComplete()], ); }); } ================================================ FILE: examples/flutter_timer/test/timer/bloc/timer_state_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_timer/timer/timer.dart'; void main() { group('TimerState', () { group('TimerInitial', () { test('supports value comparison', () { expect( TimerInitial(60), TimerInitial(60), ); }); }); group('TimerRunPause', () { test('supports value comparison', () { expect( TimerRunPause(60), TimerRunPause(60), ); }); }); group('TimerRunInProgress', () { test('supports value comparison', () { expect( TimerRunInProgress(60), TimerRunInProgress(60), ); }); }); group('TimerRunComplete', () { test('supports value comparison', () { expect( TimerRunComplete(), TimerRunComplete(), ); }); }); }); } ================================================ FILE: examples/flutter_timer/test/timer/view/timer_page_test.dart ================================================ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_timer/timer/timer.dart'; import 'package:mocktail/mocktail.dart'; class _MockTimerBloc extends MockBloc implements TimerBloc {} extension on WidgetTester { Future pumpTimerView(TimerBloc timerBloc) { return pumpWidget( MaterialApp( home: BlocProvider.value(value: timerBloc, child: const TimerView()), ), ); } } void main() { late TimerBloc timerBloc; setUpAll(() { registerFallbackValue(const TimerStarted(duration: 0)); }); setUp(() { timerBloc = _MockTimerBloc(); }); tearDown(() => reset(timerBloc)); group('TimerPage', () { testWidgets('renders TimerView', (tester) async { await tester.pumpWidget(const MaterialApp(home: TimerPage())); expect(find.byType(TimerView), findsOneWidget); }); }); group('TimerView', () { testWidgets('renders initial Timer view', (tester) async { when(() => timerBloc.state).thenReturn(const TimerInitial(60)); await tester.pumpTimerView(timerBloc); expect(find.text('01:00'), findsOneWidget); expect(find.byIcon(Icons.play_arrow), findsOneWidget); }); testWidgets('renders pause and reset button when timer is in progress', ( tester, ) async { when(() => timerBloc.state).thenReturn(const TimerRunInProgress(59)); await tester.pumpTimerView(timerBloc); expect(find.text('00:59'), findsOneWidget); expect(find.byIcon(Icons.pause), findsOneWidget); expect(find.byIcon(Icons.replay), findsOneWidget); }); testWidgets('renders play and reset button when timer is paused', ( tester, ) async { when(() => timerBloc.state).thenReturn(const TimerRunPause(600)); await tester.pumpTimerView(timerBloc); expect(find.text('10:00'), findsOneWidget); expect(find.byIcon(Icons.play_arrow), findsOneWidget); expect(find.byIcon(Icons.replay), findsOneWidget); }); testWidgets('renders replay button when timer is finished', (tester) async { when(() => timerBloc.state).thenReturn(const TimerRunComplete()); await tester.pumpTimerView(timerBloc); expect(find.text('00:00'), findsOneWidget); expect(find.byIcon(Icons.replay), findsOneWidget); }); testWidgets('timer started when play arrow button is pressed', ( tester, ) async { when(() => timerBloc.state).thenReturn(const TimerInitial(60)); await tester.pumpTimerView(timerBloc); await tester.tap(find.byIcon(Icons.play_arrow)); verify( () => timerBloc.add( any( that: isA().having((e) => e.duration, 'duration', 60), ), ), ).called(1); }); testWidgets('timer pauses when pause button is pressed ' 'while timer is in progress', (tester) async { when(() => timerBloc.state).thenReturn(const TimerRunInProgress(30)); await tester.pumpTimerView(timerBloc); await tester.tap(find.byIcon(Icons.pause)); verify(() => timerBloc.add(const TimerPaused())).called(1); }); testWidgets('timer resets when replay button is pressed ' 'while timer is in progress', (tester) async { when(() => timerBloc.state).thenReturn(const TimerRunInProgress(30)); await tester.pumpTimerView(timerBloc); await tester.tap(find.byIcon(Icons.replay)); verify(() => timerBloc.add(const TimerReset())).called(1); }); testWidgets('timer resumes when play arrow button is pressed ' 'while timer is paused', (tester) async { when(() => timerBloc.state).thenReturn(const TimerRunPause(30)); await tester.pumpTimerView(timerBloc); await tester.tap(find.byIcon(Icons.play_arrow)); verify(() => timerBloc.add(const TimerResumed())).called(1); }); testWidgets('timer resets when reset button is pressed ' 'while timer is paused', (tester) async { when(() => timerBloc.state).thenReturn(const TimerRunPause(30)); await tester.pumpTimerView(timerBloc); await tester.tap(find.byIcon(Icons.replay)); verify(() => timerBloc.add(const TimerReset())).called(1); }); testWidgets('timer resets when reset button is pressed ' 'when timer is finished', (tester) async { when(() => timerBloc.state).thenReturn(const TimerRunComplete()); await tester.pumpTimerView(timerBloc); await tester.tap(find.byIcon(Icons.replay)); verify(() => timerBloc.add(const TimerReset())).called(1); }); testWidgets('actions are not rebuilt when timer is running', ( tester, ) async { final controller = StreamController(); whenListen( timerBloc, controller.stream, initialState: const TimerRunInProgress(10), ); await tester.pumpTimerView(timerBloc); FloatingActionButton findPauseButton() { return tester.widget( find.byWidgetPredicate( (widget) => widget is FloatingActionButton && widget.child is Icon && (widget.child! as Icon).icon == Icons.pause, ), ); } final pauseButton = findPauseButton(); controller.add(const TimerRunInProgress(9)); await tester.pump(); expect(pauseButton, equals(findPauseButton())); }); }); } ================================================ FILE: examples/flutter_timer/web/index.html ================================================ flutter_timer ================================================ FILE: examples/flutter_timer/web/manifest.json ================================================ { "name": "flutter_timer", "short_name": "flutter_timer", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: examples/flutter_todos/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: examples/flutter_todos/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: web create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/flutter_todos/.vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Launch development", "request": "launch", "type": "dart", "program": "lib/main_development.dart", "args": [ "--flavor", "development", "--target", "lib/main_development.dart" ] }, { "name": "Launch staging", "request": "launch", "type": "dart", "program": "lib/main_staging.dart", "args": ["--flavor", "staging", "--target", "lib/main_staging.dart"] }, { "name": "Launch production", "request": "launch", "type": "dart", "program": "lib/main_production.dart", "args": ["--flavor", "production", "--target", "lib/main_production.dart"] } ] } ================================================ FILE: examples/flutter_todos/LICENSE ================================================ MIT License Copyright (c) 2022 Very Good Ventures Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: examples/flutter_todos/README.md ================================================ # Flutter Todos ![coverage][coverage_badge] [![License: MIT][license_badge]][license_link] Generated by the [Very Good CLI][very_good_cli_link] 🤖 An example todos app that showcases bloc state management patterns. --- ## Getting Started 🚀 This project contains 3 flavors: - development - staging - production To run the desired flavor either use the launch configuration in VSCode/Android Studio or use the following commands: ```sh # Development $ flutter run --flavor development --target lib/main_development.dart # Staging $ flutter run --flavor staging --target lib/main_staging.dart # Production $ flutter run --flavor production --target lib/main_production.dart ``` _\*Flutter Todos works on iOS, Android, and Web._ --- ## Running Tests 🧪 To run all unit and widget tests use the following command: ```sh $ flutter test --coverage --test-randomize-ordering-seed random ``` To view the generated coverage report you can use [lcov](https://github.com/linux-test-project/lcov). ```sh # Generate Coverage Report $ genhtml coverage/lcov.info -o coverage/ # Open Coverage Report $ open coverage/index.html ``` --- ## Working with Translations 🌐 This project relies on [flutter_localizations][flutter_localizations_link] and follows the [official internationalization guide for Flutter][internationalization_link]. ### Adding Strings 1. To add a new localizable string, open the `app_en.arb` file at `lib/l10n/arb/app_en.arb`. ```arb { "@@locale": "en", "counterAppBarTitle": "Counter", "@counterAppBarTitle": { "description": "Text shown in the AppBar of the Counter Page" } } ``` 2. Then add a new key/value and description ```arb { "@@locale": "en", "counterAppBarTitle": "Counter", "@counterAppBarTitle": { "description": "Text shown in the AppBar of the Counter Page" }, "helloWorld": "Hello World", "@helloWorld": { "description": "Hello World Text" } } ``` 3. Use the new string ```dart import 'package:flutter_todos/l10n/l10n.dart'; @override Widget build(BuildContext context) { final l10n = context.l10n; return Text(l10n.helloWorld); } ``` ### Adding Supported Locales Update the `CFBundleLocalizations` array in the `Info.plist` at `ios/Runner/Info.plist` to include the new locale. ```xml ... CFBundleLocalizations en es ... ``` ### Adding Translations 1. For each supported locale, add a new ARB file in `lib/l10n/arb`. ``` ├── l10n │ ├── arb │ │ ├── app_en.arb │ │ └── app_es.arb ``` 2. Add the translated strings to each `.arb` file: `app_en.arb` ```arb { "@@locale": "en", "counterAppBarTitle": "Counter", "@counterAppBarTitle": { "description": "Text shown in the AppBar of the Counter Page" } } ``` `app_es.arb` ```arb { "@@locale": "es", "counterAppBarTitle": "Contador", "@counterAppBarTitle": { "description": "Texto mostrado en la AppBar de la página del contador" } } ``` [coverage_badge]: coverage_badge.svg [flutter_localizations_link]: https://api.flutter.dev/flutter/flutter_localizations/flutter_localizations-library.html [internationalization_link]: https://flutter.dev/docs/development/accessibility-and-localization/internationalization [license_badge]: https://img.shields.io/badge/license-MIT-blue.svg [license_link]: https://opensource.org/licenses/MIT [very_good_cli_link]: https://github.com/VeryGoodOpenSource/very_good_cli ================================================ FILE: examples/flutter_todos/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml linter: rules: public_member_api_docs: false ================================================ FILE: examples/flutter_todos/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: examples/flutter_todos/ios/Podfile ================================================ # Uncomment this line to define a global platform for your project # platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end ================================================ FILE: examples/flutter_todos/l10n.yaml ================================================ arb-dir: lib/l10n template-arb-file: app_en.arb output-localization-file: app_localizations.dart nullable-getter: false ================================================ FILE: examples/flutter_todos/lib/app/app.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_todos/home/home.dart'; import 'package:flutter_todos/l10n/l10n.dart'; import 'package:flutter_todos/theme/theme.dart'; import 'package:todos_repository/todos_repository.dart'; class App extends StatelessWidget { const App({required this.createTodosRepository, super.key}); final TodosRepository Function() createTodosRepository; @override Widget build(BuildContext context) { return RepositoryProvider( create: (_) => createTodosRepository(), dispose: (repository) => repository.dispose(), child: const AppView(), ); } } class AppView extends StatelessWidget { const AppView({super.key}); @override Widget build(BuildContext context) { return MaterialApp( theme: FlutterTodosTheme.light, darkTheme: FlutterTodosTheme.dark, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: const HomePage(), ); } } ================================================ FILE: examples/flutter_todos/lib/app/app_bloc_observer.dart ================================================ import 'dart:developer'; import 'package:bloc/bloc.dart'; class AppBlocObserver extends BlocObserver { const AppBlocObserver(); @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); log('onChange(${bloc.runtimeType}, $change)'); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { log('onError(${bloc.runtimeType}, $error, $stackTrace)'); super.onError(bloc, error, stackTrace); } } ================================================ FILE: examples/flutter_todos/lib/bootstrap.dart ================================================ import 'dart:developer'; import 'package:bloc/bloc.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_todos/app/app.dart'; import 'package:flutter_todos/app/app_bloc_observer.dart'; import 'package:todos_api/todos_api.dart'; import 'package:todos_repository/todos_repository.dart'; void bootstrap({required TodosApi todosApi}) { FlutterError.onError = (details) { log(details.exceptionAsString(), stackTrace: details.stack); }; PlatformDispatcher.instance.onError = (error, stack) { log(error.toString(), stackTrace: stack); return true; }; Bloc.observer = const AppBlocObserver(); runApp(App(createTodosRepository: () => TodosRepository(todosApi: todosApi))); } ================================================ FILE: examples/flutter_todos/lib/edit_todo/bloc/edit_todo_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:todos_repository/todos_repository.dart'; part 'edit_todo_event.dart'; part 'edit_todo_state.dart'; class EditTodoBloc extends Bloc { EditTodoBloc({ required TodosRepository todosRepository, required Todo? initialTodo, }) : _todosRepository = todosRepository, super( EditTodoState( initialTodo: initialTodo, title: initialTodo?.title ?? '', description: initialTodo?.description ?? '', ), ) { on(_onTitleChanged); on(_onDescriptionChanged); on(_onSubmitted); } final TodosRepository _todosRepository; void _onTitleChanged( EditTodoTitleChanged event, Emitter emit, ) { emit(state.copyWith(title: event.title)); } void _onDescriptionChanged( EditTodoDescriptionChanged event, Emitter emit, ) { emit(state.copyWith(description: event.description)); } Future _onSubmitted( EditTodoSubmitted event, Emitter emit, ) async { emit(state.copyWith(status: EditTodoStatus.loading)); final todo = (state.initialTodo ?? Todo(title: '')).copyWith( title: state.title, description: state.description, ); try { await _todosRepository.saveTodo(todo); emit(state.copyWith(status: EditTodoStatus.success)); } catch (e) { emit(state.copyWith(status: EditTodoStatus.failure)); } } } ================================================ FILE: examples/flutter_todos/lib/edit_todo/bloc/edit_todo_event.dart ================================================ part of 'edit_todo_bloc.dart'; sealed class EditTodoEvent extends Equatable { const EditTodoEvent(); @override List get props => []; } final class EditTodoTitleChanged extends EditTodoEvent { const EditTodoTitleChanged(this.title); final String title; @override List get props => [title]; } final class EditTodoDescriptionChanged extends EditTodoEvent { const EditTodoDescriptionChanged(this.description); final String description; @override List get props => [description]; } final class EditTodoSubmitted extends EditTodoEvent { const EditTodoSubmitted(); } ================================================ FILE: examples/flutter_todos/lib/edit_todo/bloc/edit_todo_state.dart ================================================ part of 'edit_todo_bloc.dart'; enum EditTodoStatus { initial, loading, success, failure } extension EditTodoStatusX on EditTodoStatus { bool get isLoadingOrSuccess => [ EditTodoStatus.loading, EditTodoStatus.success, ].contains(this); } final class EditTodoState extends Equatable { const EditTodoState({ this.status = EditTodoStatus.initial, this.initialTodo, this.title = '', this.description = '', }); final EditTodoStatus status; final Todo? initialTodo; final String title; final String description; bool get isNewTodo => initialTodo == null; EditTodoState copyWith({ EditTodoStatus? status, Todo? initialTodo, String? title, String? description, }) { return EditTodoState( status: status ?? this.status, initialTodo: initialTodo ?? this.initialTodo, title: title ?? this.title, description: description ?? this.description, ); } @override List get props => [status, initialTodo, title, description]; } ================================================ FILE: examples/flutter_todos/lib/edit_todo/edit_todo.dart ================================================ export 'bloc/edit_todo_bloc.dart'; export 'view/view.dart'; ================================================ FILE: examples/flutter_todos/lib/edit_todo/view/edit_todo_page.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_todos/edit_todo/edit_todo.dart'; import 'package:flutter_todos/l10n/l10n.dart'; import 'package:todos_repository/todos_repository.dart'; class EditTodoPage extends StatelessWidget { const EditTodoPage({super.key}); static Route route({Todo? initialTodo}) { return MaterialPageRoute( fullscreenDialog: true, builder: (context) => BlocProvider( create: (context) => EditTodoBloc( todosRepository: context.read(), initialTodo: initialTodo, ), child: const EditTodoPage(), ), ); } @override Widget build(BuildContext context) { return BlocListener( listenWhen: (previous, current) => previous.status != current.status && current.status == EditTodoStatus.success, listener: (context, state) => Navigator.of(context).pop(), child: const EditTodoView(), ); } } class EditTodoView extends StatelessWidget { const EditTodoView({super.key}); @override Widget build(BuildContext context) { final l10n = context.l10n; final status = context.select((EditTodoBloc bloc) => bloc.state.status); final isNewTodo = context.select( (EditTodoBloc bloc) => bloc.state.isNewTodo, ); return Scaffold( appBar: AppBar( title: Text( isNewTodo ? l10n.editTodoAddAppBarTitle : l10n.editTodoEditAppBarTitle, ), ), floatingActionButton: FloatingActionButton( tooltip: l10n.editTodoSaveButtonTooltip, shape: const ContinuousRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(32)), ), onPressed: status.isLoadingOrSuccess ? null : () => context.read().add(const EditTodoSubmitted()), child: status.isLoadingOrSuccess ? const CupertinoActivityIndicator() : const Icon(Icons.check_rounded), ), body: const CupertinoScrollbar( child: SingleChildScrollView( child: Padding( padding: EdgeInsets.all(16), child: Column( children: [_TitleField(), _DescriptionField()], ), ), ), ), ); } } class _TitleField extends StatelessWidget { const _TitleField(); @override Widget build(BuildContext context) { final l10n = context.l10n; final state = context.watch().state; final hintText = state.initialTodo?.title ?? ''; return TextFormField( key: const Key('editTodoView_title_textFormField'), initialValue: state.title, decoration: InputDecoration( enabled: !state.status.isLoadingOrSuccess, labelText: l10n.editTodoTitleLabel, hintText: hintText, ), maxLength: 50, inputFormatters: [ LengthLimitingTextInputFormatter(50), FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9\s]')), ], onChanged: (value) { context.read().add(EditTodoTitleChanged(value)); }, ); } } class _DescriptionField extends StatelessWidget { const _DescriptionField(); @override Widget build(BuildContext context) { final l10n = context.l10n; final state = context.watch().state; final hintText = state.initialTodo?.description ?? ''; return TextFormField( key: const Key('editTodoView_description_textFormField'), initialValue: state.description, decoration: InputDecoration( enabled: !state.status.isLoadingOrSuccess, labelText: l10n.editTodoDescriptionLabel, hintText: hintText, ), maxLength: 300, maxLines: 7, inputFormatters: [ LengthLimitingTextInputFormatter(300), ], onChanged: (value) { context.read().add(EditTodoDescriptionChanged(value)); }, ); } } ================================================ FILE: examples/flutter_todos/lib/edit_todo/view/view.dart ================================================ export 'edit_todo_page.dart'; ================================================ FILE: examples/flutter_todos/lib/home/cubit/home_cubit.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; part 'home_state.dart'; class HomeCubit extends Cubit { HomeCubit() : super(const HomeState()); void setTab(HomeTab tab) => emit(HomeState(tab: tab)); } ================================================ FILE: examples/flutter_todos/lib/home/cubit/home_state.dart ================================================ part of 'home_cubit.dart'; enum HomeTab { todos, stats } final class HomeState extends Equatable { const HomeState({ this.tab = HomeTab.todos, }); final HomeTab tab; @override List get props => [tab]; } ================================================ FILE: examples/flutter_todos/lib/home/home.dart ================================================ export 'cubit/home_cubit.dart'; export 'view/view.dart'; ================================================ FILE: examples/flutter_todos/lib/home/view/home_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_todos/edit_todo/edit_todo.dart'; import 'package:flutter_todos/home/home.dart'; import 'package:flutter_todos/stats/stats.dart'; import 'package:flutter_todos/todos_overview/todos_overview.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => HomeCubit(), child: const HomeView(), ); } } class HomeView extends StatelessWidget { const HomeView({super.key}); @override Widget build(BuildContext context) { final selectedTab = context.select((HomeCubit cubit) => cubit.state.tab); return Scaffold( body: IndexedStack( index: selectedTab.index, children: const [TodosOverviewPage(), StatsPage()], ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, floatingActionButton: FloatingActionButton( shape: const CircleBorder(), key: const Key('homeView_addTodo_floatingActionButton'), onPressed: () => Navigator.of(context).push(EditTodoPage.route()), child: const Icon(Icons.add), ), bottomNavigationBar: BottomAppBar( shape: const CircularNotchedRectangle(), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _HomeTabButton( groupValue: selectedTab, value: HomeTab.todos, icon: const Icon(Icons.list_rounded), ), _HomeTabButton( groupValue: selectedTab, value: HomeTab.stats, icon: const Icon(Icons.show_chart_rounded), ), ], ), ), ); } } class _HomeTabButton extends StatelessWidget { const _HomeTabButton({ required this.groupValue, required this.value, required this.icon, }); final HomeTab groupValue; final HomeTab value; final Widget icon; @override Widget build(BuildContext context) { return IconButton( onPressed: () => context.read().setTab(value), iconSize: 32, color: groupValue != value ? null : Theme.of(context).colorScheme.secondary, icon: icon, ); } } ================================================ FILE: examples/flutter_todos/lib/home/view/view.dart ================================================ export 'home_page.dart'; ================================================ FILE: examples/flutter_todos/lib/l10n/app_en.arb ================================================ { "@@locale": "en", "todosOverviewAppBarTitle": "Flutter Todos", "@todosOverviewAppBarTitle": { "description": "Title text shown in the AppBar of the Todos Overview Page" }, "todosOverviewFilterTooltip": "Filter", "@todosOverviewFilterTooltip": { "description": "Tooltip text shown in the filter dropdown of the Todos Overview Page" }, "todosOverviewFilterAll": "All", "@todosOverviewFilterAll": { "description": "Text shown in the filter dropdown of the Todos Overview Page for the option to display all todos" }, "todosOverviewFilterActiveOnly": "Active only", "@todosOverviewFilterActiveOnly": { "description": "Text shown in the filter dropdown of the Todos Overview Page for the option to display active todos only" }, "todosOverviewFilterCompletedOnly": "Completed only", "@todosOverviewFilterCompletedOnly": { "description": "Text shown in the filter dropdown of the Todos Overview Page for the option to display completed todos only" }, "todosOverviewMarkAllCompleteButtonText": "Mark all complete", "@todosOverviewMarkAllCompleteButtonText": { "description": "Button text shown in the options dropdown of the Todos Overview Page that marks all current todos as complete" }, "todosOverviewClearCompletedButtonText": "Clear completed", "@todosOverviewClearCompletedButtonText": { "description": "Button text shown in the options dropdown of the Todos Overview Page that deletes all completed todos" }, "todosOverviewEmptyText": "No todos found with the selected filters.", "@todosOverviewEmptyText": { "description": "Text shown in the Todos Overview Page when no todos are found with the selected filters" }, "todosOverviewTodoDeletedSnackbarText": "Todo \"{todoTitle}\" deleted.", "@todosOverviewTodoDeletedSnackbarText": { "description": "Snackbar text shown when a todo is deleted from the Todos Overview Page", "placeholders": { "todoTitle": { "description": "The title of the todo that was deleted" } } }, "todosOverviewUndoDeletionButtonText": "Undo", "@todosOverviewUndoDeletionButtonText": { "description": "Button text shown in the snackbar that undoes a deletion of a todo" }, "todosOverviewErrorSnackbarText": "An error occurred while loading todos.", "@todosOverviewErrorSnackbarText": { "description": "Snackbar text shown when an error occurs while loading todos" }, "todosOverviewOptionsTooltip": "Options", "@todosOverviewOptionsTooltip": { "description": "Tooltip text shown in the options dropdown of the Todos Overview Page" }, "todosOverviewOptionsMarkAllComplete": "Mark all as completed", "@todosOverviewOptionsMarkAllComplete": { "description": "Button text shown in the options dropdown of the Todos Overview Page that marks all todos as complete" }, "todosOverviewOptionsMarkAllIncomplete": "Mark all as incomplete", "@todosOverviewOptionsMarkAllIncomplete": { "description": "Button text shown in the options dropdown of the Todos Overview Page that marks all todos as incomplete" }, "todosOverviewOptionsClearCompleted": "Clear completed", "@todosOverviewOptionsClearCompleted": { "description": "Button text shown in the options dropdown of the Todos Overview Page that deletes all completed todos" }, "todoDetailsAppBarTitle": "Todo Details", "@todoDetailsAppBarTitle": { "description": "Title text shown in the AppBar of the Todo Details Page" }, "todoDetailsDeleteButtonTooltip": "Delete", "@todoDetailsDeleteButtonTooltip": { "description": "Tooltip text shown in the delete button on the Todo Details Page" }, "todoDetailsEditButtonTooltip": "Edit", "@todoDetailsEditButtonTooltip": { "description": "Tooltip text shown in the edit button on the Todo Details Page" }, "editTodoEditAppBarTitle": "Edit Todo", "@editTodoEditAppBarTitle": { "description": "Title text shown in the AppBar of the Todo Edit Page when editing an existing todo" }, "editTodoAddAppBarTitle": "Add Todo", "@editTodoAddAppBarTitle": { "description": "Title text shown in the AppBar of the Todo Edit Page when adding a new todo" }, "editTodoTitleLabel": "Title", "@editTodoTitleLabel": { "description": "Label text shown in the title input field of the Todo Edit Page" }, "editTodoDescriptionLabel": "Description", "@editTodoDescriptionLabel": { "description": "Label text shown in the description input field of the Todo Edit Page" }, "editTodoSaveButtonTooltip": "Save changes", "@editTodoSaveButtonTooltip": { "description": "Tooltip text shown in the save button on the Todo Edit Page" }, "statsAppBarTitle": "Stats", "@statsAppBarTitle": { "description": "Title text shown in the AppBar of the Stats Page" }, "statsCompletedTodoCountLabel": "Completed todos", "@statsCompletedTodoCountLabel": { "description": "Label text shown in the completed todos count section of the Stats Page" }, "statsActiveTodoCountLabel": "Active todos", "@statsActiveTodoCountLabel": { "description": "Label text shown in the active todos count section of the Stats Page" } } ================================================ FILE: examples/flutter_todos/lib/l10n/app_localizations.dart ================================================ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:intl/intl.dart' as intl; import 'app_localizations_en.dart'; // ignore_for_file: type=lint /// Callers can lookup localized strings with an instance of AppLocalizations /// returned by `AppLocalizations.of(context)`. /// /// Applications need to include `AppLocalizations.delegate()` in their app's /// `localizationDelegates` list, and the locales they support in the app's /// `supportedLocales` list. For example: /// /// ```dart /// import 'l10n/app_localizations.dart'; /// /// return MaterialApp( /// localizationsDelegates: AppLocalizations.localizationsDelegates, /// supportedLocales: AppLocalizations.supportedLocales, /// home: MyApplicationHome(), /// ); /// ``` /// /// ## Update pubspec.yaml /// /// Please make sure to update your pubspec.yaml to include the following /// packages: /// /// ```yaml /// dependencies: /// # Internationalization support. /// flutter_localizations: /// sdk: flutter /// intl: any # Use the pinned version from flutter_localizations /// /// # Rest of dependencies /// ``` /// /// ## iOS Applications /// /// iOS applications define key application metadata, including supported /// locales, in an Info.plist file that is built into the application bundle. /// To configure the locales supported by your app, you’ll need to edit this /// file. /// /// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. /// Then, in the Project Navigator, open the Info.plist file under the Runner /// project’s Runner folder. /// /// Next, select the Information Property List item, select Add Item from the /// Editor menu, then select Localizations from the pop-up menu. /// /// Select and expand the newly-created Localizations item then, for each /// locale your application supports, add a new item and select the locale /// you wish to add from the pop-up menu in the Value field. This list should /// be consistent with the languages listed in the AppLocalizations.supportedLocales /// property. abstract class AppLocalizations { AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; static AppLocalizations of(BuildContext context) { return Localizations.of(context, AppLocalizations)!; } static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); /// A list of this localizations delegate along with the default localizations /// delegates. /// /// Returns a list of localizations delegates containing this delegate along with /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, /// and GlobalWidgetsLocalizations.delegate. /// /// Additional delegates can be added by appending to this list in /// MaterialApp. This list does not have to be used at all if a custom list /// of delegates is preferred or required. static const List> localizationsDelegates = >[ delegate, GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ]; /// A list of this localizations delegate's supported locales. static const List supportedLocales = [Locale('en')]; /// Title text shown in the AppBar of the Todos Overview Page /// /// In en, this message translates to: /// **'Flutter Todos'** String get todosOverviewAppBarTitle; /// Tooltip text shown in the filter dropdown of the Todos Overview Page /// /// In en, this message translates to: /// **'Filter'** String get todosOverviewFilterTooltip; /// Text shown in the filter dropdown of the Todos Overview Page for the option to display all todos /// /// In en, this message translates to: /// **'All'** String get todosOverviewFilterAll; /// Text shown in the filter dropdown of the Todos Overview Page for the option to display active todos only /// /// In en, this message translates to: /// **'Active only'** String get todosOverviewFilterActiveOnly; /// Text shown in the filter dropdown of the Todos Overview Page for the option to display completed todos only /// /// In en, this message translates to: /// **'Completed only'** String get todosOverviewFilterCompletedOnly; /// Button text shown in the options dropdown of the Todos Overview Page that marks all current todos as complete /// /// In en, this message translates to: /// **'Mark all complete'** String get todosOverviewMarkAllCompleteButtonText; /// Button text shown in the options dropdown of the Todos Overview Page that deletes all completed todos /// /// In en, this message translates to: /// **'Clear completed'** String get todosOverviewClearCompletedButtonText; /// Text shown in the Todos Overview Page when no todos are found with the selected filters /// /// In en, this message translates to: /// **'No todos found with the selected filters.'** String get todosOverviewEmptyText; /// Snackbar text shown when a todo is deleted from the Todos Overview Page /// /// In en, this message translates to: /// **'Todo \"{todoTitle}\" deleted.'** String todosOverviewTodoDeletedSnackbarText(Object todoTitle); /// Button text shown in the snackbar that undoes a deletion of a todo /// /// In en, this message translates to: /// **'Undo'** String get todosOverviewUndoDeletionButtonText; /// Snackbar text shown when an error occurs while loading todos /// /// In en, this message translates to: /// **'An error occurred while loading todos.'** String get todosOverviewErrorSnackbarText; /// Tooltip text shown in the options dropdown of the Todos Overview Page /// /// In en, this message translates to: /// **'Options'** String get todosOverviewOptionsTooltip; /// Button text shown in the options dropdown of the Todos Overview Page that marks all todos as complete /// /// In en, this message translates to: /// **'Mark all as completed'** String get todosOverviewOptionsMarkAllComplete; /// Button text shown in the options dropdown of the Todos Overview Page that marks all todos as incomplete /// /// In en, this message translates to: /// **'Mark all as incomplete'** String get todosOverviewOptionsMarkAllIncomplete; /// Button text shown in the options dropdown of the Todos Overview Page that deletes all completed todos /// /// In en, this message translates to: /// **'Clear completed'** String get todosOverviewOptionsClearCompleted; /// Title text shown in the AppBar of the Todo Details Page /// /// In en, this message translates to: /// **'Todo Details'** String get todoDetailsAppBarTitle; /// Tooltip text shown in the delete button on the Todo Details Page /// /// In en, this message translates to: /// **'Delete'** String get todoDetailsDeleteButtonTooltip; /// Tooltip text shown in the edit button on the Todo Details Page /// /// In en, this message translates to: /// **'Edit'** String get todoDetailsEditButtonTooltip; /// Title text shown in the AppBar of the Todo Edit Page when editing an existing todo /// /// In en, this message translates to: /// **'Edit Todo'** String get editTodoEditAppBarTitle; /// Title text shown in the AppBar of the Todo Edit Page when adding a new todo /// /// In en, this message translates to: /// **'Add Todo'** String get editTodoAddAppBarTitle; /// Label text shown in the title input field of the Todo Edit Page /// /// In en, this message translates to: /// **'Title'** String get editTodoTitleLabel; /// Label text shown in the description input field of the Todo Edit Page /// /// In en, this message translates to: /// **'Description'** String get editTodoDescriptionLabel; /// Tooltip text shown in the save button on the Todo Edit Page /// /// In en, this message translates to: /// **'Save changes'** String get editTodoSaveButtonTooltip; /// Title text shown in the AppBar of the Stats Page /// /// In en, this message translates to: /// **'Stats'** String get statsAppBarTitle; /// Label text shown in the completed todos count section of the Stats Page /// /// In en, this message translates to: /// **'Completed todos'** String get statsCompletedTodoCountLabel; /// Label text shown in the active todos count section of the Stats Page /// /// In en, this message translates to: /// **'Active todos'** String get statsActiveTodoCountLabel; } class _AppLocalizationsDelegate extends LocalizationsDelegate { const _AppLocalizationsDelegate(); @override Future load(Locale locale) { return SynchronousFuture(lookupAppLocalizations(locale)); } @override bool isSupported(Locale locale) => ['en'].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; } AppLocalizations lookupAppLocalizations(Locale locale) { // Lookup logic when only language code is specified. switch (locale.languageCode) { case 'en': return AppLocalizationsEn(); } throw FlutterError( 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' 'an issue with the localizations generation tool. Please file an issue ' 'on GitHub with a reproducible sample app and the gen-l10n configuration ' 'that was used.', ); } ================================================ FILE: examples/flutter_todos/lib/l10n/app_localizations_en.dart ================================================ // ignore: unused_import import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; // ignore_for_file: type=lint /// The translations for English (`en`). class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); @override String get todosOverviewAppBarTitle => 'Flutter Todos'; @override String get todosOverviewFilterTooltip => 'Filter'; @override String get todosOverviewFilterAll => 'All'; @override String get todosOverviewFilterActiveOnly => 'Active only'; @override String get todosOverviewFilterCompletedOnly => 'Completed only'; @override String get todosOverviewMarkAllCompleteButtonText => 'Mark all complete'; @override String get todosOverviewClearCompletedButtonText => 'Clear completed'; @override String get todosOverviewEmptyText => 'No todos found with the selected filters.'; @override String todosOverviewTodoDeletedSnackbarText(Object todoTitle) { return 'Todo \"$todoTitle\" deleted.'; } @override String get todosOverviewUndoDeletionButtonText => 'Undo'; @override String get todosOverviewErrorSnackbarText => 'An error occurred while loading todos.'; @override String get todosOverviewOptionsTooltip => 'Options'; @override String get todosOverviewOptionsMarkAllComplete => 'Mark all as completed'; @override String get todosOverviewOptionsMarkAllIncomplete => 'Mark all as incomplete'; @override String get todosOverviewOptionsClearCompleted => 'Clear completed'; @override String get todoDetailsAppBarTitle => 'Todo Details'; @override String get todoDetailsDeleteButtonTooltip => 'Delete'; @override String get todoDetailsEditButtonTooltip => 'Edit'; @override String get editTodoEditAppBarTitle => 'Edit Todo'; @override String get editTodoAddAppBarTitle => 'Add Todo'; @override String get editTodoTitleLabel => 'Title'; @override String get editTodoDescriptionLabel => 'Description'; @override String get editTodoSaveButtonTooltip => 'Save changes'; @override String get statsAppBarTitle => 'Stats'; @override String get statsCompletedTodoCountLabel => 'Completed todos'; @override String get statsActiveTodoCountLabel => 'Active todos'; } ================================================ FILE: examples/flutter_todos/lib/l10n/l10n.dart ================================================ import 'package:flutter/widgets.dart'; import 'package:flutter_todos/l10n/app_localizations.dart'; export 'package:flutter_todos/l10n/app_localizations.dart'; extension AppLocalizationsX on BuildContext { AppLocalizations get l10n => AppLocalizations.of(this); } ================================================ FILE: examples/flutter_todos/lib/main_development.dart ================================================ import 'package:flutter/widgets.dart'; import 'package:flutter_todos/bootstrap.dart'; import 'package:local_storage_todos_api/local_storage_todos_api.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); final todosApi = LocalStorageTodosApi( plugin: await SharedPreferences.getInstance(), ); bootstrap(todosApi: todosApi); } ================================================ FILE: examples/flutter_todos/lib/main_production.dart ================================================ import 'package:flutter/widgets.dart'; import 'package:flutter_todos/bootstrap.dart'; import 'package:local_storage_todos_api/local_storage_todos_api.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); final todosApi = LocalStorageTodosApi( plugin: await SharedPreferences.getInstance(), ); bootstrap(todosApi: todosApi); } ================================================ FILE: examples/flutter_todos/lib/main_staging.dart ================================================ import 'package:flutter/widgets.dart'; import 'package:flutter_todos/bootstrap.dart'; import 'package:local_storage_todos_api/local_storage_todos_api.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); final todosApi = LocalStorageTodosApi( plugin: await SharedPreferences.getInstance(), ); bootstrap(todosApi: todosApi); } ================================================ FILE: examples/flutter_todos/lib/stats/bloc/stats_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:todos_repository/todos_repository.dart'; part 'stats_event.dart'; part 'stats_state.dart'; class StatsBloc extends Bloc { StatsBloc({ required TodosRepository todosRepository, }) : _todosRepository = todosRepository, super(const StatsState()) { on(_onSubscriptionRequested); } final TodosRepository _todosRepository; Future _onSubscriptionRequested( StatsSubscriptionRequested event, Emitter emit, ) async { emit(state.copyWith(status: StatsStatus.loading)); await emit.forEach>( _todosRepository.getTodos(), onData: (todos) => state.copyWith( status: StatsStatus.success, completedTodos: todos.where((todo) => todo.isCompleted).length, activeTodos: todos.where((todo) => !todo.isCompleted).length, ), onError: (_, _) => state.copyWith(status: StatsStatus.failure), ); } } ================================================ FILE: examples/flutter_todos/lib/stats/bloc/stats_event.dart ================================================ part of 'stats_bloc.dart'; sealed class StatsEvent extends Equatable { const StatsEvent(); @override List get props => []; } final class StatsSubscriptionRequested extends StatsEvent { const StatsSubscriptionRequested(); } ================================================ FILE: examples/flutter_todos/lib/stats/bloc/stats_state.dart ================================================ part of 'stats_bloc.dart'; enum StatsStatus { initial, loading, success, failure } final class StatsState extends Equatable { const StatsState({ this.status = StatsStatus.initial, this.completedTodos = 0, this.activeTodos = 0, }); final StatsStatus status; final int completedTodos; final int activeTodos; @override List get props => [status, completedTodos, activeTodos]; StatsState copyWith({ StatsStatus? status, int? completedTodos, int? activeTodos, }) { return StatsState( status: status ?? this.status, completedTodos: completedTodos ?? this.completedTodos, activeTodos: activeTodos ?? this.activeTodos, ); } } ================================================ FILE: examples/flutter_todos/lib/stats/stats.dart ================================================ export 'bloc/stats_bloc.dart'; export 'view/view.dart'; ================================================ FILE: examples/flutter_todos/lib/stats/view/stats_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_todos/l10n/l10n.dart'; import 'package:flutter_todos/stats/stats.dart'; import 'package:todos_repository/todos_repository.dart'; class StatsPage extends StatelessWidget { const StatsPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => StatsBloc( todosRepository: context.read(), )..add(const StatsSubscriptionRequested()), child: const StatsView(), ); } } class StatsView extends StatelessWidget { const StatsView({super.key}); @override Widget build(BuildContext context) { final l10n = context.l10n; final state = context.watch().state; final textTheme = Theme.of(context).textTheme; return Scaffold( appBar: AppBar( title: Text(l10n.statsAppBarTitle), ), body: Column( children: [ ListTile( key: const Key('statsView_completedTodos_listTile'), leading: const Icon(Icons.check_rounded), title: Text(l10n.statsCompletedTodoCountLabel), trailing: Text( '${state.completedTodos}', style: textTheme.headlineSmall, ), ), ListTile( key: const Key('statsView_activeTodos_listTile'), leading: const Icon(Icons.radio_button_unchecked_rounded), title: Text(l10n.statsActiveTodoCountLabel), trailing: Text( '${state.activeTodos}', style: textTheme.headlineSmall, ), ), ], ), ); } } ================================================ FILE: examples/flutter_todos/lib/stats/view/view.dart ================================================ export 'stats_page.dart'; ================================================ FILE: examples/flutter_todos/lib/theme/theme.dart ================================================ import 'package:flutter/material.dart'; class FlutterTodosTheme { static ThemeData get light { return ThemeData( appBarTheme: const AppBarTheme( backgroundColor: Color.fromARGB(255, 117, 208, 247), ), colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF13B9FF), ), snackBarTheme: const SnackBarThemeData( behavior: SnackBarBehavior.floating, ), ); } static ThemeData get dark { return ThemeData( appBarTheme: const AppBarTheme( backgroundColor: Color.fromARGB(255, 16, 46, 59), ), colorScheme: ColorScheme.fromSeed( brightness: Brightness.dark, seedColor: const Color(0xFF13B9FF), ), snackBarTheme: const SnackBarThemeData( behavior: SnackBarBehavior.floating, ), ); } } ================================================ FILE: examples/flutter_todos/lib/todos_overview/bloc/todos_overview_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_todos/todos_overview/todos_overview.dart'; import 'package:todos_repository/todos_repository.dart'; part 'todos_overview_event.dart'; part 'todos_overview_state.dart'; class TodosOverviewBloc extends Bloc { TodosOverviewBloc({ required TodosRepository todosRepository, }) : _todosRepository = todosRepository, super(const TodosOverviewState()) { on(_onSubscriptionRequested); on(_onTodoCompletionToggled); on(_onTodoDeleted); on(_onUndoDeletionRequested); on(_onFilterChanged); on(_onToggleAllRequested); on(_onClearCompletedRequested); } final TodosRepository _todosRepository; Future _onSubscriptionRequested( TodosOverviewSubscriptionRequested event, Emitter emit, ) async { emit(state.copyWith(status: () => TodosOverviewStatus.loading)); await emit.forEach>( _todosRepository.getTodos(), onData: (todos) => state.copyWith( status: () => TodosOverviewStatus.success, todos: () => todos, ), onError: (_, _) => state.copyWith( status: () => TodosOverviewStatus.failure, ), ); } Future _onTodoCompletionToggled( TodosOverviewTodoCompletionToggled event, Emitter emit, ) async { final newTodo = event.todo.copyWith(isCompleted: event.isCompleted); await _todosRepository.saveTodo(newTodo); } Future _onTodoDeleted( TodosOverviewTodoDeleted event, Emitter emit, ) async { emit(state.copyWith(lastDeletedTodo: () => event.todo)); await _todosRepository.deleteTodo(event.todo.id); } Future _onUndoDeletionRequested( TodosOverviewUndoDeletionRequested event, Emitter emit, ) async { assert( state.lastDeletedTodo != null, 'Last deleted todo can not be null.', ); final todo = state.lastDeletedTodo!; emit(state.copyWith(lastDeletedTodo: () => null)); await _todosRepository.saveTodo(todo); } void _onFilterChanged( TodosOverviewFilterChanged event, Emitter emit, ) { emit(state.copyWith(filter: () => event.filter)); } Future _onToggleAllRequested( TodosOverviewToggleAllRequested event, Emitter emit, ) async { final areAllCompleted = state.todos.every((todo) => todo.isCompleted); await _todosRepository.completeAll(isCompleted: !areAllCompleted); } Future _onClearCompletedRequested( TodosOverviewClearCompletedRequested event, Emitter emit, ) async { await _todosRepository.clearCompleted(); } } ================================================ FILE: examples/flutter_todos/lib/todos_overview/bloc/todos_overview_event.dart ================================================ part of 'todos_overview_bloc.dart'; sealed class TodosOverviewEvent extends Equatable { const TodosOverviewEvent(); @override List get props => []; } final class TodosOverviewSubscriptionRequested extends TodosOverviewEvent { const TodosOverviewSubscriptionRequested(); } final class TodosOverviewTodoCompletionToggled extends TodosOverviewEvent { const TodosOverviewTodoCompletionToggled({ required this.todo, required this.isCompleted, }); final Todo todo; final bool isCompleted; @override List get props => [todo, isCompleted]; } final class TodosOverviewTodoDeleted extends TodosOverviewEvent { const TodosOverviewTodoDeleted(this.todo); final Todo todo; @override List get props => [todo]; } final class TodosOverviewUndoDeletionRequested extends TodosOverviewEvent { const TodosOverviewUndoDeletionRequested(); } class TodosOverviewFilterChanged extends TodosOverviewEvent { const TodosOverviewFilterChanged(this.filter); final TodosViewFilter filter; @override List get props => [filter]; } class TodosOverviewToggleAllRequested extends TodosOverviewEvent { const TodosOverviewToggleAllRequested(); } class TodosOverviewClearCompletedRequested extends TodosOverviewEvent { const TodosOverviewClearCompletedRequested(); } ================================================ FILE: examples/flutter_todos/lib/todos_overview/bloc/todos_overview_state.dart ================================================ part of 'todos_overview_bloc.dart'; enum TodosOverviewStatus { initial, loading, success, failure } final class TodosOverviewState extends Equatable { const TodosOverviewState({ this.status = TodosOverviewStatus.initial, this.todos = const [], this.filter = TodosViewFilter.all, this.lastDeletedTodo, }); final TodosOverviewStatus status; final List todos; final TodosViewFilter filter; final Todo? lastDeletedTodo; Iterable get filteredTodos => filter.applyAll(todos); TodosOverviewState copyWith({ TodosOverviewStatus Function()? status, List Function()? todos, TodosViewFilter Function()? filter, Todo? Function()? lastDeletedTodo, }) { return TodosOverviewState( status: status != null ? status() : this.status, todos: todos != null ? todos() : this.todos, filter: filter != null ? filter() : this.filter, lastDeletedTodo: lastDeletedTodo != null ? lastDeletedTodo() : this.lastDeletedTodo, ); } @override List get props => [ status, todos, filter, lastDeletedTodo, ]; } ================================================ FILE: examples/flutter_todos/lib/todos_overview/models/models.dart ================================================ export 'todos_view_filter.dart'; ================================================ FILE: examples/flutter_todos/lib/todos_overview/models/todos_view_filter.dart ================================================ import 'package:todos_repository/todos_repository.dart'; enum TodosViewFilter { all, activeOnly, completedOnly } extension TodosViewFilterX on TodosViewFilter { bool apply(Todo todo) { switch (this) { case TodosViewFilter.all: return true; case TodosViewFilter.activeOnly: return !todo.isCompleted; case TodosViewFilter.completedOnly: return todo.isCompleted; } } Iterable applyAll(Iterable todos) { return todos.where(apply); } } ================================================ FILE: examples/flutter_todos/lib/todos_overview/todos_overview.dart ================================================ export 'bloc/todos_overview_bloc.dart'; export 'models/models.dart'; export 'view/view.dart'; export 'widgets/widgets.dart'; ================================================ FILE: examples/flutter_todos/lib/todos_overview/view/todos_overview_page.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_todos/edit_todo/view/edit_todo_page.dart'; import 'package:flutter_todos/l10n/l10n.dart'; import 'package:flutter_todos/todos_overview/todos_overview.dart'; import 'package:todos_repository/todos_repository.dart'; class TodosOverviewPage extends StatelessWidget { const TodosOverviewPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => TodosOverviewBloc( todosRepository: context.read(), )..add(const TodosOverviewSubscriptionRequested()), child: const TodosOverviewView(), ); } } class TodosOverviewView extends StatelessWidget { const TodosOverviewView({super.key}); @override Widget build(BuildContext context) { final l10n = context.l10n; return Scaffold( appBar: AppBar( title: Text(l10n.todosOverviewAppBarTitle), actions: const [ TodosOverviewFilterButton(), TodosOverviewOptionsButton(), ], ), body: MultiBlocListener( listeners: [ BlocListener( listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { if (state.status == TodosOverviewStatus.failure) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( content: Text(l10n.todosOverviewErrorSnackbarText), ), ); } }, ), BlocListener( listenWhen: (previous, current) => previous.lastDeletedTodo != current.lastDeletedTodo && current.lastDeletedTodo != null, listener: (context, state) { final deletedTodo = state.lastDeletedTodo!; final messenger = ScaffoldMessenger.of(context); messenger ..hideCurrentSnackBar() ..showSnackBar( SnackBar( content: Text( l10n.todosOverviewTodoDeletedSnackbarText( deletedTodo.title, ), ), action: SnackBarAction( label: l10n.todosOverviewUndoDeletionButtonText, onPressed: () { messenger.hideCurrentSnackBar(); context.read().add( const TodosOverviewUndoDeletionRequested(), ); }, ), ), ); }, ), ], child: BlocBuilder( builder: (context, state) { if (state.todos.isEmpty) { if (state.status == TodosOverviewStatus.loading) { return const Center(child: CupertinoActivityIndicator()); } else if (state.status != TodosOverviewStatus.success) { return const SizedBox(); } else { return Center( child: Text( l10n.todosOverviewEmptyText, style: Theme.of(context).textTheme.bodySmall, ), ); } } return CupertinoScrollbar( child: ListView.builder( itemCount: state.filteredTodos.length, itemBuilder: (_, index) { final todo = state.filteredTodos.elementAt(index); return TodoListTile( todo: todo, onToggleCompleted: (isCompleted) { context.read().add( TodosOverviewTodoCompletionToggled( todo: todo, isCompleted: isCompleted, ), ); }, onDismissed: (_) { context.read().add( TodosOverviewTodoDeleted(todo), ); }, onTap: () { Navigator.of(context).push( EditTodoPage.route(initialTodo: todo), ); }, ); }, ), ); }, ), ), ); } } ================================================ FILE: examples/flutter_todos/lib/todos_overview/view/view.dart ================================================ export 'todos_overview_page.dart'; ================================================ FILE: examples/flutter_todos/lib/todos_overview/widgets/todo_list_tile.dart ================================================ import 'package:flutter/material.dart'; import 'package:todos_repository/todos_repository.dart'; class TodoListTile extends StatelessWidget { const TodoListTile({ required this.todo, super.key, this.onToggleCompleted, this.onDismissed, this.onTap, }); final Todo todo; final ValueChanged? onToggleCompleted; final DismissDirectionCallback? onDismissed; final VoidCallback? onTap; @override Widget build(BuildContext context) { final theme = Theme.of(context); final captionColor = theme.textTheme.bodySmall?.color; return Dismissible( key: Key('todoListTile_dismissible_${todo.id}'), onDismissed: onDismissed, direction: DismissDirection.endToStart, background: Container( alignment: Alignment.centerRight, color: theme.colorScheme.error, padding: const EdgeInsets.symmetric(horizontal: 16), child: const Icon( Icons.delete, color: Color(0xAAFFFFFF), ), ), child: ListTile( onTap: onTap, title: Text( todo.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: !todo.isCompleted ? null : TextStyle( color: captionColor, decoration: TextDecoration.lineThrough, ), ), subtitle: Text( todo.description, maxLines: 1, overflow: TextOverflow.ellipsis, ), leading: Checkbox( shape: const ContinuousRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), ), value: todo.isCompleted, onChanged: onToggleCompleted == null ? null : (value) => onToggleCompleted!(value!), ), trailing: onTap == null ? null : const Icon(Icons.chevron_right), ), ); } } ================================================ FILE: examples/flutter_todos/lib/todos_overview/widgets/todos_overview_filter_button.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_todos/l10n/l10n.dart'; import 'package:flutter_todos/todos_overview/todos_overview.dart'; class TodosOverviewFilterButton extends StatelessWidget { const TodosOverviewFilterButton({super.key}); @override Widget build(BuildContext context) { final l10n = context.l10n; final activeFilter = context.select( (TodosOverviewBloc bloc) => bloc.state.filter, ); return PopupMenuButton( shape: const ContinuousRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(16)), ), initialValue: activeFilter, tooltip: l10n.todosOverviewFilterTooltip, onSelected: (filter) { context.read().add( TodosOverviewFilterChanged(filter), ); }, itemBuilder: (context) { return [ PopupMenuItem( value: TodosViewFilter.all, child: Text(l10n.todosOverviewFilterAll), ), PopupMenuItem( value: TodosViewFilter.activeOnly, child: Text(l10n.todosOverviewFilterActiveOnly), ), PopupMenuItem( value: TodosViewFilter.completedOnly, child: Text(l10n.todosOverviewFilterCompletedOnly), ), ]; }, icon: const Icon(Icons.filter_list_rounded), ); } } ================================================ FILE: examples/flutter_todos/lib/todos_overview/widgets/todos_overview_options_button.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_todos/l10n/l10n.dart'; import 'package:flutter_todos/todos_overview/todos_overview.dart'; @visibleForTesting enum TodosOverviewOption { toggleAll, clearCompleted } class TodosOverviewOptionsButton extends StatelessWidget { const TodosOverviewOptionsButton({super.key}); @override Widget build(BuildContext context) { final l10n = context.l10n; final todos = context.select((TodosOverviewBloc bloc) => bloc.state.todos); final hasTodos = todos.isNotEmpty; final completedTodosAmount = todos.where((todo) => todo.isCompleted).length; return PopupMenuButton( shape: const ContinuousRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(16)), ), tooltip: l10n.todosOverviewOptionsTooltip, onSelected: (options) { switch (options) { case TodosOverviewOption.toggleAll: context.read().add( const TodosOverviewToggleAllRequested(), ); case TodosOverviewOption.clearCompleted: context.read().add( const TodosOverviewClearCompletedRequested(), ); } }, itemBuilder: (context) { return [ PopupMenuItem( value: TodosOverviewOption.toggleAll, enabled: hasTodos, child: Text( completedTodosAmount == todos.length ? l10n.todosOverviewOptionsMarkAllIncomplete : l10n.todosOverviewOptionsMarkAllComplete, ), ), PopupMenuItem( value: TodosOverviewOption.clearCompleted, enabled: hasTodos && completedTodosAmount > 0, child: Text(l10n.todosOverviewOptionsClearCompleted), ), ]; }, icon: const Icon(Icons.more_vert_rounded), ); } } ================================================ FILE: examples/flutter_todos/lib/todos_overview/widgets/widgets.dart ================================================ export 'todo_list_tile.dart'; export 'todos_overview_filter_button.dart'; export 'todos_overview_options_button.dart'; ================================================ FILE: examples/flutter_todos/packages/local_storage_todos_api/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # VSCode related .vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ # Web related lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols # Obfuscation related app.*.map.json ================================================ FILE: examples/flutter_todos/packages/local_storage_todos_api/README.md ================================================ # local_storage_todos_api [![License: MIT][license_badge]][license_link] A Very Good Project created by Very Good CLI. [license_badge]: https://img.shields.io/badge/license-MIT-blue.svg [license_link]: https://opensource.org/licenses/MIT ================================================ FILE: examples/flutter_todos/packages/local_storage_todos_api/analysis_options.yaml ================================================ include: ../../../../analysis_options.yaml ================================================ FILE: examples/flutter_todos/packages/local_storage_todos_api/lib/local_storage_todos_api.dart ================================================ /// A Flutter implementation of the TodosApi that uses local storage. library local_storage_todos_api; export 'package:shared_preferences/shared_preferences.dart' show SharedPreferences; export 'src/local_storage_todos_api.dart'; ================================================ FILE: examples/flutter_todos/packages/local_storage_todos_api/lib/src/local_storage_todos_api.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:meta/meta.dart'; import 'package:rxdart/subjects.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:todos_api/todos_api.dart'; /// {@template local_storage_todos_api} /// A Flutter implementation of the [TodosApi] that uses local storage. /// {@endtemplate} class LocalStorageTodosApi extends TodosApi { /// {@macro local_storage_todos_api} LocalStorageTodosApi({ required SharedPreferences plugin, }) : _plugin = plugin { _init(); } final SharedPreferences _plugin; late final _todoStreamController = BehaviorSubject>.seeded( const [], ); /// The key used for storing the todos locally. /// /// This is only exposed for testing and shouldn't be used by consumers of /// this library. @visibleForTesting static const kTodosCollectionKey = '__todos_collection_key__'; String? _getValue(String key) => _plugin.getString(key); Future _setValue(String key, String value) => _plugin.setString(key, value); void _init() { final todosJson = _getValue(kTodosCollectionKey); if (todosJson != null) { final todos = List>.from( json.decode(todosJson) as List, ) .map( (jsonMap) => Todo.fromJson(Map.from(jsonMap)), ) .toList(); _todoStreamController.add(todos); } else { _todoStreamController.add(const []); } } @override Stream> getTodos() => _todoStreamController.asBroadcastStream(); @override Future saveTodo(Todo todo) { final todos = [..._todoStreamController.value]; final todoIndex = todos.indexWhere((t) => t.id == todo.id); if (todoIndex >= 0) { todos[todoIndex] = todo; } else { todos.add(todo); } _todoStreamController.add(todos); return _setValue(kTodosCollectionKey, json.encode(todos)); } @override Future deleteTodo(String id) async { final todos = [..._todoStreamController.value]; final todoIndex = todos.indexWhere((t) => t.id == id); if (todoIndex == -1) { throw TodoNotFoundException(); } else { todos.removeAt(todoIndex); _todoStreamController.add(todos); return _setValue(kTodosCollectionKey, json.encode(todos)); } } @override Future clearCompleted() async { final todos = [..._todoStreamController.value]; final initialLength = todos.length; todos.removeWhere((t) => t.isCompleted); final completedTodosAmount = initialLength - todos.length; _todoStreamController.add(todos); await _setValue(kTodosCollectionKey, json.encode(todos)); return completedTodosAmount; } @override Future completeAll({required bool isCompleted}) async { final todos = [..._todoStreamController.value]; final changedTodosAmount = todos .where((t) => t.isCompleted != isCompleted) .length; final newTodos = [ for (final todo in todos) todo.copyWith(isCompleted: isCompleted), ]; _todoStreamController.add(newTodos); await _setValue(kTodosCollectionKey, json.encode(newTodos)); return changedTodosAmount; } @override Future close() { return _todoStreamController.close(); } } ================================================ FILE: examples/flutter_todos/packages/local_storage_todos_api/pubspec.yaml ================================================ name: local_storage_todos_api description: A Flutter implementation of the TodosApi that uses local storage. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: flutter: sdk: flutter meta: ^1.8.0 rxdart: ^0.28.0 shared_preferences: ^2.0.0 todos_api: path: ../todos_api dev_dependencies: flutter_test: sdk: flutter mocktail: ^1.0.0 ================================================ FILE: examples/flutter_todos/packages/local_storage_todos_api/test/local_storage_todos_api_test.dart ================================================ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:local_storage_todos_api/local_storage_todos_api.dart'; import 'package:mocktail/mocktail.dart'; import 'package:todos_api/todos_api.dart'; class MockSharedPreferences extends Mock implements SharedPreferences {} void main() { group('LocalStorageTodosApi', () { late SharedPreferences plugin; final todos = [ Todo( id: '1', title: 'title 1', description: 'description 1', ), Todo( id: '2', title: 'title 2', description: 'description 2', ), Todo( id: '3', title: 'title 3', description: 'description 3', isCompleted: true, ), ]; setUp(() { plugin = MockSharedPreferences(); when(() => plugin.getString(any())).thenReturn(json.encode(todos)); when(() => plugin.setString(any(), any())).thenAnswer((_) async => true); }); LocalStorageTodosApi createSubject() { return LocalStorageTodosApi( plugin: plugin, ); } group('constructor', () { test('works properly', () { expect( createSubject, returnsNormally, ); }); group('initializes the todos stream', () { test('with existing todos if present', () { final subject = createSubject(); expect(subject.getTodos(), emits(todos)); verify( () => plugin.getString( LocalStorageTodosApi.kTodosCollectionKey, ), ).called(1); }); test('with empty list if no todos present', () { when(() => plugin.getString(any())).thenReturn(null); final subject = createSubject(); expect(subject.getTodos(), emits(const [])); verify( () => plugin.getString( LocalStorageTodosApi.kTodosCollectionKey, ), ).called(1); }); }); }); test('getTodos returns stream of current list todos', () { expect( createSubject().getTodos(), emits(todos), ); }); group('saveTodo', () { test('saves new todos', () { final newTodo = Todo( id: '4', title: 'title 4', description: 'description 4', ); final newTodos = [...todos, newTodo]; final subject = createSubject(); expect(subject.saveTodo(newTodo), completes); expect(subject.getTodos(), emits(newTodos)); verify( () => plugin.setString( LocalStorageTodosApi.kTodosCollectionKey, json.encode(newTodos), ), ).called(1); }); test('updates existing todos', () { final updatedTodo = Todo( id: '1', title: 'new title 1', description: 'new description 1', isCompleted: true, ); final newTodos = [updatedTodo, ...todos.sublist(1)]; final subject = createSubject(); expect(subject.saveTodo(updatedTodo), completes); expect(subject.getTodos(), emits(newTodos)); verify( () => plugin.setString( LocalStorageTodosApi.kTodosCollectionKey, json.encode(newTodos), ), ).called(1); }); }); group('deleteTodo', () { test('deletes existing todos', () { final newTodos = todos.sublist(1); final subject = createSubject(); expect(subject.deleteTodo(todos[0].id), completes); expect(subject.getTodos(), emits(newTodos)); verify( () => plugin.setString( LocalStorageTodosApi.kTodosCollectionKey, json.encode(newTodos), ), ).called(1); }); test( 'throws TodoNotFoundException if todo ' 'with provided id is not found', () { final subject = createSubject(); expect( () => subject.deleteTodo('non-existing-id'), throwsA(isA()), ); }, ); }); group('clearCompleted', () { test('deletes all completed todos', () { final newTodos = todos.where((todo) => !todo.isCompleted).toList(); final deletedTodosAmount = todos.length - newTodos.length; final subject = createSubject(); expect( subject.clearCompleted(), completion(equals(deletedTodosAmount)), ); expect(subject.getTodos(), emits(newTodos)); verify( () => plugin.setString( LocalStorageTodosApi.kTodosCollectionKey, json.encode(newTodos), ), ).called(1); }); }); group('completeAll', () { test('sets isCompleted on all todos to provided value', () { final newTodos = todos .map((todo) => todo.copyWith(isCompleted: true)) .toList(); final changedTodosAmount = todos .where((todo) => !todo.isCompleted) .length; final subject = createSubject(); expect( subject.completeAll(isCompleted: true), completion(equals(changedTodosAmount)), ); expect(subject.getTodos(), emits(newTodos)); verify( () => plugin.setString( LocalStorageTodosApi.kTodosCollectionKey, json.encode(newTodos), ), ).called(1); }); }); group('close', () { test('closes the instance', () async { final subject = createSubject(); await subject.close(); expect( () => subject.saveTodo( Todo(id: '1', title: 'title 1'), ), throwsStateError, ); }); }); }); } ================================================ FILE: examples/flutter_todos/packages/todos_api/.gitignore ================================================ # See https://www.dartlang.org/guides/libraries/private-files # Files and directories created by pub .dart_tool/ .packages build/ pubspec.lock ================================================ FILE: examples/flutter_todos/packages/todos_api/README.md ================================================ # todos_api [![License: MIT][license_badge]][license_link] A Very Good Project created by Very Good CLI. [license_badge]: https://img.shields.io/badge/license-MIT-blue.svg [license_link]: https://opensource.org/licenses/MIT ================================================ FILE: examples/flutter_todos/packages/todos_api/analysis_options.yaml ================================================ include: ../../../../analysis_options.yaml ================================================ FILE: examples/flutter_todos/packages/todos_api/lib/src/models/json_map.dart ================================================ /// The type definition for a JSON-serializable [Map]. typedef JsonMap = Map; ================================================ FILE: examples/flutter_todos/packages/todos_api/lib/src/models/models.dart ================================================ export 'json_map.dart'; export 'todo.dart'; ================================================ FILE: examples/flutter_todos/packages/todos_api/lib/src/models/todo.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; import 'package:todos_api/todos_api.dart'; import 'package:uuid/uuid.dart'; part 'todo.g.dart'; /// {@template todo_item} /// A single `todo` item. /// /// Contains a [title], [description] and [id], in addition to a [isCompleted] /// flag. /// /// If an [id] is provided, it cannot be empty. If no [id] is provided, one /// will be generated. /// /// [Todo]s are immutable and can be copied using [copyWith], in addition to /// being serialized and deserialized using [toJson] and [fromJson] /// respectively. /// {@endtemplate} @immutable @JsonSerializable() class Todo extends Equatable { /// {@macro todo_item} Todo({ required this.title, String? id, this.description = '', this.isCompleted = false, }) : assert( id == null || id.isNotEmpty, 'id must either be null or not empty', ), id = id ?? const Uuid().v4(); /// The unique identifier of the `todo`. /// /// Cannot be empty. final String id; /// The title of the `todo`. /// /// Note that the title may be empty. final String title; /// The description of the `todo`. /// /// Defaults to an empty string. final String description; /// Whether the `todo` is completed. /// /// Defaults to `false`. final bool isCompleted; /// Returns a copy of this `todo` with the given values updated. /// /// {@macro todo_item} Todo copyWith({ String? id, String? title, String? description, bool? isCompleted, }) { return Todo( id: id ?? this.id, title: title ?? this.title, description: description ?? this.description, isCompleted: isCompleted ?? this.isCompleted, ); } /// Deserializes the given [JsonMap] into a [Todo]. static Todo fromJson(JsonMap json) => _$TodoFromJson(json); /// Converts this [Todo] into a [JsonMap]. JsonMap toJson() => _$TodoToJson(this); @override List get props => [id, title, description, isCompleted]; } ================================================ FILE: examples/flutter_todos/packages/todos_api/lib/src/models/todo.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'todo.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Todo _$TodoFromJson(Map json) => Todo( id: json['id'] as String?, title: json['title'] as String, description: json['description'] as String? ?? '', isCompleted: json['isCompleted'] as bool? ?? false, ); Map _$TodoToJson(Todo instance) => { 'id': instance.id, 'title': instance.title, 'description': instance.description, 'isCompleted': instance.isCompleted, }; ================================================ FILE: examples/flutter_todos/packages/todos_api/lib/src/todos_api.dart ================================================ import 'package:todos_api/todos_api.dart'; /// {@template todos_api} /// The interface for an API that provides access to a list of todos. /// {@endtemplate} abstract class TodosApi { /// {@macro todos_api} const TodosApi(); /// Provides a [Stream] of all todos. Stream> getTodos(); /// Saves a [todo]. /// /// If a [todo] with the same id already exists, it will be replaced. Future saveTodo(Todo todo); /// Deletes the `todo` with the given id. /// /// If no `todo` with the given id exists, a [TodoNotFoundException] error is /// thrown. Future deleteTodo(String id); /// Deletes all completed todos. /// /// Returns the number of deleted todos. Future clearCompleted(); /// Sets the `isCompleted` state of all todos to the given value. /// /// Returns the number of updated todos. Future completeAll({required bool isCompleted}); /// Closes the client and frees up any resources. Future close(); } /// Error thrown when a [Todo] with a given id is not found. class TodoNotFoundException implements Exception {} ================================================ FILE: examples/flutter_todos/packages/todos_api/lib/todos_api.dart ================================================ /// The interface and models for an API providing access to todos. library todos_api; export 'src/models/models.dart'; export 'src/todos_api.dart'; ================================================ FILE: examples/flutter_todos/packages/todos_api/pubspec.yaml ================================================ name: todos_api description: The interface and models for an API providing access to todos. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: equatable: ^2.0.0 json_annotation: ^4.6.0 meta: ^1.7.0 uuid: ^4.5.1 dev_dependencies: build_runner: ^2.2.0 json_serializable: ^6.3.1 test: ^1.21.4 ================================================ FILE: examples/flutter_todos/packages/todos_api/test/models/todo_test.dart ================================================ // ignore_for_file: avoid_redundant_argument_values import 'package:test/test.dart'; import 'package:todos_api/todos_api.dart'; void main() { group('Todo', () { Todo createSubject({ String? id = '1', String title = 'title', String description = 'description', bool isCompleted = true, }) { return Todo( id: id, title: title, description: description, isCompleted: isCompleted, ); } group('constructor', () { test('works correctly', () { expect( createSubject, returnsNormally, ); }); test('throws AssertionError when id is empty', () { expect( () => createSubject(id: ''), throwsA(isA()), ); }); test('sets id if not provided', () { expect( createSubject(id: null).id, isNotEmpty, ); }); }); test('supports value equality', () { expect( createSubject(), equals(createSubject()), ); }); test('props are correct', () { expect( createSubject().props, equals([ '1', // id 'title', // title 'description', // description true, // isCompleted ]), ); }); group('copyWith', () { test('returns the same object if not arguments are provided', () { expect( createSubject().copyWith(), equals(createSubject()), ); }); test('retains the old value for every parameter if null is provided', () { expect( createSubject().copyWith( id: null, title: null, description: null, isCompleted: null, ), equals(createSubject()), ); }); test('replaces every non-null parameter', () { expect( createSubject().copyWith( id: '2', title: 'new title', description: 'new description', isCompleted: false, ), equals( createSubject( id: '2', title: 'new title', description: 'new description', isCompleted: false, ), ), ); }); }); group('fromJson', () { test('works correctly', () { expect( Todo.fromJson({ 'id': '1', 'title': 'title', 'description': 'description', 'isCompleted': true, }), equals(createSubject()), ); }); }); group('toJson', () { test('works correctly', () { expect( createSubject().toJson(), equals({ 'id': '1', 'title': 'title', 'description': 'description', 'isCompleted': true, }), ); }); }); }); } ================================================ FILE: examples/flutter_todos/packages/todos_api/test/todos_api_test.dart ================================================ import 'package:test/test.dart'; import 'package:todos_api/todos_api.dart'; class TestTodosApi extends TodosApi { TestTodosApi() : super(); @override dynamic noSuchMethod(Invocation invocation) { return super.noSuchMethod(invocation); } } void main() { group('TodosApi', () { test('can be constructed', () { expect(TestTodosApi.new, returnsNormally); }); }); } ================================================ FILE: examples/flutter_todos/packages/todos_repository/.gitignore ================================================ # See https://www.dartlang.org/guides/libraries/private-files # Files and directories created by pub .dart_tool/ .packages build/ pubspec.lock ================================================ FILE: examples/flutter_todos/packages/todos_repository/README.md ================================================ # todos_repository [![License: MIT][license_badge]][license_link] A Very Good Project created by Very Good CLI. [license_badge]: https://img.shields.io/badge/license-MIT-blue.svg [license_link]: https://opensource.org/licenses/MIT ================================================ FILE: examples/flutter_todos/packages/todos_repository/analysis_options.yaml ================================================ include: ../../../../analysis_options.yaml ================================================ FILE: examples/flutter_todos/packages/todos_repository/lib/src/todos_repository.dart ================================================ import 'package:todos_api/todos_api.dart'; /// {@template todos_repository} /// A repository that handles `todo` related requests. /// {@endtemplate} class TodosRepository { /// {@macro todos_repository} const TodosRepository({ required TodosApi todosApi, }) : _todosApi = todosApi; final TodosApi _todosApi; /// Provides a [Stream] of all todos. Stream> getTodos() => _todosApi.getTodos(); /// Saves a [todo]. /// /// If a [todo] with the same id already exists, it will be replaced. Future saveTodo(Todo todo) => _todosApi.saveTodo(todo); /// Deletes the `todo` with the given id. /// /// If no `todo` with the given id exists, a [TodoNotFoundException] error is /// thrown. Future deleteTodo(String id) => _todosApi.deleteTodo(id); /// Deletes all completed todos. /// /// Returns the number of deleted todos. Future clearCompleted() => _todosApi.clearCompleted(); /// Sets the `isCompleted` state of all todos to the given value. /// /// Returns the number of updated todos. Future completeAll({required bool isCompleted}) => _todosApi.completeAll(isCompleted: isCompleted); /// Disposes any resources managed by the repository. void dispose() => _todosApi.close(); } ================================================ FILE: examples/flutter_todos/packages/todos_repository/lib/todos_repository.dart ================================================ /// A repository that handles `todo` related requests. library todos_repository; export 'package:todos_api/todos_api.dart' show Todo; export 'src/todos_repository.dart'; ================================================ FILE: examples/flutter_todos/packages/todos_repository/pubspec.yaml ================================================ name: todos_repository description: A repository that handles todo related requests. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: todos_api: path: ../todos_api dev_dependencies: mocktail: ^1.0.0 test: ^1.21.4 ================================================ FILE: examples/flutter_todos/packages/todos_repository/test/todos_repository_test.dart ================================================ import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; import 'package:todos_api/todos_api.dart'; import 'package:todos_repository/todos_repository.dart'; class MockTodosApi extends Mock implements TodosApi {} class FakeTodo extends Fake implements Todo {} void main() { group('TodosRepository', () { late TodosApi api; final todos = [ Todo( id: '1', title: 'title 1', description: 'description 1', ), Todo( id: '2', title: 'title 2', description: 'description 2', ), Todo( id: '3', title: 'title 3', description: 'description 3', isCompleted: true, ), ]; setUpAll(() { registerFallbackValue(FakeTodo()); }); setUp(() { api = MockTodosApi(); when(() => api.getTodos()).thenAnswer((_) => Stream.value(todos)); when(() => api.saveTodo(any())).thenAnswer((_) async {}); when(() => api.deleteTodo(any())).thenAnswer((_) async {}); when( () => api.clearCompleted(), ).thenAnswer((_) async => todos.where((todo) => todo.isCompleted).length); when( () => api.completeAll(isCompleted: any(named: 'isCompleted')), ).thenAnswer((_) async => 0); when(() => api.close()).thenAnswer((_) async {}); }); TodosRepository createSubject() => TodosRepository(todosApi: api); group('constructor', () { test('works properly', () { expect( createSubject, returnsNormally, ); }); }); group('getTodos', () { test('makes correct api request', () { final subject = createSubject(); expect( subject.getTodos(), isNot(throwsA(anything)), ); verify(() => api.getTodos()).called(1); }); test('returns stream of current list todos', () { expect( createSubject().getTodos(), emits(todos), ); }); }); group('saveTodo', () { test('makes correct api request', () { final newTodo = Todo( id: '4', title: 'title 4', description: 'description 4', ); final subject = createSubject(); expect(subject.saveTodo(newTodo), completes); verify(() => api.saveTodo(newTodo)).called(1); }); }); group('deleteTodo', () { test('makes correct api request', () { final subject = createSubject(); expect(subject.deleteTodo(todos[0].id), completes); verify(() => api.deleteTodo(todos[0].id)).called(1); }); }); group('clearCompleted', () { test('makes correct request', () { final subject = createSubject(); expect(subject.clearCompleted(), completes); verify(() => api.clearCompleted()).called(1); }); }); group('completeAll', () { test('makes correct request', () { final subject = createSubject(); expect(subject.completeAll(isCompleted: true), completes); verify(() => api.completeAll(isCompleted: true)).called(1); }); }); group('dispose', () { test('closes the underlying api client', () { final subject = createSubject(); verifyNever(api.close); subject.dispose(); verify(api.close).called(1); }); }); }); } ================================================ FILE: examples/flutter_todos/pubspec.yaml ================================================ name: flutter_todos description: An example todos app that showcases bloc state management patterns. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: bloc: ^9.0.0 equatable: ^2.0.0 flutter: sdk: flutter flutter_bloc: ^9.1.0 flutter_localizations: sdk: flutter intl: ^0.20.2 local_storage_todos_api: path: packages/local_storage_todos_api todos_api: path: packages/todos_api todos_repository: path: packages/todos_repository dev_dependencies: bloc_lint: ^0.3.0 bloc_test: ^10.0.0 flutter_test: sdk: flutter mockingjay: ^2.0.0 mocktail: ^1.0.0 flutter: uses-material-design: true generate: true ================================================ FILE: examples/flutter_todos/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../../packages/bloc bloc_lint: path: ../../packages/bloc_lint bloc_test: path: ../../packages/bloc_test flutter_bloc: path: ../../packages/flutter_bloc ================================================ FILE: examples/flutter_todos/test/app_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/app/app.dart'; import 'package:flutter_todos/home/home.dart'; import 'package:flutter_todos/theme/theme.dart'; import 'package:mocktail/mocktail.dart'; import 'package:todos_repository/todos_repository.dart'; import 'helpers/helpers.dart'; void main() { late TodosRepository todosRepository; setUp(() { todosRepository = MockTodosRepository(); when( () => todosRepository.getTodos(), ).thenAnswer((_) => const Stream.empty()); }); group('App', () { testWidgets('renders AppView', (tester) async { await tester.pumpWidget( App(createTodosRepository: () => todosRepository), ); expect(find.byType(AppView), findsOneWidget); }); }); group('AppView', () { testWidgets('renders MaterialApp with correct themes', (tester) async { await tester.pumpWidget( RepositoryProvider.value( value: todosRepository, child: const AppView(), ), ); expect(find.byType(MaterialApp), findsOneWidget); final materialApp = tester.widget(find.byType(MaterialApp)); expect(materialApp.theme, equals(FlutterTodosTheme.light)); expect(materialApp.darkTheme, equals(FlutterTodosTheme.dark)); }); testWidgets('renders HomePage', (tester) async { await tester.pumpWidget( RepositoryProvider.value( value: todosRepository, child: const AppView(), ), ); expect(find.byType(HomePage), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_todos/test/edit_todo/bloc/edit_todo_bloc_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/edit_todo/edit_todo.dart'; import 'package:mocktail/mocktail.dart'; import 'package:todos_repository/todos_repository.dart'; class MockTodosRepository extends Mock implements TodosRepository {} class FakeTodo extends Fake implements Todo {} void main() { group('EditTodoBloc', () { late TodosRepository todosRepository; setUpAll(() { registerFallbackValue(FakeTodo()); }); setUp(() { todosRepository = MockTodosRepository(); }); EditTodoBloc buildBloc() { return EditTodoBloc( todosRepository: todosRepository, initialTodo: null, ); } group('constructor', () { test('works properly', () { expect(buildBloc, returnsNormally); }); test('has correct initial state', () { expect( buildBloc().state, equals(const EditTodoState()), ); }); }); group('EditTodoTitleChanged', () { blocTest( 'emits new state with updated title', build: buildBloc, act: (bloc) => bloc.add(const EditTodoTitleChanged('newtitle')), expect: () => const [ EditTodoState(title: 'newtitle'), ], ); }); group('EditTodoDescriptionChanged', () { blocTest( 'emits new state with updated description', build: buildBloc, act: (bloc) => bloc.add(const EditTodoDescriptionChanged('newdescription')), expect: () => const [ EditTodoState(description: 'newdescription'), ], ); }); group('EditTodoSubmitted', () { blocTest( 'attempts to save new todo to repository ' 'if no initial todo was provided', setUp: () { when(() => todosRepository.saveTodo(any())).thenAnswer((_) async {}); }, build: buildBloc, seed: () => const EditTodoState( title: 'title', description: 'description', ), act: (bloc) => bloc.add(const EditTodoSubmitted()), expect: () => const [ EditTodoState( status: EditTodoStatus.loading, title: 'title', description: 'description', ), EditTodoState( status: EditTodoStatus.success, title: 'title', description: 'description', ), ], verify: (bloc) { verify( () => todosRepository.saveTodo( any( that: isA() .having((t) => t.title, 'title', equals('title')) .having( (t) => t.description, 'description', equals('description'), ), ), ), ).called(1); }, ); blocTest( 'attempts to save updated todo to repository ' 'if an initial todo was provided', setUp: () { when(() => todosRepository.saveTodo(any())).thenAnswer((_) async {}); }, build: buildBloc, seed: () => EditTodoState( initialTodo: Todo( id: 'initial-id', title: 'initial-title', ), title: 'title', description: 'description', ), act: (bloc) => bloc.add(const EditTodoSubmitted()), expect: () => [ EditTodoState( status: EditTodoStatus.loading, initialTodo: Todo( id: 'initial-id', title: 'initial-title', ), title: 'title', description: 'description', ), EditTodoState( status: EditTodoStatus.success, initialTodo: Todo( id: 'initial-id', title: 'initial-title', ), title: 'title', description: 'description', ), ], verify: (bloc) { verify( () => todosRepository.saveTodo( any( that: isA() .having((t) => t.id, 'id', equals('initial-id')) .having((t) => t.title, 'title', equals('title')) .having( (t) => t.description, 'description', equals('description'), ), ), ), ); }, ); blocTest( 'emits new state with error if save to repository fails', build: () { when( () => todosRepository.saveTodo(any()), ).thenThrow(Exception('oops')); return buildBloc(); }, seed: () => const EditTodoState( title: 'title', description: 'description', ), act: (bloc) => bloc.add(const EditTodoSubmitted()), expect: () => const [ EditTodoState( status: EditTodoStatus.loading, title: 'title', description: 'description', ), EditTodoState( status: EditTodoStatus.failure, title: 'title', description: 'description', ), ], ); }); }); } ================================================ FILE: examples/flutter_todos/test/edit_todo/bloc/edit_todo_event_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/edit_todo/edit_todo.dart'; void main() { group('EditTodoEvent', () { group('EditTodoTitleChanged', () { test('supports value equality', () { expect( EditTodoTitleChanged('title'), equals(EditTodoTitleChanged('title')), ); }); test('props are correct', () { expect( EditTodoTitleChanged('title').props, equals([ 'title', // title ]), ); }); }); group('EditTodoDescriptionChanged', () { test('supports value equality', () { expect( EditTodoDescriptionChanged('description'), equals(EditTodoDescriptionChanged('description')), ); }); test('props are correct', () { expect( EditTodoDescriptionChanged('description').props, equals([ 'description', // description ]), ); }); }); group('EditTodoSubmitted', () { test('supports value equality', () { expect( EditTodoSubmitted(), equals(EditTodoSubmitted()), ); }); test('props are correct', () { expect( EditTodoSubmitted().props, equals([]), ); }); }); }); } ================================================ FILE: examples/flutter_todos/test/edit_todo/bloc/edit_todo_state_test.dart ================================================ // ignore_for_file: avoid_redundant_argument_values import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/edit_todo/edit_todo.dart'; import 'package:todos_repository/todos_repository.dart'; void main() { group('EditTodoState', () { final mockInitialTodo = Todo( id: '1', title: 'title 1', description: 'description 1', ); EditTodoState createSubject({ EditTodoStatus status = EditTodoStatus.initial, Todo? initialTodo, String title = '', String description = '', }) { return EditTodoState( status: status, initialTodo: initialTodo, title: title, description: description, ); } test('supports value equality', () { expect( createSubject(), equals(createSubject()), ); }); test('props are correct', () { expect( createSubject( status: EditTodoStatus.initial, initialTodo: mockInitialTodo, title: 'title', description: 'description', ).props, equals([ EditTodoStatus.initial, // status mockInitialTodo, // initialTodo 'title', // title 'description', // description ]), ); }); test('isNewTodo returns true when a new todo is being created', () { expect( createSubject( initialTodo: null, ).isNewTodo, isTrue, ); }); group('copyWith', () { test('returns the same object if not arguments are provided', () { expect( createSubject().copyWith(), equals(createSubject()), ); }); test('retains the old value for every parameter if null is provided', () { expect( createSubject().copyWith( status: null, initialTodo: null, title: null, description: null, ), equals(createSubject()), ); }); test('replaces every non-null parameter', () { expect( createSubject().copyWith( status: EditTodoStatus.success, initialTodo: mockInitialTodo, title: 'title', description: 'description', ), equals( createSubject( status: EditTodoStatus.success, initialTodo: mockInitialTodo, title: 'title', description: 'description', ), ), ); }); }); }); } ================================================ FILE: examples/flutter_todos/test/edit_todo/view/edit_todo_page_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/edit_todo/edit_todo.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:todos_repository/todos_repository.dart'; import '../../helpers/helpers.dart'; class MockEditTodoBloc extends MockBloc implements EditTodoBloc {} void main() { final mockTodo = Todo( id: '1', title: 'title 1', description: 'description 1', ); late MockNavigator navigator; late EditTodoBloc editTodoBloc; setUp(() { navigator = MockNavigator(); when(() => navigator.canPop()).thenReturn(false); when(() => navigator.push(any())).thenAnswer((_) async {}); editTodoBloc = MockEditTodoBloc(); when(() => editTodoBloc.state).thenReturn( EditTodoState( initialTodo: mockTodo, title: mockTodo.title, description: mockTodo.description, ), ); }); group('EditTodoPage', () { Widget buildSubject() { return MockNavigatorProvider( navigator: navigator, child: BlocProvider.value( value: editTodoBloc, child: const EditTodoPage(), ), ); } group('route', () { testWidgets('renders EditTodoPage', (tester) async { await tester.pumpRoute(EditTodoPage.route()); expect(find.byType(EditTodoPage), findsOneWidget); }); testWidgets('supports providing an initial todo', (tester) async { await tester.pumpRoute( EditTodoPage.route( initialTodo: Todo( id: 'initial-id', title: 'initial', ), ), ); expect(find.byType(EditTodoPage), findsOneWidget); expect( find.byWidgetPredicate( (w) => w is EditableText && w.controller.text == 'initial', ), findsOneWidget, ); }); }); testWidgets('renders EditTodoView', (tester) async { await tester.pumpApp(buildSubject()); expect(find.byType(EditTodoView), findsOneWidget); }); testWidgets( 'pops when a todo was saved successfully', (tester) async { whenListen( editTodoBloc, Stream.fromIterable(const [ EditTodoState(), EditTodoState( status: EditTodoStatus.success, ), ]), ); await tester.pumpApp(buildSubject()); verify(() => navigator.pop(any())).called(1); }, ); }); group('EditTodoView', () { const titleTextFormField = Key('editTodoView_title_textFormField'); const descriptionTextFormField = Key( 'editTodoView_description_textFormField', ); Widget buildSubject() { return MockNavigatorProvider( navigator: navigator, child: BlocProvider.value( value: editTodoBloc, child: const EditTodoView(), ), ); } testWidgets( 'renders AppBar with title text for new todos ' 'when a new todo is being created', (tester) async { when(() => editTodoBloc.state).thenReturn(const EditTodoState()); await tester.pumpApp(buildSubject()); expect(find.byType(AppBar), findsOneWidget); expect( find.descendant( of: find.byType(AppBar), matching: find.text(l10n.editTodoAddAppBarTitle), ), findsOneWidget, ); }, ); testWidgets( 'renders AppBar with title text for editing todos ' 'when an existing todo is being edited', (tester) async { when(() => editTodoBloc.state).thenReturn( EditTodoState( initialTodo: Todo(title: 'title'), ), ); await tester.pumpApp(buildSubject()); expect(find.byType(AppBar), findsOneWidget); expect( find.descendant( of: find.byType(AppBar), matching: find.text(l10n.editTodoEditAppBarTitle), ), findsOneWidget, ); }, ); group('title text form field', () { testWidgets('is rendered', (tester) async { await tester.pumpApp(buildSubject()); expect(find.byKey(titleTextFormField), findsOneWidget); }); testWidgets('is disabled when loading', (tester) async { when(() => editTodoBloc.state).thenReturn( const EditTodoState( status: EditTodoStatus.loading, ), ); await tester.pumpApp(buildSubject()); final textField = tester.widget( find.byKey(descriptionTextFormField), ); expect(textField.enabled, false); }); testWidgets( 'adds EditTodoTitleChanged ' 'to EditTodoBloc ' 'when a new value is entered', (tester) async { await tester.pumpApp(buildSubject()); await tester.enterText( find.byKey(titleTextFormField), 'newtitle', ); verify( () => editTodoBloc.add(const EditTodoTitleChanged('newtitle')), ).called(1); }, ); }); group('description text form field', () { testWidgets('is rendered', (tester) async { await tester.pumpApp(buildSubject()); expect(find.byKey(descriptionTextFormField), findsOneWidget); }); testWidgets('is disabled when loading', (tester) async { when(() => editTodoBloc.state).thenReturn( const EditTodoState( status: EditTodoStatus.loading, ), ); await tester.pumpApp(buildSubject()); final textField = tester.widget( find.byKey(titleTextFormField), ); expect(textField.enabled, false); }); testWidgets( 'adds EditTodoDescriptionChanged ' 'to EditTodoBloc ' 'when a new value is entered', (tester) async { await tester.pumpApp(buildSubject()); await tester.enterText( find.byKey(descriptionTextFormField), 'newdescription', ); verify( () => editTodoBloc.add( const EditTodoDescriptionChanged('newdescription'), ), ).called(1); }, ); }); group('save fab', () { testWidgets('is rendered', (tester) async { await tester.pumpApp(buildSubject()); expect( find.descendant( of: find.byType(FloatingActionButton), matching: find.byTooltip(l10n.editTodoSaveButtonTooltip), ), findsOneWidget, ); }); testWidgets( 'adds EditTodoSubmitted ' 'to EditTodoBloc ' 'when tapped', (tester) async { await tester.pumpApp(buildSubject()); await tester.tap(find.byType(FloatingActionButton)); verify(() => editTodoBloc.add(const EditTodoSubmitted())).called(1); }, ); }); }); } ================================================ FILE: examples/flutter_todos/test/helpers/finders.dart ================================================ import 'package:flutter_test/flutter_test.dart'; extension ExtraFinders on CommonFinders { /// Finds a widget by a specific type [T]. /// /// ```dart /// find.bySpecificType>() /// ``` Finder bySpecificType() => find.byType(T); } ================================================ FILE: examples/flutter_todos/test/helpers/helpers.dart ================================================ export 'finders.dart'; export 'l10n.dart'; export 'pump_app.dart'; ================================================ FILE: examples/flutter_todos/test/helpers/l10n.dart ================================================ import 'package:flutter_todos/l10n/app_localizations.dart'; import 'package:flutter_todos/l10n/app_localizations_en.dart'; AppLocalizations get l10n => AppLocalizationsEn(); ================================================ FILE: examples/flutter_todos/test/helpers/pump_app.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/l10n/l10n.dart'; import 'package:mocktail/mocktail.dart'; import 'package:todos_repository/todos_repository.dart'; class MockTodosRepository extends Mock implements TodosRepository {} extension PumpApp on WidgetTester { Future pumpApp( Widget widget, { TodosRepository? todosRepository, }) { return pumpWidget( RepositoryProvider.value( value: todosRepository ?? MockTodosRepository(), child: MaterialApp( localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, ], supportedLocales: AppLocalizations.supportedLocales, home: Scaffold(body: widget), ), ), ); } Future pumpRoute( Route route, { TodosRepository? todosRepository, }) { return pumpApp( Navigator(onGenerateRoute: (_) => route), todosRepository: todosRepository, ); } } ================================================ FILE: examples/flutter_todos/test/home/cubit/home_cubit_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/home/home.dart'; void main() { group('HomeCubit', () { HomeCubit buildCubit() => HomeCubit(); group('constructor', () { test('works properly', () { expect(buildCubit, returnsNormally); }); test('has correct initial state', () { expect( buildCubit().state, equals(const HomeState()), ); }); }); group('setTab', () { blocTest( 'sets tab to given value', build: buildCubit, act: (cubit) => cubit.setTab(HomeTab.stats), expect: () => [ const HomeState(tab: HomeTab.stats), ], ); }); }); } ================================================ FILE: examples/flutter_todos/test/home/view/home_page_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/home/home.dart'; import 'package:flutter_todos/stats/stats.dart'; import 'package:flutter_todos/todos_overview/todos_overview.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:todos_repository/todos_repository.dart'; import '../../helpers/helpers.dart'; class MockHomeCubit extends MockCubit implements HomeCubit {} void main() { late TodosRepository todosRepository; group('HomePage', () { setUp(() { todosRepository = MockTodosRepository(); when(todosRepository.getTodos).thenAnswer((_) => const Stream.empty()); }); testWidgets('renders HomeView', (tester) async { await tester.pumpApp( const HomePage(), todosRepository: todosRepository, ); expect(find.byType(HomeView), findsOneWidget); }); }); group('HomeView', () { const addTodoFloatingActionButtonKey = Key( 'homeView_addTodo_floatingActionButton', ); late MockNavigator navigator; late HomeCubit cubit; setUp(() { navigator = MockNavigator(); when(() => navigator.canPop()).thenReturn(false); when(() => navigator.push(any())).thenAnswer((_) async {}); cubit = MockHomeCubit(); when(() => cubit.state).thenReturn(const HomeState()); todosRepository = MockTodosRepository(); when(todosRepository.getTodos).thenAnswer((_) => const Stream.empty()); }); Widget buildSubject() { return MockNavigatorProvider( navigator: navigator, child: BlocProvider.value( value: cubit, child: const HomeView(), ), ); } testWidgets( 'renders TodosOverviewPage ' 'when tab is set to HomeTab.todos', (tester) async { when(() => cubit.state).thenReturn(const HomeState()); await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); expect(find.byType(TodosOverviewPage), findsOneWidget); }, ); testWidgets( 'renders StatsPage ' 'when tab is set to HomeTab.stats', (tester) async { when(() => cubit.state).thenReturn(const HomeState(tab: HomeTab.stats)); await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); expect(find.byType(StatsPage), findsOneWidget); }, ); testWidgets( 'calls setTab with HomeTab.todos on HomeCubit ' 'when todos navigation button is pressed', (tester) async { await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); await tester.tap(find.byIcon(Icons.list_rounded)); verify(() => cubit.setTab(HomeTab.todos)).called(1); }, ); testWidgets( 'calls setTab with HomeTab.stats on HomeCubit ' 'when stats navigation button is pressed', (tester) async { await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); await tester.tap(find.byIcon(Icons.show_chart_rounded)); verify(() => cubit.setTab(HomeTab.stats)).called(1); }, ); group('add todo floating action button', () { testWidgets( 'is rendered', (tester) async { await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); expect( find.byKey(addTodoFloatingActionButtonKey), findsOneWidget, ); final addTodoFloatingActionButton = tester.widget( find.byKey(addTodoFloatingActionButtonKey), ); expect( addTodoFloatingActionButton, isA(), ); }, ); testWidgets('renders add icon', (tester) async { await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); expect( find.descendant( of: find.byKey(addTodoFloatingActionButtonKey), matching: find.byIcon(Icons.add), ), findsOneWidget, ); }); testWidgets( 'navigates to the EditTodoPage when pressed', (tester) async { await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); await tester.tap(find.byKey(addTodoFloatingActionButtonKey)); verify( () => navigator.push(any(that: isRoute())), ).called(1); }, ); }); }); } ================================================ FILE: examples/flutter_todos/test/stats/bloc/stats_bloc_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/stats/stats.dart'; import 'package:mocktail/mocktail.dart'; import 'package:todos_repository/todos_repository.dart'; class MockTodosRepository extends Mock implements TodosRepository {} void main() { final todo = Todo( id: '1', title: 'title 1', description: 'description 1', ); group('StatsBloc', () { late TodosRepository todosRepository; setUp(() { todosRepository = MockTodosRepository(); when(todosRepository.getTodos).thenAnswer((_) => const Stream.empty()); }); StatsBloc buildBloc() => StatsBloc(todosRepository: todosRepository); group('constructor', () { test('works properly', () { expect(buildBloc, returnsNormally); }); test('has correct initial state', () { expect(buildBloc().state, equals(const StatsState())); }); }); group('StatsSubscriptionRequested', () { blocTest( 'starts listening to repository getTodos stream', build: buildBloc, act: (bloc) => bloc.add(const StatsSubscriptionRequested()), verify: (bloc) { verify(() => todosRepository.getTodos()).called(1); }, ); blocTest( 'emits state with updated status, completed todo and active todo count ' 'when repository getTodos stream emits new todos', setUp: () { when( todosRepository.getTodos, ).thenAnswer((_) => Stream.value([todo])); }, build: buildBloc, act: (bloc) => bloc.add(const StatsSubscriptionRequested()), expect: () => [ const StatsState(status: StatsStatus.loading), const StatsState( status: StatsStatus.success, activeTodos: 1, ), ], ); blocTest( 'emits state with failure status ' 'when repository getTodos stream emits error', setUp: () { when( () => todosRepository.getTodos(), ).thenAnswer((_) => Stream.error(Exception('oops'))); }, build: buildBloc, act: (bloc) => bloc.add(const StatsSubscriptionRequested()), expect: () => [ const StatsState(status: StatsStatus.loading), const StatsState(status: StatsStatus.failure), ], ); }); }); } ================================================ FILE: examples/flutter_todos/test/stats/bloc/stats_event_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/stats/stats.dart'; void main() { group('StatsEvent', () { group('StatsSubscriptionRequested', () { test('supports value equality', () { expect( StatsSubscriptionRequested(), equals(StatsSubscriptionRequested()), ); }); test('props are correct', () { expect( StatsSubscriptionRequested().props, equals([]), ); }); }); }); } ================================================ FILE: examples/flutter_todos/test/stats/bloc/stats_state_test.dart ================================================ // ignore_for_file: avoid_redundant_argument_values import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/stats/stats.dart'; void main() { group('StatsState', () { StatsState createSubject({ StatsStatus status = StatsStatus.initial, int completedTodos = 0, int activeTodos = 0, }) { return StatsState( status: status, completedTodos: completedTodos, activeTodos: activeTodos, ); } test('supports value equality', () { expect( createSubject(), equals(createSubject()), ); }); test('props are correct', () { expect( createSubject( status: StatsStatus.initial, completedTodos: 1, activeTodos: 2, ).props, equals([ StatsStatus.initial, // status 1, // completedTodos 2, // activeTodos ]), ); }); group('copyWith', () { test('returns the same object if not arguments are provided', () { expect( createSubject().copyWith(), equals(createSubject()), ); }); test('retains the old value for every parameter if null is provided', () { expect( createSubject().copyWith( status: null, completedTodos: null, activeTodos: null, ), equals(createSubject()), ); }); test('replaces every non-null parameter', () { expect( createSubject().copyWith( status: StatsStatus.success, completedTodos: 1, activeTodos: 2, ), equals( createSubject( status: StatsStatus.success, completedTodos: 1, activeTodos: 2, ), ), ); }); }); }); } ================================================ FILE: examples/flutter_todos/test/stats/view/stats_page_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/stats/stats.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:todos_repository/todos_repository.dart'; import '../../helpers/helpers.dart'; class MockStatsBloc extends MockBloc implements StatsBloc {} void main() { group('StatsPage', () { late TodosRepository todosRepository; setUp(() { todosRepository = MockTodosRepository(); when(todosRepository.getTodos).thenAnswer((_) => const Stream.empty()); }); testWidgets('renders StatsView', (tester) async { await tester.pumpApp( const StatsPage(), todosRepository: todosRepository, ); expect(find.byType(StatsView), findsOneWidget); }); testWidgets( 'subscribes to todos from repository on initialization', (tester) async { await tester.pumpApp( const StatsPage(), todosRepository: todosRepository, ); verify(() => todosRepository.getTodos()).called(1); }, ); }); group('StatsView', () { const completedTodosListTileKey = Key('statsView_completedTodos_listTile'); const activeTodosListTileKey = Key('statsView_activeTodos_listTile'); late MockNavigator navigator; late StatsBloc statsBloc; setUp(() { navigator = MockNavigator(); when(() => navigator.push(any())).thenAnswer((_) async => null); when(() => navigator.canPop()).thenReturn(false); statsBloc = MockStatsBloc(); when(() => statsBloc.state).thenReturn( const StatsState(status: StatsStatus.success), ); }); Widget buildSubject() { return MockNavigatorProvider( navigator: navigator, child: BlocProvider.value( value: statsBloc, child: const StatsView(), ), ); } testWidgets( 'renders AppBar with title text', (tester) async { await tester.pumpApp(buildSubject()); expect(find.byType(AppBar), findsOneWidget); expect( find.descendant( of: find.byType(AppBar), matching: find.text(l10n.statsAppBarTitle), ), findsOneWidget, ); }, ); testWidgets( 'renders completed todos ListTile ' 'with correct icon, label and value', (tester) async { const completedTodos = 42; when(() => statsBloc.state).thenReturn( const StatsState( status: StatsStatus.success, completedTodos: completedTodos, ), ); await tester.pumpApp(buildSubject()); expect(find.byKey(completedTodosListTileKey), findsOneWidget); expect( find.descendant( of: find.byKey(completedTodosListTileKey), matching: find.text(l10n.statsCompletedTodoCountLabel), ), findsOneWidget, ); expect( find.descendant( of: find.byKey(completedTodosListTileKey), matching: find.byIcon(Icons.check_rounded), ), findsOneWidget, ); expect( find.descendant( of: find.byKey(completedTodosListTileKey), matching: find.text('$completedTodos'), ), findsOneWidget, ); }, ); testWidgets( 'renders active todos ListTile ' 'with correct icon, label and value', (tester) async { const activeTodos = 42; when(() => statsBloc.state).thenReturn( const StatsState( status: StatsStatus.success, activeTodos: activeTodos, ), ); await tester.pumpApp(buildSubject()); expect(find.byKey(activeTodosListTileKey), findsOneWidget); expect( find.descendant( of: find.byKey(activeTodosListTileKey), matching: find.text(l10n.statsActiveTodoCountLabel), ), findsOneWidget, ); expect( find.descendant( of: find.byKey(activeTodosListTileKey), matching: find.byIcon(Icons.radio_button_unchecked_rounded), ), findsOneWidget, ); expect( find.descendant( of: find.byKey(activeTodosListTileKey), matching: find.text('$activeTodos'), ), findsOneWidget, ); }, ); }); } ================================================ FILE: examples/flutter_todos/test/todos_overview/bloc/todos_overview_bloc_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/todos_overview/todos_overview.dart'; import 'package:mocktail/mocktail.dart'; import 'package:todos_repository/todos_repository.dart'; class MockTodosRepository extends Mock implements TodosRepository {} class FakeTodo extends Fake implements Todo {} void main() { final mockTodos = [ Todo( id: '1', title: 'title 1', description: 'description 1', ), Todo( id: '2', title: 'title 2', description: 'description 2', ), Todo( id: '3', title: 'title 3', description: 'description 3', isCompleted: true, ), ]; group('TodosOverviewBloc', () { late TodosRepository todosRepository; setUpAll(() { registerFallbackValue(FakeTodo()); }); setUp(() { todosRepository = MockTodosRepository(); when( () => todosRepository.getTodos(), ).thenAnswer((_) => Stream.value(mockTodos)); when(() => todosRepository.saveTodo(any())).thenAnswer((_) async {}); }); TodosOverviewBloc buildBloc() { return TodosOverviewBloc(todosRepository: todosRepository); } group('constructor', () { test('works properly', () => expect(buildBloc, returnsNormally)); test('has correct initial state', () { expect( buildBloc().state, equals(const TodosOverviewState()), ); }); }); group('TodosOverviewSubscriptionRequested', () { blocTest( 'starts listening to repository getTodos stream', build: buildBloc, act: (bloc) => bloc.add(const TodosOverviewSubscriptionRequested()), verify: (_) { verify(() => todosRepository.getTodos()).called(1); }, ); blocTest( 'emits state with updated status and todos ' 'when repository getTodos stream emits new todos', build: buildBloc, act: (bloc) => bloc.add(const TodosOverviewSubscriptionRequested()), expect: () => [ const TodosOverviewState( status: TodosOverviewStatus.loading, ), TodosOverviewState( status: TodosOverviewStatus.success, todos: mockTodos, ), ], ); blocTest( 'emits state with failure status ' 'when repository getTodos stream emits error', setUp: () { when( () => todosRepository.getTodos(), ).thenAnswer((_) => Stream.error(Exception('oops'))); }, build: buildBloc, act: (bloc) => bloc.add(const TodosOverviewSubscriptionRequested()), expect: () => [ const TodosOverviewState(status: TodosOverviewStatus.loading), const TodosOverviewState(status: TodosOverviewStatus.failure), ], ); }); group('TodosOverviewTodoCompletionToggled', () { blocTest( 'saves todo with isCompleted set to event isCompleted flag', build: buildBloc, seed: () => TodosOverviewState(todos: mockTodos), act: (bloc) => bloc.add( TodosOverviewTodoCompletionToggled( todo: mockTodos.first, isCompleted: true, ), ), verify: (_) { verify( () => todosRepository.saveTodo( mockTodos.first.copyWith(isCompleted: true), ), ).called(1); }, ); }); group('TodosOverviewTodoDeleted', () { blocTest( 'deletes todo using repository', setUp: () { when( () => todosRepository.deleteTodo(any()), ).thenAnswer((_) async {}); }, build: buildBloc, seed: () => TodosOverviewState(todos: mockTodos), act: (bloc) => bloc.add(TodosOverviewTodoDeleted(mockTodos.first)), verify: (_) { verify( () => todosRepository.deleteTodo(mockTodos.first.id), ).called(1); }, ); }); group('TodosOverviewUndoDeletionRequested', () { blocTest( 'restores last deleted undo and clears lastDeletedUndo field', build: buildBloc, seed: () => TodosOverviewState(lastDeletedTodo: mockTodos.first), act: (bloc) => bloc.add(const TodosOverviewUndoDeletionRequested()), expect: () => const [TodosOverviewState()], verify: (_) { verify(() => todosRepository.saveTodo(mockTodos.first)).called(1); }, ); }); group('TodosOverviewFilterChanged', () { blocTest( 'emits state with updated filter', build: buildBloc, act: (bloc) => bloc.add( const TodosOverviewFilterChanged(TodosViewFilter.completedOnly), ), expect: () => const [ TodosOverviewState(filter: TodosViewFilter.completedOnly), ], ); }); group('TodosOverviewToggleAllRequested', () { blocTest( 'toggles all todos to completed when some or none are uncompleted', setUp: () { when( () => todosRepository.completeAll( isCompleted: any(named: 'isCompleted'), ), ).thenAnswer((_) async => 0); }, build: buildBloc, seed: () => TodosOverviewState(todos: mockTodos), act: (bloc) => bloc.add(const TodosOverviewToggleAllRequested()), verify: (_) { verify( () => todosRepository.completeAll(isCompleted: true), ).called(1); }, ); blocTest( 'toggles all todos to uncompleted when all are completed', setUp: () { when( () => todosRepository.completeAll( isCompleted: any(named: 'isCompleted'), ), ).thenAnswer((_) async => 0); }, build: buildBloc, seed: () => TodosOverviewState( todos: mockTodos .map((todo) => todo.copyWith(isCompleted: true)) .toList(), ), act: (bloc) => bloc.add(const TodosOverviewToggleAllRequested()), verify: (_) { verify( () => todosRepository.completeAll(isCompleted: false), ).called(1); }, ); }); group('TodosOverviewClearCompletedRequested', () { blocTest( 'clears completed todos using repository', setUp: () { when( () => todosRepository.clearCompleted(), ).thenAnswer((_) async => 0); }, build: buildBloc, act: (bloc) => bloc.add(const TodosOverviewClearCompletedRequested()), verify: (_) { verify(() => todosRepository.clearCompleted()).called(1); }, ); }); }); } ================================================ FILE: examples/flutter_todos/test/todos_overview/bloc/todos_overview_event_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/todos_overview/todos_overview.dart'; import 'package:todos_repository/todos_repository.dart'; void main() { group('TodosOverviewEvent', () { final mockTodo = Todo( id: '1', title: 'title 1', description: 'description 1', ); group('TodosOverviewSubscriptionRequested', () { test('supports value equality', () { expect( TodosOverviewSubscriptionRequested(), equals(TodosOverviewSubscriptionRequested()), ); }); test('props are correct', () { expect( TodosOverviewSubscriptionRequested().props, equals([]), ); }); }); group('TodosOverviewTodoCompletionToggled', () { test('supports value equality', () { expect( TodosOverviewTodoCompletionToggled( todo: mockTodo, isCompleted: true, ), equals( TodosOverviewTodoCompletionToggled( todo: mockTodo, isCompleted: true, ), ), ); }); test('props are correct', () { expect( TodosOverviewTodoCompletionToggled( todo: mockTodo, isCompleted: true, ).props, equals([ mockTodo, // `todo` true, // isCompleted ]), ); }); }); group('TodosOverviewTodoDeleted', () { test('supports value equality', () { expect( TodosOverviewTodoDeleted(mockTodo), equals(TodosOverviewTodoDeleted(mockTodo)), ); }); test('props are correct', () { expect( TodosOverviewTodoDeleted(mockTodo).props, equals([ mockTodo, // `todo` ]), ); }); }); group('TodosOverviewUndoDeletionRequested', () { test('supports value equality', () { expect( TodosOverviewUndoDeletionRequested(), equals(TodosOverviewUndoDeletionRequested()), ); }); test('props are correct', () { expect( TodosOverviewUndoDeletionRequested().props, equals([]), ); }); }); group('TodosOverviewFilterChanged', () { test('supports value equality', () { expect( TodosOverviewFilterChanged(TodosViewFilter.all), equals(TodosOverviewFilterChanged(TodosViewFilter.all)), ); }); test('props are correct', () { expect( TodosOverviewFilterChanged(TodosViewFilter.all).props, equals([ TodosViewFilter.all, // filter ]), ); }); }); group('TodosOverviewToggleAllRequested', () { test('supports value equality', () { expect( TodosOverviewToggleAllRequested(), equals(TodosOverviewToggleAllRequested()), ); }); test('props are correct', () { expect( TodosOverviewToggleAllRequested().props, equals([]), ); }); }); group('TodosOverviewClearCompletedRequested', () { test('supports value equality', () { expect( TodosOverviewClearCompletedRequested(), equals(TodosOverviewClearCompletedRequested()), ); }); test('props are correct', () { expect( TodosOverviewClearCompletedRequested().props, equals([]), ); }); }); }); } ================================================ FILE: examples/flutter_todos/test/todos_overview/bloc/todos_overview_state_test.dart ================================================ // ignore_for_file: avoid_redundant_argument_values import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/todos_overview/todos_overview.dart'; import 'package:todos_repository/todos_repository.dart'; void main() { final mockTodo = Todo( id: '1', title: 'title 1', description: 'description 1', ); final mockTodos = [mockTodo]; group('TodosOverviewState', () { TodosOverviewState createSubject({ TodosOverviewStatus status = TodosOverviewStatus.initial, List? todos, TodosViewFilter filter = TodosViewFilter.all, Todo? lastDeletedTodo, }) { return TodosOverviewState( status: status, todos: todos ?? mockTodos, filter: filter, lastDeletedTodo: lastDeletedTodo, ); } test('supports value equality', () { expect( createSubject(), equals(createSubject()), ); }); test('props are correct', () { expect( createSubject( status: TodosOverviewStatus.initial, todos: mockTodos, filter: TodosViewFilter.all, lastDeletedTodo: null, ).props, equals([ TodosOverviewStatus.initial, // status mockTodos, // todos TodosViewFilter.all, // filter null, // lastDeletedTodo ]), ); }); test('filteredTodos returns todos filtered by filter', () { expect( createSubject( todos: mockTodos, filter: TodosViewFilter.completedOnly, ).filteredTodos, equals(mockTodos.where((todo) => todo.isCompleted).toList()), ); }); group('copyWith', () { test('returns the same object if not arguments are provided', () { expect( createSubject().copyWith(), equals(createSubject()), ); }); test('retains the old value for every parameter if null is provided', () { expect( createSubject().copyWith( status: null, todos: null, filter: null, lastDeletedTodo: null, ), equals(createSubject()), ); }); test('replaces every non-null parameter', () { expect( createSubject().copyWith( status: () => TodosOverviewStatus.success, todos: () => [], filter: () => TodosViewFilter.completedOnly, lastDeletedTodo: () => mockTodo, ), equals( createSubject( status: TodosOverviewStatus.success, todos: [], filter: TodosViewFilter.completedOnly, lastDeletedTodo: mockTodo, ), ), ); }); }); test('can copyWith null lastDeletedTodo', () { expect( createSubject(lastDeletedTodo: mockTodo).copyWith( lastDeletedTodo: () => null, ), equals(createSubject(lastDeletedTodo: null)), ); }); }); } ================================================ FILE: examples/flutter_todos/test/todos_overview/models/todos_view_filter_test.dart ================================================ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/todos_overview/todos_overview.dart'; import 'package:todos_repository/todos_repository.dart'; void main() { group('TodosViewFilter', () { final completedTodo = Todo( id: '0', title: 'completed', isCompleted: true, ); final incompleteTodo = Todo( id: '1', title: 'incomplete', ); group('apply', () { test('always returns true when filter is .all', () { expect( TodosViewFilter.all.apply(completedTodo), isTrue, ); expect( TodosViewFilter.all.apply(incompleteTodo), isTrue, ); }); test( 'returns true when filter is .activeOnly ' 'and the todo is incomplete', () { expect( TodosViewFilter.activeOnly.apply(completedTodo), isFalse, ); expect( TodosViewFilter.activeOnly.apply(incompleteTodo), isTrue, ); }, ); test('returns true when filter is .completedOnly ' 'and the todo is completed', () { expect( TodosViewFilter.completedOnly.apply(incompleteTodo), isFalse, ); expect( TodosViewFilter.completedOnly.apply(completedTodo), isTrue, ); }); }); group('applyAll', () { test('correctly filters provided iterable based on selected filter', () { final allTodos = [completedTodo, incompleteTodo]; expect( TodosViewFilter.all.applyAll(allTodos), equals(allTodos), ); expect( TodosViewFilter.activeOnly.applyAll(allTodos), equals([incompleteTodo]), ); expect( TodosViewFilter.completedOnly.applyAll(allTodos), equals([completedTodo]), ); }); }); }); } ================================================ FILE: examples/flutter_todos/test/todos_overview/view/todos_overview_page_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/todos_overview/todos_overview.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:todos_repository/todos_repository.dart'; import '../../helpers/helpers.dart'; class MockTodosRepository extends Mock implements TodosRepository {} class MockTodosOverviewBloc extends MockBloc implements TodosOverviewBloc {} void main() { final mockTodos = [ Todo( id: '1', title: 'title 1', description: 'description 1', ), Todo( id: '2', title: 'title 2', description: 'description 2', ), Todo( id: '3', title: 'title 3', description: 'description 3', isCompleted: true, ), ]; late TodosRepository todosRepository; group('TodosOverviewPage', () { setUp(() { todosRepository = MockTodosRepository(); when(todosRepository.getTodos).thenAnswer((_) => const Stream.empty()); }); testWidgets('renders TodosOverviewView', (tester) async { await tester.pumpApp( const TodosOverviewPage(), todosRepository: todosRepository, ); expect(find.byType(TodosOverviewView), findsOneWidget); }); testWidgets( 'subscribes to todos from repository on initialization', (tester) async { await tester.pumpApp( const TodosOverviewPage(), todosRepository: todosRepository, ); verify(() => todosRepository.getTodos()).called(1); }, ); }); group('TodosOverviewView', () { late MockNavigator navigator; late TodosOverviewBloc todosOverviewBloc; setUp(() { navigator = MockNavigator(); when(() => navigator.canPop()).thenReturn(false); when(() => navigator.push(any())).thenAnswer((_) async {}); todosOverviewBloc = MockTodosOverviewBloc(); when(() => todosOverviewBloc.state).thenReturn( TodosOverviewState( status: TodosOverviewStatus.success, todos: mockTodos, ), ); todosRepository = MockTodosRepository(); when(todosRepository.getTodos).thenAnswer((_) => const Stream.empty()); }); Widget buildSubject() { return MockNavigatorProvider( navigator: navigator, child: BlocProvider.value( value: todosOverviewBloc, child: const TodosOverviewView(), ), ); } testWidgets( 'renders AppBar with title text', (tester) async { await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); expect(find.byType(AppBar), findsOneWidget); expect( find.descendant( of: find.byType(AppBar), matching: find.text(l10n.todosOverviewAppBarTitle), ), findsOneWidget, ); }, ); testWidgets( 'renders error snackbar ' 'when status changes to failure', (tester) async { whenListen( todosOverviewBloc, Stream.fromIterable([ const TodosOverviewState(), const TodosOverviewState( status: TodosOverviewStatus.failure, ), ]), ); await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); await tester.pumpAndSettle(); expect(find.byType(SnackBar), findsOneWidget); expect( find.descendant( of: find.byType(SnackBar), matching: find.text(l10n.todosOverviewErrorSnackbarText), ), findsOneWidget, ); }, ); group('TodoDeletionConfirmationSnackBar', () { setUp(() { when(() => todosOverviewBloc.state).thenReturn( TodosOverviewState( lastDeletedTodo: mockTodos.first, ), ); whenListen( todosOverviewBloc, Stream.fromIterable([ const TodosOverviewState(), TodosOverviewState( lastDeletedTodo: mockTodos.first, ), ]), ); }); testWidgets('is rendered when lastDeletedTodo changes', (tester) async { await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); await tester.pumpAndSettle(); expect(find.byType(SnackBar), findsOneWidget); final snackBar = tester.widget(find.byType(SnackBar)); expect( snackBar.content, isA().having( (text) => text.data, 'text', contains(mockTodos.first.title), ), ); }); testWidgets( 'adds TodosOverviewUndoDeletionRequested ' 'to TodosOverviewBloc ' 'when onUndo is called', (tester) async { await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); await tester.pumpAndSettle(); final snackBarAction = tester.widget( find.byType(SnackBarAction), ); snackBarAction.onPressed(); verify( () => todosOverviewBloc.add( const TodosOverviewUndoDeletionRequested(), ), ).called(1); }, ); }); group('when todos is empty', () { setUp(() { when( () => todosOverviewBloc.state, ).thenReturn(const TodosOverviewState()); }); testWidgets( 'renders nothing ' 'when status is initial or error', (tester) async { await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); expect(find.byType(ListView), findsNothing); expect(find.byType(CupertinoActivityIndicator), findsNothing); }, ); testWidgets( 'renders loading indicator ' 'when status is loading', (tester) async { when(() => todosOverviewBloc.state).thenReturn( const TodosOverviewState(status: TodosOverviewStatus.loading), ); await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); expect(find.byType(CupertinoActivityIndicator), findsOneWidget); }, ); testWidgets( 'renders todos empty text ' 'when status is success', (tester) async { when(() => todosOverviewBloc.state).thenReturn( const TodosOverviewState( status: TodosOverviewStatus.success, ), ); await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); expect(find.text(l10n.todosOverviewEmptyText), findsOneWidget); }, ); }); group('when todos is not empty', () { setUp(() { when(() => todosOverviewBloc.state).thenReturn( TodosOverviewState( status: TodosOverviewStatus.success, todos: mockTodos, ), ); }); testWidgets('renders ListView with TodoListTiles', (tester) async { await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); expect(find.byType(ListView), findsOneWidget); expect(find.byType(TodoListTile), findsNWidgets(mockTodos.length)); }); testWidgets( 'adds TodosOverviewTodoCompletionToggled ' 'to TodosOverviewBloc ' 'when TodoListTile.onToggleCompleted is called', (tester) async { await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); final todo = mockTodos.first; final todoListTile = tester.widget( find.byType(TodoListTile).first, ); todoListTile.onToggleCompleted!(!todo.isCompleted); verify( () => todosOverviewBloc.add( TodosOverviewTodoCompletionToggled( todo: todo, isCompleted: !todo.isCompleted, ), ), ).called(1); }, ); testWidgets( 'adds TodosOverviewTodoDeleted ' 'to TodosOverviewBloc ' 'when TodoListTile.onDismissed is called', (tester) async { await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); final todo = mockTodos.first; final todoListTile = tester.widget( find.byType(TodoListTile).first, ); todoListTile.onDismissed!(DismissDirection.startToEnd); verify( () => todosOverviewBloc.add(TodosOverviewTodoDeleted(todo)), ).called(1); }, ); testWidgets( 'navigates to EditTodoPage ' 'when TodoListTile.onTap is called', (tester) async { await tester.pumpApp( buildSubject(), todosRepository: todosRepository, ); final todoListTile = tester.widget( find.byType(TodoListTile).first, ); todoListTile.onTap!(); verify( () => navigator.push(any(that: isRoute())), ).called(1); }, ); }); }); } ================================================ FILE: examples/flutter_todos/test/todos_overview/widgets/todo_list_tile_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/todos_overview/todos_overview.dart'; import 'package:todos_repository/todos_repository.dart'; import '../../helpers/helpers.dart'; void main() { group('TodoListTile', () { final uncompletedTodo = Todo( id: '1', title: 'title 1', description: 'description 1', ); final completedTodo = Todo( id: '1', title: 'title 1', description: 'description 1', isCompleted: true, ); final onToggleCompletedCalls = []; final dismissibleKey = Key( 'todoListTile_dismissible_${uncompletedTodo.id}', ); late int onDismissedCallCount; late int onTapCallCount; Widget buildSubject({Todo? todo}) { return TodoListTile( todo: todo ?? uncompletedTodo, onToggleCompleted: onToggleCompletedCalls.add, onDismissed: (_) => onDismissedCallCount++, onTap: () => onTapCallCount++, ); } setUp(() { onDismissedCallCount = 0; onTapCallCount = 0; }); group('constructor', () { test('works properly', () { expect( () => TodoListTile(todo: uncompletedTodo), returnsNormally, ); }); }); group('checkbox', () { testWidgets('is rendered', (tester) async { await tester.pumpApp(buildSubject()); expect(find.byType(Checkbox), findsOneWidget); }); testWidgets('is checked when todo is completed', (tester) async { await tester.pumpApp( buildSubject(todo: completedTodo), ); final checkbox = tester.widget(find.byType(Checkbox)); expect(checkbox.value, isTrue); }); testWidgets('is unchecked when todo is not completed', (tester) async { await tester.pumpApp( buildSubject(todo: uncompletedTodo), ); final checkbox = tester.widget(find.byType(Checkbox)); expect(checkbox.value, isFalse); }); testWidgets( 'calls onToggleCompleted with correct value when tapped', (tester) async { await tester.pumpApp( buildSubject(todo: uncompletedTodo), ); await tester.tap(find.byType(Checkbox)); expect(onToggleCompletedCalls, equals([true])); }, ); }); group('dismissible', () { testWidgets('is rendered', (tester) async { await tester.pumpApp(buildSubject()); expect(find.byType(Dismissible), findsOneWidget); expect(find.byKey(dismissibleKey), findsOneWidget); }); testWidgets('calls onDismissed when swiped to the left', (tester) async { await tester.pumpApp(buildSubject()); await tester.fling( find.byKey(dismissibleKey), const Offset(-300, 0), 3000, ); await tester.pumpAndSettle(); expect(onDismissedCallCount, equals(1)); }); }); testWidgets('calls onTap when pressed', (tester) async { await tester.pumpApp(buildSubject()); await tester.tap(find.byType(TodoListTile)); expect(onTapCallCount, equals(1)); }); group('todo title', () { testWidgets('is rendered', (tester) async { await tester.pumpApp(buildSubject()); expect(find.text(uncompletedTodo.title), findsOneWidget); }); testWidgets('is struckthrough when todo is completed', (tester) async { await tester.pumpApp( buildSubject(todo: completedTodo), ); final text = tester.widget(find.text(completedTodo.title)); expect( text.style, isA().having( (s) => s.decoration, 'decoration', TextDecoration.lineThrough, ), ); }); }); group('todo description', () { testWidgets('is rendered', (tester) async { await tester.pumpApp(buildSubject()); expect(find.text(uncompletedTodo.description), findsOneWidget); }); }); }); } ================================================ FILE: examples/flutter_todos/test/todos_overview/widgets/todos_overview_filter_button_test.dart ================================================ // ignore_for_file: avoid_redundant_argument_values import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/todos_overview/todos_overview.dart'; import 'package:mocktail/mocktail.dart'; import '../../helpers/helpers.dart'; class MockTodosOverviewBloc extends MockBloc implements TodosOverviewBloc {} extension on CommonFinders { Finder filterMenuItem({ required TodosViewFilter filter, required String title, }) { return find.descendant( of: find.byWidgetPredicate( (w) => w is PopupMenuItem && w.value == filter, ), matching: find.text(title), ); } } extension on WidgetTester { Future openPopup() async { await tap(find.byType(TodosOverviewFilterButton)); await pumpAndSettle(); } } void main() { group('TodosOverviewFilterButton', () { late TodosOverviewBloc todosOverviewBloc; setUp(() { todosOverviewBloc = MockTodosOverviewBloc(); when(() => todosOverviewBloc.state).thenReturn( const TodosOverviewState( status: TodosOverviewStatus.success, todos: [], ), ); }); Widget buildSubject() { return BlocProvider.value( value: todosOverviewBloc, child: const TodosOverviewFilterButton(), ); } group('constructor', () { test('works properly', () { expect( () => const TodosOverviewFilterButton(), returnsNormally, ); }); }); testWidgets('renders filter list icon', (tester) async { await tester.pumpApp(buildSubject()); expect( find.byIcon(Icons.filter_list_rounded), findsOneWidget, ); }); group('internal PopupMenuButton', () { testWidgets('is rendered', (tester) async { await tester.pumpApp(buildSubject()); expect( find.bySpecificType>(), findsOneWidget, ); }); testWidgets('has initial value set to active filter', (tester) async { when(() => todosOverviewBloc.state).thenReturn( const TodosOverviewState( filter: TodosViewFilter.completedOnly, ), ); await tester.pumpApp(buildSubject()); final popupMenuButton = tester.widget>( find.bySpecificType>(), ); expect( popupMenuButton.initialValue, equals(TodosViewFilter.completedOnly), ); }); testWidgets( 'renders items for each filter type when pressed', (tester) async { await tester.pumpApp(buildSubject()); await tester.openPopup(); expect( find.filterMenuItem( filter: TodosViewFilter.all, title: l10n.todosOverviewFilterAll, ), findsOneWidget, ); expect( find.filterMenuItem( filter: TodosViewFilter.activeOnly, title: l10n.todosOverviewFilterActiveOnly, ), findsOneWidget, ); expect( find.filterMenuItem( filter: TodosViewFilter.completedOnly, title: l10n.todosOverviewFilterCompletedOnly, ), findsOneWidget, ); }, ); testWidgets( 'adds TodosOverviewFilterChanged ' 'to TodosOverviewBloc ' 'when new filter is pressed', (tester) async { when(() => todosOverviewBloc.state).thenReturn( const TodosOverviewState( filter: TodosViewFilter.all, ), ); await tester.pumpApp(buildSubject()); await tester.openPopup(); await tester.tap(find.text(l10n.todosOverviewFilterCompletedOnly)); await tester.pumpAndSettle(); verify( () => todosOverviewBloc.add( const TodosOverviewFilterChanged(TodosViewFilter.completedOnly), ), ).called(1); }, ); }); }); } ================================================ FILE: examples/flutter_todos/test/todos_overview/widgets/todos_overview_options_button_test.dart ================================================ // ignore_for_file: avoid_redundant_argument_values import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_todos/todos_overview/todos_overview.dart' hide TodosViewFilter; import 'package:mocktail/mocktail.dart'; import 'package:todos_repository/todos_repository.dart'; import '../../helpers/helpers.dart'; class MockTodosOverviewBloc extends MockBloc implements TodosOverviewBloc {} extension on CommonFinders { Finder optionMenuItem({ required String title, bool enabled = true, }) { return find.descendant( of: find.byWidgetPredicate( (w) => w is PopupMenuItem && w.enabled == enabled, ), matching: find.text(title), ); } } extension on WidgetTester { Future openPopup() async { await tap(find.byType(TodosOverviewOptionsButton)); await pumpAndSettle(); } } void main() { group('TodosOverviewOptionsButton', () { late TodosOverviewBloc todosOverviewBloc; setUp(() { todosOverviewBloc = MockTodosOverviewBloc(); when(() => todosOverviewBloc.state).thenReturn( const TodosOverviewState( status: TodosOverviewStatus.success, todos: [], ), ); }); Widget buildSubject() { return BlocProvider.value( value: todosOverviewBloc, child: const TodosOverviewOptionsButton(), ); } group('constructor', () { test('works properly', () { expect( () => const TodosOverviewOptionsButton(), returnsNormally, ); }); }); group('internal PopupMenuButton', () { testWidgets('is rendered', (tester) async { await tester.pumpApp(buildSubject()); expect( find.bySpecificType>(), findsOneWidget, ); expect( find.byTooltip(l10n.todosOverviewOptionsTooltip), findsOneWidget, ); }); group('mark all todos button', () { testWidgets('is disabled when there are no todos', (tester) async { when( () => todosOverviewBloc.state, ).thenReturn(const TodosOverviewState(todos: [])); await tester.pumpApp(buildSubject()); await tester.openPopup(); expect( find.optionMenuItem( title: l10n.todosOverviewOptionsMarkAllIncomplete, enabled: false, ), findsOneWidget, ); }); testWidgets( 'renders mark all complete button ' 'when not all todos are marked completed', (tester) async { when(() => todosOverviewBloc.state).thenReturn( TodosOverviewState( todos: [ Todo(title: 'a', isCompleted: true), Todo(title: 'b', isCompleted: false), ], ), ); await tester.pumpApp(buildSubject()); await tester.openPopup(); expect( find.optionMenuItem( title: l10n.todosOverviewOptionsMarkAllComplete, ), findsOneWidget, ); }, ); testWidgets( 'renders mark all incomplete button ' 'when all todos are marked completed', (tester) async { when(() => todosOverviewBloc.state).thenReturn( TodosOverviewState( todos: [ Todo(title: 'a', isCompleted: true), Todo(title: 'b', isCompleted: true), ], ), ); await tester.pumpApp(buildSubject()); await tester.openPopup(); expect( find.optionMenuItem( title: l10n.todosOverviewOptionsMarkAllIncomplete, ), findsOneWidget, ); }, ); testWidgets( 'adds TodosOverviewToggleAllRequested ' 'to TodosOverviewBloc ' 'when tapped', (tester) async { when(() => todosOverviewBloc.state).thenReturn( TodosOverviewState( todos: [ Todo(title: 'a', isCompleted: true), Todo(title: 'b', isCompleted: false), ], ), ); await tester.pumpApp(buildSubject()); await tester.openPopup(); await tester.tap( find.optionMenuItem( title: l10n.todosOverviewOptionsMarkAllComplete, ), ); verify( () => todosOverviewBloc.add( const TodosOverviewToggleAllRequested(), ), ).called(1); }, ); }); group('clear completed button', () { testWidgets( 'is disabled when there are no completed todos', (tester) async { when( () => todosOverviewBloc.state, ).thenReturn(const TodosOverviewState(todos: [])); await tester.pumpApp(buildSubject()); await tester.openPopup(); expect( find.optionMenuItem( title: l10n.todosOverviewOptionsClearCompleted, enabled: false, ), findsOneWidget, ); }, ); testWidgets( 'renders clear completed button ' 'when there are completed todos', (tester) async { when(() => todosOverviewBloc.state).thenReturn( TodosOverviewState( todos: [ Todo(title: 'a', isCompleted: true), Todo(title: 'b', isCompleted: false), ], ), ); await tester.pumpApp(buildSubject()); await tester.openPopup(); expect( find.optionMenuItem( title: l10n.todosOverviewOptionsClearCompleted, enabled: true, ), findsOneWidget, ); }, ); testWidgets( 'adds TodosOverviewClearCompletedRequested ' 'to TodosOverviewBloc ' 'when tapped', (tester) async { when(() => todosOverviewBloc.state).thenReturn( TodosOverviewState( todos: [ Todo(title: 'a', isCompleted: true), Todo(title: 'b', isCompleted: false), ], ), ); await tester.pumpApp(buildSubject()); await tester.openPopup(); await tester.tap( find.optionMenuItem( title: l10n.todosOverviewOptionsClearCompleted, ), ); verify( () => todosOverviewBloc.add( const TodosOverviewClearCompletedRequested(), ), ).called(1); }, ); }); }); }); } ================================================ FILE: examples/flutter_todos/web/index.html ================================================ flutter_todos ================================================ FILE: examples/flutter_todos/web/manifest.json ================================================ { "name": "flutter_todos", "short_name": "flutter_todos", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: examples/flutter_weather/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: examples/flutter_weather/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: web create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/flutter_weather/README.md ================================================ [![build](https://github.com/felangel/bloc/actions/workflows/main.yaml/badge.svg)](https://github.com/felangel/bloc/actions) # flutter_weather A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: examples/flutter_weather/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml analyzer: exclude: - lib/**/*.g.dart linter: rules: public_member_api_docs: false ================================================ FILE: examples/flutter_weather/build.yaml ================================================ targets: $default: builders: json_serializable: options: field_rename: snake checked: true explicit_to_json: true ================================================ FILE: examples/flutter_weather/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: examples/flutter_weather/ios/Podfile ================================================ # Uncomment this line to define a global platform for your project # platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end ================================================ FILE: examples/flutter_weather/lib/app.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_weather/weather/weather.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:weather_repository/weather_repository.dart' show WeatherRepository; class WeatherApp extends StatelessWidget { const WeatherApp({super.key}); @override Widget build(BuildContext context) { return RepositoryProvider( create: (_) => WeatherRepository(), dispose: (repository) => repository.dispose(), child: BlocProvider( create: (context) => WeatherCubit(context.read()), child: const WeatherAppView(), ), ); } } class WeatherAppView extends StatelessWidget { const WeatherAppView({super.key}); @override Widget build(BuildContext context) { final seedColor = context.select( (WeatherCubit cubit) => cubit.state.weather.toColor, ); return MaterialApp( theme: ThemeData( appBarTheme: const AppBarTheme( backgroundColor: Colors.transparent, elevation: 0, ), colorScheme: ColorScheme.fromSeed(seedColor: seedColor), textTheme: GoogleFonts.rajdhaniTextTheme(), ), home: const WeatherPage(), ); } } extension on Weather { Color get toColor { switch (condition) { case WeatherCondition.clear: return Colors.yellow; case WeatherCondition.snowy: return Colors.lightBlueAccent; case WeatherCondition.cloudy: return Colors.blueGrey; case WeatherCondition.rainy: return Colors.indigoAccent; case WeatherCondition.unknown: return Colors.cyan; } } } ================================================ FILE: examples/flutter_weather/lib/main.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_weather/app.dart'; import 'package:flutter_weather/weather_bloc_observer.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:path_provider/path_provider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); Bloc.observer = const WeatherBlocObserver(); HydratedBloc.storage = await HydratedStorage.build( storageDirectory: kIsWeb ? HydratedStorageDirectory.web : HydratedStorageDirectory((await getTemporaryDirectory()).path), ); runApp(const WeatherApp()); } ================================================ FILE: examples/flutter_weather/lib/search/search.dart ================================================ export 'view/search_page.dart'; ================================================ FILE: examples/flutter_weather/lib/search/view/search_page.dart ================================================ import 'package:flutter/material.dart'; class SearchPage extends StatefulWidget { const SearchPage._(); static Route route() { return MaterialPageRoute(builder: (_) => const SearchPage._()); } @override State createState() => _SearchPageState(); } class _SearchPageState extends State { final TextEditingController _textController = TextEditingController(); String get _text => _textController.text; @override void dispose() { _textController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('City Search')), body: Row( children: [ Expanded( child: Padding( padding: const EdgeInsets.all(8), child: TextField( controller: _textController, decoration: const InputDecoration( labelText: 'City', hintText: 'Chicago', ), ), ), ), IconButton( key: const Key('searchPage_search_iconButton'), icon: const Icon(Icons.search, semanticLabel: 'Submit'), onPressed: () => Navigator.of(context).pop(_text), ), ], ), ); } } ================================================ FILE: examples/flutter_weather/lib/settings/settings.dart ================================================ export 'view/settings_page.dart'; ================================================ FILE: examples/flutter_weather/lib/settings/view/settings_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_weather/weather/weather.dart'; class SettingsPage extends StatelessWidget { const SettingsPage._(); static Route route() { return MaterialPageRoute( builder: (_) => const SettingsPage._(), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Settings')), body: ListView( children: [ BlocBuilder( buildWhen: (previous, current) => previous.temperatureUnits != current.temperatureUnits, builder: (context, state) { return ListTile( title: const Text('Temperature Units'), isThreeLine: true, subtitle: const Text( 'Use metric measurements for temperature units.', ), trailing: Switch( value: state.temperatureUnits.isCelsius, onChanged: (_) => context.read().toggleUnits(), ), ); }, ), ], ), ); } } ================================================ FILE: examples/flutter_weather/lib/weather/cubit/weather_cubit.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:flutter_weather/weather/weather.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:weather_repository/weather_repository.dart' show WeatherRepository; part 'weather_cubit.g.dart'; part 'weather_state.dart'; class WeatherCubit extends HydratedCubit { WeatherCubit(this._weatherRepository) : super(WeatherState()); final WeatherRepository _weatherRepository; Future fetchWeather(String? city) async { if (city == null || city.isEmpty) return; emit(state.copyWith(status: WeatherStatus.loading)); try { final weather = Weather.fromRepository( await _weatherRepository.getWeather(city), ); final units = state.temperatureUnits; final value = units.isFahrenheit ? weather.temperature.value.toFahrenheit() : weather.temperature.value; emit( state.copyWith( status: WeatherStatus.success, temperatureUnits: units, weather: weather.copyWith(temperature: Temperature(value: value)), ), ); } on Exception { emit(state.copyWith(status: WeatherStatus.failure)); } } Future refreshWeather() async { if (!state.status.isSuccess) return; if (state.weather == Weather.empty) return; try { final weather = Weather.fromRepository( await _weatherRepository.getWeather(state.weather.location), ); final units = state.temperatureUnits; final value = units.isFahrenheit ? weather.temperature.value.toFahrenheit() : weather.temperature.value; emit( state.copyWith( status: WeatherStatus.success, temperatureUnits: units, weather: weather.copyWith(temperature: Temperature(value: value)), ), ); } on Exception { emit(state); } } void toggleUnits() { final units = state.temperatureUnits.isFahrenheit ? TemperatureUnits.celsius : TemperatureUnits.fahrenheit; if (!state.status.isSuccess) { emit(state.copyWith(temperatureUnits: units)); return; } final weather = state.weather; if (weather != Weather.empty) { final temperature = weather.temperature; final value = units.isCelsius ? temperature.value.toCelsius() : temperature.value.toFahrenheit(); emit( state.copyWith( temperatureUnits: units, weather: weather.copyWith(temperature: Temperature(value: value)), ), ); } } @override WeatherState fromJson(Map json) => WeatherState.fromJson(json); @override Map toJson(WeatherState state) => state.toJson(); } extension TemperatureConversion on double { double toFahrenheit() => (this * 9 / 5) + 32; double toCelsius() => (this - 32) * 5 / 9; } ================================================ FILE: examples/flutter_weather/lib/weather/cubit/weather_cubit.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'weather_cubit.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** WeatherState _$WeatherStateFromJson(Map json) => $checkedCreate( 'WeatherState', json, ($checkedConvert) { final val = WeatherState( status: $checkedConvert( 'status', (v) => $enumDecodeNullable(_$WeatherStatusEnumMap, v) ?? WeatherStatus.initial, ), temperatureUnits: $checkedConvert( 'temperature_units', (v) => $enumDecodeNullable(_$TemperatureUnitsEnumMap, v) ?? TemperatureUnits.celsius, ), weather: $checkedConvert( 'weather', (v) => v == null ? null : Weather.fromJson(v as Map), ), ); return val; }, fieldKeyMap: const {'temperatureUnits': 'temperature_units'}, ); Map _$WeatherStateToJson( WeatherState instance, ) => { 'status': _$WeatherStatusEnumMap[instance.status]!, 'weather': instance.weather.toJson(), 'temperature_units': _$TemperatureUnitsEnumMap[instance.temperatureUnits]!, }; const _$WeatherStatusEnumMap = { WeatherStatus.initial: 'initial', WeatherStatus.loading: 'loading', WeatherStatus.success: 'success', WeatherStatus.failure: 'failure', }; const _$TemperatureUnitsEnumMap = { TemperatureUnits.fahrenheit: 'fahrenheit', TemperatureUnits.celsius: 'celsius', }; ================================================ FILE: examples/flutter_weather/lib/weather/cubit/weather_state.dart ================================================ part of 'weather_cubit.dart'; enum WeatherStatus { initial, loading, success, failure } extension WeatherStatusX on WeatherStatus { bool get isInitial => this == WeatherStatus.initial; bool get isLoading => this == WeatherStatus.loading; bool get isSuccess => this == WeatherStatus.success; bool get isFailure => this == WeatherStatus.failure; } @JsonSerializable() final class WeatherState extends Equatable { WeatherState({ this.status = WeatherStatus.initial, this.temperatureUnits = TemperatureUnits.celsius, Weather? weather, }) : weather = weather ?? Weather.empty; factory WeatherState.fromJson(Map json) => _$WeatherStateFromJson(json); final WeatherStatus status; final Weather weather; final TemperatureUnits temperatureUnits; WeatherState copyWith({ WeatherStatus? status, TemperatureUnits? temperatureUnits, Weather? weather, }) { return WeatherState( status: status ?? this.status, temperatureUnits: temperatureUnits ?? this.temperatureUnits, weather: weather ?? this.weather, ); } Map toJson() => _$WeatherStateToJson(this); @override List get props => [status, temperatureUnits, weather]; } ================================================ FILE: examples/flutter_weather/lib/weather/models/models.dart ================================================ export 'weather.dart'; ================================================ FILE: examples/flutter_weather/lib/weather/models/weather.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:weather_repository/weather_repository.dart' hide Weather; import 'package:weather_repository/weather_repository.dart' as weather_repository; part 'weather.g.dart'; enum TemperatureUnits { fahrenheit, celsius } extension TemperatureUnitsX on TemperatureUnits { bool get isFahrenheit => this == TemperatureUnits.fahrenheit; bool get isCelsius => this == TemperatureUnits.celsius; } @JsonSerializable() class Temperature extends Equatable { const Temperature({required this.value}); factory Temperature.fromJson(Map json) => _$TemperatureFromJson(json); final double value; Map toJson() => _$TemperatureToJson(this); @override List get props => [value]; } @JsonSerializable() class Weather extends Equatable { const Weather({ required this.condition, required this.lastUpdated, required this.location, required this.temperature, }); factory Weather.fromJson(Map json) => _$WeatherFromJson(json); factory Weather.fromRepository(weather_repository.Weather weather) { return Weather( condition: weather.condition, lastUpdated: DateTime.now(), location: weather.location, temperature: Temperature(value: weather.temperature), ); } static final empty = Weather( condition: WeatherCondition.unknown, lastUpdated: DateTime(0), temperature: const Temperature(value: 0), location: '--', ); final WeatherCondition condition; final DateTime lastUpdated; final String location; final Temperature temperature; @override List get props => [condition, lastUpdated, location, temperature]; Map toJson() => _$WeatherToJson(this); Weather copyWith({ WeatherCondition? condition, DateTime? lastUpdated, String? location, Temperature? temperature, }) { return Weather( condition: condition ?? this.condition, lastUpdated: lastUpdated ?? this.lastUpdated, location: location ?? this.location, temperature: temperature ?? this.temperature, ); } } ================================================ FILE: examples/flutter_weather/lib/weather/models/weather.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'weather.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Temperature _$TemperatureFromJson(Map json) => $checkedCreate( 'Temperature', json, ($checkedConvert) { final val = Temperature( value: $checkedConvert('value', (v) => (v as num).toDouble()), ); return val; }, ); Map _$TemperatureToJson(Temperature instance) => { 'value': instance.value, }; Weather _$WeatherFromJson(Map json) => $checkedCreate( 'Weather', json, ($checkedConvert) { final val = Weather( condition: $checkedConvert( 'condition', (v) => $enumDecode(_$WeatherConditionEnumMap, v), ), lastUpdated: $checkedConvert( 'last_updated', (v) => DateTime.parse(v as String), ), location: $checkedConvert('location', (v) => v as String), temperature: $checkedConvert( 'temperature', (v) => Temperature.fromJson(v as Map), ), ); return val; }, fieldKeyMap: const {'lastUpdated': 'last_updated'}, ); Map _$WeatherToJson(Weather instance) => { 'condition': _$WeatherConditionEnumMap[instance.condition]!, 'last_updated': instance.lastUpdated.toIso8601String(), 'location': instance.location, 'temperature': instance.temperature.toJson(), }; const _$WeatherConditionEnumMap = { WeatherCondition.clear: 'clear', WeatherCondition.rainy: 'rainy', WeatherCondition.cloudy: 'cloudy', WeatherCondition.snowy: 'snowy', WeatherCondition.unknown: 'unknown', }; ================================================ FILE: examples/flutter_weather/lib/weather/view/weather_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_weather/search/search.dart'; import 'package:flutter_weather/settings/settings.dart'; import 'package:flutter_weather/weather/weather.dart'; class WeatherPage extends StatelessWidget { const WeatherPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( extendBodyBehindAppBar: true, appBar: AppBar( actions: [ IconButton( icon: const Icon(Icons.settings), onPressed: () => Navigator.of(context).push( SettingsPage.route(), ), ), ], ), body: Center( child: BlocBuilder( builder: (context, state) { return switch (state.status) { WeatherStatus.initial => const WeatherEmpty(), WeatherStatus.loading => const WeatherLoading(), WeatherStatus.failure => const WeatherError(), WeatherStatus.success => WeatherPopulated( weather: state.weather, units: state.temperatureUnits, onRefresh: () { return context.read().refreshWeather(); }, ), }; }, ), ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.search, semanticLabel: 'Search'), onPressed: () async { final city = await Navigator.of(context).push(SearchPage.route()); if (!context.mounted) return; await context.read().fetchWeather(city); }, ), ); } } ================================================ FILE: examples/flutter_weather/lib/weather/weather.dart ================================================ export 'package:weather_repository/weather_repository.dart' show WeatherCondition; export 'cubit/weather_cubit.dart'; export 'models/models.dart'; export 'view/weather_page.dart'; export 'widgets/widgets.dart'; ================================================ FILE: examples/flutter_weather/lib/weather/widgets/weather_empty.dart ================================================ import 'package:flutter/material.dart'; class WeatherEmpty extends StatelessWidget { const WeatherEmpty({super.key}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( mainAxisSize: MainAxisSize.min, children: [ const Text('🏙️', style: TextStyle(fontSize: 64)), Text( 'Please Select a City!', style: theme.textTheme.headlineSmall, ), ], ); } } ================================================ FILE: examples/flutter_weather/lib/weather/widgets/weather_error.dart ================================================ import 'package:flutter/material.dart'; class WeatherError extends StatelessWidget { const WeatherError({super.key}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( mainAxisSize: MainAxisSize.min, children: [ const Text('🙈', style: TextStyle(fontSize: 64)), Text( 'Something went wrong!', style: theme.textTheme.headlineSmall, ), ], ); } } ================================================ FILE: examples/flutter_weather/lib/weather/widgets/weather_loading.dart ================================================ import 'package:flutter/material.dart'; class WeatherLoading extends StatelessWidget { const WeatherLoading({super.key}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( mainAxisSize: MainAxisSize.min, children: [ const Text('⛅', style: TextStyle(fontSize: 64)), Text( 'Loading Weather', style: theme.textTheme.headlineSmall, ), const Padding( padding: EdgeInsets.all(16), child: CircularProgressIndicator(), ), ], ); } } ================================================ FILE: examples/flutter_weather/lib/weather/widgets/weather_populated.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_weather/weather/weather.dart'; class WeatherPopulated extends StatelessWidget { const WeatherPopulated({ required this.weather, required this.units, required this.onRefresh, super.key, }); final Weather weather; final TemperatureUnits units; final ValueGetter> onRefresh; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Stack( children: [ _WeatherBackground(), RefreshIndicator( onRefresh: onRefresh, child: Align( alignment: const Alignment(0, -1 / 3), child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), clipBehavior: Clip.none, child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 48), _WeatherIcon(condition: weather.condition), Text( weather.location, style: theme.textTheme.displayMedium?.copyWith( fontWeight: FontWeight.w200, ), ), Text( weather.formattedTemperature(units), style: theme.textTheme.displaySmall?.copyWith( fontWeight: FontWeight.bold, ), ), Text( '''Last Updated at ${TimeOfDay.fromDateTime(weather.lastUpdated).format(context)}''', ), ], ), ), ), ), ], ); } } class _WeatherIcon extends StatelessWidget { const _WeatherIcon({required this.condition}); static const _iconSize = 75.0; final WeatherCondition condition; @override Widget build(BuildContext context) { return Text( condition.toEmoji, style: const TextStyle(fontSize: _iconSize), ); } } extension on WeatherCondition { String get toEmoji { switch (this) { case WeatherCondition.clear: return '☀️'; case WeatherCondition.rainy: return '🌧️'; case WeatherCondition.cloudy: return '☁️'; case WeatherCondition.snowy: return '🌨️'; case WeatherCondition.unknown: return '❓'; } } } class _WeatherBackground extends StatelessWidget { @override Widget build(BuildContext context) { final color = Theme.of(context).colorScheme.primaryContainer; return SizedBox.expand( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: const [0.25, 0.75, 0.90, 1.0], colors: [ color, color.brighten(), color.brighten(33), color.brighten(50), ], ), ), ), ); } } extension on Color { Color brighten([int percent = 10]) { assert( 1 <= percent && percent <= 100, 'percentage must be between 1 and 100', ); final p = percent / 100; final alpha = a.round(); final red = r.round(); final green = g.round(); final blue = b.round(); return Color.fromARGB( alpha, red + ((255 - red) * p).round(), green + ((255 - green) * p).round(), blue + ((255 - blue) * p).round(), ); } } extension on Weather { String formattedTemperature(TemperatureUnits units) { return '''${temperature.value.toStringAsPrecision(2)}°${units.isCelsius ? 'C' : 'F'}'''; } } ================================================ FILE: examples/flutter_weather/lib/weather/widgets/widgets.dart ================================================ export 'weather_empty.dart'; export 'weather_error.dart'; export 'weather_loading.dart'; export 'weather_populated.dart'; ================================================ FILE: examples/flutter_weather/lib/weather_bloc_observer.dart ================================================ import 'dart:developer'; import 'package:bloc/bloc.dart'; class WeatherBlocObserver extends BlocObserver { const WeatherBlocObserver(); @override void onEvent(Bloc bloc, Object? event) { super.onEvent(bloc, event); log('onEvent $event'); } @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); log('onChange $change'); } @override void onTransition( Bloc bloc, Transition transition, ) { super.onTransition(bloc, transition); log('onTransition $transition'); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { super.onError(bloc, error, stackTrace); log('onError $error'); } } ================================================ FILE: examples/flutter_weather/linux/.gitignore ================================================ flutter/ephemeral ================================================ FILE: examples/flutter_weather/linux/flutter/generated_plugin_registrant.cc ================================================ // // Generated file. Do not edit. // // clang-format off #include "generated_plugin_registrant.h" void fl_register_plugins(FlPluginRegistry* registry) { } ================================================ FILE: examples/flutter_weather/linux/flutter/generated_plugin_registrant.h ================================================ // // Generated file. Do not edit. // // clang-format off #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ #include // Registers Flutter plugins. void fl_register_plugins(FlPluginRegistry* registry); #endif // GENERATED_PLUGIN_REGISTRANT_ ================================================ FILE: examples/flutter_weather/linux/flutter/generated_plugins.cmake ================================================ # # Generated file, do not edit. # list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST ) set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) endforeach(ffi_plugin) ================================================ FILE: examples/flutter_weather/macos/.gitignore ================================================ # Flutter-related **/Flutter/ephemeral/ **/Pods/ # Xcode-related **/dgph **/xcuserdata/ ================================================ FILE: examples/flutter_weather/macos/Flutter/GeneratedPluginRegistrant.swift ================================================ // // Generated file. Do not edit. // import FlutterMacOS import Foundation import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } ================================================ FILE: examples/flutter_weather/macos/Podfile ================================================ platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_macos_podfile_setup target 'Runner' do use_frameworks! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) end end ================================================ FILE: examples/flutter_weather/packages/open_meteo_api/analysis_options.yaml ================================================ include: ../../../../analysis_options.yaml analyzer: exclude: - lib/**/*.g.dart linter: rules: public_member_api_docs: false ================================================ FILE: examples/flutter_weather/packages/open_meteo_api/build.yaml ================================================ targets: $default: builders: source_gen|combining_builder: options: ignore_for_file: - implicit_dynamic_parameter json_serializable: options: field_rename: snake create_to_json: false checked: true ================================================ FILE: examples/flutter_weather/packages/open_meteo_api/lib/open_meteo_api.dart ================================================ export 'src/models/models.dart'; export 'src/open_meteo_api_client.dart'; ================================================ FILE: examples/flutter_weather/packages/open_meteo_api/lib/src/models/location.dart ================================================ import 'package:json_annotation/json_annotation.dart'; part 'location.g.dart'; @JsonSerializable() class Location { const Location({ required this.id, required this.name, required this.latitude, required this.longitude, }); factory Location.fromJson(Map json) => _$LocationFromJson(json); final int id; final String name; final double latitude; final double longitude; } ================================================ FILE: examples/flutter_weather/packages/open_meteo_api/lib/src/models/location.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: implicit_dynamic_parameter part of 'location.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Location _$LocationFromJson(Map json) => $checkedCreate( 'Location', json, ($checkedConvert) { final val = Location( id: $checkedConvert('id', (v) => v as int), name: $checkedConvert('name', (v) => v as String), latitude: $checkedConvert('latitude', (v) => (v as num).toDouble()), longitude: $checkedConvert('longitude', (v) => (v as num).toDouble()), ); return val; }, ); ================================================ FILE: examples/flutter_weather/packages/open_meteo_api/lib/src/models/models.dart ================================================ export 'location.dart'; export 'weather.dart'; ================================================ FILE: examples/flutter_weather/packages/open_meteo_api/lib/src/models/weather.dart ================================================ import 'package:json_annotation/json_annotation.dart'; part 'weather.g.dart'; @JsonSerializable() class Weather { const Weather({required this.temperature, required this.weatherCode}); factory Weather.fromJson(Map json) => _$WeatherFromJson(json); final double temperature; @JsonKey(name: 'weathercode') final double weatherCode; } ================================================ FILE: examples/flutter_weather/packages/open_meteo_api/lib/src/models/weather.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: implicit_dynamic_parameter part of 'weather.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Weather _$WeatherFromJson(Map json) => $checkedCreate( 'Weather', json, ($checkedConvert) { final val = Weather( temperature: $checkedConvert('temperature', (v) => (v as num).toDouble()), weatherCode: $checkedConvert('weathercode', (v) => (v as num).toDouble()), ); return val; }, fieldKeyMap: const {'weatherCode': 'weathercode'}, ); ================================================ FILE: examples/flutter_weather/packages/open_meteo_api/lib/src/open_meteo_api_client.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:open_meteo_api/open_meteo_api.dart'; /// Exception thrown when locationSearch fails. class LocationRequestFailure implements Exception {} /// Exception thrown when the provided location is not found. class LocationNotFoundFailure implements Exception {} /// Exception thrown when getWeather fails. class WeatherRequestFailure implements Exception {} /// Exception thrown when weather for provided location is not found. class WeatherNotFoundFailure implements Exception {} /// {@template open_meteo_api_client} /// Dart API Client which wraps the [Open Meteo API](https://open-meteo.com). /// {@endtemplate} class OpenMeteoApiClient { /// {@macro open_meteo_api_client} OpenMeteoApiClient({http.Client? httpClient}) : _httpClient = httpClient ?? http.Client(); static const _baseUrlWeather = 'api.open-meteo.com'; static const _baseUrlGeocoding = 'geocoding-api.open-meteo.com'; final http.Client _httpClient; /// Finds a [Location] `/v1/search/?name=(query)`. Future locationSearch(String query) async { final locationRequest = Uri.https( _baseUrlGeocoding, '/v1/search', {'name': query, 'count': '1'}, ); final locationResponse = await _httpClient.get(locationRequest); if (locationResponse.statusCode != 200) { throw LocationRequestFailure(); } final locationJson = jsonDecode(locationResponse.body) as Map; if (!locationJson.containsKey('results')) throw LocationNotFoundFailure(); final results = locationJson['results'] as List; if (results.isEmpty) throw LocationNotFoundFailure(); return Location.fromJson(results.first as Map); } /// Fetches [Weather] for a given [latitude] and [longitude]. Future getWeather({ required double latitude, required double longitude, }) async { final weatherRequest = Uri.https(_baseUrlWeather, 'v1/forecast', { 'latitude': '$latitude', 'longitude': '$longitude', 'current_weather': 'true', }); final weatherResponse = await _httpClient.get(weatherRequest); if (weatherResponse.statusCode != 200) { throw WeatherRequestFailure(); } final bodyJson = jsonDecode(weatherResponse.body) as Map; if (!bodyJson.containsKey('current_weather')) { throw WeatherNotFoundFailure(); } final weatherJson = bodyJson['current_weather'] as Map; return Weather.fromJson(weatherJson); } /// Closes the underlying http client. void close() { _httpClient.close(); } } ================================================ FILE: examples/flutter_weather/packages/open_meteo_api/pubspec.yaml ================================================ name: open_meteo_api description: A Dart API Client for the Open-Meteo API. version: 1.0.0+1 environment: sdk: ">=3.10.0 <4.0.0" dependencies: http: ^1.0.0 json_annotation: ^4.6.0 dev_dependencies: build_runner: ^2.0.0 json_serializable: ^6.3.1 mocktail: ^1.0.0 test: ^1.16.4 ================================================ FILE: examples/flutter_weather/packages/open_meteo_api/test/location_test.dart ================================================ import 'package:open_meteo_api/open_meteo_api.dart'; import 'package:test/test.dart'; void main() { group('Location', () { group('fromJson', () { test('returns correct Location object', () { expect( Location.fromJson( { 'id': 4887398, 'name': 'Chicago', 'latitude': 41.85003, 'longitude': -87.65005, }, ), isA() .having((w) => w.id, 'id', 4887398) .having((w) => w.name, 'name', 'Chicago') .having((w) => w.latitude, 'latitude', 41.85003) .having((w) => w.longitude, 'longitude', -87.65005), ); }); }); }); } ================================================ FILE: examples/flutter_weather/packages/open_meteo_api/test/open_meteo_api_client_test.dart ================================================ import 'package:http/http.dart' as http; import 'package:mocktail/mocktail.dart'; import 'package:open_meteo_api/open_meteo_api.dart'; import 'package:test/test.dart'; class MockHttpClient extends Mock implements http.Client {} class MockResponse extends Mock implements http.Response {} class FakeUri extends Fake implements Uri {} void main() { group('OpenMeteoApiClient', () { late http.Client httpClient; late OpenMeteoApiClient apiClient; setUpAll(() { registerFallbackValue(FakeUri()); }); setUp(() { httpClient = MockHttpClient(); apiClient = OpenMeteoApiClient(httpClient: httpClient); }); group('constructor', () { test('does not require an httpClient', () { expect(OpenMeteoApiClient(), isNotNull); }); }); group('locationSearch', () { const query = 'mock-query'; test('makes correct http request', () async { final response = MockResponse(); when(() => response.statusCode).thenReturn(200); when(() => response.body).thenReturn('{}'); when(() => httpClient.get(any())).thenAnswer((_) async => response); try { await apiClient.locationSearch(query); } catch (_) {} verify( () => httpClient.get( Uri.https( 'geocoding-api.open-meteo.com', '/v1/search', {'name': query, 'count': '1'}, ), ), ).called(1); }); test('throws LocationRequestFailure on non-200 response', () async { final response = MockResponse(); when(() => response.statusCode).thenReturn(400); when(() => httpClient.get(any())).thenAnswer((_) async => response); expect( () async => apiClient.locationSearch(query), throwsA(isA()), ); }); test('throws LocationNotFoundFailure on error response', () async { final response = MockResponse(); when(() => response.statusCode).thenReturn(200); when(() => response.body).thenReturn('{}'); when(() => httpClient.get(any())).thenAnswer((_) async => response); await expectLater( apiClient.locationSearch(query), throwsA(isA()), ); }); test('throws LocationNotFoundFailure on empty response', () async { final response = MockResponse(); when(() => response.statusCode).thenReturn(200); when(() => response.body).thenReturn('{"results": []}'); when(() => httpClient.get(any())).thenAnswer((_) async => response); await expectLater( apiClient.locationSearch(query), throwsA(isA()), ); }); test('returns Location on valid response', () async { final response = MockResponse(); when(() => response.statusCode).thenReturn(200); when(() => response.body).thenReturn( ''' { "results": [ { "id": 4887398, "name": "Chicago", "latitude": 41.85003, "longitude": -87.65005 } ] }''', ); when(() => httpClient.get(any())).thenAnswer((_) async => response); final actual = await apiClient.locationSearch(query); expect( actual, isA() .having((l) => l.name, 'name', 'Chicago') .having((l) => l.id, 'id', 4887398) .having((l) => l.latitude, 'latitude', 41.85003) .having((l) => l.longitude, 'longitude', -87.65005), ); }); }); group('getWeather', () { const latitude = 41.85003; const longitude = -87.6500; test('makes correct http request', () async { final response = MockResponse(); when(() => response.statusCode).thenReturn(200); when(() => response.body).thenReturn('{}'); when(() => httpClient.get(any())).thenAnswer((_) async => response); try { await apiClient.getWeather(latitude: latitude, longitude: longitude); } catch (_) {} verify( () => httpClient.get( Uri.https('api.open-meteo.com', 'v1/forecast', { 'latitude': '$latitude', 'longitude': '$longitude', 'current_weather': 'true', }), ), ).called(1); }); test('throws WeatherRequestFailure on non-200 response', () async { final response = MockResponse(); when(() => response.statusCode).thenReturn(400); when(() => httpClient.get(any())).thenAnswer((_) async => response); expect( () async => apiClient.getWeather( latitude: latitude, longitude: longitude, ), throwsA(isA()), ); }); test('throws WeatherNotFoundFailure on empty response', () async { final response = MockResponse(); when(() => response.statusCode).thenReturn(200); when(() => response.body).thenReturn('{}'); when(() => httpClient.get(any())).thenAnswer((_) async => response); expect( () async => apiClient.getWeather( latitude: latitude, longitude: longitude, ), throwsA(isA()), ); }); test('returns weather on valid response', () async { final response = MockResponse(); when(() => response.statusCode).thenReturn(200); when(() => response.body).thenReturn( ''' { "latitude": 43, "longitude": -87.875, "generationtime_ms": 0.2510547637939453, "utc_offset_seconds": 0, "timezone": "GMT", "timezone_abbreviation": "GMT", "elevation": 189, "current_weather": { "temperature": 15.3, "windspeed": 25.8, "winddirection": 310, "weathercode": 63, "time": "2022-09-12T01:00" } } ''', ); when(() => httpClient.get(any())).thenAnswer((_) async => response); final actual = await apiClient.getWeather( latitude: latitude, longitude: longitude, ); expect( actual, isA() .having((w) => w.temperature, 'temperature', 15.3) .having((w) => w.weatherCode, 'weatherCode', 63.0), ); }); }); group('close', () { test('closes the underlying http client', () { apiClient.close(); verify(httpClient.close).called(1); }); }); }); } ================================================ FILE: examples/flutter_weather/packages/open_meteo_api/test/weather_test.dart ================================================ import 'package:open_meteo_api/open_meteo_api.dart'; import 'package:test/test.dart'; void main() { group('Weather', () { group('fromJson', () { test('returns correct Weather object', () { expect( Weather.fromJson( {'temperature': 15.3, 'weathercode': 63}, ), isA() .having((w) => w.temperature, 'temperature', 15.3) .having((w) => w.weatherCode, 'weatherCode', 63), ); }); }); }); } ================================================ FILE: examples/flutter_weather/packages/weather_repository/analysis_options.yaml ================================================ include: ../../../../analysis_options.yaml analyzer: exclude: - lib/**/*.g.dart linter: rules: public_member_api_docs: false ================================================ FILE: examples/flutter_weather/packages/weather_repository/build.yaml ================================================ targets: $default: builders: json_serializable: options: field_rename: snake checked: true ================================================ FILE: examples/flutter_weather/packages/weather_repository/lib/src/models/models.dart ================================================ export 'weather.dart'; ================================================ FILE: examples/flutter_weather/packages/weather_repository/lib/src/models/weather.dart ================================================ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'weather.g.dart'; enum WeatherCondition { clear, rainy, cloudy, snowy, unknown, } @JsonSerializable() class Weather extends Equatable { const Weather({ required this.location, required this.temperature, required this.condition, }); factory Weather.fromJson(Map json) => _$WeatherFromJson(json); Map toJson() => _$WeatherToJson(this); final String location; final double temperature; final WeatherCondition condition; @override List get props => [location, temperature, condition]; } ================================================ FILE: examples/flutter_weather/packages/weather_repository/lib/src/models/weather.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'weather.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Weather _$WeatherFromJson(Map json) => $checkedCreate( 'Weather', json, ($checkedConvert) { final val = Weather( location: $checkedConvert('location', (v) => v as String), temperature: $checkedConvert('temperature', (v) => (v as num).toDouble()), condition: $checkedConvert( 'condition', (v) => $enumDecode(_$WeatherConditionEnumMap, v), ), ); return val; }, ); Map _$WeatherToJson(Weather instance) => { 'location': instance.location, 'temperature': instance.temperature, 'condition': _$WeatherConditionEnumMap[instance.condition]!, }; const _$WeatherConditionEnumMap = { WeatherCondition.clear: 'clear', WeatherCondition.rainy: 'rainy', WeatherCondition.cloudy: 'cloudy', WeatherCondition.snowy: 'snowy', WeatherCondition.unknown: 'unknown', }; ================================================ FILE: examples/flutter_weather/packages/weather_repository/lib/src/weather_repository.dart ================================================ import 'dart:async'; import 'package:open_meteo_api/open_meteo_api.dart' hide Weather; import 'package:weather_repository/weather_repository.dart'; class WeatherRepository { WeatherRepository({OpenMeteoApiClient? weatherApiClient}) : _weatherApiClient = weatherApiClient ?? OpenMeteoApiClient(); final OpenMeteoApiClient _weatherApiClient; Future getWeather(String city) async { final location = await _weatherApiClient.locationSearch(city); final weather = await _weatherApiClient.getWeather( latitude: location.latitude, longitude: location.longitude, ); return Weather( temperature: weather.temperature, location: location.name, condition: weather.weatherCode.toInt().toCondition, ); } void dispose() => _weatherApiClient.close(); } extension on int { WeatherCondition get toCondition { switch (this) { case 0: return WeatherCondition.clear; case 1: case 2: case 3: case 45: case 48: return WeatherCondition.cloudy; case 51: case 53: case 55: case 56: case 57: case 61: case 63: case 65: case 66: case 67: case 80: case 81: case 82: case 95: case 96: case 99: return WeatherCondition.rainy; case 71: case 73: case 75: case 77: case 85: case 86: return WeatherCondition.snowy; default: return WeatherCondition.unknown; } } } ================================================ FILE: examples/flutter_weather/packages/weather_repository/lib/weather_repository.dart ================================================ export 'src/models/models.dart'; export 'src/weather_repository.dart'; ================================================ FILE: examples/flutter_weather/packages/weather_repository/pubspec.yaml ================================================ name: weather_repository description: A Dart Repository which manages the weather domain. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: equatable: ^2.0.0 json_annotation: ^4.6.0 open_meteo_api: path: ../open_meteo_api dev_dependencies: build_runner: ^2.0.0 coverage: ^1.0.3 json_serializable: ^6.3.1 mocktail: ^1.0.0 test: ^1.16.4 ================================================ FILE: examples/flutter_weather/packages/weather_repository/test/src/models/weather_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:test/test.dart'; import 'package:weather_repository/weather_repository.dart'; void main() { group('Weather', () { test('can be (de)serialized', () { final weather = Weather( condition: WeatherCondition.cloudy, temperature: 10.2, location: 'Chicago', ); expect(Weather.fromJson(weather.toJson()), equals(weather)); }); }); } ================================================ FILE: examples/flutter_weather/packages/weather_repository/test/weather_repository_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:mocktail/mocktail.dart'; import 'package:open_meteo_api/open_meteo_api.dart' as open_meteo_api; import 'package:test/test.dart'; import 'package:weather_repository/weather_repository.dart'; class MockOpenMeteoApiClient extends Mock implements open_meteo_api.OpenMeteoApiClient {} class MockLocation extends Mock implements open_meteo_api.Location {} class MockWeather extends Mock implements open_meteo_api.Weather {} void main() { group('WeatherRepository', () { late open_meteo_api.OpenMeteoApiClient weatherApiClient; late WeatherRepository weatherRepository; setUp(() { weatherApiClient = MockOpenMeteoApiClient(); weatherRepository = WeatherRepository( weatherApiClient: weatherApiClient, ); }); group('constructor', () { test('instantiates internal weather api client when not injected', () { expect(WeatherRepository(), isNotNull); }); }); group('getWeather', () { const city = 'chicago'; const latitude = 41.85003; const longitude = -87.65005; test('calls locationSearch with correct city', () async { try { await weatherRepository.getWeather(city); } catch (_) {} verify(() => weatherApiClient.locationSearch(city)).called(1); }); test('throws when locationSearch fails', () async { final exception = Exception('oops'); when(() => weatherApiClient.locationSearch(any())).thenThrow(exception); expect( () async => weatherRepository.getWeather(city), throwsA(exception), ); }); test('calls getWeather with correct latitude/longitude', () async { final location = MockLocation(); when(() => location.latitude).thenReturn(latitude); when(() => location.longitude).thenReturn(longitude); when(() => weatherApiClient.locationSearch(any())).thenAnswer( (_) async => location, ); try { await weatherRepository.getWeather(city); } catch (_) {} verify( () => weatherApiClient.getWeather( latitude: latitude, longitude: longitude, ), ).called(1); }); test('throws when getWeather fails', () async { final exception = Exception('oops'); final location = MockLocation(); when(() => location.latitude).thenReturn(latitude); when(() => location.longitude).thenReturn(longitude); when(() => weatherApiClient.locationSearch(any())).thenAnswer( (_) async => location, ); when( () => weatherApiClient.getWeather( latitude: any(named: 'latitude'), longitude: any(named: 'longitude'), ), ).thenThrow(exception); expect( () async => weatherRepository.getWeather(city), throwsA(exception), ); }); test('returns correct weather on success (clear)', () async { final location = MockLocation(); final weather = MockWeather(); when(() => location.name).thenReturn(city); when(() => location.latitude).thenReturn(latitude); when(() => location.longitude).thenReturn(longitude); when(() => weather.temperature).thenReturn(42.42); when(() => weather.weatherCode).thenReturn(0); when(() => weatherApiClient.locationSearch(any())).thenAnswer( (_) async => location, ); when( () => weatherApiClient.getWeather( latitude: any(named: 'latitude'), longitude: any(named: 'longitude'), ), ).thenAnswer((_) async => weather); final actual = await weatherRepository.getWeather(city); expect( actual, Weather( temperature: 42.42, location: city, condition: WeatherCondition.clear, ), ); }); test('returns correct weather on success (cloudy)', () async { final location = MockLocation(); final weather = MockWeather(); when(() => location.name).thenReturn(city); when(() => location.latitude).thenReturn(latitude); when(() => location.longitude).thenReturn(longitude); when(() => weather.temperature).thenReturn(42.42); when(() => weather.weatherCode).thenReturn(1); when(() => weatherApiClient.locationSearch(any())).thenAnswer( (_) async => location, ); when( () => weatherApiClient.getWeather( latitude: any(named: 'latitude'), longitude: any(named: 'longitude'), ), ).thenAnswer((_) async => weather); final actual = await weatherRepository.getWeather(city); expect( actual, Weather( temperature: 42.42, location: city, condition: WeatherCondition.cloudy, ), ); }); test('returns correct weather on success (rainy)', () async { final location = MockLocation(); final weather = MockWeather(); when(() => location.name).thenReturn(city); when(() => location.latitude).thenReturn(latitude); when(() => location.longitude).thenReturn(longitude); when(() => weather.temperature).thenReturn(42.42); when(() => weather.weatherCode).thenReturn(51); when(() => weatherApiClient.locationSearch(any())).thenAnswer( (_) async => location, ); when( () => weatherApiClient.getWeather( latitude: any(named: 'latitude'), longitude: any(named: 'longitude'), ), ).thenAnswer((_) async => weather); final actual = await weatherRepository.getWeather(city); expect( actual, Weather( temperature: 42.42, location: city, condition: WeatherCondition.rainy, ), ); }); test('returns correct weather on success (snowy)', () async { final location = MockLocation(); final weather = MockWeather(); when(() => location.name).thenReturn(city); when(() => location.latitude).thenReturn(latitude); when(() => location.longitude).thenReturn(longitude); when(() => weather.temperature).thenReturn(42.42); when(() => weather.weatherCode).thenReturn(71); when(() => weatherApiClient.locationSearch(any())).thenAnswer( (_) async => location, ); when( () => weatherApiClient.getWeather( latitude: any(named: 'latitude'), longitude: any(named: 'longitude'), ), ).thenAnswer((_) async => weather); final actual = await weatherRepository.getWeather(city); expect( actual, Weather( temperature: 42.42, location: city, condition: WeatherCondition.snowy, ), ); }); test('returns correct weather on success (unknown)', () async { final location = MockLocation(); final weather = MockWeather(); when(() => location.name).thenReturn(city); when(() => location.latitude).thenReturn(latitude); when(() => location.longitude).thenReturn(longitude); when(() => weather.temperature).thenReturn(42.42); when(() => weather.weatherCode).thenReturn(-1); when(() => weatherApiClient.locationSearch(any())).thenAnswer( (_) async => location, ); when( () => weatherApiClient.getWeather( latitude: any(named: 'latitude'), longitude: any(named: 'longitude'), ), ).thenAnswer((_) async => weather); final actual = await weatherRepository.getWeather(city); expect( actual, Weather( temperature: 42.42, location: city, condition: WeatherCondition.unknown, ), ); }); }); group('dispose', () { test('closes the weather api client', () { weatherRepository.dispose(); verify(weatherApiClient.close).called(1); }); }); }); } ================================================ FILE: examples/flutter_weather/pubspec.yaml ================================================ name: flutter_weather description: A new Flutter project. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: bloc: ^9.0.0 equatable: ^2.0.0 flutter: sdk: flutter flutter_bloc: ^9.1.0 google_fonts: ^6.0.0 hydrated_bloc: ^10.0.0 json_annotation: ^4.8.1 path_provider: ^2.0.8 weather_repository: path: packages/weather_repository dev_dependencies: bloc_lint: ^0.3.0 bloc_test: ^10.0.0 build_runner: ^2.0.0 flutter_test: sdk: flutter json_serializable: ^6.0.0 mocktail: ^1.0.0 flutter: uses-material-design: true assets: - assets/ ================================================ FILE: examples/flutter_weather/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../../packages/bloc bloc_lint: path: ../../packages/bloc_lint bloc_test: path: ../../packages/bloc_test flutter_bloc: path: ../../packages/flutter_bloc hydrated_bloc: path: ../../packages/hydrated_bloc ================================================ FILE: examples/flutter_weather/test/app_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_weather/app.dart'; import 'package:flutter_weather/weather/weather.dart'; import 'package:mocktail/mocktail.dart'; import 'package:weather_repository/weather_repository.dart' show WeatherRepository; import 'helpers/hydrated_bloc.dart'; class MockWeatherCubit extends MockCubit implements WeatherCubit {} class MockWeatherRepository extends Mock implements WeatherRepository {} void main() { initHydratedStorage(); group('WeatherApp', () { testWidgets('renders WeatherAppView', (tester) async { await tester.pumpWidget(const WeatherApp()); expect(find.byType(WeatherAppView), findsOneWidget); }); }); group('WeatherAppView', () { late WeatherCubit weatherCubit; late WeatherRepository weatherRepository; setUp(() { weatherCubit = MockWeatherCubit(); weatherRepository = MockWeatherRepository(); }); testWidgets('renders WeatherPage', (tester) async { when(() => weatherCubit.state).thenReturn(WeatherState()); await tester.pumpWidget( RepositoryProvider.value( value: weatherRepository, child: BlocProvider.value( value: weatherCubit, child: const WeatherAppView(), ), ), ); expect(find.byType(WeatherPage), findsOneWidget); }); testWidgets('has correct theme color scheme', (tester) async { final state = WeatherState( status: WeatherStatus.success, weather: Weather( condition: WeatherCondition.rainy, lastUpdated: DateTime(2024), location: 'Seattle', temperature: const Temperature(value: 20), ), ); when(() => weatherCubit.state).thenReturn(state); await tester.pumpWidget( RepositoryProvider.value( value: weatherRepository, child: BlocProvider.value( value: weatherCubit, child: const WeatherAppView(), ), ), ); final materialApp = tester.widget(find.byType(MaterialApp)); expect( materialApp.theme?.colorScheme, ColorScheme.fromSeed(seedColor: Colors.indigoAccent), ); }); }); } ================================================ FILE: examples/flutter_weather/test/helpers/hydrated_bloc.dart ================================================ import 'package:flutter_test/flutter_test.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:mocktail/mocktail.dart'; class MockStorage extends Mock implements Storage {} late Storage hydratedStorage; void initHydratedStorage() { TestWidgetsFlutterBinding.ensureInitialized(); hydratedStorage = MockStorage(); when( () => hydratedStorage.write(any(), any()), ).thenAnswer((_) async {}); HydratedBloc.storage = hydratedStorage; } ================================================ FILE: examples/flutter_weather/test/search/view/search_page_test.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_weather/search/search.dart'; void main() { group('SearchPage', () { testWidgets('is routable', (tester) async { await tester.pumpWidget( MaterialApp( home: Builder( builder: (context) => Scaffold( floatingActionButton: FloatingActionButton( onPressed: () { Navigator.of(context).push(SearchPage.route()); }, ), ), ), ), ); await tester.tap(find.byType(FloatingActionButton)); await tester.pumpAndSettle(); expect(find.byType(SearchPage), findsOneWidget); }); testWidgets('returns selected text when popped', (tester) async { String? location; await tester.pumpWidget( MaterialApp( home: Builder( builder: (context) => Scaffold( floatingActionButton: FloatingActionButton( onPressed: () async { location = await Navigator.of(context).push( SearchPage.route(), ); }, ), ), ), ), ); await tester.tap(find.byType(FloatingActionButton)); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), 'Chicago'); await tester.tap(find.byKey(const Key('searchPage_search_iconButton'))); await tester.pumpAndSettle(); expect(find.byType(SearchPage), findsNothing); expect(location, 'Chicago'); }); }); } ================================================ FILE: examples/flutter_weather/test/settings/view/settings_page_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_weather/settings/settings.dart'; import 'package:flutter_weather/weather/weather.dart'; import 'package:mocktail/mocktail.dart'; class MockWeatherCubit extends MockCubit implements WeatherCubit {} void main() { group('SettingsPage', () { late WeatherCubit weatherCubit; setUp(() { weatherCubit = MockWeatherCubit(); }); testWidgets('is routable', (tester) async { when(() => weatherCubit.state).thenReturn(WeatherState()); await tester.pumpWidget( BlocProvider.value( value: weatherCubit, child: MaterialApp( home: Builder( builder: (context) => Scaffold( floatingActionButton: FloatingActionButton( onPressed: () { Navigator.of(context).push( SettingsPage.route(), ); }, ), ), ), ), ), ); await tester.tap(find.byType(FloatingActionButton)); await tester.pumpAndSettle(); expect(find.byType(SettingsPage), findsOneWidget); }); testWidgets('calls toggleUnits when switch is changed', (tester) async { whenListen( weatherCubit, Stream.fromIterable([ WeatherState(), WeatherState(temperatureUnits: TemperatureUnits.fahrenheit), ]), ); when(() => weatherCubit.state).thenReturn(WeatherState()); await tester.pumpWidget( BlocProvider.value( value: weatherCubit, child: MaterialApp( home: Builder( builder: (context) => Scaffold( floatingActionButton: FloatingActionButton( onPressed: () { Navigator.of(context).push( SettingsPage.route(), ); }, ), ), ), ), ), ); await tester.tap(find.byType(FloatingActionButton)); await tester.pumpAndSettle(); await tester.tap(find.byType(Switch)); verify(() => weatherCubit.toggleUnits()).called(1); }); }); } ================================================ FILE: examples/flutter_weather/test/weather/cubit/weather_cubit_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_weather/weather/weather.dart'; import 'package:mocktail/mocktail.dart'; import 'package:weather_repository/weather_repository.dart' as weather_repository; import '../../helpers/hydrated_bloc.dart'; const weatherLocation = 'London'; const weatherCondition = weather_repository.WeatherCondition.rainy; const weatherTemperature = 9.8; class MockWeatherRepository extends Mock implements weather_repository.WeatherRepository {} class MockWeather extends Mock implements weather_repository.Weather {} void main() { initHydratedStorage(); group('WeatherCubit', () { late weather_repository.Weather weather; late weather_repository.WeatherRepository weatherRepository; late WeatherCubit weatherCubit; setUp(() async { weather = MockWeather(); weatherRepository = MockWeatherRepository(); when(() => weather.condition).thenReturn(weatherCondition); when(() => weather.location).thenReturn(weatherLocation); when(() => weather.temperature).thenReturn(weatherTemperature); when( () => weatherRepository.getWeather(any()), ).thenAnswer((_) async => weather); weatherCubit = WeatherCubit(weatherRepository); }); test('initial state is correct', () { final weatherCubit = WeatherCubit(weatherRepository); expect(weatherCubit.state, WeatherState()); }); group('toJson/fromJson', () { test('work properly', () { final weatherCubit = WeatherCubit(weatherRepository); expect( weatherCubit.fromJson(weatherCubit.toJson(weatherCubit.state)), weatherCubit.state, ); }); }); group('fetchWeather', () { blocTest( 'emits nothing when city is null', build: () => weatherCubit, act: (cubit) => cubit.fetchWeather(null), expect: () => [], ); blocTest( 'emits nothing when city is empty', build: () => weatherCubit, act: (cubit) => cubit.fetchWeather(''), expect: () => [], ); blocTest( 'calls getWeather with correct city', build: () => weatherCubit, act: (cubit) => cubit.fetchWeather(weatherLocation), verify: (_) { verify(() => weatherRepository.getWeather(weatherLocation)).called(1); }, ); blocTest( 'emits [loading, failure] when getWeather throws', setUp: () { when( () => weatherRepository.getWeather(any()), ).thenThrow(Exception('oops')); }, build: () => weatherCubit, act: (cubit) => cubit.fetchWeather(weatherLocation), expect: () => [ WeatherState(status: WeatherStatus.loading), WeatherState(status: WeatherStatus.failure), ], ); blocTest( 'emits [loading, success] when getWeather returns (celsius)', build: () => weatherCubit, act: (cubit) => cubit.fetchWeather(weatherLocation), expect: () => [ WeatherState(status: WeatherStatus.loading), isA() .having((w) => w.status, 'status', WeatherStatus.success) .having( (w) => w.weather, 'weather', isA() .having((w) => w.lastUpdated, 'lastUpdated', isNotNull) .having((w) => w.condition, 'condition', weatherCondition) .having( (w) => w.temperature, 'temperature', Temperature(value: weatherTemperature), ) .having((w) => w.location, 'location', weatherLocation), ), ], ); blocTest( 'emits [loading, success] when getWeather returns (fahrenheit)', build: () => weatherCubit, seed: () => WeatherState(temperatureUnits: TemperatureUnits.fahrenheit), act: (cubit) => cubit.fetchWeather(weatherLocation), expect: () => [ WeatherState( status: WeatherStatus.loading, temperatureUnits: TemperatureUnits.fahrenheit, ), isA() .having((w) => w.status, 'status', WeatherStatus.success) .having( (w) => w.weather, 'weather', isA() .having((w) => w.lastUpdated, 'lastUpdated', isNotNull) .having((w) => w.condition, 'condition', weatherCondition) .having( (w) => w.temperature, 'temperature', Temperature(value: weatherTemperature.toFahrenheit()), ) .having((w) => w.location, 'location', weatherLocation), ), ], ); }); group('refreshWeather', () { blocTest( 'emits nothing when status is not success', build: () => weatherCubit, act: (cubit) => cubit.refreshWeather(), expect: () => [], verify: (_) { verifyNever(() => weatherRepository.getWeather(any())); }, ); blocTest( 'emits nothing when location is null', build: () => weatherCubit, seed: () => WeatherState(status: WeatherStatus.success), act: (cubit) => cubit.refreshWeather(), expect: () => [], verify: (_) { verifyNever(() => weatherRepository.getWeather(any())); }, ); blocTest( 'invokes getWeather with correct location', build: () => weatherCubit, seed: () => WeatherState( status: WeatherStatus.success, weather: Weather( location: weatherLocation, temperature: Temperature(value: weatherTemperature), lastUpdated: DateTime(2020), condition: weatherCondition, ), ), act: (cubit) => cubit.refreshWeather(), verify: (_) { verify(() => weatherRepository.getWeather(weatherLocation)).called(1); }, ); blocTest( 'emits nothing when exception is thrown', setUp: () { when( () => weatherRepository.getWeather(any()), ).thenThrow(Exception('oops')); }, build: () => weatherCubit, seed: () => WeatherState( status: WeatherStatus.success, weather: Weather( location: weatherLocation, temperature: Temperature(value: weatherTemperature), lastUpdated: DateTime(2020), condition: weatherCondition, ), ), act: (cubit) => cubit.refreshWeather(), expect: () => [], ); blocTest( 'emits updated weather (celsius)', build: () => weatherCubit, seed: () => WeatherState( status: WeatherStatus.success, weather: Weather( location: weatherLocation, temperature: Temperature(value: 0), lastUpdated: DateTime(2020), condition: weatherCondition, ), ), act: (cubit) => cubit.refreshWeather(), expect: () => [ isA() .having((w) => w.status, 'status', WeatherStatus.success) .having( (w) => w.weather, 'weather', isA() .having((w) => w.lastUpdated, 'lastUpdated', isNotNull) .having((w) => w.condition, 'condition', weatherCondition) .having( (w) => w.temperature, 'temperature', Temperature(value: weatherTemperature), ) .having((w) => w.location, 'location', weatherLocation), ), ], ); blocTest( 'emits updated weather (fahrenheit)', build: () => weatherCubit, seed: () => WeatherState( temperatureUnits: TemperatureUnits.fahrenheit, status: WeatherStatus.success, weather: Weather( location: weatherLocation, temperature: Temperature(value: 0), lastUpdated: DateTime(2020), condition: weatherCondition, ), ), act: (cubit) => cubit.refreshWeather(), expect: () => [ isA() .having((w) => w.status, 'status', WeatherStatus.success) .having( (w) => w.weather, 'weather', isA() .having((w) => w.lastUpdated, 'lastUpdated', isNotNull) .having((w) => w.condition, 'condition', weatherCondition) .having( (w) => w.temperature, 'temperature', Temperature(value: weatherTemperature.toFahrenheit()), ) .having((w) => w.location, 'location', weatherLocation), ), ], ); }); group('toggleUnits', () { blocTest( 'emits updated units when status is not success', build: () => weatherCubit, act: (cubit) => cubit.toggleUnits(), expect: () => [ WeatherState(temperatureUnits: TemperatureUnits.fahrenheit), ], ); blocTest( 'emits updated units and temperature ' 'when status is success (celsius)', build: () => weatherCubit, seed: () => WeatherState( status: WeatherStatus.success, temperatureUnits: TemperatureUnits.fahrenheit, weather: Weather( location: weatherLocation, temperature: Temperature(value: weatherTemperature), lastUpdated: DateTime(2020), condition: WeatherCondition.rainy, ), ), act: (cubit) => cubit.toggleUnits(), expect: () => [ WeatherState( status: WeatherStatus.success, weather: Weather( location: weatherLocation, temperature: Temperature(value: weatherTemperature.toCelsius()), lastUpdated: DateTime(2020), condition: WeatherCondition.rainy, ), ), ], ); blocTest( 'emits updated units and temperature ' 'when status is success (fahrenheit)', build: () => weatherCubit, seed: () => WeatherState( status: WeatherStatus.success, weather: Weather( location: weatherLocation, temperature: Temperature(value: weatherTemperature), lastUpdated: DateTime(2020), condition: WeatherCondition.rainy, ), ), act: (cubit) => cubit.toggleUnits(), expect: () => [ WeatherState( status: WeatherStatus.success, temperatureUnits: TemperatureUnits.fahrenheit, weather: Weather( location: weatherLocation, temperature: Temperature( value: weatherTemperature.toFahrenheit(), ), lastUpdated: DateTime(2020), condition: WeatherCondition.rainy, ), ), ], ); }); }); } ================================================ FILE: examples/flutter_weather/test/weather/cubit/weather_state_test.dart ================================================ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_weather/weather/weather.dart'; void main() { group('WeatherStatusX', () { test('returns correct values for WeatherStatus.initial', () { const status = WeatherStatus.initial; expect(status.isInitial, isTrue); expect(status.isLoading, isFalse); expect(status.isSuccess, isFalse); expect(status.isFailure, isFalse); }); test('returns correct values for WeatherStatus.loading', () { const status = WeatherStatus.loading; expect(status.isInitial, isFalse); expect(status.isLoading, isTrue); expect(status.isSuccess, isFalse); expect(status.isFailure, isFalse); }); test('returns correct values for WeatherStatus.success', () { const status = WeatherStatus.success; expect(status.isInitial, isFalse); expect(status.isLoading, isFalse); expect(status.isSuccess, isTrue); expect(status.isFailure, isFalse); }); test('returns correct values for WeatherStatus.failure', () { const status = WeatherStatus.failure; expect(status.isInitial, isFalse); expect(status.isLoading, isFalse); expect(status.isSuccess, isFalse); expect(status.isFailure, isTrue); }); }); } ================================================ FILE: examples/flutter_weather/test/weather/view/weather_page_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_weather/search/search.dart'; import 'package:flutter_weather/settings/settings.dart'; import 'package:flutter_weather/weather/weather.dart'; import 'package:mocktail/mocktail.dart'; import 'package:weather_repository/weather_repository.dart' hide Weather; import '../../helpers/hydrated_bloc.dart'; class MockWeatherRepository extends Mock implements WeatherRepository {} class MockWeatherCubit extends MockCubit implements WeatherCubit {} void main() { initHydratedStorage(); group('WeatherPage', () { final weather = Weather( temperature: Temperature(value: 4.2), condition: WeatherCondition.cloudy, lastUpdated: DateTime(2020), location: 'London', ); late WeatherCubit weatherCubit; setUp(() { weatherCubit = MockWeatherCubit(); }); testWidgets('renders WeatherEmpty for WeatherStatus.initial', ( tester, ) async { when(() => weatherCubit.state).thenReturn(WeatherState()); await tester.pumpWidget( BlocProvider.value( value: weatherCubit, child: MaterialApp(home: WeatherPage()), ), ); expect(find.byType(WeatherEmpty), findsOneWidget); }); testWidgets('renders WeatherLoading for WeatherStatus.loading', ( tester, ) async { when(() => weatherCubit.state).thenReturn( WeatherState( status: WeatherStatus.loading, ), ); await tester.pumpWidget( BlocProvider.value( value: weatherCubit, child: MaterialApp(home: WeatherPage()), ), ); expect(find.byType(WeatherLoading), findsOneWidget); }); testWidgets('renders WeatherPopulated for WeatherStatus.success', ( tester, ) async { when(() => weatherCubit.state).thenReturn( WeatherState( status: WeatherStatus.success, weather: weather, ), ); await tester.pumpWidget( BlocProvider.value( value: weatherCubit, child: MaterialApp(home: WeatherPage()), ), ); expect(find.byType(WeatherPopulated), findsOneWidget); }); testWidgets('renders WeatherError for WeatherStatus.failure', ( tester, ) async { when(() => weatherCubit.state).thenReturn( WeatherState( status: WeatherStatus.failure, ), ); await tester.pumpWidget( BlocProvider.value( value: weatherCubit, child: MaterialApp(home: WeatherPage()), ), ); expect(find.byType(WeatherError), findsOneWidget); }); testWidgets('state is cached', (tester) async { when(() => hydratedStorage.read('$WeatherCubit')).thenReturn( WeatherState( status: WeatherStatus.success, weather: weather, temperatureUnits: TemperatureUnits.fahrenheit, ).toJson(), ); await tester.pumpWidget( BlocProvider.value( value: WeatherCubit(MockWeatherRepository()), child: MaterialApp(home: WeatherPage()), ), ); expect(find.byType(WeatherPopulated), findsOneWidget); }); testWidgets('navigates to SettingsPage when settings icon is tapped', ( tester, ) async { when(() => weatherCubit.state).thenReturn(WeatherState()); await tester.pumpWidget( BlocProvider.value( value: weatherCubit, child: MaterialApp(home: WeatherPage()), ), ); await tester.tap(find.byType(IconButton)); await tester.pumpAndSettle(); expect(find.byType(SettingsPage), findsOneWidget); }); testWidgets('navigates to SearchPage when search button is tapped', ( tester, ) async { when(() => weatherCubit.state).thenReturn(WeatherState()); await tester.pumpWidget( BlocProvider.value( value: weatherCubit, child: MaterialApp(home: WeatherPage()), ), ); await tester.tap(find.byType(FloatingActionButton)); await tester.pumpAndSettle(); expect(find.byType(SearchPage), findsOneWidget); }); testWidgets('triggers refreshWeather on pull to refresh', (tester) async { when(() => weatherCubit.state).thenReturn( WeatherState( status: WeatherStatus.success, weather: weather, ), ); when(() => weatherCubit.refreshWeather()).thenAnswer((_) async {}); await tester.pumpWidget( BlocProvider.value( value: weatherCubit, child: MaterialApp(home: WeatherPage()), ), ); await tester.fling( find.text('London'), const Offset(0, 500), 1000, ); await tester.pumpAndSettle(); verify(() => weatherCubit.refreshWeather()).called(1); }); testWidgets('triggers fetch on search pop', (tester) async { when(() => weatherCubit.state).thenReturn(WeatherState()); when(() => weatherCubit.fetchWeather(any())).thenAnswer((_) async {}); await tester.pumpWidget( BlocProvider.value( value: weatherCubit, child: MaterialApp(home: WeatherPage()), ), ); await tester.tap(find.byType(FloatingActionButton)); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), 'Chicago'); await tester.tap(find.byKey(const Key('searchPage_search_iconButton'))); await tester.pumpAndSettle(); verify(() => weatherCubit.fetchWeather('Chicago')).called(1); }); }); } ================================================ FILE: examples/flutter_weather/test/weather/widgets/weather_empty_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_weather/weather/weather.dart'; void main() { group('WeatherEmpty', () { testWidgets('renders correct text and icon', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: WeatherEmpty(), ), ), ); expect(find.text('Please Select a City!'), findsOneWidget); expect(find.text('🏙️'), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_weather/test/weather/widgets/weather_error_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_weather/weather/weather.dart'; void main() { group('WeatherError', () { testWidgets('renders correct text and icon', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: WeatherError(), ), ), ); expect(find.text('Something went wrong!'), findsOneWidget); expect(find.text('🙈'), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_weather/test/weather/widgets/weather_loading_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_weather/weather/weather.dart'; void main() { group('WeatherLoading', () { testWidgets('renders correct text and icon', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: WeatherLoading(), ), ), ); expect(find.text('Loading Weather'), findsOneWidget); expect(find.byType(CircularProgressIndicator), findsOneWidget); expect(find.text('⛅'), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_weather/test/weather/widgets/weather_populated_test.dart ================================================ // ignore_for_file: prefer_const_constructors import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_weather/weather/weather.dart'; void main() { group('WeatherPopulated', () { final weather = Weather( condition: WeatherCondition.clear, temperature: Temperature(value: 42), location: 'Chicago', lastUpdated: DateTime(2020), ); testWidgets('renders correct emoji (clear)', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: WeatherPopulated( weather: weather, units: TemperatureUnits.fahrenheit, onRefresh: () async {}, ), ), ), ); expect(find.text('☀️'), findsOneWidget); }); testWidgets('renders correct emoji (rainy)', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: WeatherPopulated( weather: weather.copyWith(condition: WeatherCondition.rainy), units: TemperatureUnits.fahrenheit, onRefresh: () async {}, ), ), ), ); expect(find.text('🌧️'), findsOneWidget); }); testWidgets('renders correct emoji (cloudy)', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: WeatherPopulated( weather: weather.copyWith(condition: WeatherCondition.cloudy), units: TemperatureUnits.fahrenheit, onRefresh: () async {}, ), ), ), ); expect(find.text('☁️'), findsOneWidget); }); testWidgets('renders correct emoji (snowy)', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: WeatherPopulated( weather: weather.copyWith(condition: WeatherCondition.snowy), units: TemperatureUnits.fahrenheit, onRefresh: () async {}, ), ), ), ); expect(find.text('🌨️'), findsOneWidget); }); testWidgets('renders correct emoji (unknown)', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: WeatherPopulated( weather: weather.copyWith(condition: WeatherCondition.unknown), units: TemperatureUnits.fahrenheit, onRefresh: () async {}, ), ), ), ); expect(find.text('❓'), findsOneWidget); }); }); } ================================================ FILE: examples/flutter_weather/web/index.html ================================================ flutter_weather ================================================ FILE: examples/flutter_weather/web/manifest.json ================================================ { "name": "flutter_weather", "short_name": "flutter_weather", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: examples/flutter_wizard/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: examples/flutter_wizard/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: web create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/flutter_wizard/README.md ================================================ [![build](https://github.com/felangel/bloc/actions/workflows/main.yaml/badge.svg)](https://github.com/felangel/bloc/actions) # flutter_wizard A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: examples/flutter_wizard/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml linter: rules: public_member_api_docs: false ================================================ FILE: examples/flutter_wizard/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: examples/flutter_wizard/lib/bloc/profile_wizard_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; part 'profile_wizard_event.dart'; part 'profile_wizard_state.dart'; class ProfileWizardBloc extends Bloc { ProfileWizardBloc() : super(ProfileWizardState.initial()) { on((event, emit) { emit(state.copyWith(profile: state.profile.copyWith(name: event.name))); }); on((event, emit) { emit(state.copyWith(profile: state.profile.copyWith(age: event.age))); }); } } ================================================ FILE: examples/flutter_wizard/lib/bloc/profile_wizard_event.dart ================================================ part of 'profile_wizard_bloc.dart'; sealed class ProfileWizardEvent extends Equatable { const ProfileWizardEvent(); @override List get props => []; } final class ProfileWizardNameSubmitted extends ProfileWizardEvent { const ProfileWizardNameSubmitted(this.name); final String name; @override List get props => [name]; } final class ProfileWizardAgeSubmitted extends ProfileWizardEvent { const ProfileWizardAgeSubmitted(this.age); final int? age; @override List get props => [age]; } ================================================ FILE: examples/flutter_wizard/lib/bloc/profile_wizard_state.dart ================================================ part of 'profile_wizard_bloc.dart'; final class Profile extends Equatable { const Profile({required this.name, required this.age}); final String? name; final int? age; Profile copyWith({String? name, int? age}) { return Profile( name: name ?? this.name, age: age ?? this.age, ); } @override List get props => [name, age]; } final class ProfileWizardState extends Equatable { ProfileWizardState({required this.profile}) : lastUpdated = DateTime.now(); ProfileWizardState.initial() : this(profile: const Profile(name: null, age: null)); final Profile profile; final DateTime lastUpdated; ProfileWizardState copyWith({Profile? profile}) { return ProfileWizardState( profile: profile ?? this.profile, ); } @override List get props => [profile, lastUpdated]; } ================================================ FILE: examples/flutter_wizard/lib/main.dart ================================================ import 'package:flow_builder/flow_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_wizard/bloc/profile_wizard_bloc.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) => const MaterialApp(home: Home()); } class Home extends StatefulWidget { const Home({super.key}); @override State createState() => _HomeState(); } class _HomeState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Home')), body: Center( child: Builder( builder: (context) { return ElevatedButton( onPressed: () async { final profile = await Navigator.of(context).push( ProfileWizard.route(), ); if (!context.mounted) return; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text('$profile'))); }, child: const Text('Start Profile Wizard'), ); }, ), ), ); } } class ProfileWizard extends StatelessWidget { const ProfileWizard({super.key}); static Route route() { return MaterialPageRoute(builder: (_) => const ProfileWizard()); } @override Widget build(BuildContext context) { return BlocProvider( create: (_) => ProfileWizardBloc(), child: ProfileWizardFlow( onComplete: (profile) => Navigator.of(context).pop(profile), ), ); } } class ProfileWizardFlow extends StatelessWidget { const ProfileWizardFlow({required this.onComplete, super.key}); final ValueSetter onComplete; @override Widget build(BuildContext context) { return BlocListener( listenWhen: (_, state) => state.profile.isComplete, listener: (context, state) => onComplete(state.profile), child: FlowBuilder( state: context.watch().state, onGeneratePages: (state, pages) { return [ ProfileNameForm.page(), if (state.profile.name != null) ProfileAgeForm.page(), ]; }, ), ); } } class ProfileNameForm extends StatefulWidget { const ProfileNameForm({super.key}); static Page page() { return const MaterialPage(child: ProfileNameForm()); } @override State createState() => _ProfileNameFormState(); } class _ProfileNameFormState extends State { var _name = ''; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Name')), body: Center( child: Padding( padding: const EdgeInsets.all(8), child: Column( children: [ TextField( onChanged: (value) => setState(() => _name = value), decoration: const InputDecoration( labelText: 'Name', hintText: 'John Doe', ), ), ElevatedButton( onPressed: _name.isNotEmpty ? () => context.read().add( ProfileWizardNameSubmitted(_name), ) : null, child: const Text('Continue'), ), ], ), ), ), ); } } class ProfileAgeForm extends StatefulWidget { const ProfileAgeForm({super.key}); static Page page() => const MaterialPage(child: ProfileAgeForm()); @override State createState() => _ProfileAgeFormState(); } class _ProfileAgeFormState extends State { int? _age; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Age')), body: Center( child: Padding( padding: const EdgeInsets.all(8), child: Column( children: [ TextField( onChanged: (value) => setState(() => _age = int.parse(value)), decoration: const InputDecoration( labelText: 'Age', hintText: '42', ), keyboardType: TextInputType.number, ), ElevatedButton( onPressed: _age != null ? () => context.read().add( ProfileWizardAgeSubmitted(_age), ) : null, child: const Text('Continue'), ), ], ), ), ), ); } } extension on Profile { bool get isComplete => name != null && age != null; } ================================================ FILE: examples/flutter_wizard/pubspec.yaml ================================================ name: flutter_wizard description: A new Flutter project. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: bloc: ^9.0.0 equatable: ^2.0.0 flow_builder: ^0.1.0 flutter: sdk: flutter flutter_bloc: ^9.1.0 flutter: uses-material-design: true dev_dependencies: bloc_lint: ^0.3.0 ================================================ FILE: examples/flutter_wizard/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../../packages/bloc bloc_lint: path: ../../packages/bloc_lint flutter_bloc: path: ../../packages/flutter_bloc ================================================ FILE: examples/flutter_wizard/web/index.html ================================================ flutter_wizard ================================================ FILE: examples/flutter_wizard/web/manifest.json ================================================ { "name": "flutter_wizard", "short_name": "flutter_wizard", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: examples/github_search/README.md ================================================ [![build](https://github.com/felangel/bloc/actions/workflows/main.yaml/badge.svg)](https://github.com/felangel/bloc/actions) # Github Search Sample project which illustrates how to setup a Flutter and AngularDart project with code sharing. ## Quick Start _Make sure you have the [Dart SDK](https://dart.dev/tools/sdk) and [Flutter SDK](https://flutter.dev/docs/get-started/install) installed before proceeding._ Open this project in your editor of choice (VSCode is recommended). ### Setup 1. Install dependencies for `common_github_search`: ```bash # change directories into common_github_search cd common_github_search # install dependencies dart pub get # change directories back out to the root directory cd ../ ``` 2. Install dependencies for `flutter_github_search` ```bash # change directories into flutter_github_search cd flutter_github_search # install dependencies flutter pub get # change directories back out to the root directory cd ../ ``` 3. Install dependencies for `angular_github_search` ```bash # change directories into flutter_github_search cd angular_github_search # install dependencies dart pub get # change directories into flutter_github_search cd ../ ``` ### Run Flutter ```bash # change directories into flutter_github_search cd flutter_github_search # run the flutter project flutter run ``` ### Run AngularDart ```bash # change directories into angular_github_search cd angular_github_search # run the angular project webdev serve ``` ================================================ FILE: examples/github_search/angular_github_search/.gitignore ================================================ # Files and directories created by pub .dart_tool/ .packages # Remove the following pattern if you wish to check in your lock file pubspec.lock # Conventional directory for build outputs build/ # Directory created by dartdoc doc/api/ ================================================ FILE: examples/github_search/angular_github_search/CHANGELOG.md ================================================ ## 1.0.0 - Initial version, created by Stagehand ================================================ FILE: examples/github_search/angular_github_search/README.md ================================================ [![build](https://github.com/felangel/bloc/actions/workflows/main.yaml/badge.svg)](https://github.com/felangel/bloc/actions) # angular_github_search A web app that uses [AngularDart](https://angulardart.dev/) and [AngularDart Components](https://angulardart.dev/components). Created from templates made available by Stagehand under a BSD-style [license](https://github.com/dart-lang/stagehand/blob/master/LICENSE). ================================================ FILE: examples/github_search/angular_github_search/analysis_options.yaml ================================================ include: ../../../analysis_options.yaml analyzer: exclude: [build/**] errors: uri_has_not_been_generated: ignore linter: rules: public_member_api_docs: false ================================================ FILE: examples/github_search/angular_github_search/lib/app_component.dart ================================================ import 'package:angular_github_search/src/github_search.dart'; import 'package:common_github_search/common_github_search.dart'; import 'package:ngdart/angular.dart'; @Component( selector: 'my-app', template: '', directives: [SearchFormComponent], ) class AppComponent { final githubRepository = GithubRepository(); } ================================================ FILE: examples/github_search/angular_github_search/lib/src/github_search.dart ================================================ export './search_form/search_bar/search_bar_component.dart'; export './search_form/search_body/search_body_component.dart'; export './search_form/search_body/search_results/search_result_item/search_result_item_component.dart'; export './search_form/search_body/search_results/search_results_component.dart'; export './search_form/search_form_component.dart'; ================================================ FILE: examples/github_search/angular_github_search/lib/src/search_form/search_bar/search_bar_component.dart ================================================ import 'package:common_github_search/common_github_search.dart'; import 'package:ngdart/angular.dart'; @Component( selector: 'search-bar', templateUrl: 'search_bar_component.html', ) class SearchBarComponent { @Input() late GithubSearchBloc githubSearchBloc; void onTextChanged(String text) { githubSearchBloc.add(TextChanged(text: text)); } } ================================================ FILE: examples/github_search/angular_github_search/lib/src/search_form/search_bar/search_bar_component.html ================================================ ================================================ FILE: examples/github_search/angular_github_search/lib/src/search_form/search_body/search_body_component.dart ================================================ import 'package:angular_github_search/src/github_search.dart'; import 'package:common_github_search/common_github_search.dart'; import 'package:ngdart/angular.dart'; @Component( selector: 'search-body', templateUrl: 'search_body_component.html', directives: [ coreDirectives, SearchResultsComponent, ], ) class SearchBodyComponent { @Input() late GithubSearchState state; bool get isEmpty => state is SearchStateEmpty; bool get isLoading => state is SearchStateLoading; bool get isSuccess => state is SearchStateSuccess; bool get isError => state is SearchStateError; List get items => isSuccess ? (state as SearchStateSuccess).items : []; String get error => isError ? (state as SearchStateError).error : ''; } ================================================ FILE: examples/github_search/angular_github_search/lib/src/search_form/search_body/search_body_component.html ================================================
🔍

Please enter a term to begin

‼️

{{ error }}

⚠️

No Results

================================================ FILE: examples/github_search/angular_github_search/lib/src/search_form/search_body/search_results/search_result_item/search_result_item_component.dart ================================================ import 'package:common_github_search/common_github_search.dart'; import 'package:ngdart/angular.dart'; @Component( selector: 'search-result-item', templateUrl: 'search_result_item_component.html', ) class SearchResultItemComponent { @Input() late SearchResultItem item; } ================================================ FILE: examples/github_search/angular_github_search/lib/src/search_form/search_body/search_results/search_result_item/search_result_item_component.html ================================================

{{ item.fullName }}

{{ item.htmlUrl }}

================================================ FILE: examples/github_search/angular_github_search/lib/src/search_form/search_body/search_results/search_results_component.dart ================================================ import 'package:angular_github_search/src/github_search.dart'; import 'package:common_github_search/common_github_search.dart'; import 'package:ngdart/angular.dart'; @Component( selector: 'search-results', templateUrl: 'search_results_component.html', directives: [coreDirectives, SearchResultItemComponent], ) class SearchResultsComponent { @Input() late List items; } ================================================ FILE: examples/github_search/angular_github_search/lib/src/search_form/search_body/search_results/search_results_component.html ================================================
================================================ FILE: examples/github_search/angular_github_search/lib/src/search_form/search_form_component.dart ================================================ import 'package:angular_bloc/angular_bloc.dart'; import 'package:angular_github_search/src/github_search.dart'; import 'package:common_github_search/common_github_search.dart'; import 'package:ngdart/angular.dart'; @Component( selector: 'search-form', templateUrl: 'search_form_component.html', directives: [ SearchBarComponent, SearchBodyComponent, ], pipes: [BlocPipe], ) class SearchFormComponent implements OnInit, OnDestroy { @Input() late GithubRepository githubRepository; late GithubSearchBloc githubSearchBloc; @override void ngOnInit() { githubSearchBloc = GithubSearchBloc( githubRepository: githubRepository, ); } @override void ngOnDestroy() { githubSearchBloc.close(); } } ================================================ FILE: examples/github_search/angular_github_search/lib/src/search_form/search_form_component.html ================================================

GitHub Search

================================================ FILE: examples/github_search/angular_github_search/lib/src/src.dart ================================================ export 'github_search.dart'; ================================================ FILE: examples/github_search/angular_github_search/pubspec.yaml ================================================ name: angular_github_search description: A web app that uses AngularDart Components environment: sdk: ">=3.10.0 <4.0.0" dependencies: angular_bloc: ^10.0.0-dev.5 bloc: ^9.0.0 common_github_search: path: ../common_github_search ngdart: ^8.0.0-dev.4 dev_dependencies: build_daemon: ^4.0.0 build_runner: ^2.0.0 build_web_compilers: ^4.0.0 ================================================ FILE: examples/github_search/angular_github_search/pubspec_overrides.yaml ================================================ dependency_overrides: angular_bloc: path: ../../../packages/angular_bloc bloc: path: ../../../packages/bloc build_modules: ^5.0.0 build_web_compilers: ^4.0.0 ================================================ FILE: examples/github_search/angular_github_search/web/index.html ================================================ angular_github_search Loading... ================================================ FILE: examples/github_search/angular_github_search/web/main.dart ================================================ import 'package:angular_github_search/app_component.template.dart' as ng; import 'package:ngdart/angular.dart'; void main() { runApp(ng.AppComponentNgFactory); } ================================================ FILE: examples/github_search/angular_github_search/web/styles.css ================================================ @import url(https://fonts.googleapis.com/css?family=Material+Icons); .sk-chase { width: 40px; height: 40px; position: relative; animation: sk-chase 2.5s infinite linear both; } .sk-chase-dot { width: 100%; height: 100%; position: absolute; left: 0; top: 0; animation: sk-chase-dot 2.0s infinite ease-in-out both; } .sk-chase-dot:before { content: ''; display: block; width: 25%; height: 25%; background-color: #2196F3; border-radius: 100%; animation: sk-chase-dot-before 2.0s infinite ease-in-out both; } .sk-chase-dot:nth-child(1) { animation-delay: -1.1s; } .sk-chase-dot:nth-child(2) { animation-delay: -1.0s; } .sk-chase-dot:nth-child(3) { animation-delay: -0.9s; } .sk-chase-dot:nth-child(4) { animation-delay: -0.8s; } .sk-chase-dot:nth-child(5) { animation-delay: -0.7s; } .sk-chase-dot:nth-child(6) { animation-delay: -0.6s; } .sk-chase-dot:nth-child(1):before { animation-delay: -1.1s; } .sk-chase-dot:nth-child(2):before { animation-delay: -1.0s; } .sk-chase-dot:nth-child(3):before { animation-delay: -0.9s; } .sk-chase-dot:nth-child(4):before { animation-delay: -0.8s; } .sk-chase-dot:nth-child(5):before { animation-delay: -0.7s; } .sk-chase-dot:nth-child(6):before { animation-delay: -0.6s; } @keyframes sk-chase { 100% { transform: rotate(360deg); } } @keyframes sk-chase-dot { 80%, 100% { transform: rotate(360deg); } } @keyframes sk-chase-dot-before { 50% { transform: scale(0.4); } 100%, 0% { transform: scale(1.0); } } .bb-ridge { border-bottom-style: ridge; } /*! TACHYONS v4.9.1 | http://tachyons.io */ /* * * ________ ______ * ___ __/_____ _________ /______ ______________________ * __ / _ __ `/ ___/_ __ \_ / / / __ \_ __ \_ ___/ * _ / / /_/ // /__ _ / / / /_/ // /_/ / / / /(__ ) * /_/ \__,_/ \___/ /_/ /_/_\__, / \____//_/ /_//____/ * /____/ * * TABLE OF CONTENTS * * 1. External Library Includes * - Normalize.css | http://normalize.css.github.io * 2. Tachyons Modules * 3. Variables * - Media Queries * - Colors * 4. Debugging * - Debug all * - Debug children * */ /* External Library Includes */ /*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */ /* Document ========================================================================== */ /** * 1. Correct the line height in all browsers. * 2. Prevent adjustments of font size after orientation changes in iOS. */ html { line-height: 1.15; /* 1 */ -webkit-text-size-adjust: 100%; /* 2 */ } /* Sections ========================================================================== */ /** * Remove the margin in all browsers. */ body { margin: 0; } /** * Correct the font size and margin on `h1` elements within `section` and * `article` contexts in Chrome, Firefox, and Safari. */ h1 { font-size: 2em; margin: 0.67em 0; } /* Grouping content ========================================================================== */ /** * 1. Add the correct box sizing in Firefox. * 2. Show the overflow in Edge and IE. */ hr { box-sizing: content-box; /* 1 */ height: 0; /* 1 */ overflow: visible; /* 2 */ } /** * 1. Correct the inheritance and scaling of font size in all browsers. * 2. Correct the odd `em` font sizing in all browsers. */ pre { font-family: monospace, monospace; /* 1 */ font-size: 1em; /* 2 */ } /* Text-level semantics ========================================================================== */ /** * Remove the gray background on active links in IE 10. */ a { background-color: transparent; } /** * 1. Remove the bottom border in Chrome 57- * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. */ abbr[title] { border-bottom: none; /* 1 */ text-decoration: underline; /* 2 */ -webkit-text-decoration: underline dotted; text-decoration: underline dotted; /* 2 */ } /** * Add the correct font weight in Chrome, Edge, and Safari. */ b, strong { font-weight: bolder; } /** * 1. Correct the inheritance and scaling of font size in all browsers. * 2. Correct the odd `em` font sizing in all browsers. */ code, kbd, samp { font-family: monospace, monospace; /* 1 */ font-size: 1em; /* 2 */ } /** * Add the correct font size in all browsers. */ small { font-size: 80%; } /** * Prevent `sub` and `sup` elements from affecting the line height in * all browsers. */ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.5em; } /* Embedded content ========================================================================== */ /** * Remove the border on images inside links in IE 10. */ img { border-style: none; } /* Forms ========================================================================== */ /** * 1. Change the font styles in all browsers. * 2. Remove the margin in Firefox and Safari. */ button, input, optgroup, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 1 */ line-height: 1.15; /* 1 */ margin: 0; /* 2 */ } /** * Show the overflow in IE. * 1. Show the overflow in Edge. */ button, input { /* 1 */ overflow: visible; } /** * Remove the inheritance of text transform in Edge, Firefox, and IE. * 1. Remove the inheritance of text transform in Firefox. */ button, select { /* 1 */ text-transform: none; } /** * Correct the inability to style clickable types in iOS and Safari. */ button, [type="button"], [type="reset"], [type="submit"] { -webkit-appearance: button; } /** * Remove the inner border and padding in Firefox. */ button::-moz-focus-inner, [type="button"]::-moz-focus-inner, [type="reset"]::-moz-focus-inner, [type="submit"]::-moz-focus-inner { border-style: none; padding: 0; } /** * Restore the focus styles unset by the previous rule. */ button:-moz-focusring, [type="button"]:-moz-focusring, [type="reset"]:-moz-focusring, [type="submit"]:-moz-focusring { outline: 1px dotted ButtonText; } /** * Correct the padding in Firefox. */ fieldset { padding: 0.35em 0.75em 0.625em; } /** * 1. Correct the text wrapping in Edge and IE. * 2. Correct the color inheritance from `fieldset` elements in IE. * 3. Remove the padding so developers are not caught out when they zero out * `fieldset` elements in all browsers. */ legend { box-sizing: border-box; /* 1 */ color: inherit; /* 2 */ display: table; /* 1 */ max-width: 100%; /* 1 */ padding: 0; /* 3 */ white-space: normal; /* 1 */ } /** * Add the correct vertical alignment in Chrome, Firefox, and Opera. */ progress { vertical-align: baseline; } /** * Remove the default vertical scrollbar in IE 10+. */ textarea { overflow: auto; } /** * 1. Add the correct box sizing in IE 10. * 2. Remove the padding in IE 10. */ [type="checkbox"], [type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } /** * Correct the cursor style of increment and decrement buttons in Chrome. */ [type="number"]::-webkit-inner-spin-button, [type="number"]::-webkit-outer-spin-button { height: auto; } /** * 1. Correct the odd appearance in Chrome and Safari. * 2. Correct the outline style in Safari. */ [type="search"] { -webkit-appearance: textfield; /* 1 */ outline-offset: -2px; /* 2 */ } /** * Remove the inner padding in Chrome and Safari on macOS. */ [type="search"]::-webkit-search-decoration { -webkit-appearance: none; } /** * 1. Correct the inability to style clickable types in iOS and Safari. * 2. Change font properties to `inherit` in Safari. */ ::-webkit-file-upload-button { -webkit-appearance: button; /* 1 */ font: inherit; /* 2 */ } /* Interactive ========================================================================== */ /* * Add the correct display in Edge, IE 10+, and Firefox. */ details { display: block; } /* * Add the correct display in all browsers. */ summary { display: list-item; } /* Misc ========================================================================== */ /** * Add the correct display in IE 10+. */ template { display: none; } /** * Add the correct display in IE 10. */ [hidden] { display: none; } /* Modules */ /* BOX SIZING */ html, body, div, article, aside, section, main, nav, footer, header, form, fieldset, legend, pre, code, a, h1, h2, h3, h4, h5, h6, p, ul, ol, li, dl, dt, dd, blockquote, figcaption, figure, textarea, table, td, th, tr, input[type="email"], input[type="number"], input[type="password"], input[type="tel"], input[type="text"], input[type="url"], .border-box { box-sizing: border-box; } /* ASPECT RATIOS */ /* This is for fluid media that is embedded from third party sites like youtube, vimeo etc. * Wrap the outer element in aspect-ratio and then extend it with the desired ratio i.e * Make sure there are no height and width attributes on the embedded media. * Adapted from: https://github.com/suitcss/components-flex-embed * * Example: * *
* *
* * */ .aspect-ratio { height: 0; position: relative; } .aspect-ratio--16x9 { padding-bottom: 56.25%; } .aspect-ratio--9x16 { padding-bottom: 177.77%; } .aspect-ratio--4x3 { padding-bottom: 75%; } .aspect-ratio--3x4 { padding-bottom: 133.33%; } .aspect-ratio--6x4 { padding-bottom: 66.6%; } .aspect-ratio--4x6 { padding-bottom: 150%; } .aspect-ratio--8x5 { padding-bottom: 62.5%; } .aspect-ratio--5x8 { padding-bottom: 160%; } .aspect-ratio--7x5 { padding-bottom: 71.42%; } .aspect-ratio--5x7 { padding-bottom: 140%; } .aspect-ratio--1x1 { padding-bottom: 100%; } .aspect-ratio--object { position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%; z-index: 100; } /* IMAGES Docs: http://tachyons.io/docs/elements/images/ */ /* Responsive images! */ img { max-width: 100%; } /* BACKGROUND SIZE Docs: http://tachyons.io/docs/themes/background-size/ Media Query Extensions: -ns = not-small -m = medium -l = large */ /* Often used in combination with background image set as an inline style on an html element. */ .cover { background-size: cover !important; } .contain { background-size: contain !important; } /* BACKGROUND POSITION Base: bg = background Modifiers: -center = center center -top = top center -right = center right -bottom = bottom center -left = center left Media Query Extensions: -ns = not-small -m = medium -l = large */ .bg-center { background-repeat: no-repeat; background-position: center center; } .bg-top { background-repeat: no-repeat; background-position: top center; } .bg-right { background-repeat: no-repeat; background-position: center right; } .bg-bottom { background-repeat: no-repeat; background-position: bottom center; } .bg-left { background-repeat: no-repeat; background-position: center left; } /* OUTLINES Media Query Extensions: -ns = not-small -m = medium -l = large */ .outline { outline: 1px solid; } .outline-transparent { outline: 1px solid transparent; } .outline-0 { outline: 0; } /* BORDERS Docs: http://tachyons.io/docs/themes/borders/ Base: b = border Modifiers: a = all t = top r = right b = bottom l = left n = none Media Query Extensions: -ns = not-small -m = medium -l = large */ .ba { border-style: solid; border-width: 1px; } .bt { border-top-style: solid; border-top-width: 1px; } .br { border-right-style: solid; border-right-width: 1px; } .bb { border-bottom-style: solid; border-bottom-width: 1px; } .bl { border-left-style: solid; border-left-width: 1px; } .bn { border-style: none; border-width: 0; } /* BORDER COLORS Docs: http://tachyons.io/docs/themes/borders/ Border colors can be used to extend the base border classes ba,bt,bb,br,bl found in the _borders.css file. The base border class by default will set the color of the border to that of the current text color. These classes are for the cases where you desire for the text and border colors to be different. Base: b = border Modifiers: --color-name = each color variable name is also a border color name */ .b--black { border-color: #000; } .b--near-black { border-color: #111; } .b--dark-gray { border-color: #333; } .b--mid-gray { border-color: #555; } .b--gray { border-color: #777; } .b--silver { border-color: #999; } .b--light-silver { border-color: #aaa; } .b--moon-gray { border-color: #ccc; } .b--light-gray { border-color: #eee; } .b--near-white { border-color: #f4f4f4; } .b--white { border-color: #fff; } .b--white-90 { border-color: rgba(255, 255, 255, 0.9); } .b--white-80 { border-color: rgba(255, 255, 255, 0.8); } .b--white-70 { border-color: rgba(255, 255, 255, 0.7); } .b--white-60 { border-color: rgba(255, 255, 255, 0.6); } .b--white-50 { border-color: rgba(255, 255, 255, 0.5); } .b--white-40 { border-color: rgba(255, 255, 255, 0.4); } .b--white-30 { border-color: rgba(255, 255, 255, 0.3); } .b--white-20 { border-color: rgba(255, 255, 255, 0.2); } .b--white-10 { border-color: rgba(255, 255, 255, 0.1); } .b--white-05 { border-color: rgba(255, 255, 255, 0.05); } .b--white-025 { border-color: rgba(255, 255, 255, 0.025); } .b--white-0125 { border-color: rgba(255, 255, 255, 0.0125); } .b--black-90 { border-color: rgba(0, 0, 0, 0.9); } .b--black-80 { border-color: rgba(0, 0, 0, 0.8); } .b--black-70 { border-color: rgba(0, 0, 0, 0.7); } .b--black-60 { border-color: rgba(0, 0, 0, 0.6); } .b--black-50 { border-color: rgba(0, 0, 0, 0.5); } .b--black-40 { border-color: rgba(0, 0, 0, 0.4); } .b--black-30 { border-color: rgba(0, 0, 0, 0.3); } .b--black-20 { border-color: rgba(0, 0, 0, 0.2); } .b--black-10 { border-color: rgba(0, 0, 0, 0.1); } .b--black-05 { border-color: rgba(0, 0, 0, 0.05); } .b--black-025 { border-color: rgba(0, 0, 0, 0.025); } .b--black-0125 { border-color: rgba(0, 0, 0, 0.0125); } .b--dark-red { border-color: #e7040f; } .b--red { border-color: #ff4136; } .b--light-red { border-color: #ff725c; } .b--orange { border-color: #ff6300; } .b--gold { border-color: #ffb700; } .b--yellow { border-color: #ffd700; } .b--light-yellow { border-color: #fbf1a9; } .b--purple { border-color: #5e2ca5; } .b--light-purple { border-color: #a463f2; } .b--dark-pink { border-color: #d5008f; } .b--hot-pink { border-color: #ff41b4; } .b--pink { border-color: #ff80cc; } .b--light-pink { border-color: #ffa3d7; } .b--dark-green { border-color: #137752; } .b--green { border-color: #19a974; } .b--light-green { border-color: #9eebcf; } .b--navy { border-color: #001b44; } .b--dark-blue { border-color: #00449e; } .b--blue { border-color: #357edd; } .b--light-blue { border-color: #96ccff; } .b--lightest-blue { border-color: #cdecff; } .b--washed-blue { border-color: #f6fffe; } .b--washed-green { border-color: #e8fdf5; } .b--washed-yellow { border-color: #fffceb; } .b--washed-red { border-color: #ffdfdf; } .b--transparent { border-color: transparent; } .b--inherit { border-color: inherit; } /* BORDER RADIUS Docs: http://tachyons.io/docs/themes/border-radius/ Base: br = border-radius Modifiers: 0 = 0/none 1 = 1st step in scale 2 = 2nd step in scale 3 = 3rd step in scale 4 = 4th step in scale Literal values: -100 = 100% -pill = 9999px Media Query Extensions: -ns = not-small -m = medium -l = large */ .br0 { border-radius: 0; } .br1 { border-radius: 0.125rem; } .br2 { border-radius: 0.25rem; } .br3 { border-radius: 0.5rem; } .br4 { border-radius: 1rem; } .br-100 { border-radius: 100%; } .br-pill { border-radius: 9999px; } .br--bottom { border-top-left-radius: 0; border-top-right-radius: 0; } .br--top { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } .br--right { border-top-left-radius: 0; border-bottom-left-radius: 0; } .br--left { border-top-right-radius: 0; border-bottom-right-radius: 0; } /* BORDER STYLES Docs: http://tachyons.io/docs/themes/borders/ Depends on base border module in _borders.css Base: b = border-style Modifiers: --none = none --dotted = dotted --dashed = dashed --solid = solid Media Query Extensions: -ns = not-small -m = medium -l = large */ .b--dotted { border-style: dotted; } .b--dashed { border-style: dashed; } .b--solid { border-style: solid; } .b--none { border-style: none; } /* BORDER WIDTHS Docs: http://tachyons.io/docs/themes/borders/ Base: bw = border-width Modifiers: 0 = 0 width border 1 = 1st step in border-width scale 2 = 2nd step in border-width scale 3 = 3rd step in border-width scale 4 = 4th step in border-width scale 5 = 5th step in border-width scale Media Query Extensions: -ns = not-small -m = medium -l = large */ .bw0 { border-width: 0; } .bw1 { border-width: 0.125rem; } .bw2 { border-width: 0.25rem; } .bw3 { border-width: 0.5rem; } .bw4 { border-width: 1rem; } .bw5 { border-width: 2rem; } /* Resets */ .bt-0 { border-top-width: 0; } .br-0 { border-right-width: 0; } .bb-0 { border-bottom-width: 0; } .bl-0 { border-left-width: 0; } /* BOX-SHADOW Docs: http://tachyons.io/docs/themes/box-shadow/ Media Query Extensions: -ns = not-small -m = medium -l = large */ .shadow-1 { box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.2); } .shadow-2 { box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.2); } .shadow-3 { box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, 0.2); } .shadow-4 { box-shadow: 2px 2px 8px 0 rgba(0, 0, 0, 0.2); } .shadow-5 { box-shadow: 4px 4px 8px 0 rgba(0, 0, 0, 0.2); } /* CODE */ .pre { overflow-x: auto; overflow-y: hidden; overflow: scroll; } /* COORDINATES Docs: http://tachyons.io/docs/layout/position/ Use in combination with the position module. Base: top bottom right left Modifiers: -0 = literal value 0 -1 = literal value 1 -2 = literal value 2 --1 = literal value -1 --2 = literal value -2 Media Query Extensions: -ns = not-small -m = medium -l = large */ .top-0 { top: 0; } .right-0 { right: 0; } .bottom-0 { bottom: 0; } .left-0 { left: 0; } .top-1 { top: 1rem; } .right-1 { right: 1rem; } .bottom-1 { bottom: 1rem; } .left-1 { left: 1rem; } .top-2 { top: 2rem; } .right-2 { right: 2rem; } .bottom-2 { bottom: 2rem; } .left-2 { left: 2rem; } .top--1 { top: -1rem; } .right--1 { right: -1rem; } .bottom--1 { bottom: -1rem; } .left--1 { left: -1rem; } .top--2 { top: -2rem; } .right--2 { right: -2rem; } .bottom--2 { bottom: -2rem; } .left--2 { left: -2rem; } .absolute--fill { top: 0; right: 0; bottom: 0; left: 0; } /* CLEARFIX http://tachyons.io/docs/layout/clearfix/ */ /* Nicolas Gallaghers Clearfix solution Ref: http://nicolasgallagher.com/micro-clearfix-hack/ */ .cf:before, .cf:after { content: " "; display: table; } .cf:after { clear: both; } .cf { *zoom: 1; } .cl { clear: left; } .cr { clear: right; } .cb { clear: both; } .cn { clear: none; } /* DISPLAY Docs: http://tachyons.io/docs/layout/display Base: d = display Modifiers: n = none b = block ib = inline-block it = inline-table t = table tc = table-cell t-row = table-row t-columm = table-column t-column-group = table-column-group Media Query Extensions: -ns = not-small -m = medium -l = large */ .dn { display: none; } .di { display: inline; } .db { display: block; } .dib { display: inline-block; } .dit { display: inline-table; } .dt { display: table; } .dtc { display: table-cell; } .dt-row { display: table-row; } .dt-row-group { display: table-row-group; } .dt-column { display: table-column; } .dt-column-group { display: table-column-group; } /* This will set table to full width and then all cells will be equal width */ .dt--fixed { table-layout: fixed; width: 100%; } /* FLEXBOX Media Query Extensions: -ns = not-small -m = medium -l = large */ .flex { display: flex; } .inline-flex { display: inline-flex; } /* 1. Fix for Chrome 44 bug. * https://code.google.com/p/chromium/issues/detail?id=506893 */ .flex-auto { flex: 1 1 auto; min-width: 0; /* 1 */ min-height: 0; /* 1 */ } .flex-none { flex: none; } .flex-column { flex-direction: column; } .flex-row { flex-direction: row; } .flex-wrap { flex-wrap: wrap; } .flex-nowrap { flex-wrap: nowrap; } .flex-wrap-reverse { flex-wrap: wrap-reverse; } .flex-column-reverse { flex-direction: column-reverse; } .flex-row-reverse { flex-direction: row-reverse; } .items-start { align-items: flex-start; } .items-end { align-items: flex-end; } .items-center { align-items: center; } .items-baseline { align-items: baseline; } .items-stretch { align-items: stretch; } .self-start { align-self: flex-start; } .self-end { align-self: flex-end; } .self-center { align-self: center; } .self-baseline { align-self: baseline; } .self-stretch { align-self: stretch; } .justify-start { justify-content: flex-start; } .justify-end { justify-content: flex-end; } .justify-center { justify-content: center; } .justify-between { justify-content: space-between; } .justify-around { justify-content: space-around; } .content-start { align-content: flex-start; } .content-end { align-content: flex-end; } .content-center { align-content: center; } .content-between { align-content: space-between; } .content-around { align-content: space-around; } .content-stretch { align-content: stretch; } .order-0 { order: 0; } .order-1 { order: 1; } .order-2 { order: 2; } .order-3 { order: 3; } .order-4 { order: 4; } .order-5 { order: 5; } .order-6 { order: 6; } .order-7 { order: 7; } .order-8 { order: 8; } .order-last { order: 99999; } .flex-grow-0 { flex-grow: 0; } .flex-grow-1 { flex-grow: 1; } .flex-shrink-0 { flex-shrink: 0; } .flex-shrink-1 { flex-shrink: 1; } /* FLOATS http://tachyons.io/docs/layout/floats/ 1. Floated elements are automatically rendered as block level elements. Setting floats to display inline will fix the double margin bug in ie6. You know... just in case. 2. Don't forget to clearfix your floats with .cf Base: f = float Modifiers: l = left r = right n = none Media Query Extensions: -ns = not-small -m = medium -l = large */ .fl { float: left; _display: inline; } .fr { float: right; _display: inline; } .fn { float: none; } /* FONT FAMILY GROUPS Docs: http://tachyons.io/docs/typography/font-family/ */ .sans-serif { font-family: -apple-system, BlinkMacSystemFont, "avenir next", avenir, "helvetica neue", helvetica, ubuntu, roboto, noto, "segoe ui", arial, sans-serif; } .serif { font-family: georgia, times, serif; } .system-sans-serif { font-family: sans-serif; } .system-serif { font-family: serif; } /* Monospaced Typefaces (for code) */ /* From http://cssfontstack.com */ code, .code { font-family: Consolas, monaco, monospace; } .courier { font-family: "Courier Next", courier, monospace; } /* Sans-Serif Typefaces */ .helvetica { font-family: "helvetica neue", helvetica, sans-serif; } .avenir { font-family: "avenir next", avenir, sans-serif; } /* Serif Typefaces */ .athelas { font-family: athelas, georgia, serif; } .georgia { font-family: georgia, serif; } .times { font-family: times, serif; } .bodoni { font-family: "Bodoni MT", serif; } .calisto { font-family: "Calisto MT", serif; } .garamond { font-family: garamond, serif; } .baskerville { font-family: baskerville, serif; } /* FONT STYLE Docs: http://tachyons.io/docs/typography/font-style/ Media Query Extensions: -ns = not-small -m = medium -l = large */ .i { font-style: italic; } .fs-normal { font-style: normal; } /* FONT WEIGHT Docs: http://tachyons.io/docs/typography/font-weight/ Base fw = font-weight Modifiers: 1 = literal value 100 2 = literal value 200 3 = literal value 300 4 = literal value 400 5 = literal value 500 6 = literal value 600 7 = literal value 700 8 = literal value 800 9 = literal value 900 Media Query Extensions: -ns = not-small -m = medium -l = large */ .normal { font-weight: normal; } .b { font-weight: bold; } .fw1 { font-weight: 100; } .fw2 { font-weight: 200; } .fw3 { font-weight: 300; } .fw4 { font-weight: 400; } .fw5 { font-weight: 500; } .fw6 { font-weight: 600; } .fw7 { font-weight: 700; } .fw8 { font-weight: 800; } .fw9 { font-weight: 900; } /* FORMS */ .input-reset { -webkit-appearance: none; -moz-appearance: none; } .button-reset::-moz-focus-inner, .input-reset::-moz-focus-inner { border: 0; padding: 0; } /* HEIGHTS Docs: http://tachyons.io/docs/layout/heights/ Base: h = height min-h = min-height min-vh = min-height vertical screen height vh = vertical screen height Modifiers 1 = 1st step in height scale 2 = 2nd step in height scale 3 = 3rd step in height scale 4 = 4th step in height scale 5 = 5th step in height scale -25 = literal value 25% -50 = literal value 50% -75 = literal value 75% -100 = literal value 100% -auto = string value of auto -inherit = string value of inherit Media Query Extensions: -ns = not-small -m = medium -l = large */ /* Height Scale */ .h1 { height: 1rem; } .h2 { height: 2rem; } .h3 { height: 4rem; } .h4 { height: 8rem; } .h5 { height: 16rem; } /* Height Percentages - Based off of height of parent */ .h-25 { height: 25%; } .h-50 { height: 50%; } .h-75 { height: 75%; } .h-100 { height: 100%; } .min-h-100 { min-height: 100%; } /* Screen Height Percentage */ .vh-25 { height: 25vh; } .vh-50 { height: 50vh; } .vh-75 { height: 75vh; } .vh-100 { height: 100vh; } .min-vh-100 { min-height: 100vh; } /* String Properties */ .h-auto { height: auto; } .h-inherit { height: inherit; } /* LETTER SPACING Docs: http://tachyons.io/docs/typography/tracking/ Media Query Extensions: -ns = not-small -m = medium -l = large */ .tracked { letter-spacing: 0.1em; } .tracked-tight { letter-spacing: -0.05em; } .tracked-mega { letter-spacing: 0.25em; } /* LINE HEIGHT / LEADING Docs: http://tachyons.io/docs/typography/line-height Media Query Extensions: -ns = not-small -m = medium -l = large */ .lh-solid { line-height: 1; } .lh-title { line-height: 1.25; } .lh-copy { line-height: 1.5; } /* LINKS Docs: http://tachyons.io/docs/elements/links/ */ .link { text-decoration: none; transition: color 0.15s ease-in; } .link:link, .link:visited { transition: color 0.15s ease-in; } .link:hover { transition: color 0.15s ease-in; } .link:active { transition: color 0.15s ease-in; } .link:focus { transition: color 0.15s ease-in; outline: 1px dotted currentColor; } /* LISTS http://tachyons.io/docs/elements/lists/ */ .list { list-style-type: none; } /* MAX WIDTHS Docs: http://tachyons.io/docs/layout/max-widths/ Base: mw = max-width Modifiers 1 = 1st step in width scale 2 = 2nd step in width scale 3 = 3rd step in width scale 4 = 4th step in width scale 5 = 5th step in width scale 6 = 6st step in width scale 7 = 7nd step in width scale 8 = 8rd step in width scale 9 = 9th step in width scale -100 = literal value 100% -none = string value none Media Query Extensions: -ns = not-small -m = medium -l = large */ /* Max Width Percentages */ .mw-100 { max-width: 100%; } /* Max Width Scale */ .mw1 { max-width: 1rem; } .mw2 { max-width: 2rem; } .mw3 { max-width: 4rem; } .mw4 { max-width: 8rem; } .mw5 { max-width: 16rem; } .mw6 { max-width: 32rem; } .mw7 { max-width: 48rem; } .mw8 { max-width: 64rem; } .mw9 { max-width: 96rem; } /* Max Width String Properties */ .mw-none { max-width: none; } /* WIDTHS Docs: http://tachyons.io/docs/layout/widths/ Base: w = width Modifiers 1 = 1st step in width scale 2 = 2nd step in width scale 3 = 3rd step in width scale 4 = 4th step in width scale 5 = 5th step in width scale -10 = literal value 10% -20 = literal value 20% -25 = literal value 25% -30 = literal value 30% -33 = literal value 33% -34 = literal value 34% -40 = literal value 40% -50 = literal value 50% -60 = literal value 60% -70 = literal value 70% -75 = literal value 75% -80 = literal value 80% -90 = literal value 90% -100 = literal value 100% -third = 100% / 3 (Not supported in opera mini or IE8) -two-thirds = 100% / 1.5 (Not supported in opera mini or IE8) -auto = string value auto Media Query Extensions: -ns = not-small -m = medium -l = large */ /* Width Scale */ .w1 { width: 1rem; } .w2 { width: 2rem; } .w3 { width: 4rem; } .w4 { width: 8rem; } .w5 { width: 16rem; } .w-10 { width: 10%; } .w-20 { width: 20%; } .w-25 { width: 25%; } .w-30 { width: 30%; } .w-33 { width: 33%; } .w-34 { width: 34%; } .w-40 { width: 40%; } .w-50 { width: 50%; } .w-60 { width: 60%; } .w-70 { width: 70%; } .w-75 { width: 75%; } .w-80 { width: 80%; } .w-90 { width: 90%; } .w-100 { width: 100%; } .w-third { width: calc(100% / 3); } .w-two-thirds { width: calc(100% / 1.5); } .w-auto { width: auto; } /* OVERFLOW Media Query Extensions: -ns = not-small -m = medium -l = large */ .overflow-visible { overflow: visible; } .overflow-hidden { overflow: hidden; } .overflow-scroll { overflow: scroll; } .overflow-auto { overflow: auto; } .overflow-x-visible { overflow-x: visible; } .overflow-x-hidden { overflow-x: hidden; } .overflow-x-scroll { overflow-x: scroll; } .overflow-x-auto { overflow-x: auto; } .overflow-y-visible { overflow-y: visible; } .overflow-y-hidden { overflow-y: hidden; } .overflow-y-scroll { overflow-y: scroll; } .overflow-y-auto { overflow-y: auto; } /* POSITIONING Docs: http://tachyons.io/docs/layout/position/ Media Query Extensions: -ns = not-small -m = medium -l = large */ .static { position: static; } .relative { position: relative; } .absolute { position: absolute; } .fixed { position: fixed; } /* OPACITY Docs: http://tachyons.io/docs/themes/opacity/ */ .o-100 { opacity: 1; } .o-90 { opacity: 0.9; } .o-80 { opacity: 0.8; } .o-70 { opacity: 0.7; } .o-60 { opacity: 0.6; } .o-50 { opacity: 0.5; } .o-40 { opacity: 0.4; } .o-30 { opacity: 0.3; } .o-20 { opacity: 0.2; } .o-10 { opacity: 0.1; } .o-05 { opacity: 0.05; } .o-025 { opacity: 0.025; } .o-0 { opacity: 0; } /* ROTATIONS */ .rotate-45 { -webkit-transform: rotate(45deg); transform: rotate(45deg); } .rotate-90 { -webkit-transform: rotate(90deg); transform: rotate(90deg); } .rotate-135 { -webkit-transform: rotate(135deg); transform: rotate(135deg); } .rotate-180 { -webkit-transform: rotate(180deg); transform: rotate(180deg); } .rotate-225 { -webkit-transform: rotate(225deg); transform: rotate(225deg); } .rotate-270 { -webkit-transform: rotate(270deg); transform: rotate(270deg); } .rotate-315 { -webkit-transform: rotate(315deg); transform: rotate(315deg); } /* SKINS Docs: http://tachyons.io/docs/themes/skins/ Classes for setting foreground and background colors on elements. If you haven't declared a border color, but set border on an element, it will be set to the current text color. */ /* Text colors */ .black-90 { color: rgba(0, 0, 0, 0.9); } .black-80 { color: rgba(0, 0, 0, 0.8); } .black-70 { color: rgba(0, 0, 0, 0.7); } .black-60 { color: rgba(0, 0, 0, 0.6); } .black-50 { color: rgba(0, 0, 0, 0.5); } .black-40 { color: rgba(0, 0, 0, 0.4); } .black-30 { color: rgba(0, 0, 0, 0.3); } .black-20 { color: rgba(0, 0, 0, 0.2); } .black-10 { color: rgba(0, 0, 0, 0.1); } .black-05 { color: rgba(0, 0, 0, 0.05); } .white-90 { color: rgba(255, 255, 255, 0.9); } .white-80 { color: rgba(255, 255, 255, 0.8); } .white-70 { color: rgba(255, 255, 255, 0.7); } .white-60 { color: rgba(255, 255, 255, 0.6); } .white-50 { color: rgba(255, 255, 255, 0.5); } .white-40 { color: rgba(255, 255, 255, 0.4); } .white-30 { color: rgba(255, 255, 255, 0.3); } .white-20 { color: rgba(255, 255, 255, 0.2); } .white-10 { color: rgba(255, 255, 255, 0.1); } .black { color: #000; } .near-black { color: #111; } .dark-gray { color: #333; } .mid-gray { color: #555; } .gray { color: #777; } .silver { color: #999; } .light-silver { color: #aaa; } .moon-gray { color: #ccc; } .light-gray { color: #eee; } .near-white { color: #f4f4f4; } .white { color: #fff; } .dark-red { color: #e7040f; } .red { color: #ff4136; } .light-red { color: #ff725c; } .orange { color: #ff6300; } .gold { color: #ffb700; } .yellow { color: #ffd700; } .light-yellow { color: #fbf1a9; } .purple { color: #5e2ca5; } .light-purple { color: #a463f2; } .dark-pink { color: #d5008f; } .hot-pink { color: #ff41b4; } .pink { color: #ff80cc; } .light-pink { color: #ffa3d7; } .dark-green { color: #137752; } .green { color: #19a974; } .light-green { color: #9eebcf; } .navy { color: #001b44; } .dark-blue { color: #00449e; } .blue { color: #357edd; } .light-blue { color: #96ccff; } .lightest-blue { color: #cdecff; } .washed-blue { color: #f6fffe; } .washed-green { color: #e8fdf5; } .washed-yellow { color: #fffceb; } .washed-red { color: #ffdfdf; } .color-inherit { color: inherit; } .bg-black-90 { background-color: rgba(0, 0, 0, 0.9); } .bg-black-80 { background-color: rgba(0, 0, 0, 0.8); } .bg-black-70 { background-color: rgba(0, 0, 0, 0.7); } .bg-black-60 { background-color: rgba(0, 0, 0, 0.6); } .bg-black-50 { background-color: rgba(0, 0, 0, 0.5); } .bg-black-40 { background-color: rgba(0, 0, 0, 0.4); } .bg-black-30 { background-color: rgba(0, 0, 0, 0.3); } .bg-black-20 { background-color: rgba(0, 0, 0, 0.2); } .bg-black-10 { background-color: rgba(0, 0, 0, 0.1); } .bg-black-05 { background-color: rgba(0, 0, 0, 0.05); } .bg-white-90 { background-color: rgba(255, 255, 255, 0.9); } .bg-white-80 { background-color: rgba(255, 255, 255, 0.8); } .bg-white-70 { background-color: rgba(255, 255, 255, 0.7); } .bg-white-60 { background-color: rgba(255, 255, 255, 0.6); } .bg-white-50 { background-color: rgba(255, 255, 255, 0.5); } .bg-white-40 { background-color: rgba(255, 255, 255, 0.4); } .bg-white-30 { background-color: rgba(255, 255, 255, 0.3); } .bg-white-20 { background-color: rgba(255, 255, 255, 0.2); } .bg-white-10 { background-color: rgba(255, 255, 255, 0.1); } /* Background colors */ .bg-black { background-color: #000; } .bg-near-black { background-color: #111; } .bg-dark-gray { background-color: #333; } .bg-mid-gray { background-color: #555; } .bg-gray { background-color: #777; } .bg-silver { background-color: #999; } .bg-light-silver { background-color: #aaa; } .bg-moon-gray { background-color: #ccc; } .bg-light-gray { background-color: #eee; } .bg-near-white { background-color: #f4f4f4; } .bg-white { background-color: #fff; } .bg-transparent { background-color: transparent; } .bg-dark-red { background-color: #e7040f; } .bg-red { background-color: #ff4136; } .bg-light-red { background-color: #ff725c; } .bg-orange { background-color: #ff6300; } .bg-gold { background-color: #ffb700; } .bg-yellow { background-color: #ffd700; } .bg-light-yellow { background-color: #fbf1a9; } .bg-purple { background-color: #5e2ca5; } .bg-light-purple { background-color: #a463f2; } .bg-dark-pink { background-color: #d5008f; } .bg-hot-pink { background-color: #ff41b4; } .bg-pink { background-color: #ff80cc; } .bg-light-pink { background-color: #ffa3d7; } .bg-dark-green { background-color: #137752; } .bg-green { background-color: #19a974; } .bg-light-green { background-color: #9eebcf; } .bg-navy { background-color: #001b44; } .bg-dark-blue { background-color: #00449e; } .bg-blue { background-color: #357edd; } .bg-light-blue { background-color: #96ccff; } .bg-lightest-blue { background-color: #cdecff; } .bg-washed-blue { background-color: #f6fffe; } .bg-washed-green { background-color: #e8fdf5; } .bg-washed-yellow { background-color: #fffceb; } .bg-washed-red { background-color: #ffdfdf; } .bg-inherit { background-color: inherit; } /* SKINS:PSEUDO Customize the color of an element when it is focused or hovered over. */ .hover-black:hover { color: #000; } .hover-black:focus { color: #000; } .hover-near-black:hover { color: #111; } .hover-near-black:focus { color: #111; } .hover-dark-gray:hover { color: #333; } .hover-dark-gray:focus { color: #333; } .hover-mid-gray:hover { color: #555; } .hover-mid-gray:focus { color: #555; } .hover-gray:hover { color: #777; } .hover-gray:focus { color: #777; } .hover-silver:hover { color: #999; } .hover-silver:focus { color: #999; } .hover-light-silver:hover { color: #aaa; } .hover-light-silver:focus { color: #aaa; } .hover-moon-gray:hover { color: #ccc; } .hover-moon-gray:focus { color: #ccc; } .hover-light-gray:hover { color: #eee; } .hover-light-gray:focus { color: #eee; } .hover-near-white:hover { color: #f4f4f4; } .hover-near-white:focus { color: #f4f4f4; } .hover-white:hover { color: #fff; } .hover-white:focus { color: #fff; } .hover-black-90:hover { color: rgba(0, 0, 0, 0.9); } .hover-black-90:focus { color: rgba(0, 0, 0, 0.9); } .hover-black-80:hover { color: rgba(0, 0, 0, 0.8); } .hover-black-80:focus { color: rgba(0, 0, 0, 0.8); } .hover-black-70:hover { color: rgba(0, 0, 0, 0.7); } .hover-black-70:focus { color: rgba(0, 0, 0, 0.7); } .hover-black-60:hover { color: rgba(0, 0, 0, 0.6); } .hover-black-60:focus { color: rgba(0, 0, 0, 0.6); } .hover-black-50:hover { color: rgba(0, 0, 0, 0.5); } .hover-black-50:focus { color: rgba(0, 0, 0, 0.5); } .hover-black-40:hover { color: rgba(0, 0, 0, 0.4); } .hover-black-40:focus { color: rgba(0, 0, 0, 0.4); } .hover-black-30:hover { color: rgba(0, 0, 0, 0.3); } .hover-black-30:focus { color: rgba(0, 0, 0, 0.3); } .hover-black-20:hover { color: rgba(0, 0, 0, 0.2); } .hover-black-20:focus { color: rgba(0, 0, 0, 0.2); } .hover-black-10:hover { color: rgba(0, 0, 0, 0.1); } .hover-black-10:focus { color: rgba(0, 0, 0, 0.1); } .hover-white-90:hover { color: rgba(255, 255, 255, 0.9); } .hover-white-90:focus { color: rgba(255, 255, 255, 0.9); } .hover-white-80:hover { color: rgba(255, 255, 255, 0.8); } .hover-white-80:focus { color: rgba(255, 255, 255, 0.8); } .hover-white-70:hover { color: rgba(255, 255, 255, 0.7); } .hover-white-70:focus { color: rgba(255, 255, 255, 0.7); } .hover-white-60:hover { color: rgba(255, 255, 255, 0.6); } .hover-white-60:focus { color: rgba(255, 255, 255, 0.6); } .hover-white-50:hover { color: rgba(255, 255, 255, 0.5); } .hover-white-50:focus { color: rgba(255, 255, 255, 0.5); } .hover-white-40:hover { color: rgba(255, 255, 255, 0.4); } .hover-white-40:focus { color: rgba(255, 255, 255, 0.4); } .hover-white-30:hover { color: rgba(255, 255, 255, 0.3); } .hover-white-30:focus { color: rgba(255, 255, 255, 0.3); } .hover-white-20:hover { color: rgba(255, 255, 255, 0.2); } .hover-white-20:focus { color: rgba(255, 255, 255, 0.2); } .hover-white-10:hover { color: rgba(255, 255, 255, 0.1); } .hover-white-10:focus { color: rgba(255, 255, 255, 0.1); } .hover-inherit:hover, .hover-inherit:focus { color: inherit; } .hover-bg-black:hover { background-color: #000; } .hover-bg-black:focus { background-color: #000; } .hover-bg-near-black:hover { background-color: #111; } .hover-bg-near-black:focus { background-color: #111; } .hover-bg-dark-gray:hover { background-color: #333; } .hover-bg-dark-gray:focus { background-color: #333; } .hover-bg-mid-gray:hover { background-color: #555; } .hover-bg-mid-gray:focus { background-color: #555; } .hover-bg-gray:hover { background-color: #777; } .hover-bg-gray:focus { background-color: #777; } .hover-bg-silver:hover { background-color: #999; } .hover-bg-silver:focus { background-color: #999; } .hover-bg-light-silver:hover { background-color: #aaa; } .hover-bg-light-silver:focus { background-color: #aaa; } .hover-bg-moon-gray:hover { background-color: #ccc; } .hover-bg-moon-gray:focus { background-color: #ccc; } .hover-bg-light-gray:hover { background-color: #eee; } .hover-bg-light-gray:focus { background-color: #eee; } .hover-bg-near-white:hover { background-color: #f4f4f4; } .hover-bg-near-white:focus { background-color: #f4f4f4; } .hover-bg-white:hover { background-color: #fff; } .hover-bg-white:focus { background-color: #fff; } .hover-bg-transparent:hover { background-color: transparent; } .hover-bg-transparent:focus { background-color: transparent; } .hover-bg-black-90:hover { background-color: rgba(0, 0, 0, 0.9); } .hover-bg-black-90:focus { background-color: rgba(0, 0, 0, 0.9); } .hover-bg-black-80:hover { background-color: rgba(0, 0, 0, 0.8); } .hover-bg-black-80:focus { background-color: rgba(0, 0, 0, 0.8); } .hover-bg-black-70:hover { background-color: rgba(0, 0, 0, 0.7); } .hover-bg-black-70:focus { background-color: rgba(0, 0, 0, 0.7); } .hover-bg-black-60:hover { background-color: rgba(0, 0, 0, 0.6); } .hover-bg-black-60:focus { background-color: rgba(0, 0, 0, 0.6); } .hover-bg-black-50:hover { background-color: rgba(0, 0, 0, 0.5); } .hover-bg-black-50:focus { background-color: rgba(0, 0, 0, 0.5); } .hover-bg-black-40:hover { background-color: rgba(0, 0, 0, 0.4); } .hover-bg-black-40:focus { background-color: rgba(0, 0, 0, 0.4); } .hover-bg-black-30:hover { background-color: rgba(0, 0, 0, 0.3); } .hover-bg-black-30:focus { background-color: rgba(0, 0, 0, 0.3); } .hover-bg-black-20:hover { background-color: rgba(0, 0, 0, 0.2); } .hover-bg-black-20:focus { background-color: rgba(0, 0, 0, 0.2); } .hover-bg-black-10:hover { background-color: rgba(0, 0, 0, 0.1); } .hover-bg-black-10:focus { background-color: rgba(0, 0, 0, 0.1); } .hover-bg-white-90:hover { background-color: rgba(255, 255, 255, 0.9); } .hover-bg-white-90:focus { background-color: rgba(255, 255, 255, 0.9); } .hover-bg-white-80:hover { background-color: rgba(255, 255, 255, 0.8); } .hover-bg-white-80:focus { background-color: rgba(255, 255, 255, 0.8); } .hover-bg-white-70:hover { background-color: rgba(255, 255, 255, 0.7); } .hover-bg-white-70:focus { background-color: rgba(255, 255, 255, 0.7); } .hover-bg-white-60:hover { background-color: rgba(255, 255, 255, 0.6); } .hover-bg-white-60:focus { background-color: rgba(255, 255, 255, 0.6); } .hover-bg-white-50:hover { background-color: rgba(255, 255, 255, 0.5); } .hover-bg-white-50:focus { background-color: rgba(255, 255, 255, 0.5); } .hover-bg-white-40:hover { background-color: rgba(255, 255, 255, 0.4); } .hover-bg-white-40:focus { background-color: rgba(255, 255, 255, 0.4); } .hover-bg-white-30:hover { background-color: rgba(255, 255, 255, 0.3); } .hover-bg-white-30:focus { background-color: rgba(255, 255, 255, 0.3); } .hover-bg-white-20:hover { background-color: rgba(255, 255, 255, 0.2); } .hover-bg-white-20:focus { background-color: rgba(255, 255, 255, 0.2); } .hover-bg-white-10:hover { background-color: rgba(255, 255, 255, 0.1); } .hover-bg-white-10:focus { background-color: rgba(255, 255, 255, 0.1); } .hover-dark-red:hover { color: #e7040f; } .hover-dark-red:focus { color: #e7040f; } .hover-red:hover { color: #ff4136; } .hover-red:focus { color: #ff4136; } .hover-light-red:hover { color: #ff725c; } .hover-light-red:focus { color: #ff725c; } .hover-orange:hover { color: #ff6300; } .hover-orange:focus { color: #ff6300; } .hover-gold:hover { color: #ffb700; } .hover-gold:focus { color: #ffb700; } .hover-yellow:hover { color: #ffd700; } .hover-yellow:focus { color: #ffd700; } .hover-light-yellow:hover { color: #fbf1a9; } .hover-light-yellow:focus { color: #fbf1a9; } .hover-purple:hover { color: #5e2ca5; } .hover-purple:focus { color: #5e2ca5; } .hover-light-purple:hover { color: #a463f2; } .hover-light-purple:focus { color: #a463f2; } .hover-dark-pink:hover { color: #d5008f; } .hover-dark-pink:focus { color: #d5008f; } .hover-hot-pink:hover { color: #ff41b4; } .hover-hot-pink:focus { color: #ff41b4; } .hover-pink:hover { color: #ff80cc; } .hover-pink:focus { color: #ff80cc; } .hover-light-pink:hover { color: #ffa3d7; } .hover-light-pink:focus { color: #ffa3d7; } .hover-dark-green:hover { color: #137752; } .hover-dark-green:focus { color: #137752; } .hover-green:hover { color: #19a974; } .hover-green:focus { color: #19a974; } .hover-light-green:hover { color: #9eebcf; } .hover-light-green:focus { color: #9eebcf; } .hover-navy:hover { color: #001b44; } .hover-navy:focus { color: #001b44; } .hover-dark-blue:hover { color: #00449e; } .hover-dark-blue:focus { color: #00449e; } .hover-blue:hover { color: #357edd; } .hover-blue:focus { color: #357edd; } .hover-light-blue:hover { color: #96ccff; } .hover-light-blue:focus { color: #96ccff; } .hover-lightest-blue:hover { color: #cdecff; } .hover-lightest-blue:focus { color: #cdecff; } .hover-washed-blue:hover { color: #f6fffe; } .hover-washed-blue:focus { color: #f6fffe; } .hover-washed-green:hover { color: #e8fdf5; } .hover-washed-green:focus { color: #e8fdf5; } .hover-washed-yellow:hover { color: #fffceb; } .hover-washed-yellow:focus { color: #fffceb; } .hover-washed-red:hover { color: #ffdfdf; } .hover-washed-red:focus { color: #ffdfdf; } .hover-bg-dark-red:hover { background-color: #e7040f; } .hover-bg-dark-red:focus { background-color: #e7040f; } .hover-bg-red:hover { background-color: #ff4136; } .hover-bg-red:focus { background-color: #ff4136; } .hover-bg-light-red:hover { background-color: #ff725c; } .hover-bg-light-red:focus { background-color: #ff725c; } .hover-bg-orange:hover { background-color: #ff6300; } .hover-bg-orange:focus { background-color: #ff6300; } .hover-bg-gold:hover { background-color: #ffb700; } .hover-bg-gold:focus { background-color: #ffb700; } .hover-bg-yellow:hover { background-color: #ffd700; } .hover-bg-yellow:focus { background-color: #ffd700; } .hover-bg-light-yellow:hover { background-color: #fbf1a9; } .hover-bg-light-yellow:focus { background-color: #fbf1a9; } .hover-bg-purple:hover { background-color: #5e2ca5; } .hover-bg-purple:focus { background-color: #5e2ca5; } .hover-bg-light-purple:hover { background-color: #a463f2; } .hover-bg-light-purple:focus { background-color: #a463f2; } .hover-bg-dark-pink:hover { background-color: #d5008f; } .hover-bg-dark-pink:focus { background-color: #d5008f; } .hover-bg-hot-pink:hover { background-color: #ff41b4; } .hover-bg-hot-pink:focus { background-color: #ff41b4; } .hover-bg-pink:hover { background-color: #ff80cc; } .hover-bg-pink:focus { background-color: #ff80cc; } .hover-bg-light-pink:hover { background-color: #ffa3d7; } .hover-bg-light-pink:focus { background-color: #ffa3d7; } .hover-bg-dark-green:hover { background-color: #137752; } .hover-bg-dark-green:focus { background-color: #137752; } .hover-bg-green:hover { background-color: #19a974; } .hover-bg-green:focus { background-color: #19a974; } .hover-bg-light-green:hover { background-color: #9eebcf; } .hover-bg-light-green:focus { background-color: #9eebcf; } .hover-bg-navy:hover { background-color: #001b44; } .hover-bg-navy:focus { background-color: #001b44; } .hover-bg-dark-blue:hover { background-color: #00449e; } .hover-bg-dark-blue:focus { background-color: #00449e; } .hover-bg-blue:hover { background-color: #357edd; } .hover-bg-blue:focus { background-color: #357edd; } .hover-bg-light-blue:hover { background-color: #96ccff; } .hover-bg-light-blue:focus { background-color: #96ccff; } .hover-bg-lightest-blue:hover { background-color: #cdecff; } .hover-bg-lightest-blue:focus { background-color: #cdecff; } .hover-bg-washed-blue:hover { background-color: #f6fffe; } .hover-bg-washed-blue:focus { background-color: #f6fffe; } .hover-bg-washed-green:hover { background-color: #e8fdf5; } .hover-bg-washed-green:focus { background-color: #e8fdf5; } .hover-bg-washed-yellow:hover { background-color: #fffceb; } .hover-bg-washed-yellow:focus { background-color: #fffceb; } .hover-bg-washed-red:hover { background-color: #ffdfdf; } .hover-bg-washed-red:focus { background-color: #ffdfdf; } .hover-bg-inherit:hover, .hover-bg-inherit:focus { background-color: inherit; } /* Variables */ /* SPACING Docs: http://tachyons.io/docs/layout/spacing/ An eight step powers of two scale ranging from 0 to 16rem. Base: p = padding m = margin Modifiers: a = all h = horizontal v = vertical t = top r = right b = bottom l = left 0 = none 1 = 1st step in spacing scale 2 = 2nd step in spacing scale 3 = 3rd step in spacing scale 4 = 4th step in spacing scale 5 = 5th step in spacing scale 6 = 6th step in spacing scale 7 = 7th step in spacing scale Media Query Extensions: -ns = not-small -m = medium -l = large */ .pa0 { padding: 0; } .pa1 { padding: 0.25rem; } .pa2 { padding: 0.5rem; } .pa3 { padding: 1rem; } .pa4 { padding: 2rem; } .pa5 { padding: 4rem; } .pa6 { padding: 8rem; } .pa7 { padding: 16rem; } .pl0 { padding-left: 0; } .pl1 { padding-left: 0.25rem; } .pl2 { padding-left: 0.5rem; } .pl3 { padding-left: 1rem; } .pl4 { padding-left: 2rem; } .pl5 { padding-left: 4rem; } .pl6 { padding-left: 8rem; } .pl7 { padding-left: 16rem; } .pr0 { padding-right: 0; } .pr1 { padding-right: 0.25rem; } .pr2 { padding-right: 0.5rem; } .pr3 { padding-right: 1rem; } .pr4 { padding-right: 2rem; } .pr5 { padding-right: 4rem; } .pr6 { padding-right: 8rem; } .pr7 { padding-right: 16rem; } .pb0 { padding-bottom: 0; } .pb1 { padding-bottom: 0.25rem; } .pb2 { padding-bottom: 0.5rem; } .pb3 { padding-bottom: 1rem; } .pb4 { padding-bottom: 2rem; } .pb5 { padding-bottom: 4rem; } .pb6 { padding-bottom: 8rem; } .pb7 { padding-bottom: 16rem; } .pt0 { padding-top: 0; } .pt1 { padding-top: 0.25rem; } .pt2 { padding-top: 0.5rem; } .pt3 { padding-top: 1rem; } .pt4 { padding-top: 2rem; } .pt5 { padding-top: 4rem; } .pt6 { padding-top: 8rem; } .pt7 { padding-top: 16rem; } .pv0 { padding-top: 0; padding-bottom: 0; } .pv1 { padding-top: 0.25rem; padding-bottom: 0.25rem; } .pv2 { padding-top: 0.5rem; padding-bottom: 0.5rem; } .pv3 { padding-top: 1rem; padding-bottom: 1rem; } .pv4 { padding-top: 2rem; padding-bottom: 2rem; } .pv5 { padding-top: 4rem; padding-bottom: 4rem; } .pv6 { padding-top: 8rem; padding-bottom: 8rem; } .pv7 { padding-top: 16rem; padding-bottom: 16rem; } .ph0 { padding-left: 0; padding-right: 0; } .ph1 { padding-left: 0.25rem; padding-right: 0.25rem; } .ph2 { padding-left: 0.5rem; padding-right: 0.5rem; } .ph3 { padding-left: 1rem; padding-right: 1rem; } .ph4 { padding-left: 2rem; padding-right: 2rem; } .ph5 { padding-left: 4rem; padding-right: 4rem; } .ph6 { padding-left: 8rem; padding-right: 8rem; } .ph7 { padding-left: 16rem; padding-right: 16rem; } .ma0 { margin: 0; } .ma1 { margin: 0.25rem; } .ma2 { margin: 0.5rem; } .ma3 { margin: 1rem; } .ma4 { margin: 2rem; } .ma5 { margin: 4rem; } .ma6 { margin: 8rem; } .ma7 { margin: 16rem; } .ml0 { margin-left: 0; } .ml1 { margin-left: 0.25rem; } .ml2 { margin-left: 0.5rem; } .ml3 { margin-left: 1rem; } .ml4 { margin-left: 2rem; } .ml5 { margin-left: 4rem; } .ml6 { margin-left: 8rem; } .ml7 { margin-left: 16rem; } .mr0 { margin-right: 0; } .mr1 { margin-right: 0.25rem; } .mr2 { margin-right: 0.5rem; } .mr3 { margin-right: 1rem; } .mr4 { margin-right: 2rem; } .mr5 { margin-right: 4rem; } .mr6 { margin-right: 8rem; } .mr7 { margin-right: 16rem; } .mb0 { margin-bottom: 0; } .mb1 { margin-bottom: 0.25rem; } .mb2 { margin-bottom: 0.5rem; } .mb3 { margin-bottom: 1rem; } .mb4 { margin-bottom: 2rem; } .mb5 { margin-bottom: 4rem; } .mb6 { margin-bottom: 8rem; } .mb7 { margin-bottom: 16rem; } .mt0 { margin-top: 0; } .mt1 { margin-top: 0.25rem; } .mt2 { margin-top: 0.5rem; } .mt3 { margin-top: 1rem; } .mt4 { margin-top: 2rem; } .mt5 { margin-top: 4rem; } .mt6 { margin-top: 8rem; } .mt7 { margin-top: 16rem; } .mv0 { margin-top: 0; margin-bottom: 0; } .mv1 { margin-top: 0.25rem; margin-bottom: 0.25rem; } .mv2 { margin-top: 0.5rem; margin-bottom: 0.5rem; } .mv3 { margin-top: 1rem; margin-bottom: 1rem; } .mv4 { margin-top: 2rem; margin-bottom: 2rem; } .mv5 { margin-top: 4rem; margin-bottom: 4rem; } .mv6 { margin-top: 8rem; margin-bottom: 8rem; } .mv7 { margin-top: 16rem; margin-bottom: 16rem; } .mh0 { margin-left: 0; margin-right: 0; } .mh1 { margin-left: 0.25rem; margin-right: 0.25rem; } .mh2 { margin-left: 0.5rem; margin-right: 0.5rem; } .mh3 { margin-left: 1rem; margin-right: 1rem; } .mh4 { margin-left: 2rem; margin-right: 2rem; } .mh5 { margin-left: 4rem; margin-right: 4rem; } .mh6 { margin-left: 8rem; margin-right: 8rem; } .mh7 { margin-left: 16rem; margin-right: 16rem; } /* NEGATIVE MARGINS Base: n = negative Modifiers: a = all t = top r = right b = bottom l = left 1 = 1st step in spacing scale 2 = 2nd step in spacing scale 3 = 3rd step in spacing scale 4 = 4th step in spacing scale 5 = 5th step in spacing scale 6 = 6th step in spacing scale 7 = 7th step in spacing scale Media Query Extensions: -ns = not-small -m = medium -l = large */ .na1 { margin: -0.25rem; } .na2 { margin: -0.5rem; } .na3 { margin: -1rem; } .na4 { margin: -2rem; } .na5 { margin: -4rem; } .na6 { margin: -8rem; } .na7 { margin: -16rem; } .nl1 { margin-left: -0.25rem; } .nl2 { margin-left: -0.5rem; } .nl3 { margin-left: -1rem; } .nl4 { margin-left: -2rem; } .nl5 { margin-left: -4rem; } .nl6 { margin-left: -8rem; } .nl7 { margin-left: -16rem; } .nr1 { margin-right: -0.25rem; } .nr2 { margin-right: -0.5rem; } .nr3 { margin-right: -1rem; } .nr4 { margin-right: -2rem; } .nr5 { margin-right: -4rem; } .nr6 { margin-right: -8rem; } .nr7 { margin-right: -16rem; } .nb1 { margin-bottom: -0.25rem; } .nb2 { margin-bottom: -0.5rem; } .nb3 { margin-bottom: -1rem; } .nb4 { margin-bottom: -2rem; } .nb5 { margin-bottom: -4rem; } .nb6 { margin-bottom: -8rem; } .nb7 { margin-bottom: -16rem; } .nt1 { margin-top: -0.25rem; } .nt2 { margin-top: -0.5rem; } .nt3 { margin-top: -1rem; } .nt4 { margin-top: -2rem; } .nt5 { margin-top: -4rem; } .nt6 { margin-top: -8rem; } .nt7 { margin-top: -16rem; } /* TABLES Docs: http://tachyons.io/docs/elements/tables/ */ .collapse { border-collapse: collapse; border-spacing: 0; } .striped--light-silver:nth-child(odd) { background-color: #aaa; } .striped--moon-gray:nth-child(odd) { background-color: #ccc; } .striped--light-gray:nth-child(odd) { background-color: #eee; } .striped--near-white:nth-child(odd) { background-color: #f4f4f4; } .stripe-light:nth-child(odd) { background-color: rgba(255, 255, 255, 0.1); } .stripe-dark:nth-child(odd) { background-color: rgba(0, 0, 0, 0.1); } /* TEXT DECORATION Docs: http://tachyons.io/docs/typography/text-decoration/ Media Query Extensions: -ns = not-small -m = medium -l = large */ .strike { text-decoration: line-through; } .underline { text-decoration: underline; } .no-underline { text-decoration: none; } /* TEXT ALIGN Docs: http://tachyons.io/docs/typography/text-align/ Base t = text-align Modifiers l = left r = right c = center j = justify Media Query Extensions: -ns = not-small -m = medium -l = large */ .tl { text-align: left; } .tr { text-align: right; } .tc { text-align: center; } .tj { text-align: justify; } /* TEXT TRANSFORM Docs: http://tachyons.io/docs/typography/text-transform/ Base: tt = text-transform Modifiers c = capitalize l = lowercase u = uppercase n = none Media Query Extensions: -ns = not-small -m = medium -l = large */ .ttc { text-transform: capitalize; } .ttl { text-transform: lowercase; } .ttu { text-transform: uppercase; } .ttn { text-transform: none; } /* TYPE SCALE Docs: http://tachyons.io/docs/typography/scale/ Base: f = font-size Modifiers 1 = 1st step in size scale 2 = 2nd step in size scale 3 = 3rd step in size scale 4 = 4th step in size scale 5 = 5th step in size scale 6 = 6th step in size scale 7 = 7th step in size scale Media Query Extensions: -ns = not-small -m = medium -l = large */ /* * For Hero/Marketing Titles * * These generally are too large for mobile * so be careful using them on smaller screens. * */ .f-6, .f-headline { font-size: 6rem; } .f-5, .f-subheadline { font-size: 5rem; } /* Type Scale */ .f1 { font-size: 3rem; } .f2 { font-size: 2.25rem; } .f3 { font-size: 1.5rem; } .f4 { font-size: 1.25rem; } .f5 { font-size: 1rem; } .f6 { font-size: 0.875rem; } .f7 { font-size: 0.75rem; } /* Small and hard to read for many people so use with extreme caution */ /* TYPOGRAPHY http://tachyons.io/docs/typography/measure/ Media Query Extensions: -ns = not-small -m = medium -l = large */ /* Measure is limited to ~66 characters */ .measure { max-width: 30em; } /* Measure is limited to ~80 characters */ .measure-wide { max-width: 34em; } /* Measure is limited to ~45 characters */ .measure-narrow { max-width: 20em; } /* Book paragraph style - paragraphs are indented with no vertical spacing. */ .indent { text-indent: 1em; margin-top: 0; margin-bottom: 0; } .small-caps { font-variant: small-caps; } /* Combine this class with a width to truncate text (or just leave as is to truncate at width of containing element. */ .truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* UTILITIES Media Query Extensions: -ns = not-small -m = medium -l = large */ /* Equivalent to .overflow-y-scroll */ .overflow-container { overflow-y: scroll; } .center { margin-right: auto; margin-left: auto; } .mr-auto { margin-right: auto; } .ml-auto { margin-left: auto; } /* VISIBILITY Media Query Extensions: -ns = not-small -m = medium -l = large */ /* Text that is hidden but accessible Ref: http://snook.ca/archives/html_and_css/hiding-content-for-accessibility */ .clip { position: fixed !important; _position: absolute !important; clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ clip: rect(1px, 1px, 1px, 1px); } /* WHITE SPACE Media Query Extensions: -ns = not-small -m = medium -l = large */ .ws-normal { white-space: normal; } .nowrap { white-space: nowrap; } .pre { white-space: pre; } /* VERTICAL ALIGN Media Query Extensions: -ns = not-small -m = medium -l = large */ .v-base { vertical-align: baseline; } .v-mid { vertical-align: middle; } .v-top { vertical-align: top; } .v-btm { vertical-align: bottom; } /* HOVER EFFECTS Docs: http://tachyons.io/docs/themes/hovers/ - Dim - Glow - Hide Child - Underline text - Grow - Pointer - Shadow */ /* Dim element on hover by adding the dim class. */ .dim { opacity: 1; transition: opacity 0.15s ease-in; } .dim:hover, .dim:focus { opacity: 0.5; transition: opacity 0.15s ease-in; } .dim:active { opacity: 0.8; transition: opacity 0.15s ease-out; } /* Animate opacity to 100% on hover by adding the glow class. */ .glow { transition: opacity 0.15s ease-in; } .glow:hover, .glow:focus { opacity: 1; transition: opacity 0.15s ease-in; } /* Hide child & reveal on hover: Put the hide-child class on a parent element and any nested element with the child class will be hidden and displayed on hover or focus.
Hidden until hover or focus
Hidden until hover or focus
Hidden until hover or focus
Hidden until hover or focus
*/ .hide-child .child { opacity: 0; transition: opacity 0.15s ease-in; } .hide-child:hover .child, .hide-child:focus .child, .hide-child:active .child { opacity: 1; transition: opacity 0.15s ease-in; } .underline-hover:hover, .underline-hover:focus { text-decoration: underline; } /* Can combine this with overflow-hidden to make background images grow on hover * even if you are using background-size: cover */ .grow { -moz-osx-font-smoothing: grayscale; -webkit-backface-visibility: hidden; backface-visibility: hidden; -webkit-transform: translateZ(0); transform: translateZ(0); transition: -webkit-transform 0.25s ease-out; transition: transform 0.25s ease-out; transition: transform 0.25s ease-out, -webkit-transform 0.25s ease-out; } .grow:hover, .grow:focus { -webkit-transform: scale(1.05); transform: scale(1.05); } .grow:active { -webkit-transform: scale(0.9); transform: scale(0.9); } .grow-large { -moz-osx-font-smoothing: grayscale; -webkit-backface-visibility: hidden; backface-visibility: hidden; -webkit-transform: translateZ(0); transform: translateZ(0); transition: -webkit-transform 0.25s ease-in-out; transition: transform 0.25s ease-in-out; transition: transform 0.25s ease-in-out, -webkit-transform 0.25s ease-in-out; } .grow-large:hover, .grow-large:focus { -webkit-transform: scale(1.2); transform: scale(1.2); } .grow-large:active { -webkit-transform: scale(0.95); transform: scale(0.95); } /* Add pointer on hover */ .pointer:hover { cursor: pointer; } /* Add shadow on hover. Performant box-shadow animation pattern from http://tobiasahlin.com/blog/how-to-animate-box-shadow/ */ .shadow-hover { cursor: pointer; position: relative; transition: all 0.5s cubic-bezier(0.165, 0.84, 0.44, 1); } .shadow-hover::after { content: ""; box-shadow: 0 0 16px 2px rgba(0, 0, 0, 0.2); border-radius: inherit; opacity: 0; position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; transition: opacity 0.5s cubic-bezier(0.165, 0.84, 0.44, 1); } .shadow-hover:hover::after, .shadow-hover:focus::after { opacity: 1; } /* Combine with classes in skins and skins-pseudo for * many different transition possibilities. */ .bg-animate, .bg-animate:hover, .bg-animate:focus { transition: background-color 0.15s ease-in-out; } /* Z-INDEX Base z = z-index Modifiers -0 = literal value 0 -1 = literal value 1 -2 = literal value 2 -3 = literal value 3 -4 = literal value 4 -5 = literal value 5 -999 = literal value 999 -9999 = literal value 9999 -max = largest accepted z-index value as integer -inherit = string value inherit -initial = string value initial -unset = string value unset MDN: https://developer.mozilla.org/en/docs/Web/CSS/z-index Spec: http://www.w3.org/TR/CSS2/zindex.html Articles: https://philipwalton.com/articles/what-no-one-told-you-about-z-index/ Tips on extending: There might be a time worth using negative z-index values. Or if you are using tachyons with another project, you might need to adjust these values to suit your needs. */ .z-0 { z-index: 0; } .z-1 { z-index: 1; } .z-2 { z-index: 2; } .z-3 { z-index: 3; } .z-4 { z-index: 4; } .z-5 { z-index: 5; } .z-999 { z-index: 999; } .z-9999 { z-index: 9999; } .z-max { z-index: 2147483647; } .z-inherit { z-index: inherit; } .z-initial { z-index: initial; } .z-unset { z-index: unset; } /* NESTED Tachyons module for styling nested elements that are generated by a cms. */ .nested-copy-line-height p, .nested-copy-line-height ul, .nested-copy-line-height ol { line-height: 1.5; } .nested-headline-line-height h1, .nested-headline-line-height h2, .nested-headline-line-height h3, .nested-headline-line-height h4, .nested-headline-line-height h5, .nested-headline-line-height h6 { line-height: 1.25; } .nested-list-reset ul, .nested-list-reset ol { padding-left: 0; margin-left: 0; list-style-type: none; } .nested-copy-indent p + p { text-indent: 1em; margin-top: 0; margin-bottom: 0; } .nested-copy-separator p + p { margin-top: 1.5em; } .nested-img img { width: 100%; max-width: 100%; display: block; } .nested-links a { color: #357edd; transition: color 0.15s ease-in; } .nested-links a:hover { color: #96ccff; transition: color 0.15s ease-in; } .nested-links a:focus { color: #96ccff; transition: color 0.15s ease-in; } /* STYLES Add custom styles here. */ /* Variables */ /* Importing here will allow you to override any variables in the modules */ /* Tachyons COLOR VARIABLES Grayscale - Solids - Transparencies Colors */ /* CUSTOM MEDIA QUERIES Media query values can be changed to fit your own content. There are no magic bullets when it comes to media query width values. They should be declared in em units - and they should be set to meet the needs of your content. You can also add additional media queries, or remove some of the existing ones. These media queries can be referenced like so: @media (--breakpoint-not-small) { .medium-and-larger-specific-style { background-color: red; } } @media (--breakpoint-medium) { .medium-screen-specific-style { background-color: red; } } @media (--breakpoint-large) { .large-and-larger-screen-specific-style { background-color: red; } } */ /* Media Queries */ /* Debugging */ /* DEBUG CHILDREN Docs: http://tachyons.io/docs/debug/ Just add the debug class to any element to see outlines on its children. */ .debug * { outline: 1px solid gold; } .debug-white * { outline: 1px solid white; } .debug-black * { outline: 1px solid black; } /* DEBUG GRID http://tachyons.io/docs/debug-grid/ Can be useful for debugging layout issues or helping to make sure things line up perfectly. Just tack one of these classes onto a parent element. */ .debug-grid { background: transparent url( data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAFElEQVR4AWPAC97/9x0eCsAEPgwAVLshdpENIxcAAAAASUVORK5CYII= ) repeat top left; } .debug-grid-16 { background: transparent url( data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMklEQVR4AWOgCLz/b0epAa6UGuBOqQHOQHLUgFEDnAbcBZ4UGwDOkiCnkIhdgNgNxAYAiYlD+8sEuo8AAAAASUVORK5CYII= ) repeat top left; } .debug-grid-8-solid { background: white url( data:image/gif;base64,R0lGODdhCAAIAPEAAADw/wDx/////wAAACwAAAAACAAIAAACDZQvgaeb/lxbAIKA8y0AOw== ) repeat top left; } .debug-grid-16-solid { background: white url( data:image/gif;base64,R0lGODdhEAAQAPEAAADw/wDx/xXy/////ywAAAAAEAAQAAACIZyPKckYDQFsb6ZqD85jZ2+BkwiRFKehhqQCQgDHcgwEBQA7 ) repeat top left; } /* Uncomment out the line below to help debug layout issues */ /* @import './_debug'; */ @media screen and (min-width: 30em) { .aspect-ratio-ns { height: 0; position: relative; } .aspect-ratio--16x9-ns { padding-bottom: 56.25%; } .aspect-ratio--9x16-ns { padding-bottom: 177.77%; } .aspect-ratio--4x3-ns { padding-bottom: 75%; } .aspect-ratio--3x4-ns { padding-bottom: 133.33%; } .aspect-ratio--6x4-ns { padding-bottom: 66.6%; } .aspect-ratio--4x6-ns { padding-bottom: 150%; } .aspect-ratio--8x5-ns { padding-bottom: 62.5%; } .aspect-ratio--5x8-ns { padding-bottom: 160%; } .aspect-ratio--7x5-ns { padding-bottom: 71.42%; } .aspect-ratio--5x7-ns { padding-bottom: 140%; } .aspect-ratio--1x1-ns { padding-bottom: 100%; } .aspect-ratio--object-ns { position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%; z-index: 100; } .cover-ns { background-size: cover !important; } .contain-ns { background-size: contain !important; } .bg-center-ns { background-repeat: no-repeat; background-position: center center; } .bg-top-ns { background-repeat: no-repeat; background-position: top center; } .bg-right-ns { background-repeat: no-repeat; background-position: center right; } .bg-bottom-ns { background-repeat: no-repeat; background-position: bottom center; } .bg-left-ns { background-repeat: no-repeat; background-position: center left; } .outline-ns { outline: 1px solid; } .outline-transparent-ns { outline: 1px solid transparent; } .outline-0-ns { outline: 0; } .ba-ns { border-style: solid; border-width: 1px; } .bt-ns { border-top-style: solid; border-top-width: 1px; } .br-ns { border-right-style: solid; border-right-width: 1px; } .bb-ns { border-bottom-style: solid; border-bottom-width: 1px; } .bl-ns { border-left-style: solid; border-left-width: 1px; } .bn-ns { border-style: none; border-width: 0; } .br0-ns { border-radius: 0; } .br1-ns { border-radius: 0.125rem; } .br2-ns { border-radius: 0.25rem; } .br3-ns { border-radius: 0.5rem; } .br4-ns { border-radius: 1rem; } .br-100-ns { border-radius: 100%; } .br-pill-ns { border-radius: 9999px; } .br--bottom-ns { border-top-left-radius: 0; border-top-right-radius: 0; } .br--top-ns { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } .br--right-ns { border-top-left-radius: 0; border-bottom-left-radius: 0; } .br--left-ns { border-top-right-radius: 0; border-bottom-right-radius: 0; } .b--dotted-ns { border-style: dotted; } .b--dashed-ns { border-style: dashed; } .b--solid-ns { border-style: solid; } .b--none-ns { border-style: none; } .bw0-ns { border-width: 0; } .bw1-ns { border-width: 0.125rem; } .bw2-ns { border-width: 0.25rem; } .bw3-ns { border-width: 0.5rem; } .bw4-ns { border-width: 1rem; } .bw5-ns { border-width: 2rem; } .bt-0-ns { border-top-width: 0; } .br-0-ns { border-right-width: 0; } .bb-0-ns { border-bottom-width: 0; } .bl-0-ns { border-left-width: 0; } .shadow-1-ns { box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.2); } .shadow-2-ns { box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.2); } .shadow-3-ns { box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, 0.2); } .shadow-4-ns { box-shadow: 2px 2px 8px 0 rgba(0, 0, 0, 0.2); } .shadow-5-ns { box-shadow: 4px 4px 8px 0 rgba(0, 0, 0, 0.2); } .top-0-ns { top: 0; } .left-0-ns { left: 0; } .right-0-ns { right: 0; } .bottom-0-ns { bottom: 0; } .top-1-ns { top: 1rem; } .left-1-ns { left: 1rem; } .right-1-ns { right: 1rem; } .bottom-1-ns { bottom: 1rem; } .top-2-ns { top: 2rem; } .left-2-ns { left: 2rem; } .right-2-ns { right: 2rem; } .bottom-2-ns { bottom: 2rem; } .top--1-ns { top: -1rem; } .right--1-ns { right: -1rem; } .bottom--1-ns { bottom: -1rem; } .left--1-ns { left: -1rem; } .top--2-ns { top: -2rem; } .right--2-ns { right: -2rem; } .bottom--2-ns { bottom: -2rem; } .left--2-ns { left: -2rem; } .absolute--fill-ns { top: 0; right: 0; bottom: 0; left: 0; } .cl-ns { clear: left; } .cr-ns { clear: right; } .cb-ns { clear: both; } .cn-ns { clear: none; } .dn-ns { display: none; } .di-ns { display: inline; } .db-ns { display: block; } .dib-ns { display: inline-block; } .dit-ns { display: inline-table; } .dt-ns { display: table; } .dtc-ns { display: table-cell; } .dt-row-ns { display: table-row; } .dt-row-group-ns { display: table-row-group; } .dt-column-ns { display: table-column; } .dt-column-group-ns { display: table-column-group; } .dt--fixed-ns { table-layout: fixed; width: 100%; } .flex-ns { display: flex; } .inline-flex-ns { display: inline-flex; } .flex-auto-ns { flex: 1 1 auto; min-width: 0; /* 1 */ min-height: 0; /* 1 */ } .flex-none-ns { flex: none; } .flex-column-ns { flex-direction: column; } .flex-row-ns { flex-direction: row; } .flex-wrap-ns { flex-wrap: wrap; } .flex-nowrap-ns { flex-wrap: nowrap; } .flex-wrap-reverse-ns { flex-wrap: wrap-reverse; } .flex-column-reverse-ns { flex-direction: column-reverse; } .flex-row-reverse-ns { flex-direction: row-reverse; } .items-start-ns { align-items: flex-start; } .items-end-ns { align-items: flex-end; } .items-center-ns { align-items: center; } .items-baseline-ns { align-items: baseline; } .items-stretch-ns { align-items: stretch; } .self-start-ns { align-self: flex-start; } .self-end-ns { align-self: flex-end; } .self-center-ns { align-self: center; } .self-baseline-ns { align-self: baseline; } .self-stretch-ns { align-self: stretch; } .justify-start-ns { justify-content: flex-start; } .justify-end-ns { justify-content: flex-end; } .justify-center-ns { justify-content: center; } .justify-between-ns { justify-content: space-between; } .justify-around-ns { justify-content: space-around; } .content-start-ns { align-content: flex-start; } .content-end-ns { align-content: flex-end; } .content-center-ns { align-content: center; } .content-between-ns { align-content: space-between; } .content-around-ns { align-content: space-around; } .content-stretch-ns { align-content: stretch; } .order-0-ns { order: 0; } .order-1-ns { order: 1; } .order-2-ns { order: 2; } .order-3-ns { order: 3; } .order-4-ns { order: 4; } .order-5-ns { order: 5; } .order-6-ns { order: 6; } .order-7-ns { order: 7; } .order-8-ns { order: 8; } .order-last-ns { order: 99999; } .flex-grow-0-ns { flex-grow: 0; } .flex-grow-1-ns { flex-grow: 1; } .flex-shrink-0-ns { flex-shrink: 0; } .flex-shrink-1-ns { flex-shrink: 1; } .fl-ns { float: left; _display: inline; } .fr-ns { float: right; _display: inline; } .fn-ns { float: none; } .i-ns { font-style: italic; } .fs-normal-ns { font-style: normal; } .normal-ns { font-weight: normal; } .b-ns { font-weight: bold; } .fw1-ns { font-weight: 100; } .fw2-ns { font-weight: 200; } .fw3-ns { font-weight: 300; } .fw4-ns { font-weight: 400; } .fw5-ns { font-weight: 500; } .fw6-ns { font-weight: 600; } .fw7-ns { font-weight: 700; } .fw8-ns { font-weight: 800; } .fw9-ns { font-weight: 900; } .h1-ns { height: 1rem; } .h2-ns { height: 2rem; } .h3-ns { height: 4rem; } .h4-ns { height: 8rem; } .h5-ns { height: 16rem; } .h-25-ns { height: 25%; } .h-50-ns { height: 50%; } .h-75-ns { height: 75%; } .h-100-ns { height: 100%; } .min-h-100-ns { min-height: 100%; } .vh-25-ns { height: 25vh; } .vh-50-ns { height: 50vh; } .vh-75-ns { height: 75vh; } .vh-100-ns { height: 100vh; } .min-vh-100-ns { min-height: 100vh; } .h-auto-ns { height: auto; } .h-inherit-ns { height: inherit; } .tracked-ns { letter-spacing: 0.1em; } .tracked-tight-ns { letter-spacing: -0.05em; } .tracked-mega-ns { letter-spacing: 0.25em; } .lh-solid-ns { line-height: 1; } .lh-title-ns { line-height: 1.25; } .lh-copy-ns { line-height: 1.5; } .mw-100-ns { max-width: 100%; } .mw1-ns { max-width: 1rem; } .mw2-ns { max-width: 2rem; } .mw3-ns { max-width: 4rem; } .mw4-ns { max-width: 8rem; } .mw5-ns { max-width: 16rem; } .mw6-ns { max-width: 32rem; } .mw7-ns { max-width: 48rem; } .mw8-ns { max-width: 64rem; } .mw9-ns { max-width: 96rem; } .mw-none-ns { max-width: none; } .w1-ns { width: 1rem; } .w2-ns { width: 2rem; } .w3-ns { width: 4rem; } .w4-ns { width: 8rem; } .w5-ns { width: 16rem; } .w-10-ns { width: 10%; } .w-20-ns { width: 20%; } .w-25-ns { width: 25%; } .w-30-ns { width: 30%; } .w-33-ns { width: 33%; } .w-34-ns { width: 34%; } .w-40-ns { width: 40%; } .w-50-ns { width: 50%; } .w-60-ns { width: 60%; } .w-70-ns { width: 70%; } .w-75-ns { width: 75%; } .w-80-ns { width: 80%; } .w-90-ns { width: 90%; } .w-100-ns { width: 100%; } .w-third-ns { width: calc(100% / 3); } .w-two-thirds-ns { width: calc(100% / 1.5); } .w-auto-ns { width: auto; } .overflow-visible-ns { overflow: visible; } .overflow-hidden-ns { overflow: hidden; } .overflow-scroll-ns { overflow: scroll; } .overflow-auto-ns { overflow: auto; } .overflow-x-visible-ns { overflow-x: visible; } .overflow-x-hidden-ns { overflow-x: hidden; } .overflow-x-scroll-ns { overflow-x: scroll; } .overflow-x-auto-ns { overflow-x: auto; } .overflow-y-visible-ns { overflow-y: visible; } .overflow-y-hidden-ns { overflow-y: hidden; } .overflow-y-scroll-ns { overflow-y: scroll; } .overflow-y-auto-ns { overflow-y: auto; } .static-ns { position: static; } .relative-ns { position: relative; } .absolute-ns { position: absolute; } .fixed-ns { position: fixed; } .rotate-45-ns { -webkit-transform: rotate(45deg); transform: rotate(45deg); } .rotate-90-ns { -webkit-transform: rotate(90deg); transform: rotate(90deg); } .rotate-135-ns { -webkit-transform: rotate(135deg); transform: rotate(135deg); } .rotate-180-ns { -webkit-transform: rotate(180deg); transform: rotate(180deg); } .rotate-225-ns { -webkit-transform: rotate(225deg); transform: rotate(225deg); } .rotate-270-ns { -webkit-transform: rotate(270deg); transform: rotate(270deg); } .rotate-315-ns { -webkit-transform: rotate(315deg); transform: rotate(315deg); } .pa0-ns { padding: 0; } .pa1-ns { padding: 0.25rem; } .pa2-ns { padding: 0.5rem; } .pa3-ns { padding: 1rem; } .pa4-ns { padding: 2rem; } .pa5-ns { padding: 4rem; } .pa6-ns { padding: 8rem; } .pa7-ns { padding: 16rem; } .pl0-ns { padding-left: 0; } .pl1-ns { padding-left: 0.25rem; } .pl2-ns { padding-left: 0.5rem; } .pl3-ns { padding-left: 1rem; } .pl4-ns { padding-left: 2rem; } .pl5-ns { padding-left: 4rem; } .pl6-ns { padding-left: 8rem; } .pl7-ns { padding-left: 16rem; } .pr0-ns { padding-right: 0; } .pr1-ns { padding-right: 0.25rem; } .pr2-ns { padding-right: 0.5rem; } .pr3-ns { padding-right: 1rem; } .pr4-ns { padding-right: 2rem; } .pr5-ns { padding-right: 4rem; } .pr6-ns { padding-right: 8rem; } .pr7-ns { padding-right: 16rem; } .pb0-ns { padding-bottom: 0; } .pb1-ns { padding-bottom: 0.25rem; } .pb2-ns { padding-bottom: 0.5rem; } .pb3-ns { padding-bottom: 1rem; } .pb4-ns { padding-bottom: 2rem; } .pb5-ns { padding-bottom: 4rem; } .pb6-ns { padding-bottom: 8rem; } .pb7-ns { padding-bottom: 16rem; } .pt0-ns { padding-top: 0; } .pt1-ns { padding-top: 0.25rem; } .pt2-ns { padding-top: 0.5rem; } .pt3-ns { padding-top: 1rem; } .pt4-ns { padding-top: 2rem; } .pt5-ns { padding-top: 4rem; } .pt6-ns { padding-top: 8rem; } .pt7-ns { padding-top: 16rem; } .pv0-ns { padding-top: 0; padding-bottom: 0; } .pv1-ns { padding-top: 0.25rem; padding-bottom: 0.25rem; } .pv2-ns { padding-top: 0.5rem; padding-bottom: 0.5rem; } .pv3-ns { padding-top: 1rem; padding-bottom: 1rem; } .pv4-ns { padding-top: 2rem; padding-bottom: 2rem; } .pv5-ns { padding-top: 4rem; padding-bottom: 4rem; } .pv6-ns { padding-top: 8rem; padding-bottom: 8rem; } .pv7-ns { padding-top: 16rem; padding-bottom: 16rem; } .ph0-ns { padding-left: 0; padding-right: 0; } .ph1-ns { padding-left: 0.25rem; padding-right: 0.25rem; } .ph2-ns { padding-left: 0.5rem; padding-right: 0.5rem; } .ph3-ns { padding-left: 1rem; padding-right: 1rem; } .ph4-ns { padding-left: 2rem; padding-right: 2rem; } .ph5-ns { padding-left: 4rem; padding-right: 4rem; } .ph6-ns { padding-left: 8rem; padding-right: 8rem; } .ph7-ns { padding-left: 16rem; padding-right: 16rem; } .ma0-ns { margin: 0; } .ma1-ns { margin: 0.25rem; } .ma2-ns { margin: 0.5rem; } .ma3-ns { margin: 1rem; } .ma4-ns { margin: 2rem; } .ma5-ns { margin: 4rem; } .ma6-ns { margin: 8rem; } .ma7-ns { margin: 16rem; } .ml0-ns { margin-left: 0; } .ml1-ns { margin-left: 0.25rem; } .ml2-ns { margin-left: 0.5rem; } .ml3-ns { margin-left: 1rem; } .ml4-ns { margin-left: 2rem; } .ml5-ns { margin-left: 4rem; } .ml6-ns { margin-left: 8rem; } .ml7-ns { margin-left: 16rem; } .mr0-ns { margin-right: 0; } .mr1-ns { margin-right: 0.25rem; } .mr2-ns { margin-right: 0.5rem; } .mr3-ns { margin-right: 1rem; } .mr4-ns { margin-right: 2rem; } .mr5-ns { margin-right: 4rem; } .mr6-ns { margin-right: 8rem; } .mr7-ns { margin-right: 16rem; } .mb0-ns { margin-bottom: 0; } .mb1-ns { margin-bottom: 0.25rem; } .mb2-ns { margin-bottom: 0.5rem; } .mb3-ns { margin-bottom: 1rem; } .mb4-ns { margin-bottom: 2rem; } .mb5-ns { margin-bottom: 4rem; } .mb6-ns { margin-bottom: 8rem; } .mb7-ns { margin-bottom: 16rem; } .mt0-ns { margin-top: 0; } .mt1-ns { margin-top: 0.25rem; } .mt2-ns { margin-top: 0.5rem; } .mt3-ns { margin-top: 1rem; } .mt4-ns { margin-top: 2rem; } .mt5-ns { margin-top: 4rem; } .mt6-ns { margin-top: 8rem; } .mt7-ns { margin-top: 16rem; } .mv0-ns { margin-top: 0; margin-bottom: 0; } .mv1-ns { margin-top: 0.25rem; margin-bottom: 0.25rem; } .mv2-ns { margin-top: 0.5rem; margin-bottom: 0.5rem; } .mv3-ns { margin-top: 1rem; margin-bottom: 1rem; } .mv4-ns { margin-top: 2rem; margin-bottom: 2rem; } .mv5-ns { margin-top: 4rem; margin-bottom: 4rem; } .mv6-ns { margin-top: 8rem; margin-bottom: 8rem; } .mv7-ns { margin-top: 16rem; margin-bottom: 16rem; } .mh0-ns { margin-left: 0; margin-right: 0; } .mh1-ns { margin-left: 0.25rem; margin-right: 0.25rem; } .mh2-ns { margin-left: 0.5rem; margin-right: 0.5rem; } .mh3-ns { margin-left: 1rem; margin-right: 1rem; } .mh4-ns { margin-left: 2rem; margin-right: 2rem; } .mh5-ns { margin-left: 4rem; margin-right: 4rem; } .mh6-ns { margin-left: 8rem; margin-right: 8rem; } .mh7-ns { margin-left: 16rem; margin-right: 16rem; } .na1-ns { margin: -0.25rem; } .na2-ns { margin: -0.5rem; } .na3-ns { margin: -1rem; } .na4-ns { margin: -2rem; } .na5-ns { margin: -4rem; } .na6-ns { margin: -8rem; } .na7-ns { margin: -16rem; } .nl1-ns { margin-left: -0.25rem; } .nl2-ns { margin-left: -0.5rem; } .nl3-ns { margin-left: -1rem; } .nl4-ns { margin-left: -2rem; } .nl5-ns { margin-left: -4rem; } .nl6-ns { margin-left: -8rem; } .nl7-ns { margin-left: -16rem; } .nr1-ns { margin-right: -0.25rem; } .nr2-ns { margin-right: -0.5rem; } .nr3-ns { margin-right: -1rem; } .nr4-ns { margin-right: -2rem; } .nr5-ns { margin-right: -4rem; } .nr6-ns { margin-right: -8rem; } .nr7-ns { margin-right: -16rem; } .nb1-ns { margin-bottom: -0.25rem; } .nb2-ns { margin-bottom: -0.5rem; } .nb3-ns { margin-bottom: -1rem; } .nb4-ns { margin-bottom: -2rem; } .nb5-ns { margin-bottom: -4rem; } .nb6-ns { margin-bottom: -8rem; } .nb7-ns { margin-bottom: -16rem; } .nt1-ns { margin-top: -0.25rem; } .nt2-ns { margin-top: -0.5rem; } .nt3-ns { margin-top: -1rem; } .nt4-ns { margin-top: -2rem; } .nt5-ns { margin-top: -4rem; } .nt6-ns { margin-top: -8rem; } .nt7-ns { margin-top: -16rem; } .strike-ns { text-decoration: line-through; } .underline-ns { text-decoration: underline; } .no-underline-ns { text-decoration: none; } .tl-ns { text-align: left; } .tr-ns { text-align: right; } .tc-ns { text-align: center; } .tj-ns { text-align: justify; } .ttc-ns { text-transform: capitalize; } .ttl-ns { text-transform: lowercase; } .ttu-ns { text-transform: uppercase; } .ttn-ns { text-transform: none; } .f-6-ns, .f-headline-ns { font-size: 6rem; } .f-5-ns, .f-subheadline-ns { font-size: 5rem; } .f1-ns { font-size: 3rem; } .f2-ns { font-size: 2.25rem; } .f3-ns { font-size: 1.5rem; } .f4-ns { font-size: 1.25rem; } .f5-ns { font-size: 1rem; } .f6-ns { font-size: 0.875rem; } .f7-ns { font-size: 0.75rem; } .measure-ns { max-width: 30em; } .measure-wide-ns { max-width: 34em; } .measure-narrow-ns { max-width: 20em; } .indent-ns { text-indent: 1em; margin-top: 0; margin-bottom: 0; } .small-caps-ns { font-variant: small-caps; } .truncate-ns { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .center-ns { margin-right: auto; margin-left: auto; } .mr-auto-ns { margin-right: auto; } .ml-auto-ns { margin-left: auto; } .clip-ns { position: fixed !important; _position: absolute !important; clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ clip: rect(1px, 1px, 1px, 1px); } .ws-normal-ns { white-space: normal; } .nowrap-ns { white-space: nowrap; } .pre-ns { white-space: pre; } .v-base-ns { vertical-align: baseline; } .v-mid-ns { vertical-align: middle; } .v-top-ns { vertical-align: top; } .v-btm-ns { vertical-align: bottom; } } @media screen and (min-width: 30em) and (max-width: 60em) { .aspect-ratio-m { height: 0; position: relative; } .aspect-ratio--16x9-m { padding-bottom: 56.25%; } .aspect-ratio--9x16-m { padding-bottom: 177.77%; } .aspect-ratio--4x3-m { padding-bottom: 75%; } .aspect-ratio--3x4-m { padding-bottom: 133.33%; } .aspect-ratio--6x4-m { padding-bottom: 66.6%; } .aspect-ratio--4x6-m { padding-bottom: 150%; } .aspect-ratio--8x5-m { padding-bottom: 62.5%; } .aspect-ratio--5x8-m { padding-bottom: 160%; } .aspect-ratio--7x5-m { padding-bottom: 71.42%; } .aspect-ratio--5x7-m { padding-bottom: 140%; } .aspect-ratio--1x1-m { padding-bottom: 100%; } .aspect-ratio--object-m { position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%; z-index: 100; } .cover-m { background-size: cover !important; } .contain-m { background-size: contain !important; } .bg-center-m { background-repeat: no-repeat; background-position: center center; } .bg-top-m { background-repeat: no-repeat; background-position: top center; } .bg-right-m { background-repeat: no-repeat; background-position: center right; } .bg-bottom-m { background-repeat: no-repeat; background-position: bottom center; } .bg-left-m { background-repeat: no-repeat; background-position: center left; } .outline-m { outline: 1px solid; } .outline-transparent-m { outline: 1px solid transparent; } .outline-0-m { outline: 0; } .ba-m { border-style: solid; border-width: 1px; } .bt-m { border-top-style: solid; border-top-width: 1px; } .br-m { border-right-style: solid; border-right-width: 1px; } .bb-m { border-bottom-style: solid; border-bottom-width: 1px; } .bl-m { border-left-style: solid; border-left-width: 1px; } .bn-m { border-style: none; border-width: 0; } .br0-m { border-radius: 0; } .br1-m { border-radius: 0.125rem; } .br2-m { border-radius: 0.25rem; } .br3-m { border-radius: 0.5rem; } .br4-m { border-radius: 1rem; } .br-100-m { border-radius: 100%; } .br-pill-m { border-radius: 9999px; } .br--bottom-m { border-top-left-radius: 0; border-top-right-radius: 0; } .br--top-m { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } .br--right-m { border-top-left-radius: 0; border-bottom-left-radius: 0; } .br--left-m { border-top-right-radius: 0; border-bottom-right-radius: 0; } .b--dotted-m { border-style: dotted; } .b--dashed-m { border-style: dashed; } .b--solid-m { border-style: solid; } .b--none-m { border-style: none; } .bw0-m { border-width: 0; } .bw1-m { border-width: 0.125rem; } .bw2-m { border-width: 0.25rem; } .bw3-m { border-width: 0.5rem; } .bw4-m { border-width: 1rem; } .bw5-m { border-width: 2rem; } .bt-0-m { border-top-width: 0; } .br-0-m { border-right-width: 0; } .bb-0-m { border-bottom-width: 0; } .bl-0-m { border-left-width: 0; } .shadow-1-m { box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.2); } .shadow-2-m { box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.2); } .shadow-3-m { box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, 0.2); } .shadow-4-m { box-shadow: 2px 2px 8px 0 rgba(0, 0, 0, 0.2); } .shadow-5-m { box-shadow: 4px 4px 8px 0 rgba(0, 0, 0, 0.2); } .top-0-m { top: 0; } .left-0-m { left: 0; } .right-0-m { right: 0; } .bottom-0-m { bottom: 0; } .top-1-m { top: 1rem; } .left-1-m { left: 1rem; } .right-1-m { right: 1rem; } .bottom-1-m { bottom: 1rem; } .top-2-m { top: 2rem; } .left-2-m { left: 2rem; } .right-2-m { right: 2rem; } .bottom-2-m { bottom: 2rem; } .top--1-m { top: -1rem; } .right--1-m { right: -1rem; } .bottom--1-m { bottom: -1rem; } .left--1-m { left: -1rem; } .top--2-m { top: -2rem; } .right--2-m { right: -2rem; } .bottom--2-m { bottom: -2rem; } .left--2-m { left: -2rem; } .absolute--fill-m { top: 0; right: 0; bottom: 0; left: 0; } .cl-m { clear: left; } .cr-m { clear: right; } .cb-m { clear: both; } .cn-m { clear: none; } .dn-m { display: none; } .di-m { display: inline; } .db-m { display: block; } .dib-m { display: inline-block; } .dit-m { display: inline-table; } .dt-m { display: table; } .dtc-m { display: table-cell; } .dt-row-m { display: table-row; } .dt-row-group-m { display: table-row-group; } .dt-column-m { display: table-column; } .dt-column-group-m { display: table-column-group; } .dt--fixed-m { table-layout: fixed; width: 100%; } .flex-m { display: flex; } .inline-flex-m { display: inline-flex; } .flex-auto-m { flex: 1 1 auto; min-width: 0; /* 1 */ min-height: 0; /* 1 */ } .flex-none-m { flex: none; } .flex-column-m { flex-direction: column; } .flex-row-m { flex-direction: row; } .flex-wrap-m { flex-wrap: wrap; } .flex-nowrap-m { flex-wrap: nowrap; } .flex-wrap-reverse-m { flex-wrap: wrap-reverse; } .flex-column-reverse-m { flex-direction: column-reverse; } .flex-row-reverse-m { flex-direction: row-reverse; } .items-start-m { align-items: flex-start; } .items-end-m { align-items: flex-end; } .items-center-m { align-items: center; } .items-baseline-m { align-items: baseline; } .items-stretch-m { align-items: stretch; } .self-start-m { align-self: flex-start; } .self-end-m { align-self: flex-end; } .self-center-m { align-self: center; } .self-baseline-m { align-self: baseline; } .self-stretch-m { align-self: stretch; } .justify-start-m { justify-content: flex-start; } .justify-end-m { justify-content: flex-end; } .justify-center-m { justify-content: center; } .justify-between-m { justify-content: space-between; } .justify-around-m { justify-content: space-around; } .content-start-m { align-content: flex-start; } .content-end-m { align-content: flex-end; } .content-center-m { align-content: center; } .content-between-m { align-content: space-between; } .content-around-m { align-content: space-around; } .content-stretch-m { align-content: stretch; } .order-0-m { order: 0; } .order-1-m { order: 1; } .order-2-m { order: 2; } .order-3-m { order: 3; } .order-4-m { order: 4; } .order-5-m { order: 5; } .order-6-m { order: 6; } .order-7-m { order: 7; } .order-8-m { order: 8; } .order-last-m { order: 99999; } .flex-grow-0-m { flex-grow: 0; } .flex-grow-1-m { flex-grow: 1; } .flex-shrink-0-m { flex-shrink: 0; } .flex-shrink-1-m { flex-shrink: 1; } .fl-m { float: left; _display: inline; } .fr-m { float: right; _display: inline; } .fn-m { float: none; } .i-m { font-style: italic; } .fs-normal-m { font-style: normal; } .normal-m { font-weight: normal; } .b-m { font-weight: bold; } .fw1-m { font-weight: 100; } .fw2-m { font-weight: 200; } .fw3-m { font-weight: 300; } .fw4-m { font-weight: 400; } .fw5-m { font-weight: 500; } .fw6-m { font-weight: 600; } .fw7-m { font-weight: 700; } .fw8-m { font-weight: 800; } .fw9-m { font-weight: 900; } .h1-m { height: 1rem; } .h2-m { height: 2rem; } .h3-m { height: 4rem; } .h4-m { height: 8rem; } .h5-m { height: 16rem; } .h-25-m { height: 25%; } .h-50-m { height: 50%; } .h-75-m { height: 75%; } .h-100-m { height: 100%; } .min-h-100-m { min-height: 100%; } .vh-25-m { height: 25vh; } .vh-50-m { height: 50vh; } .vh-75-m { height: 75vh; } .vh-100-m { height: 100vh; } .min-vh-100-m { min-height: 100vh; } .h-auto-m { height: auto; } .h-inherit-m { height: inherit; } .tracked-m { letter-spacing: 0.1em; } .tracked-tight-m { letter-spacing: -0.05em; } .tracked-mega-m { letter-spacing: 0.25em; } .lh-solid-m { line-height: 1; } .lh-title-m { line-height: 1.25; } .lh-copy-m { line-height: 1.5; } .mw-100-m { max-width: 100%; } .mw1-m { max-width: 1rem; } .mw2-m { max-width: 2rem; } .mw3-m { max-width: 4rem; } .mw4-m { max-width: 8rem; } .mw5-m { max-width: 16rem; } .mw6-m { max-width: 32rem; } .mw7-m { max-width: 48rem; } .mw8-m { max-width: 64rem; } .mw9-m { max-width: 96rem; } .mw-none-m { max-width: none; } .w1-m { width: 1rem; } .w2-m { width: 2rem; } .w3-m { width: 4rem; } .w4-m { width: 8rem; } .w5-m { width: 16rem; } .w-10-m { width: 10%; } .w-20-m { width: 20%; } .w-25-m { width: 25%; } .w-30-m { width: 30%; } .w-33-m { width: 33%; } .w-34-m { width: 34%; } .w-40-m { width: 40%; } .w-50-m { width: 50%; } .w-60-m { width: 60%; } .w-70-m { width: 70%; } .w-75-m { width: 75%; } .w-80-m { width: 80%; } .w-90-m { width: 90%; } .w-100-m { width: 100%; } .w-third-m { width: calc(100% / 3); } .w-two-thirds-m { width: calc(100% / 1.5); } .w-auto-m { width: auto; } .overflow-visible-m { overflow: visible; } .overflow-hidden-m { overflow: hidden; } .overflow-scroll-m { overflow: scroll; } .overflow-auto-m { overflow: auto; } .overflow-x-visible-m { overflow-x: visible; } .overflow-x-hidden-m { overflow-x: hidden; } .overflow-x-scroll-m { overflow-x: scroll; } .overflow-x-auto-m { overflow-x: auto; } .overflow-y-visible-m { overflow-y: visible; } .overflow-y-hidden-m { overflow-y: hidden; } .overflow-y-scroll-m { overflow-y: scroll; } .overflow-y-auto-m { overflow-y: auto; } .static-m { position: static; } .relative-m { position: relative; } .absolute-m { position: absolute; } .fixed-m { position: fixed; } .rotate-45-m { -webkit-transform: rotate(45deg); transform: rotate(45deg); } .rotate-90-m { -webkit-transform: rotate(90deg); transform: rotate(90deg); } .rotate-135-m { -webkit-transform: rotate(135deg); transform: rotate(135deg); } .rotate-180-m { -webkit-transform: rotate(180deg); transform: rotate(180deg); } .rotate-225-m { -webkit-transform: rotate(225deg); transform: rotate(225deg); } .rotate-270-m { -webkit-transform: rotate(270deg); transform: rotate(270deg); } .rotate-315-m { -webkit-transform: rotate(315deg); transform: rotate(315deg); } .pa0-m { padding: 0; } .pa1-m { padding: 0.25rem; } .pa2-m { padding: 0.5rem; } .pa3-m { padding: 1rem; } .pa4-m { padding: 2rem; } .pa5-m { padding: 4rem; } .pa6-m { padding: 8rem; } .pa7-m { padding: 16rem; } .pl0-m { padding-left: 0; } .pl1-m { padding-left: 0.25rem; } .pl2-m { padding-left: 0.5rem; } .pl3-m { padding-left: 1rem; } .pl4-m { padding-left: 2rem; } .pl5-m { padding-left: 4rem; } .pl6-m { padding-left: 8rem; } .pl7-m { padding-left: 16rem; } .pr0-m { padding-right: 0; } .pr1-m { padding-right: 0.25rem; } .pr2-m { padding-right: 0.5rem; } .pr3-m { padding-right: 1rem; } .pr4-m { padding-right: 2rem; } .pr5-m { padding-right: 4rem; } .pr6-m { padding-right: 8rem; } .pr7-m { padding-right: 16rem; } .pb0-m { padding-bottom: 0; } .pb1-m { padding-bottom: 0.25rem; } .pb2-m { padding-bottom: 0.5rem; } .pb3-m { padding-bottom: 1rem; } .pb4-m { padding-bottom: 2rem; } .pb5-m { padding-bottom: 4rem; } .pb6-m { padding-bottom: 8rem; } .pb7-m { padding-bottom: 16rem; } .pt0-m { padding-top: 0; } .pt1-m { padding-top: 0.25rem; } .pt2-m { padding-top: 0.5rem; } .pt3-m { padding-top: 1rem; } .pt4-m { padding-top: 2rem; } .pt5-m { padding-top: 4rem; } .pt6-m { padding-top: 8rem; } .pt7-m { padding-top: 16rem; } .pv0-m { padding-top: 0; padding-bottom: 0; } .pv1-m { padding-top: 0.25rem; padding-bottom: 0.25rem; } .pv2-m { padding-top: 0.5rem; padding-bottom: 0.5rem; } .pv3-m { padding-top: 1rem; padding-bottom: 1rem; } .pv4-m { padding-top: 2rem; padding-bottom: 2rem; } .pv5-m { padding-top: 4rem; padding-bottom: 4rem; } .pv6-m { padding-top: 8rem; padding-bottom: 8rem; } .pv7-m { padding-top: 16rem; padding-bottom: 16rem; } .ph0-m { padding-left: 0; padding-right: 0; } .ph1-m { padding-left: 0.25rem; padding-right: 0.25rem; } .ph2-m { padding-left: 0.5rem; padding-right: 0.5rem; } .ph3-m { padding-left: 1rem; padding-right: 1rem; } .ph4-m { padding-left: 2rem; padding-right: 2rem; } .ph5-m { padding-left: 4rem; padding-right: 4rem; } .ph6-m { padding-left: 8rem; padding-right: 8rem; } .ph7-m { padding-left: 16rem; padding-right: 16rem; } .ma0-m { margin: 0; } .ma1-m { margin: 0.25rem; } .ma2-m { margin: 0.5rem; } .ma3-m { margin: 1rem; } .ma4-m { margin: 2rem; } .ma5-m { margin: 4rem; } .ma6-m { margin: 8rem; } .ma7-m { margin: 16rem; } .ml0-m { margin-left: 0; } .ml1-m { margin-left: 0.25rem; } .ml2-m { margin-left: 0.5rem; } .ml3-m { margin-left: 1rem; } .ml4-m { margin-left: 2rem; } .ml5-m { margin-left: 4rem; } .ml6-m { margin-left: 8rem; } .ml7-m { margin-left: 16rem; } .mr0-m { margin-right: 0; } .mr1-m { margin-right: 0.25rem; } .mr2-m { margin-right: 0.5rem; } .mr3-m { margin-right: 1rem; } .mr4-m { margin-right: 2rem; } .mr5-m { margin-right: 4rem; } .mr6-m { margin-right: 8rem; } .mr7-m { margin-right: 16rem; } .mb0-m { margin-bottom: 0; } .mb1-m { margin-bottom: 0.25rem; } .mb2-m { margin-bottom: 0.5rem; } .mb3-m { margin-bottom: 1rem; } .mb4-m { margin-bottom: 2rem; } .mb5-m { margin-bottom: 4rem; } .mb6-m { margin-bottom: 8rem; } .mb7-m { margin-bottom: 16rem; } .mt0-m { margin-top: 0; } .mt1-m { margin-top: 0.25rem; } .mt2-m { margin-top: 0.5rem; } .mt3-m { margin-top: 1rem; } .mt4-m { margin-top: 2rem; } .mt5-m { margin-top: 4rem; } .mt6-m { margin-top: 8rem; } .mt7-m { margin-top: 16rem; } .mv0-m { margin-top: 0; margin-bottom: 0; } .mv1-m { margin-top: 0.25rem; margin-bottom: 0.25rem; } .mv2-m { margin-top: 0.5rem; margin-bottom: 0.5rem; } .mv3-m { margin-top: 1rem; margin-bottom: 1rem; } .mv4-m { margin-top: 2rem; margin-bottom: 2rem; } .mv5-m { margin-top: 4rem; margin-bottom: 4rem; } .mv6-m { margin-top: 8rem; margin-bottom: 8rem; } .mv7-m { margin-top: 16rem; margin-bottom: 16rem; } .mh0-m { margin-left: 0; margin-right: 0; } .mh1-m { margin-left: 0.25rem; margin-right: 0.25rem; } .mh2-m { margin-left: 0.5rem; margin-right: 0.5rem; } .mh3-m { margin-left: 1rem; margin-right: 1rem; } .mh4-m { margin-left: 2rem; margin-right: 2rem; } .mh5-m { margin-left: 4rem; margin-right: 4rem; } .mh6-m { margin-left: 8rem; margin-right: 8rem; } .mh7-m { margin-left: 16rem; margin-right: 16rem; } .na1-m { margin: -0.25rem; } .na2-m { margin: -0.5rem; } .na3-m { margin: -1rem; } .na4-m { margin: -2rem; } .na5-m { margin: -4rem; } .na6-m { margin: -8rem; } .na7-m { margin: -16rem; } .nl1-m { margin-left: -0.25rem; } .nl2-m { margin-left: -0.5rem; } .nl3-m { margin-left: -1rem; } .nl4-m { margin-left: -2rem; } .nl5-m { margin-left: -4rem; } .nl6-m { margin-left: -8rem; } .nl7-m { margin-left: -16rem; } .nr1-m { margin-right: -0.25rem; } .nr2-m { margin-right: -0.5rem; } .nr3-m { margin-right: -1rem; } .nr4-m { margin-right: -2rem; } .nr5-m { margin-right: -4rem; } .nr6-m { margin-right: -8rem; } .nr7-m { margin-right: -16rem; } .nb1-m { margin-bottom: -0.25rem; } .nb2-m { margin-bottom: -0.5rem; } .nb3-m { margin-bottom: -1rem; } .nb4-m { margin-bottom: -2rem; } .nb5-m { margin-bottom: -4rem; } .nb6-m { margin-bottom: -8rem; } .nb7-m { margin-bottom: -16rem; } .nt1-m { margin-top: -0.25rem; } .nt2-m { margin-top: -0.5rem; } .nt3-m { margin-top: -1rem; } .nt4-m { margin-top: -2rem; } .nt5-m { margin-top: -4rem; } .nt6-m { margin-top: -8rem; } .nt7-m { margin-top: -16rem; } .strike-m { text-decoration: line-through; } .underline-m { text-decoration: underline; } .no-underline-m { text-decoration: none; } .tl-m { text-align: left; } .tr-m { text-align: right; } .tc-m { text-align: center; } .tj-m { text-align: justify; } .ttc-m { text-transform: capitalize; } .ttl-m { text-transform: lowercase; } .ttu-m { text-transform: uppercase; } .ttn-m { text-transform: none; } .f-6-m, .f-headline-m { font-size: 6rem; } .f-5-m, .f-subheadline-m { font-size: 5rem; } .f1-m { font-size: 3rem; } .f2-m { font-size: 2.25rem; } .f3-m { font-size: 1.5rem; } .f4-m { font-size: 1.25rem; } .f5-m { font-size: 1rem; } .f6-m { font-size: 0.875rem; } .f7-m { font-size: 0.75rem; } .measure-m { max-width: 30em; } .measure-wide-m { max-width: 34em; } .measure-narrow-m { max-width: 20em; } .indent-m { text-indent: 1em; margin-top: 0; margin-bottom: 0; } .small-caps-m { font-variant: small-caps; } .truncate-m { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .center-m { margin-right: auto; margin-left: auto; } .mr-auto-m { margin-right: auto; } .ml-auto-m { margin-left: auto; } .clip-m { position: fixed !important; _position: absolute !important; clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ clip: rect(1px, 1px, 1px, 1px); } .ws-normal-m { white-space: normal; } .nowrap-m { white-space: nowrap; } .pre-m { white-space: pre; } .v-base-m { vertical-align: baseline; } .v-mid-m { vertical-align: middle; } .v-top-m { vertical-align: top; } .v-btm-m { vertical-align: bottom; } } @media screen and (min-width: 60em) { .aspect-ratio-l { height: 0; position: relative; } .aspect-ratio--16x9-l { padding-bottom: 56.25%; } .aspect-ratio--9x16-l { padding-bottom: 177.77%; } .aspect-ratio--4x3-l { padding-bottom: 75%; } .aspect-ratio--3x4-l { padding-bottom: 133.33%; } .aspect-ratio--6x4-l { padding-bottom: 66.6%; } .aspect-ratio--4x6-l { padding-bottom: 150%; } .aspect-ratio--8x5-l { padding-bottom: 62.5%; } .aspect-ratio--5x8-l { padding-bottom: 160%; } .aspect-ratio--7x5-l { padding-bottom: 71.42%; } .aspect-ratio--5x7-l { padding-bottom: 140%; } .aspect-ratio--1x1-l { padding-bottom: 100%; } .aspect-ratio--object-l { position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%; z-index: 100; } .cover-l { background-size: cover !important; } .contain-l { background-size: contain !important; } .bg-center-l { background-repeat: no-repeat; background-position: center center; } .bg-top-l { background-repeat: no-repeat; background-position: top center; } .bg-right-l { background-repeat: no-repeat; background-position: center right; } .bg-bottom-l { background-repeat: no-repeat; background-position: bottom center; } .bg-left-l { background-repeat: no-repeat; background-position: center left; } .outline-l { outline: 1px solid; } .outline-transparent-l { outline: 1px solid transparent; } .outline-0-l { outline: 0; } .ba-l { border-style: solid; border-width: 1px; } .bt-l { border-top-style: solid; border-top-width: 1px; } .br-l { border-right-style: solid; border-right-width: 1px; } .bb-l { border-bottom-style: solid; border-bottom-width: 1px; } .bl-l { border-left-style: solid; border-left-width: 1px; } .bn-l { border-style: none; border-width: 0; } .br0-l { border-radius: 0; } .br1-l { border-radius: 0.125rem; } .br2-l { border-radius: 0.25rem; } .br3-l { border-radius: 0.5rem; } .br4-l { border-radius: 1rem; } .br-100-l { border-radius: 100%; } .br-pill-l { border-radius: 9999px; } .br--bottom-l { border-top-left-radius: 0; border-top-right-radius: 0; } .br--top-l { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } .br--right-l { border-top-left-radius: 0; border-bottom-left-radius: 0; } .br--left-l { border-top-right-radius: 0; border-bottom-right-radius: 0; } .b--dotted-l { border-style: dotted; } .b--dashed-l { border-style: dashed; } .b--solid-l { border-style: solid; } .b--none-l { border-style: none; } .bw0-l { border-width: 0; } .bw1-l { border-width: 0.125rem; } .bw2-l { border-width: 0.25rem; } .bw3-l { border-width: 0.5rem; } .bw4-l { border-width: 1rem; } .bw5-l { border-width: 2rem; } .bt-0-l { border-top-width: 0; } .br-0-l { border-right-width: 0; } .bb-0-l { border-bottom-width: 0; } .bl-0-l { border-left-width: 0; } .shadow-1-l { box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.2); } .shadow-2-l { box-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.2); } .shadow-3-l { box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, 0.2); } .shadow-4-l { box-shadow: 2px 2px 8px 0 rgba(0, 0, 0, 0.2); } .shadow-5-l { box-shadow: 4px 4px 8px 0 rgba(0, 0, 0, 0.2); } .top-0-l { top: 0; } .left-0-l { left: 0; } .right-0-l { right: 0; } .bottom-0-l { bottom: 0; } .top-1-l { top: 1rem; } .left-1-l { left: 1rem; } .right-1-l { right: 1rem; } .bottom-1-l { bottom: 1rem; } .top-2-l { top: 2rem; } .left-2-l { left: 2rem; } .right-2-l { right: 2rem; } .bottom-2-l { bottom: 2rem; } .top--1-l { top: -1rem; } .right--1-l { right: -1rem; } .bottom--1-l { bottom: -1rem; } .left--1-l { left: -1rem; } .top--2-l { top: -2rem; } .right--2-l { right: -2rem; } .bottom--2-l { bottom: -2rem; } .left--2-l { left: -2rem; } .absolute--fill-l { top: 0; right: 0; bottom: 0; left: 0; } .cl-l { clear: left; } .cr-l { clear: right; } .cb-l { clear: both; } .cn-l { clear: none; } .dn-l { display: none; } .di-l { display: inline; } .db-l { display: block; } .dib-l { display: inline-block; } .dit-l { display: inline-table; } .dt-l { display: table; } .dtc-l { display: table-cell; } .dt-row-l { display: table-row; } .dt-row-group-l { display: table-row-group; } .dt-column-l { display: table-column; } .dt-column-group-l { display: table-column-group; } .dt--fixed-l { table-layout: fixed; width: 100%; } .flex-l { display: flex; } .inline-flex-l { display: inline-flex; } .flex-auto-l { flex: 1 1 auto; min-width: 0; /* 1 */ min-height: 0; /* 1 */ } .flex-none-l { flex: none; } .flex-column-l { flex-direction: column; } .flex-row-l { flex-direction: row; } .flex-wrap-l { flex-wrap: wrap; } .flex-nowrap-l { flex-wrap: nowrap; } .flex-wrap-reverse-l { flex-wrap: wrap-reverse; } .flex-column-reverse-l { flex-direction: column-reverse; } .flex-row-reverse-l { flex-direction: row-reverse; } .items-start-l { align-items: flex-start; } .items-end-l { align-items: flex-end; } .items-center-l { align-items: center; } .items-baseline-l { align-items: baseline; } .items-stretch-l { align-items: stretch; } .self-start-l { align-self: flex-start; } .self-end-l { align-self: flex-end; } .self-center-l { align-self: center; } .self-baseline-l { align-self: baseline; } .self-stretch-l { align-self: stretch; } .justify-start-l { justify-content: flex-start; } .justify-end-l { justify-content: flex-end; } .justify-center-l { justify-content: center; } .justify-between-l { justify-content: space-between; } .justify-around-l { justify-content: space-around; } .content-start-l { align-content: flex-start; } .content-end-l { align-content: flex-end; } .content-center-l { align-content: center; } .content-between-l { align-content: space-between; } .content-around-l { align-content: space-around; } .content-stretch-l { align-content: stretch; } .order-0-l { order: 0; } .order-1-l { order: 1; } .order-2-l { order: 2; } .order-3-l { order: 3; } .order-4-l { order: 4; } .order-5-l { order: 5; } .order-6-l { order: 6; } .order-7-l { order: 7; } .order-8-l { order: 8; } .order-last-l { order: 99999; } .flex-grow-0-l { flex-grow: 0; } .flex-grow-1-l { flex-grow: 1; } .flex-shrink-0-l { flex-shrink: 0; } .flex-shrink-1-l { flex-shrink: 1; } .fl-l { float: left; _display: inline; } .fr-l { float: right; _display: inline; } .fn-l { float: none; } .i-l { font-style: italic; } .fs-normal-l { font-style: normal; } .normal-l { font-weight: normal; } .b-l { font-weight: bold; } .fw1-l { font-weight: 100; } .fw2-l { font-weight: 200; } .fw3-l { font-weight: 300; } .fw4-l { font-weight: 400; } .fw5-l { font-weight: 500; } .fw6-l { font-weight: 600; } .fw7-l { font-weight: 700; } .fw8-l { font-weight: 800; } .fw9-l { font-weight: 900; } .h1-l { height: 1rem; } .h2-l { height: 2rem; } .h3-l { height: 4rem; } .h4-l { height: 8rem; } .h5-l { height: 16rem; } .h-25-l { height: 25%; } .h-50-l { height: 50%; } .h-75-l { height: 75%; } .h-100-l { height: 100%; } .min-h-100-l { min-height: 100%; } .vh-25-l { height: 25vh; } .vh-50-l { height: 50vh; } .vh-75-l { height: 75vh; } .vh-100-l { height: 100vh; } .min-vh-100-l { min-height: 100vh; } .h-auto-l { height: auto; } .h-inherit-l { height: inherit; } .tracked-l { letter-spacing: 0.1em; } .tracked-tight-l { letter-spacing: -0.05em; } .tracked-mega-l { letter-spacing: 0.25em; } .lh-solid-l { line-height: 1; } .lh-title-l { line-height: 1.25; } .lh-copy-l { line-height: 1.5; } .mw-100-l { max-width: 100%; } .mw1-l { max-width: 1rem; } .mw2-l { max-width: 2rem; } .mw3-l { max-width: 4rem; } .mw4-l { max-width: 8rem; } .mw5-l { max-width: 16rem; } .mw6-l { max-width: 32rem; } .mw7-l { max-width: 48rem; } .mw8-l { max-width: 64rem; } .mw9-l { max-width: 96rem; } .mw-none-l { max-width: none; } .w1-l { width: 1rem; } .w2-l { width: 2rem; } .w3-l { width: 4rem; } .w4-l { width: 8rem; } .w5-l { width: 16rem; } .w-10-l { width: 10%; } .w-20-l { width: 20%; } .w-25-l { width: 25%; } .w-30-l { width: 30%; } .w-33-l { width: 33%; } .w-34-l { width: 34%; } .w-40-l { width: 40%; } .w-50-l { width: 50%; } .w-60-l { width: 60%; } .w-70-l { width: 70%; } .w-75-l { width: 75%; } .w-80-l { width: 80%; } .w-90-l { width: 90%; } .w-100-l { width: 100%; } .w-third-l { width: calc(100% / 3); } .w-two-thirds-l { width: calc(100% / 1.5); } .w-auto-l { width: auto; } .overflow-visible-l { overflow: visible; } .overflow-hidden-l { overflow: hidden; } .overflow-scroll-l { overflow: scroll; } .overflow-auto-l { overflow: auto; } .overflow-x-visible-l { overflow-x: visible; } .overflow-x-hidden-l { overflow-x: hidden; } .overflow-x-scroll-l { overflow-x: scroll; } .overflow-x-auto-l { overflow-x: auto; } .overflow-y-visible-l { overflow-y: visible; } .overflow-y-hidden-l { overflow-y: hidden; } .overflow-y-scroll-l { overflow-y: scroll; } .overflow-y-auto-l { overflow-y: auto; } .static-l { position: static; } .relative-l { position: relative; } .absolute-l { position: absolute; } .fixed-l { position: fixed; } .rotate-45-l { -webkit-transform: rotate(45deg); transform: rotate(45deg); } .rotate-90-l { -webkit-transform: rotate(90deg); transform: rotate(90deg); } .rotate-135-l { -webkit-transform: rotate(135deg); transform: rotate(135deg); } .rotate-180-l { -webkit-transform: rotate(180deg); transform: rotate(180deg); } .rotate-225-l { -webkit-transform: rotate(225deg); transform: rotate(225deg); } .rotate-270-l { -webkit-transform: rotate(270deg); transform: rotate(270deg); } .rotate-315-l { -webkit-transform: rotate(315deg); transform: rotate(315deg); } .pa0-l { padding: 0; } .pa1-l { padding: 0.25rem; } .pa2-l { padding: 0.5rem; } .pa3-l { padding: 1rem; } .pa4-l { padding: 2rem; } .pa5-l { padding: 4rem; } .pa6-l { padding: 8rem; } .pa7-l { padding: 16rem; } .pl0-l { padding-left: 0; } .pl1-l { padding-left: 0.25rem; } .pl2-l { padding-left: 0.5rem; } .pl3-l { padding-left: 1rem; } .pl4-l { padding-left: 2rem; } .pl5-l { padding-left: 4rem; } .pl6-l { padding-left: 8rem; } .pl7-l { padding-left: 16rem; } .pr0-l { padding-right: 0; } .pr1-l { padding-right: 0.25rem; } .pr2-l { padding-right: 0.5rem; } .pr3-l { padding-right: 1rem; } .pr4-l { padding-right: 2rem; } .pr5-l { padding-right: 4rem; } .pr6-l { padding-right: 8rem; } .pr7-l { padding-right: 16rem; } .pb0-l { padding-bottom: 0; } .pb1-l { padding-bottom: 0.25rem; } .pb2-l { padding-bottom: 0.5rem; } .pb3-l { padding-bottom: 1rem; } .pb4-l { padding-bottom: 2rem; } .pb5-l { padding-bottom: 4rem; } .pb6-l { padding-bottom: 8rem; } .pb7-l { padding-bottom: 16rem; } .pt0-l { padding-top: 0; } .pt1-l { padding-top: 0.25rem; } .pt2-l { padding-top: 0.5rem; } .pt3-l { padding-top: 1rem; } .pt4-l { padding-top: 2rem; } .pt5-l { padding-top: 4rem; } .pt6-l { padding-top: 8rem; } .pt7-l { padding-top: 16rem; } .pv0-l { padding-top: 0; padding-bottom: 0; } .pv1-l { padding-top: 0.25rem; padding-bottom: 0.25rem; } .pv2-l { padding-top: 0.5rem; padding-bottom: 0.5rem; } .pv3-l { padding-top: 1rem; padding-bottom: 1rem; } .pv4-l { padding-top: 2rem; padding-bottom: 2rem; } .pv5-l { padding-top: 4rem; padding-bottom: 4rem; } .pv6-l { padding-top: 8rem; padding-bottom: 8rem; } .pv7-l { padding-top: 16rem; padding-bottom: 16rem; } .ph0-l { padding-left: 0; padding-right: 0; } .ph1-l { padding-left: 0.25rem; padding-right: 0.25rem; } .ph2-l { padding-left: 0.5rem; padding-right: 0.5rem; } .ph3-l { padding-left: 1rem; padding-right: 1rem; } .ph4-l { padding-left: 2rem; padding-right: 2rem; } .ph5-l { padding-left: 4rem; padding-right: 4rem; } .ph6-l { padding-left: 8rem; padding-right: 8rem; } .ph7-l { padding-left: 16rem; padding-right: 16rem; } .ma0-l { margin: 0; } .ma1-l { margin: 0.25rem; } .ma2-l { margin: 0.5rem; } .ma3-l { margin: 1rem; } .ma4-l { margin: 2rem; } .ma5-l { margin: 4rem; } .ma6-l { margin: 8rem; } .ma7-l { margin: 16rem; } .ml0-l { margin-left: 0; } .ml1-l { margin-left: 0.25rem; } .ml2-l { margin-left: 0.5rem; } .ml3-l { margin-left: 1rem; } .ml4-l { margin-left: 2rem; } .ml5-l { margin-left: 4rem; } .ml6-l { margin-left: 8rem; } .ml7-l { margin-left: 16rem; } .mr0-l { margin-right: 0; } .mr1-l { margin-right: 0.25rem; } .mr2-l { margin-right: 0.5rem; } .mr3-l { margin-right: 1rem; } .mr4-l { margin-right: 2rem; } .mr5-l { margin-right: 4rem; } .mr6-l { margin-right: 8rem; } .mr7-l { margin-right: 16rem; } .mb0-l { margin-bottom: 0; } .mb1-l { margin-bottom: 0.25rem; } .mb2-l { margin-bottom: 0.5rem; } .mb3-l { margin-bottom: 1rem; } .mb4-l { margin-bottom: 2rem; } .mb5-l { margin-bottom: 4rem; } .mb6-l { margin-bottom: 8rem; } .mb7-l { margin-bottom: 16rem; } .mt0-l { margin-top: 0; } .mt1-l { margin-top: 0.25rem; } .mt2-l { margin-top: 0.5rem; } .mt3-l { margin-top: 1rem; } .mt4-l { margin-top: 2rem; } .mt5-l { margin-top: 4rem; } .mt6-l { margin-top: 8rem; } .mt7-l { margin-top: 16rem; } .mv0-l { margin-top: 0; margin-bottom: 0; } .mv1-l { margin-top: 0.25rem; margin-bottom: 0.25rem; } .mv2-l { margin-top: 0.5rem; margin-bottom: 0.5rem; } .mv3-l { margin-top: 1rem; margin-bottom: 1rem; } .mv4-l { margin-top: 2rem; margin-bottom: 2rem; } .mv5-l { margin-top: 4rem; margin-bottom: 4rem; } .mv6-l { margin-top: 8rem; margin-bottom: 8rem; } .mv7-l { margin-top: 16rem; margin-bottom: 16rem; } .mh0-l { margin-left: 0; margin-right: 0; } .mh1-l { margin-left: 0.25rem; margin-right: 0.25rem; } .mh2-l { margin-left: 0.5rem; margin-right: 0.5rem; } .mh3-l { margin-left: 1rem; margin-right: 1rem; } .mh4-l { margin-left: 2rem; margin-right: 2rem; } .mh5-l { margin-left: 4rem; margin-right: 4rem; } .mh6-l { margin-left: 8rem; margin-right: 8rem; } .mh7-l { margin-left: 16rem; margin-right: 16rem; } .na1-l { margin: -0.25rem; } .na2-l { margin: -0.5rem; } .na3-l { margin: -1rem; } .na4-l { margin: -2rem; } .na5-l { margin: -4rem; } .na6-l { margin: -8rem; } .na7-l { margin: -16rem; } .nl1-l { margin-left: -0.25rem; } .nl2-l { margin-left: -0.5rem; } .nl3-l { margin-left: -1rem; } .nl4-l { margin-left: -2rem; } .nl5-l { margin-left: -4rem; } .nl6-l { margin-left: -8rem; } .nl7-l { margin-left: -16rem; } .nr1-l { margin-right: -0.25rem; } .nr2-l { margin-right: -0.5rem; } .nr3-l { margin-right: -1rem; } .nr4-l { margin-right: -2rem; } .nr5-l { margin-right: -4rem; } .nr6-l { margin-right: -8rem; } .nr7-l { margin-right: -16rem; } .nb1-l { margin-bottom: -0.25rem; } .nb2-l { margin-bottom: -0.5rem; } .nb3-l { margin-bottom: -1rem; } .nb4-l { margin-bottom: -2rem; } .nb5-l { margin-bottom: -4rem; } .nb6-l { margin-bottom: -8rem; } .nb7-l { margin-bottom: -16rem; } .nt1-l { margin-top: -0.25rem; } .nt2-l { margin-top: -0.5rem; } .nt3-l { margin-top: -1rem; } .nt4-l { margin-top: -2rem; } .nt5-l { margin-top: -4rem; } .nt6-l { margin-top: -8rem; } .nt7-l { margin-top: -16rem; } .strike-l { text-decoration: line-through; } .underline-l { text-decoration: underline; } .no-underline-l { text-decoration: none; } .tl-l { text-align: left; } .tr-l { text-align: right; } .tc-l { text-align: center; } .tj-l { text-align: justify; } .ttc-l { text-transform: capitalize; } .ttl-l { text-transform: lowercase; } .ttu-l { text-transform: uppercase; } .ttn-l { text-transform: none; } .f-6-l, .f-headline-l { font-size: 6rem; } .f-5-l, .f-subheadline-l { font-size: 5rem; } .f1-l { font-size: 3rem; } .f2-l { font-size: 2.25rem; } .f3-l { font-size: 1.5rem; } .f4-l { font-size: 1.25rem; } .f5-l { font-size: 1rem; } .f6-l { font-size: 0.875rem; } .f7-l { font-size: 0.75rem; } .measure-l { max-width: 30em; } .measure-wide-l { max-width: 34em; } .measure-narrow-l { max-width: 20em; } .indent-l { text-indent: 1em; margin-top: 0; margin-bottom: 0; } .small-caps-l { font-variant: small-caps; } .truncate-l { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .center-l { margin-right: auto; margin-left: auto; } .mr-auto-l { margin-right: auto; } .ml-auto-l { margin-left: auto; } .clip-l { position: fixed !important; _position: absolute !important; clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ clip: rect(1px, 1px, 1px, 1px); } .ws-normal-l { white-space: normal; } .nowrap-l { white-space: nowrap; } .pre-l { white-space: pre; } .v-base-l { vertical-align: baseline; } .v-mid-l { vertical-align: middle; } .v-top-l { vertical-align: top; } .v-btm-l { vertical-align: bottom; } } ================================================ FILE: examples/github_search/common_github_search/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../../analysis_options.yaml linter: rules: public_member_api_docs: false ================================================ FILE: examples/github_search/common_github_search/lib/common_github_search.dart ================================================ export 'src/github_cache.dart'; export 'src/github_client.dart'; export 'src/github_repository.dart'; export 'src/github_search_bloc/github_search_bloc.dart'; export 'src/github_search_bloc/github_search_event.dart'; export 'src/github_search_bloc/github_search_state.dart'; export 'src/models/models.dart'; ================================================ FILE: examples/github_search/common_github_search/lib/src/github_cache.dart ================================================ import 'package:common_github_search/common_github_search.dart'; class GithubCache { final _cache = {}; SearchResult? get(String term) => _cache[term]; void set(String term, SearchResult result) => _cache[term] = result; bool contains(String term) => _cache.containsKey(term); void remove(String term) => _cache.remove(term); void close() { _cache.clear(); } } ================================================ FILE: examples/github_search/common_github_search/lib/src/github_client.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:common_github_search/common_github_search.dart'; import 'package:http/http.dart' as http; class GithubClient { GithubClient({ http.Client? httpClient, this.baseUrl = 'https://api.github.com/search/repositories?q=', }) : _httpClient = httpClient ?? http.Client(); final String baseUrl; final http.Client _httpClient; Future search(String term) async { final response = await _httpClient.get(Uri.parse('$baseUrl$term')); final results = json.decode(response.body) as Map; if (response.statusCode == 200) { return SearchResult.fromJson(results); } else { throw SearchResultError.fromJson(results); } } void close() { _httpClient.close(); } } ================================================ FILE: examples/github_search/common_github_search/lib/src/github_repository.dart ================================================ import 'dart:async'; import 'package:common_github_search/common_github_search.dart'; class GithubRepository { GithubRepository({GithubCache? cache, GithubClient? client}) : _cache = cache ?? GithubCache(), _client = client ?? GithubClient(); final GithubCache _cache; final GithubClient _client; Future search(String term) async { final cachedResult = _cache.get(term); if (cachedResult != null) { return cachedResult; } final result = await _client.search(term); _cache.set(term, result); return result; } void dispose() { _cache.close(); _client.close(); } } ================================================ FILE: examples/github_search/common_github_search/lib/src/github_search_bloc/github_search_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:common_github_search/common_github_search.dart'; import 'package:stream_transform/stream_transform.dart'; const _duration = Duration(milliseconds: 300); EventTransformer debounce(Duration duration) { return (events, mapper) => events.debounce(duration).switchMap(mapper); } class GithubSearchBloc extends Bloc { GithubSearchBloc({required GithubRepository githubRepository}) : _githubRepository = githubRepository, super(SearchStateEmpty()) { on(_onTextChanged, transformer: debounce(_duration)); } final GithubRepository _githubRepository; Future _onTextChanged( TextChanged event, Emitter emit, ) async { final searchTerm = event.text; if (searchTerm.isEmpty) return emit(SearchStateEmpty()); emit(SearchStateLoading()); try { final results = await _githubRepository.search(searchTerm); emit(SearchStateSuccess(results.items)); } catch (error) { emit( error is SearchResultError ? SearchStateError(error.message) : const SearchStateError('something went wrong'), ); } } } ================================================ FILE: examples/github_search/common_github_search/lib/src/github_search_bloc/github_search_event.dart ================================================ import 'package:equatable/equatable.dart'; sealed class GithubSearchEvent extends Equatable { const GithubSearchEvent(); } final class TextChanged extends GithubSearchEvent { const TextChanged({required this.text}); final String text; @override List get props => [text]; @override String toString() => 'TextChanged { text: $text }'; } ================================================ FILE: examples/github_search/common_github_search/lib/src/github_search_bloc/github_search_state.dart ================================================ import 'package:common_github_search/common_github_search.dart'; import 'package:equatable/equatable.dart'; sealed class GithubSearchState extends Equatable { const GithubSearchState(); @override List get props => []; } final class SearchStateEmpty extends GithubSearchState {} final class SearchStateLoading extends GithubSearchState {} final class SearchStateSuccess extends GithubSearchState { const SearchStateSuccess(this.items); final List items; @override List get props => [items]; @override String toString() => 'SearchStateSuccess { items: ${items.length} }'; } final class SearchStateError extends GithubSearchState { const SearchStateError(this.error); final String error; @override List get props => [error]; } ================================================ FILE: examples/github_search/common_github_search/lib/src/models/github_user.dart ================================================ class GithubUser { const GithubUser({ required this.login, required this.avatarUrl, }); factory GithubUser.fromJson(Map json) { return GithubUser( login: json['login'] as String, avatarUrl: json['avatar_url'] as String, ); } final String login; final String avatarUrl; } ================================================ FILE: examples/github_search/common_github_search/lib/src/models/models.dart ================================================ export 'github_user.dart'; export 'search_result.dart'; export 'search_result_error.dart'; export 'search_result_item.dart'; ================================================ FILE: examples/github_search/common_github_search/lib/src/models/search_result.dart ================================================ import 'package:common_github_search/common_github_search.dart'; class SearchResult { const SearchResult({required this.items}); factory SearchResult.fromJson(Map json) { final items = (json['items'] as List) .map( (dynamic item) => SearchResultItem.fromJson(item as Map), ) .toList(); return SearchResult(items: items); } final List items; } ================================================ FILE: examples/github_search/common_github_search/lib/src/models/search_result_error.dart ================================================ class SearchResultError implements Exception { SearchResultError({required this.message}); factory SearchResultError.fromJson(Map json) { return SearchResultError( message: json['message'] as String, ); } final String message; } ================================================ FILE: examples/github_search/common_github_search/lib/src/models/search_result_item.dart ================================================ import 'package:common_github_search/common_github_search.dart'; class SearchResultItem { const SearchResultItem({ required this.fullName, required this.htmlUrl, required this.owner, }); factory SearchResultItem.fromJson(Map json) { return SearchResultItem( fullName: json['full_name'] as String, htmlUrl: json['html_url'] as String, owner: GithubUser.fromJson(json['owner'] as Map), ); } final String fullName; final String htmlUrl; final GithubUser owner; } ================================================ FILE: examples/github_search/common_github_search/pubspec.yaml ================================================ name: common_github_search description: Shared Code between AngularDart and Flutter version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: bloc: ^9.0.0 equatable: ^2.0.0 http: ^1.0.0 stream_transform: ^2.0.0 dev_dependencies: bloc_lint: ^0.3.0 ================================================ FILE: examples/github_search/common_github_search/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../../../packages/bloc bloc_lint: path: ../../../packages/bloc_lint ================================================ FILE: examples/github_search/flutter_github_search/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .build/ .buildlog/ .history .svn/ .swiftpm/ migrate_working_dir/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: examples/github_search/flutter_github_search/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" channel: "stable" project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - platform: root create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a - platform: web create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a # User provided section # List of Local paths (relative to this file) that should be # ignored by the migrate tool. # # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - 'ios/Runner.xcodeproj/project.pbxproj' ================================================ FILE: examples/github_search/flutter_github_search/README.md ================================================ [![build](https://github.com/felangel/bloc/actions/workflows/main.yaml/badge.svg)](https://github.com/felangel/bloc/actions) # flutter_github_search A new Flutter project. ## Getting Started This project is a starting point for a Flutter application. A few resources to get you started if this is your first Flutter project: - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) For help getting started with Flutter, view our [online documentation](https://flutter.dev/docs), which offers tutorials, samples, guidance on mobile development, and a full API reference. ================================================ FILE: examples/github_search/flutter_github_search/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../../analysis_options.yaml analyzer: exclude: [build/**] linter: rules: public_member_api_docs: false ================================================ FILE: examples/github_search/flutter_github_search/ios/.gitignore ================================================ **/dgph *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: examples/github_search/flutter_github_search/ios/Podfile ================================================ # Uncomment this line to define a global platform for your project # platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end ================================================ FILE: examples/github_search/flutter_github_search/lib/main.dart ================================================ import 'package:common_github_search/common_github_search.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_github_search/search_form.dart'; void main() => runApp(const App()); class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return RepositoryProvider( create: (_) => GithubRepository(), dispose: (repository) => repository.dispose(), child: MaterialApp( title: 'GitHub Search', home: Scaffold( appBar: AppBar(title: const Text('GitHub Search')), body: BlocProvider( create: (context) => GithubSearchBloc( githubRepository: context.read(), ), child: const SearchForm(), ), ), ), ); } } ================================================ FILE: examples/github_search/flutter_github_search/lib/search_form.dart ================================================ import 'package:common_github_search/common_github_search.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:url_launcher/url_launcher.dart'; class SearchForm extends StatelessWidget { const SearchForm({super.key}); @override Widget build(BuildContext context) { return Column( children: [ _SearchBar(), _SearchBody(), ], ); } } class _SearchBar extends StatefulWidget { @override State<_SearchBar> createState() => _SearchBarState(); } class _SearchBarState extends State<_SearchBar> { final _textController = TextEditingController(); late GithubSearchBloc _githubSearchBloc; @override void initState() { super.initState(); _githubSearchBloc = context.read(); } @override void dispose() { _textController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return TextField( controller: _textController, autocorrect: false, onChanged: (text) { _githubSearchBloc.add( TextChanged(text: text), ); }, decoration: InputDecoration( prefixIcon: const Icon(Icons.search), suffixIcon: GestureDetector( onTap: _onClearTapped, child: const Icon(Icons.clear), ), border: InputBorder.none, hintText: 'Enter a search term', ), ); } void _onClearTapped() { _textController.text = ''; _githubSearchBloc.add(const TextChanged(text: '')); } } class _SearchBody extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { return switch (state) { SearchStateEmpty() => const Text('Please enter a term to begin'), SearchStateLoading() => const CircularProgressIndicator.adaptive(), SearchStateError() => Text(state.error), SearchStateSuccess() => state.items.isEmpty ? const Text('No Results') : Expanded(child: _SearchResults(items: state.items)), }; }, ); } } class _SearchResults extends StatelessWidget { const _SearchResults({required this.items}); final List items; @override Widget build(BuildContext context) { return ListView.builder( itemCount: items.length, itemBuilder: (BuildContext context, int index) { return _SearchResultItem(item: items[index]); }, ); } } class _SearchResultItem extends StatelessWidget { const _SearchResultItem({required this.item}); final SearchResultItem item; @override Widget build(BuildContext context) { return ListTile( leading: CircleAvatar( child: Image.network(item.owner.avatarUrl), ), title: Text(item.fullName), onTap: () => launchUrl(Uri.parse(item.htmlUrl)), ); } } ================================================ FILE: examples/github_search/flutter_github_search/pubspec.yaml ================================================ name: flutter_github_search description: A new Flutter project. version: 1.0.0+1 publish_to: none environment: sdk: ">=3.10.0 <4.0.0" dependencies: bloc: ^9.0.0 common_github_search: path: ../common_github_search flutter: sdk: flutter flutter_bloc: ^9.0.1 url_launcher: ^6.0.0 flutter: uses-material-design: true dev_dependencies: bloc_lint: ^0.3.0 ================================================ FILE: examples/github_search/flutter_github_search/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../../../packages/bloc bloc_lint: path: ../../../packages/bloc_lint flutter_bloc: path: ../../../packages/flutter_bloc ================================================ FILE: examples/github_search/flutter_github_search/web/index.html ================================================ flutter_github_search ================================================ FILE: examples/github_search/flutter_github_search/web/manifest.json ================================================ { "name": "flutter_github_search", "short_name": "flutter_github_search", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] } ================================================ FILE: extensions/intellij/README.md ================================================ # Bloc Plugin for IntelliJ and Android Studio ![dialog](https://github.com/felangel/bloc/raw/master/extensions/intellij/assets/dialog.png) ## Introduction Bloc plugin for [IntelliJ](https://www.jetbrains.com/idea/) and [Android Studio](https://developer.android.com/studio/) with support for the [Bloc Library](https://bloclibrary.dev) and provides tools for effectively creating Blocs and Cubits for both [Flutter](https://flutter.dev/) and [AngularDart](https://angulardart.dev/) apps. ## Installation You can find the plugin in the official IntelliJ and Android Studio marketplace: - [Bloc](https://plugins.jetbrains.com/plugin/12129-bloc) ### How to use Simply right click on the File Project view, go to `New -> Bloc Class`, give it a name, select if you want to use [Equatable](https://github.com/felangel/equatable), and click on `OK` to see all the boilerplate generated. ### Quick code action Wrapping a widget is also possible with `Alt + ENTER` shortcut. If you wish to disable this quick code action `(Bloc) Wrap with` you can do it so by going to `Settings - Editor - Intentions - Bloc`. ![intention_settings](https://github.com/felangel/bloc/raw/master/extensions/intellij/assets/intention_settings.png) ### Equatable props generator Right click and use `Generate -> Equatable Props` to automatically generate the `props` override when using `Equatable`. ![equatable_props_override](https://github.com/felangel/bloc/raw/master/extensions/intellij/assets/equatable_props_override.png) ## Snippets ### Bloc | Shortcut | Description | | ------------------- | ----------------------------------------------- | | `importbloc` | Imports `package:bloc` | | `importflutterbloc` | Imports `package:flutter_bloc` | | `importbloctest` | Imports `package:bloc_test` | | `bloc` | Creates a bloc class | | `cubit` | Creates a cubit class | | `blocobserver` | Creates a `BlocObserver` class | | `blocprovider` | Creates a `BlocProvider` widget | | `multiblocprovider` | Creates a `MultiBlocProvider` widget | | `repoprovider` | Creates a `RepositoryProvider` widget | | `multirepoprovider` | Creates a `MultiRepositoryProvider` widget | | `blocbuilder` | Creates a `BlocBuilder` widget | | `bloclistener` | Creates a `BlocListener` widget | | `multibloclistener` | Creates a `MultiBlocListener` widget | | `blocconsumer` | Creates a `BlocConsumer` widget | | `blocof` | Shortcut for `BlocProvider.of()` | | `repoof` | Shortcut for `RepositoryProvider.of()` | | `read` | Shortcut for `context.read()` | | `watch` | Shortcut for `context.watch()` | | `select` | Shortcut for `context.select()` | | `blocstate` | Creates a state class | | `blocevent` | Creates an event class | | `bloctest` | Creates a `blocTest` with build, act and expect | | `mockbloc` | Creates a class extenting `MockBloc` | | `mockcubit` | Creates a class extending `MockCubit` | | `fake` | Creates a class extending `Fake` | ### Freezed Bloc | Shortcut | Description | | ------------ | -------------------------------------------------- | | `feventwhen` | Creates an event handler with freeze.when function | | `feventmap` | Creates an event handler with freeze.map function | | `fstate` | Creates a sub state | | `fevent` | Creates a sub event | ## Deployment Using [Plugin Repository](http://www.jetbrains.org/intellij/sdk/docs/plugin_repository/index.html) ================================================ FILE: extensions/intellij/intellij_generator_plugin/.gitignore ================================================ # User-specific stuff: .idea/**/workspace.xml .idea/**/tasks.xml .idea/dictionaries local.properties # Sensitive or high-churn files: .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.xml .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml # Gradle: .idea/**/gradle.xml .idea/**/libraries .gradle .gradle/* build/ build/* ## File-based project format: *.iws ## Plugin-specific files: # IntelliJ /out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml ================================================ FILE: extensions/intellij/intellij_generator_plugin/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { java id("org.jetbrains.intellij.platform") version "2.5.0" kotlin("jvm") version "2.1.0" idea } group = "com.bloc" version = "4.1.11" val lsp4ijVersion: String by project val lsp4jVersion: String by project val caseFormatVersion: String by project val apacheCommonsTextVersion: String by project java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } repositories { mavenCentral() exclusiveContent { forRepository { maven { setUrl("https://mvn.falsepattern.com/releases") name = "mavenpattern" } } filter { includeModule("com.redhat.devtools.intellij", "lsp4ij") } } intellijPlatform { defaultRepositories() } } intellijPlatform { pluginConfiguration { ideaVersion { untilBuild = provider { null } } } pluginVerification { ides { recommended() } } } dependencies { intellijPlatform { intellijIdeaCommunity("2023.3") bundledPlugin("com.intellij.java") plugin("com.redhat.devtools.lsp4ij:$lsp4ijVersion") } testImplementation(kotlin("test")) compileOnly("com.redhat.devtools.intellij:lsp4ij:$lsp4ijVersion") compileOnly("org.eclipse.lsp4j:org.eclipse.lsp4j:$lsp4jVersion") implementation("com.fleshgrinder.kotlin:case-format:$caseFormatVersion") implementation("org.apache.commons:commons-text:$apacheCommonsTextVersion") } tasks { compileKotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } compileTestKotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: extensions/intellij/intellij_generator_plugin/gradle.properties ================================================ kotlin.code.style=official kotlin.stdlib.default.dependency=true # Libraries versions lsp4jVersion=0.21.1 lsp4ijVersion=0.12.0 caseFormatVersion=0.2.0 apacheCommonsTextVersion=1.13.1 ================================================ FILE: extensions/intellij/intellij_generator_plugin/gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: extensions/intellij/intellij_generator_plugin/gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: extensions/intellij/intellij_generator_plugin/settings.gradle ================================================ rootProject.name = 'intellij_generator_plugin' ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/action/BlocTemplateType.java ================================================ package com.bloc.intellij_generator_plugin.action; public enum BlocTemplateType { BASIC, EQUATABLE, FREEZED } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/action/GenerateBlocAction.kt ================================================ package com.bloc.intellij_generator_plugin.action import com.bloc.intellij_generator_plugin.generator.BlocGeneratorFactory import com.bloc.intellij_generator_plugin.generator.BlocGenerator import com.intellij.lang.java.JavaLanguage import com.intellij.openapi.actionSystem.* import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.project.Project import com.intellij.psi.* class GenerateBlocAction : AnAction(), GenerateBlocDialog.Listener { override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT private lateinit var dataContext: DataContext override fun actionPerformed(e: AnActionEvent) { val dialog = GenerateBlocDialog(this) dialog.show() } override fun onGenerateBlocClicked( blocName: String?, blocTemplateType: BlocTemplateType, ) { blocName?.let { name -> val generators = BlocGeneratorFactory.getBlocGenerators(name, blocTemplateType) generate(generators) } } override fun update(e: AnActionEvent) { e.dataContext.let { this.dataContext = it val presentation = e.presentation presentation.isEnabled = true } } private fun generate(mainSourceGenerators: List) { val project = CommonDataKeys.PROJECT.getData(dataContext) val view = LangDataKeys.IDE_VIEW.getData(dataContext) val directory = view?.orChooseDirectory ApplicationManager.getApplication().runWriteAction { CommandProcessor.getInstance().executeCommand( project, { mainSourceGenerators.forEach { createSourceFile(project!!, it, directory!!) } }, "Generate a new Bloc", null ) } } private fun createSourceFile(project: Project, generator: BlocGenerator, directory: PsiDirectory) { val fileName = generator.fileName() val existingPsiFile = directory.findFile(fileName) if (existingPsiFile != null) { val document = PsiDocumentManager.getInstance(project).getDocument(existingPsiFile) document?.insertString(document.textLength, "\n" + generator.generate()) return } val psiFile = PsiFileFactory.getInstance(project) .createFileFromText(fileName, JavaLanguage.INSTANCE, generator.generate()) directory.add(psiFile) } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/action/GenerateBlocDialog.form ================================================
================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/action/GenerateBlocDialog.java ================================================ package com.bloc.intellij_generator_plugin.action; import com.intellij.openapi.ui.DialogWrapper; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.Objects; public class GenerateBlocDialog extends DialogWrapper { private final Listener listener; private JTextField blocNameTextField; private JPanel contentPanel; private JComboBox style; public GenerateBlocDialog(final Listener listener) { super(false); this.listener = listener; init(); } @Nullable @Override protected JComponent createCenterPanel() { return contentPanel; } @Override protected void doOKAction() { super.doOKAction(); BlocTemplateType blocTemplateType; final String selectedStyle = Objects.requireNonNull(style.getSelectedItem()).toString(); if (Objects.equals(selectedStyle, "Equatable")) { blocTemplateType = BlocTemplateType.EQUATABLE; } else if (Objects.equals(selectedStyle, "Freezed")) { blocTemplateType = BlocTemplateType.FREEZED; } else { blocTemplateType = BlocTemplateType.BASIC; } this.listener.onGenerateBlocClicked(blocNameTextField.getText(), blocTemplateType); } @Nullable @Override public JComponent getPreferredFocusedComponent() { return blocNameTextField; } public interface Listener { void onGenerateBlocClicked(String blocName, BlocTemplateType blocTemplateType); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/action/GenerateCubitAction.kt ================================================ package com.bloc.intellij_generator_plugin.action import com.bloc.intellij_generator_plugin.generator.CubitGeneratorFactory import com.bloc.intellij_generator_plugin.generator.CubitGenerator import com.intellij.lang.java.JavaLanguage import com.intellij.openapi.actionSystem.* import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.project.Project import com.intellij.psi.* class GenerateCubitAction : AnAction(), GenerateBlocDialog.Listener { override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT private lateinit var dataContext: DataContext override fun actionPerformed(e: AnActionEvent) { val dialog = GenerateBlocDialog(this) dialog.show() } override fun onGenerateBlocClicked(name: String?, blocTemplateType: BlocTemplateType) { name?.let { val generators = CubitGeneratorFactory.getCubitGenerators(it, blocTemplateType) generate(generators) } } override fun update(e: AnActionEvent) { e.dataContext.let { this.dataContext = it val presentation = e.presentation presentation.isEnabled = true } } protected fun generate(mainSourceGenerators: List) { val project = CommonDataKeys.PROJECT.getData(dataContext) val view = LangDataKeys.IDE_VIEW.getData(dataContext) val directory = view?.orChooseDirectory ApplicationManager.getApplication().runWriteAction { CommandProcessor.getInstance().executeCommand( project, { mainSourceGenerators.forEach { createSourceFile(project!!, it, directory!!) } }, "Generate a new Cubit", null ) } } private fun createSourceFile(project: Project, generator: CubitGenerator, directory: PsiDirectory) { val fileName = generator.fileName() val existingPsiFile = directory.findFile(fileName) if (existingPsiFile != null) { val document = PsiDocumentManager.getInstance(project).getDocument(existingPsiFile) document?.insertString(document.textLength, "\n" + generator.generate()) return } val psiFile = PsiFileFactory.getInstance(project) .createFileFromText(fileName, JavaLanguage.INSTANCE, generator.generate()) directory.add(psiFile) } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/action/GenerateEquatablePropsAction.kt ================================================ package com.bloc.intellij_generator_plugin.action import com.intellij.lang.ASTNode import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.Document import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.psi.codeStyle.CodeStyleManager import com.intellij.psi.util.PsiUtilBase class GenerateEquatablePropsAction : AnAction() { override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT private var propsNullable = false override fun update(event: AnActionEvent) { super.update(event) val action = event.presentation val editor = event.dataContext.getData(CommonDataKeys.EDITOR) ?: return val project = event.project ?: return val currentFile = PsiUtilBase.getPsiFileInEditor(editor, project) action.isEnabledAndVisible = currentFile?.name?.endsWith(".dart") == true } override fun actionPerformed(event: AnActionEvent) { val project = event.project ?: return val editor = event.dataContext.getData(CommonDataKeys.EDITOR) ?: return val currentFile = PsiUtilBase.getPsiFileInEditor(editor, project) ?: return val currentOffset = editor.caretModel.currentCaret.offset val element = currentFile.findElementAt(currentOffset) ?: return val classNode = findClassDefinition(element) ?: return val members = findAllClassMembers(classNode) ?: return val memberNames = getPropsList(editor.document, members) val nullStr = if (propsNullable) "?" else "" propsNullable = false val props: String = reformatProps(memberNames.filter { s -> s != "" }) val propsStr = "@override\nList get props => [$props];" WriteCommandAction.runWriteCommandAction(project) { editor.document.insertString(currentOffset, propsStr) PsiDocumentManager.getInstance(project).commitDocument(editor.document) CodeStyleManager.getInstance(project).reformat(currentFile) } } private fun findClassDefinition(element: PsiElement): ASTNode? { var node: ASTNode? = element.node while (node != null) { if (node.toString() == "Element(CLASS_DEFINITION)") { break } node = node.treeParent } return node } private fun findAllClassMembers(node: ASTNode) = node.getChildren(null).find { astNode -> astNode.toString() == "Element(CLASS_BODY)" } ?.getChildren(null)?.find { astNode -> astNode.toString() == "Element(CLASS_MEMBERS)" }?.getChildren(null) ?.filter { astNode -> astNode.toString() == "Element(VAR_DECLARATION_LIST)" } private fun findNonAccessModifiers(memberNode: ASTNode) = memberNode.getChildren(null) .filter { astNode -> astNode.toString().startsWith("PsiElement(") } private fun findMemberType(memberNode: ASTNode) = memberNode.getChildren(null) .find { astNode -> astNode.toString() == "Element(TYPE)" } private fun findMemberName(memberNode: ASTNode) = memberNode.getChildren(null).find { astNode -> astNode.toString() == "Element(COMPONENT_NAME)" } ?.getChildren(null) ?.find { astNode -> astNode.toString() == "Element(ID)" }?.getChildren(null) ?.find { astNode -> astNode.toString() == "PsiElement(IDENTIFIER)" } private fun getPropsList( doc: Document, members: List ) = members.map { n -> val memberNode = n.firstChildNode if (memberNode.toString() == "Element(VAR_ACCESS_DECLARATION)") { val accessModifiers = findNonAccessModifiers(memberNode) // list only class members with non-access modifier "final", but not with "static final" or without any if (accessModifiers.size == 1 && accessModifiers[0].toString() == "PsiElement(final)") { val type = findMemberType(memberNode)?.psi if (!propsNullable && type != null && doc.getText(type.textRange).endsWith("?")) { propsNullable = true } val member = findMemberName(memberNode)?.psi return@map if (member == null) "" else doc.getText(member.textRange) } } return@map "" } private fun reformatProps(memberNames: List): String { var props = "" for ((i, s) in memberNames.withIndex()) { if (memberNames.size == 1 || (i == memberNames.size - 1)) { props += s } else { props += "$s, " } } if (props.length >= 45) { // line probably longer than 80 chars props += "," } return props } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/generator/BlocGenerator.kt ================================================ package com.bloc.intellij_generator_plugin.generator import com.bloc.intellij_generator_plugin.action.BlocTemplateType import com.fleshgrinder.extensions.kotlin.toLowerSnakeCase import com.fleshgrinder.extensions.kotlin.toUpperCamelCase import com.google.common.io.CharStreams import org.apache.commons.text.StringSubstitutor import java.io.InputStreamReader abstract class BlocGenerator( private val name: String, blocTemplateType: BlocTemplateType, templateName: String ) { private val TEMPLATE_BLOC_PASCAL_CASE = "bloc_pascal_case" private val TEMPLATE_BLOC_SNAKE_CASE = "bloc_snake_case" private val templateString: String private val templateValues: MutableMap init { templateValues = mutableMapOf( TEMPLATE_BLOC_PASCAL_CASE to pascalCase(), TEMPLATE_BLOC_SNAKE_CASE to snakeCase() ) try { val templateFolder = when (blocTemplateType) { BlocTemplateType.BASIC -> "bloc_basic" BlocTemplateType.EQUATABLE -> "bloc_equatable" BlocTemplateType.FREEZED -> "bloc_freezed" } val resource = "/templates/$templateFolder/$templateName.dart.template" val resourceAsStream = BlocGenerator::class.java.getResourceAsStream(resource) templateString = CharStreams.toString(InputStreamReader(resourceAsStream!!, Charsets.UTF_8)).replace("\r\n", "\n").replace("\r","\n"); } catch (e: Exception) { throw RuntimeException(e) } } abstract fun fileName(): String fun generate(): String { val substitutor = StringSubstitutor(templateValues, "{{", "}}", '\\') return substitutor.replace(templateString) } private fun pascalCase(): String = name.toUpperCamelCase().replace("Bloc", "") fun snakeCase() = name.toLowerSnakeCase().replace("_bloc", "") fun fileExtension() = "dart" } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/generator/BlocGeneratorFactory.kt ================================================ package com.bloc.intellij_generator_plugin.generator import com.bloc.intellij_generator_plugin.action.BlocTemplateType import com.bloc.intellij_generator_plugin.generator.components.BlocEventGenerator import com.bloc.intellij_generator_plugin.generator.components.BlocGenerator import com.bloc.intellij_generator_plugin.generator.components.BlocStateGenerator object BlocGeneratorFactory { fun getBlocGenerators( name: String, blocTemplateType: BlocTemplateType, ): List { val bloc = BlocGenerator(name, blocTemplateType) val event = BlocEventGenerator(name, blocTemplateType) val state = BlocStateGenerator(name, blocTemplateType) return listOf(bloc, event, state) } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/generator/CubitGenerator.kt ================================================ package com.bloc.intellij_generator_plugin.generator import com.bloc.intellij_generator_plugin.action.BlocTemplateType import com.fleshgrinder.extensions.kotlin.toLowerSnakeCase import com.fleshgrinder.extensions.kotlin.toUpperCamelCase import com.google.common.io.CharStreams import org.apache.commons.text.StringSubstitutor import java.io.InputStreamReader abstract class CubitGenerator( private val name: String, blocTemplateType: BlocTemplateType, templateName: String ) { private val TEMPLATE_CUBIT_PASCAL_CASE = "cubit_pascal_case" private val TEMPLATE_CUBIT_SNAKE_CASE = "cubit_snake_case" private val templateString: String private val templateValues: MutableMap init { templateValues = mutableMapOf( TEMPLATE_CUBIT_PASCAL_CASE to pascalCase(), TEMPLATE_CUBIT_SNAKE_CASE to snakeCase() ) try { val templateFolder = when (blocTemplateType) { BlocTemplateType.BASIC -> "cubit_basic" BlocTemplateType.EQUATABLE -> "cubit_equatable" BlocTemplateType.FREEZED -> "cubit_freezed" } val resource = "/templates/$templateFolder/$templateName.dart.template" val resourceAsStream = CubitGenerator::class.java.getResourceAsStream(resource) templateString = CharStreams.toString(InputStreamReader(resourceAsStream!!, Charsets.UTF_8)).replace("\r\n", "\n").replace("\r","\n"); } catch (e: Exception) { throw RuntimeException(e) } } abstract fun fileName(): String fun generate(): String { val substitutor = StringSubstitutor(templateValues, "{{", "}}", '\\') return substitutor.replace(templateString) } private fun pascalCase(): String = name.toUpperCamelCase().replace("Cubit", "") fun snakeCase() = name.toLowerSnakeCase().replace("_cubit", "") fun fileExtension() = "dart" } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/generator/CubitGeneratorFactory.kt ================================================ package com.bloc.intellij_generator_plugin.generator import com.bloc.intellij_generator_plugin.action.BlocTemplateType import com.bloc.intellij_generator_plugin.generator.components.CubitGenerator import com.bloc.intellij_generator_plugin.generator.components.CubitStateGenerator object CubitGeneratorFactory { fun getCubitGenerators( name: String, blocTemplateType: BlocTemplateType, ): List { val cubit = CubitGenerator(name, blocTemplateType) val state = CubitStateGenerator(name, blocTemplateType) return listOf(cubit, state) } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/generator/components/BlocEventGenerator.kt ================================================ package com.bloc.intellij_generator_plugin.generator.components import com.bloc.intellij_generator_plugin.action.BlocTemplateType import com.bloc.intellij_generator_plugin.generator.BlocGenerator class BlocEventGenerator( blocName: String, blocTemplateType: BlocTemplateType ) : BlocGenerator(blocName, blocTemplateType, templateName = "bloc_event") { override fun fileName() = "${snakeCase()}_event.${fileExtension()}" } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/generator/components/BlocGenerator.kt ================================================ package com.bloc.intellij_generator_plugin.generator.components import com.bloc.intellij_generator_plugin.action.BlocTemplateType import com.bloc.intellij_generator_plugin.generator.BlocGenerator class BlocGenerator( name: String, blocTemplateType: BlocTemplateType ) : BlocGenerator(name, blocTemplateType, templateName = "bloc") { override fun fileName() = "${snakeCase()}_bloc.${fileExtension()}" } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/generator/components/BlocStateGenerator.kt ================================================ package com.bloc.intellij_generator_plugin.generator.components import com.bloc.intellij_generator_plugin.action.BlocTemplateType import com.bloc.intellij_generator_plugin.generator.BlocGenerator class BlocStateGenerator( name: String, blocTemplateType: BlocTemplateType ) : BlocGenerator(name, blocTemplateType, templateName = "bloc_state") { override fun fileName() = "${snakeCase()}_state.${fileExtension()}" } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/generator/components/CubitGenerator.kt ================================================ package com.bloc.intellij_generator_plugin.generator.components import com.bloc.intellij_generator_plugin.action.BlocTemplateType import com.bloc.intellij_generator_plugin.generator.CubitGenerator class CubitGenerator( name: String, blocTemplateType: BlocTemplateType ) : CubitGenerator(name, blocTemplateType, templateName = "cubit") { override fun fileName() = "${snakeCase()}_cubit.${fileExtension()}" } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/generator/components/CubitStateGenerator.kt ================================================ package com.bloc.intellij_generator_plugin.generator.components import com.bloc.intellij_generator_plugin.action.BlocTemplateType import com.bloc.intellij_generator_plugin.generator.CubitGenerator class CubitStateGenerator( name: String, blocTemplateType: BlocTemplateType ) : CubitGenerator(name, blocTemplateType, templateName = "cubit_state") { override fun fileName() = "${snakeCase()}_state.${fileExtension()}" } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/intention_action/BlocConvertToMultiBlocListenerIntentionAction.kt ================================================ package com.bloc.intellij_generator_plugin.intention_action class BlocConvertToMultiBlocListenerIntentionAction : BlocConvertToMultiIntentionAction(SnippetType.MultiBlocListener) { /** * If this action is applicable, returns the text to be shown in the list of intention actions available. */ override fun getText(): String { return "Convert to MultiBlocListener" } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/intention_action/BlocConvertToMultiBlocProviderIntentionAction.kt ================================================ package com.bloc.intellij_generator_plugin.intention_action class BlocConvertToMultiBlocProviderIntentionAction : BlocConvertToMultiIntentionAction(SnippetType.MultiBlocProvider) { /** * If this action is applicable, returns the text to be shown in the list of intention actions available. */ override fun getText(): String { return "Convert to MultiBlocProvider" } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/intention_action/BlocConvertToMultiIntentionAction.kt ================================================ package com.bloc.intellij_generator_plugin.intention_action import com.bloc.intellij_generator_plugin.intention_action.Common.Companion.invokeSnippetAction import com.bloc.intellij_generator_plugin.intention_action.WrapHelper.Companion.blocWidgetChildFinder import com.bloc.intellij_generator_plugin.intention_action.WrapHelper.Companion.callExpressionFinder import com.intellij.codeInsight.intention.IntentionAction import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement import com.intellij.util.IncorrectOperationException abstract class BlocConvertToMultiIntentionAction(private val snippetType: SnippetType) : PsiElementBaseIntentionAction(), IntentionAction { var callExpressionElement: PsiElement? = null var blocChildCallExpressionElement: PsiElement? = null override fun getFamilyName(): String = text override fun isAvailable(project: Project, editor: Editor?, psiElement: PsiElement): Boolean { val shouldDisplayWrapMenu = Common.shouldDisplayWrapMenu(editor, project, psiElement) if (!shouldDisplayWrapMenu) return false callExpressionElement = callExpressionFinder(psiElement) if (callExpressionElement == null) return false return shouldDisplayConvertMenu() } private fun shouldDisplayConvertMenu(): Boolean { val widgetName = callExpressionElement?.text ?: return false if (widgetName.startsWith(snippetType.toString().removePrefix("Multi")) ) { val blocChildWidget = blocWidgetChildFinder(callExpressionElement!!) ?: return false blocChildCallExpressionElement = blocChildWidget return true } return false } @Throws(IncorrectOperationException::class) override fun invoke(project: Project, editor: Editor, element: PsiElement) { val runnable = Runnable { invokeSnippetAction( project, editor, snippetType, callExpressionElement!!, blocChildCallExpressionElement!! ) } WriteCommandAction.runWriteCommandAction(project, runnable) } override fun startInWriteAction(): Boolean = true } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/intention_action/BlocConvertToMultiRepositoryProviderIntentionAction.kt ================================================ package com.bloc.intellij_generator_plugin.intention_action class BlocConvertToMultiRepositoryProviderIntentionAction : BlocConvertToMultiIntentionAction(SnippetType.MultiRepositoryProvider) { /** * If this action is applicable, returns the text to be shown in the list of intention actions available. */ override fun getText(): String { return "Convert to MultiRepositoryProvider" } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/intention_action/BlocWrapWithBlocBuilderIntentionAction.kt ================================================ package com.bloc.intellij_generator_plugin.intention_action class BlocWrapWithBlocBuilderIntentionAction : BlocWrapWithIntentionAction(SnippetType.BlocBuilder) { /** * If this action is applicable, returns the text to be shown in the list of intention actions available. */ override fun getText(): String { return "Wrap with BlocBuilder" } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/intention_action/BlocWrapWithBlocConsumerIntentionAction.kt ================================================ package com.bloc.intellij_generator_plugin.intention_action class BlocWrapWithBlocConsumerIntentionAction : BlocWrapWithIntentionAction(SnippetType.BlocConsumer) { /** * If this action is applicable, returns the text to be shown in the list of intention actions available. */ override fun getText(): String { return "Wrap with BlocConsumer" } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/intention_action/BlocWrapWithBlocListenerIntentionAction.kt ================================================ package com.bloc.intellij_generator_plugin.intention_action class BlocWrapWithBlocListenerIntentionAction : BlocWrapWithIntentionAction(SnippetType.BlocListener) { /** * If this action is applicable, returns the text to be shown in the list of intention actions available. */ override fun getText(): String { return "Wrap with BlocListener" } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/intention_action/BlocWrapWithBlocProviderIntentionAction.kt ================================================ package com.bloc.intellij_generator_plugin.intention_action class BlocWrapWithBlocProviderIntentionAction : BlocWrapWithIntentionAction(SnippetType.BlocProvider) { /** * If this action is applicable, returns the text to be shown in the list of intention actions available. */ override fun getText(): String { return "Wrap with BlocProvider" } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/intention_action/BlocWrapWithBlocSelectorIntentionAction.kt ================================================ package com.bloc.intellij_generator_plugin.intention_action class BlocWrapWithBlocSelectorIntentionAction : BlocWrapWithIntentionAction(SnippetType.BlocSelector) { /** * If this action is applicable, returns the text to be shown in the list of intention actions available. */ override fun getText(): String { return "Wrap with BlocSelector" } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/intention_action/BlocWrapWithIntentionAction.kt ================================================ package com.bloc.intellij_generator_plugin.intention_action import com.bloc.intellij_generator_plugin.intention_action.Common.Companion.invokeSnippetAction import com.bloc.intellij_generator_plugin.intention_action.WrapHelper.Companion.callExpressionFinder import com.intellij.codeInsight.intention.IntentionAction import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement import com.intellij.util.IncorrectOperationException abstract class BlocWrapWithIntentionAction(private val snippetType: SnippetType) : PsiElementBaseIntentionAction(), IntentionAction { var callExpressionElement: PsiElement? = null /** * Returns text for name of this family of intentions. * It is used to externalize "auto-show" state of intentions. * It is also the directory name for the descriptions. * * @return the intention family name. */ override fun getFamilyName(): String = text /** * Checks whether this intention is available at the caret offset in file - the caret must sit on a widget call. * If this condition is met, this intention's entry is shown in the available intentions list. * * * Note: this method must do its checks quickly and return. * * @param project a reference to the Project object being edited. * @param editor a reference to the object editing the project source * @param psiElement a reference to the PSI element currently under the caret * @return `true` if the caret is in a literal string element, so this functionality should be added to the * intention menu or `false` for all other types of caret positions */ override fun isAvailable(project: Project, editor: Editor?, psiElement: PsiElement): Boolean { val shouldDisplay = Common.shouldDisplayWrapMenu(editor, project, psiElement) if (shouldDisplay) { callExpressionElement = callExpressionFinder(psiElement) return callExpressionElement != null } return false } /** * Called when user selects this intention action from the available intentions list. * * @param project a reference to the Project object being edited. * @param editor a reference to the object editing the project source * @param element a reference to the PSI element currently under the caret * @throws IncorrectOperationException Thrown by underlying (Psi model) write action context * when manipulation of the psi tree fails. */ @Throws(IncorrectOperationException::class) override fun invoke(project: Project, editor: Editor, element: PsiElement) { val runnable = Runnable { invokeSnippetAction(project, editor, snippetType, callExpressionElement!!, null) } WriteCommandAction.runWriteCommandAction(project, runnable) } /** * Indicates this intention action expects the Psi framework to provide the write action context for any changes. * * @return `true` if the intention requires a write action context to be provided or `false` if this * intention action will start a write action */ override fun startInWriteAction(): Boolean = true } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/intention_action/BlocWrapWithRepositoryProviderIntentionAction.kt ================================================ package com.bloc.intellij_generator_plugin.intention_action class BlocWrapWithRepositoryProviderIntentionAction : BlocWrapWithIntentionAction(SnippetType.RepositoryProvider) { /** * If this action is applicable, returns the text to be shown in the list of intention actions available. */ override fun getText(): String { return "Wrap with RepositoryProvider" } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/intention_action/Common.kt ================================================ package com.bloc.intellij_generator_plugin.intention_action import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.codeStyle.CodeStyleManager class Common { companion object { fun shouldDisplayWrapMenu( editor: Editor?, project: Project, psiElement: PsiElement ): Boolean { if (editor == null) { return false } val currentFile = getCurrentFile(project, editor) if (currentFile != null && !currentFile.name.endsWith(".dart")) { return false } if (psiElement.toString() != "PsiElement(IDENTIFIER)") { return false } return true } fun getCurrentFile(project: Project, editor: Editor): PsiFile? = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) fun invokeSnippetAction( project: Project, editor: Editor, snippetType: SnippetType?, callExpressionElement: PsiElement, blocChildWidget: PsiElement? ) { val document = editor.document val elementSelectionRange = callExpressionElement.textRange val offsetStart = elementSelectionRange.startOffset val offsetEnd = elementSelectionRange.endOffset if (!WrapHelper.isSelectionValid(offsetStart, offsetEnd)) { return } val selectedText = document.getText(TextRange.create(offsetStart, offsetEnd)) val replaceWith: String if (blocChildWidget != null) { val blocChildWidgetText = blocChildWidget.text val movedBlocWithoutChild = selectedText.replaceFirst(blocChildWidgetText, "") .replaceFirst("[^\\S\\r\\n]*child: ,\\s*".toRegex(), "") replaceWith = Snippets.getSnippet(snippetType, blocChildWidgetText, movedBlocWithoutChild) } else { replaceWith = Snippets.getSnippet(snippetType, "", selectedText) } // wrap the widget: WriteCommandAction.runWriteCommandAction(project) { document.replaceString( offsetStart, offsetEnd, replaceWith ) } // place cursors to specify types: val prefixSelection = Snippets.PREFIX_SELECTION val snippetArr = arrayOf(Snippets.BLOC_SNIPPET_KEY, Snippets.STATE_SNIPPET_KEY, Snippets.REPOSITORY_SNIPPET_KEY) val caretModel = editor.caretModel caretModel.removeSecondaryCarets() for (snippet in snippetArr) { if (!replaceWith.contains(snippet)) { continue } val caretOffset = offsetStart + replaceWith.indexOf(snippet) val visualPos = editor.offsetToVisualPosition(caretOffset) caretModel.addCaret(visualPos) // select snippet prefix keys: val currentCaret = caretModel.currentCaret currentCaret.setSelection(caretOffset, caretOffset + prefixSelection.length) } val initialCaret = caretModel.allCarets[0] if (!initialCaret.hasSelection()) { // initial position from where was triggered the intention action caretModel.removeCaret(initialCaret) } // reformat file: ApplicationManager.getApplication().runWriteAction { PsiDocumentManager.getInstance(project).commitDocument(document) val currentFile = getCurrentFile(project, editor) if (currentFile != null) { val unformattedText = document.text val unformattedLineCount = document.lineCount CodeStyleManager.getInstance(project).reformat(currentFile) val formattedLineCount = document.lineCount // file was incorrectly formatted, revert formatting if (formattedLineCount > unformattedLineCount + 3) { document.setText(unformattedText) PsiDocumentManager.getInstance(project).commitDocument(document) } } } } } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/intention_action/SnippetType.kt ================================================ package com.bloc.intellij_generator_plugin.intention_action enum class SnippetType { BlocBuilder, BlocSelector, BlocListener, BlocProvider, BlocConsumer, RepositoryProvider, MultiBlocProvider, MultiRepositoryProvider, MultiBlocListener, } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/intention_action/Snippets.kt ================================================ package com.bloc.intellij_generator_plugin.intention_action object Snippets { const val PREFIX_SELECTION = "Subject" private const val SUFFIX_BLOC = "Bloc" private const val SUFFIX_STATE = "State" private const val SUFFIX_REPOSITORY = "Repository" const val SELECTED_STATE_SNIPPET_KEY = "SelectedState" const val BLOC_SNIPPET_KEY = PREFIX_SELECTION + SUFFIX_BLOC const val STATE_SNIPPET_KEY = PREFIX_SELECTION + SUFFIX_STATE const val REPOSITORY_SNIPPET_KEY = PREFIX_SELECTION + SUFFIX_REPOSITORY @JvmStatic fun getSnippet(snippetType: SnippetType?, blocWidget: String, widget: String): String { return when (snippetType) { SnippetType.BlocSelector -> blocSelectorSnippet(widget) SnippetType.BlocListener -> blocListenerSnippet(widget) SnippetType.BlocProvider -> blocProviderSnippet(widget) SnippetType.BlocConsumer -> blocConsumerSnippet(widget) SnippetType.RepositoryProvider -> repositoryProviderSnippet(widget) SnippetType.MultiBlocProvider -> multiBlocProviderSnippet(blocWidget, widget) SnippetType.MultiBlocListener -> multiBlocListenerSnippet(blocWidget, widget) SnippetType.MultiRepositoryProvider -> multiRepositoryProviderSnippet(blocWidget, widget) else -> blocBuilderSnippet(widget) } } private fun blocBuilderSnippet(widget: String): String { return "BlocBuilder<$BLOC_SNIPPET_KEY, $STATE_SNIPPET_KEY>(\n" + " builder: (context, state) {\n" + " return $widget;\n" + " },\n" + ")" } private fun blocSelectorSnippet(widget: String): String { return "BlocSelector<$BLOC_SNIPPET_KEY, $STATE_SNIPPET_KEY, $SELECTED_STATE_SNIPPET_KEY>(\n" + " selector: (state) {\n" + " // TODO: return selected state\n" + " },\n" + " builder: (context, state) {\n" + " return $widget;\n" + " },\n" + ")" } private fun blocListenerSnippet(widget: String): String { return "BlocListener<$BLOC_SNIPPET_KEY, $STATE_SNIPPET_KEY>(\n" + " listener: (context, state) {\n" + " // TODO: implement listener\n" + " },\n" + " child: $widget,\n" + ")" } private fun blocProviderSnippet(widget: String): String { return "BlocProvider(\n" + " create: (context) => $BLOC_SNIPPET_KEY(),\n" + " child: $widget,\n" + ")" } private fun blocConsumerSnippet(widget: String): String { return "BlocConsumer<$BLOC_SNIPPET_KEY, $STATE_SNIPPET_KEY>(\n" + " listener: (context, state) {\n" + " // TODO: implement listener\n" + " },\n" + " builder: (context, state) {\n" + " return $widget;\n" + " },\n" + ")" } private fun repositoryProviderSnippet(widget: String): String { return "RepositoryProvider(\n" + " create: (context) => $REPOSITORY_SNIPPET_KEY(),\n" + " child: $widget,\n" + ")" } private fun multiBlocProviderSnippet(blocChildWidget: String, widget: String): String { return "MultiBlocProvider(\n" + " providers: [\n" + " $widget,\n" + " BlocProvider(\n" + " create: (context) => $BLOC_SNIPPET_KEY(),\n" + " ),\n" + " ],\n" + " child: $blocChildWidget,\n" + ")" } private fun multiBlocListenerSnippet(blocChildWidget: String, widget: String): String { return "MultiBlocListener(\n" + " listeners: [\n" + " $widget,\n" + " BlocListener<$BLOC_SNIPPET_KEY, $STATE_SNIPPET_KEY>(\n" + " listener: (context, state) {\n" + " // TODO: implement listener\n" + " },\n" + " ),\n" + " ],\n" + " child: $blocChildWidget,\n" + ")" } private fun multiRepositoryProviderSnippet(blocChildWidget: String, widget: String): String { return "MultiRepositoryProvider(\n" + " providers: [\n" + " $widget,\n" + " RepositoryProvider(\n" + " create: (context) => $REPOSITORY_SNIPPET_KEY(),\n" + " ),\n" + " ],\n" + " child: $blocChildWidget,\n" + ")" } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/intention_action/WrapHelper.kt ================================================ package com.bloc.intellij_generator_plugin.intention_action import com.intellij.lang.ASTNode import com.intellij.psi.PsiElement class WrapHelper { companion object { fun callExpressionFinder(psiElement: PsiElement): PsiElement? { var psiElementFinder: PsiElement? = psiElement.parent for (i in 1..10) { if (psiElementFinder == null) { return null } if (psiElementFinder.toString() == "CALL_EXPRESSION") { if (psiElementFinder.text.startsWith(psiElement.text)) { return psiElementFinder } return null } psiElementFinder = psiElementFinder.parent } return null } fun blocWidgetChildFinder(element: PsiElement): PsiElement? { val node: ASTNode = element.node ?: return null val childArgument = findChildArgument(node) ?: return null return findChildWidget(childArgument) } private fun findChildArgument(node: ASTNode) = node.getChildren(null) .find { astNode -> astNode.toString() == "Element(ARGUMENTS)" }?.getChildren(null) ?.find { astNode -> astNode.toString() == "Element(ARGUMENT_LIST)" }?.getChildren(null) ?.firstOrNull { astNode -> astNode.toString() == "Element(NAMED_ARGUMENT)" && astNode.text.startsWith("child: ") } private fun findChildWidget(childArgument: ASTNode) = childArgument.getChildren(null) .firstOrNull { astNode -> astNode.toString() == "Element(CALL_EXPRESSION)" }?.psi fun isSelectionValid(start: Int, end: Int): Boolean { if (start <= -1 || end <= -1) { return false } if (start >= end) { return false } return true } } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/language_server/BlocLanguageServer.kt ================================================ package com.bloc.intellij_generator_plugin.language_server import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.diagnostic.Logger import com.redhat.devtools.lsp4ij.server.OSProcessStreamConnectionProvider import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider import com.intellij.openapi.application.PathManager import java.io.File import java.io.FileOutputStream import java.net.URL import java.net.URI private const val BLOC_TOOLS_VERSION = "0.1.0-dev.21" enum class OperatingSystem(val value: String) { Linux("linux"), MacOS("macos"), Windows("windows"), Unknown("-"); companion object { fun fromSystemProperty(): OperatingSystem { val os = System.getProperty("os.name").lowercase() return when { os.contains("win") -> Windows os.contains("mac") -> MacOS os.contains("nix") || os.contains("nux") || os.contains("aix") -> Linux else -> Unknown } } } } enum class Architecture(val value: String) { X64("x64"), Arm64("arm64"), Unknown("-"); companion object { fun fromSystemProperty(): Architecture { val arch = System.getProperty("os.arch").lowercase() return when (arch) { in listOf("amd64", "x86_64") -> X64 in listOf("aarch64", "arm64") -> Arm64 else -> Unknown } } } } class BlocLanguageServer() { private val logger = Logger.getInstance(BlocLanguageServer::class.java) private var provider: OSProcessStreamConnectionProvider? = null fun areBlocToolsInstalled(): Boolean { val executable = getBlocToolsExecutableFile() return executable?.exists() == true } fun installBlocTools(): Boolean { val executableFile = getBlocToolsExecutableFile() ?: return false try { val uri = URI("https://github.com/felangel/bloc/releases/download/bloc_tools-v$BLOC_TOOLS_VERSION/${executableFile.name}") val cacheDir = getCacheDirectory() cacheDir.mkdirs() downloadFile(uri.toURL(), executableFile) makeExecutable(executableFile) } catch (e: Exception) { logger.error("Failed to download bloc tools", e) } return executableFile.exists() } fun getConnectionProvider(): StreamConnectionProvider? { val executable = getBlocToolsExecutableFile() ?: return null if (!executable.exists()) return null val commandLine = GeneralCommandLine(executable.absolutePath, "language-server") provider = OSProcessStreamConnectionProvider(commandLine) return provider } private fun getBlocToolsExecutableFile(): File? { val os = OperatingSystem.fromSystemProperty() val arch = Architecture.fromSystemProperty() if (os == OperatingSystem.Unknown || arch == Architecture.Unknown) { logger.warn("Unsupported OS or architecture") return null } val fileName = "bloc_${os.value}_${arch.value}" val cacheDir = getCacheDirectory() return File(cacheDir, fileName) } private fun makeExecutable(file: File) { if (!file.setExecutable(true, false)) { logger.error("Failed to set executable: ${file.absolutePath}") } } private fun downloadFile(url: URL, outputFile: File) { url.openStream().use { input -> FileOutputStream(outputFile).use { output -> input.copyTo(output) } } } private fun getCacheDirectory(): File { val pluginCacheDir = File(PathManager.getSystemPath(), "bloc-tools/$BLOC_TOOLS_VERSION") pluginCacheDir.mkdirs() return pluginCacheDir } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/language_server/BlocLanguageServerFactory.kt ================================================ package com.bloc.intellij_generator_plugin.language_server import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.redhat.devtools.lsp4ij.LanguageServerFactory import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider import java.io.InputStream import java.io.OutputStream @Suppress("UnstableApiUsage") class BlocLanguageServerFactory : LanguageServerFactory { private val logger = Logger.getInstance(BlocLanguageServer::class.java) override fun createClientFeatures(): LSPClientFeatures { val features = LSPClientFeatures() features.setServerInstaller(BlocLanguageServerInstaller()) return features } override fun createConnectionProvider(project: Project): StreamConnectionProvider { val languageServer = BlocLanguageServer() return languageServer.getConnectionProvider() ?: run { logger.warn("Bloc Language Server could not be initialized — falling back to no-op connection provider") NoopConnectionProvider() } } } class NoopConnectionProvider : StreamConnectionProvider { override fun start() { // no-op } override fun stop() { // no-op } override fun getInputStream(): InputStream? = null override fun getOutputStream(): OutputStream? = null } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/language_server/BlocLanguageServerInstaller.kt ================================================ package com.bloc.intellij_generator_plugin.language_server import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.redhat.devtools.lsp4ij.installation.LanguageServerInstallerBase class BlocLanguageServerInstaller : LanguageServerInstallerBase() { private val languageServer = BlocLanguageServer(); override fun checkServerInstalled(indicator: ProgressIndicator): Boolean { progress("Checking if the language server is installed...", indicator) val installed = languageServer.areBlocToolsInstalled() ProgressManager.checkCanceled() return installed } @Throws(Exception::class) override fun install(indicator: ProgressIndicator) { progress("Installing bloc tools...", indicator) val installed = languageServer.installBlocTools() ProgressManager.checkCanceled() if (!installed) throw Exception("Failed to install bloc tools") } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/live_templates/BlocContext.kt ================================================ package com.bloc.intellij_generator_plugin.live_templates import com.intellij.codeInsight.template.TemplateActionContext import com.intellij.codeInsight.template.TemplateContextType class BlocContext : TemplateContextType("Bloc") { override fun isInContext(templateActionContext: TemplateActionContext): Boolean { return templateActionContext.file.name.endsWith(".dart") } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/java/com/bloc/intellij_generator_plugin/util/BlocPluginNotification.kt ================================================ package com.bloc.intellij_generator_plugin.util import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType import com.intellij.openapi.project.Project object BlocPluginNotification { private const val NOTIFICATION_GROUP = "Bloc Plugin Notifications" fun notify(project: Project, content: String, type: NotificationType) { NotificationGroupManager.getInstance() .getNotificationGroup(NOTIFICATION_GROUP) .createNotification(content, type) .notify(project) } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/META-INF/plugin.xml ================================================ com.bloc.intellij_generator_plugin Bloc felangel
  • v4.1.11 - Bump bloc_tools version to 0.1.0-dev.21
  • v4.1.10 - Bump bloc_tools version to 0.1.0-dev.20
  • v4.1.9 - Bump bloc_tools version to 0.1.0-dev.19
  • v4.1.8 - Bump bloc_tools version to 0.1.0-dev.18
  • v4.1.7 - Various linter improvements and performance optimizations
  • v4.1.6 - Fix increased CPU usage when using fvm
  • v4.1.5 - Use precompiled bloc_tools executable for LSP
  • v4.1.4 - Include environment during command execution
  • v4.1.3 - Bump bloc_tools version to 0.1.0-dev.12
  • v4.1.2 - Fix language server execution on Windows and New Bloc/Cubit Actions
  • v4.1.1 - Add Dart SDK constraint to language server
  • v4.1.0 - Add language server with bloc lint integration
  • v4.0.2 - Support for Intellij 2024.2
  • v4.0.1 - Support for Intellij 2024
  • v4.0.0 - Support for Sealed Classes and Dependency Upgrades
  • v3.4.0 - Support for Freezed
  • v3.3.0 - Support for Mock snippet
  • v3.2.0 - Support for MockBloc, MockCubit, and Fake snippets
  • v3.1.0 - Support for Wrap with BlocSelector
  • v3.0.0 - Support for Bloc v7.2.0 (on API)
  • v2.4.0 - Add Convert to Multi Widget Actions
  • v2.3.0 - Add Equatable Props Generator and Live Templates Bloc/Cubit selection support
  • v2.2.1 - Fix Wrapping Errors
  • v2.2.0 - Wrap With Quick Action Support
  • v2.1.0 - Live Template Support
  • v2.0.0 - Support for Bloc v6.0.0
  • v1.8.0 - Support for Cubit
  • v1.7.1 - Fix handle trailing _bloc in name
  • v1.7.0 - Support for Equatable v1.0.0 & Bloc v5.0.0
  • v1.6.0 - Support for Equatable v0.6.0
  • v1.5.1 - Minor dependency updates
  • v1.5 - Fix Overwrite Issue and Updates for implicit-dynamic support with Equatable
  • v1.4 - Bug Fixes for Bloc Generator Name (Case Sensitivity)
  • v1.3 - Bug Fixes for IntelliJ and Android Studio Compatibility
  • v1.2 - Support for IntelliJ 2019
  • v1.1 - Removed currentState from mapEventToState method
  • v1.0 - Initial release
  • ]]>
    com.intellij.modules.lang com.intellij.modules.java com.redhat.devtools.lsp4ij com.bloc.intellij_generator_plugin.intention_action.BlocWrapWithBlocProviderIntentionAction Bloc com.bloc.intellij_generator_plugin.intention_action.BlocWrapWithBlocBuilderIntentionAction Bloc com.bloc.intellij_generator_plugin.intention_action.BlocWrapWithBlocSelectorIntentionAction Bloc com.bloc.intellij_generator_plugin.intention_action.BlocWrapWithBlocListenerIntentionAction Bloc com.bloc.intellij_generator_plugin.intention_action.BlocWrapWithBlocConsumerIntentionAction Bloc com.bloc.intellij_generator_plugin.intention_action.BlocWrapWithRepositoryProviderIntentionAction Bloc com.bloc.intellij_generator_plugin.intention_action.BlocConvertToMultiBlocProviderIntentionAction Bloc com.bloc.intellij_generator_plugin.intention_action.BlocConvertToMultiBlocListenerIntentionAction Bloc com.bloc.intellij_generator_plugin.intention_action.BlocConvertToMultiRepositoryProviderIntentionAction Bloc
    ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocConvertToMultiBlocListenerIntentionAction/after.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: MultiBlocListener( listeners: [ BlocListener( listener: (context, state) { print(state); }, ), BlocListener( listener: (context, state) { // TODO: implement listener }, ), ], child: Text('Example'), ), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocConvertToMultiBlocListenerIntentionAction/before.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: BlocListener( listener: (context, state) { print(state); }, child: Text('Example'), ), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocConvertToMultiBlocListenerIntentionAction/description.html ================================================ Converts BlocListener widget to MultiBlocListener. ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocConvertToMultiBlocProviderIntentionAction/after.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: MultiBlocProvider( providers: [ BlocProvider( create: (context) => CounterBloc(), ), BlocProvider( create: (context) => SubjectBloc(), ), ], child: Text('Example'), ), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocConvertToMultiBlocProviderIntentionAction/before.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: BlocProvider( create: (context) => CounterBloc(), child: Text('Example'), ), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocConvertToMultiBlocProviderIntentionAction/description.html ================================================ Converts BlocProvider widget to MultiBlocProvider. ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocConvertToMultiRepositoryProviderIntentionAction/after.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: MultiRepositoryProvider( providers: [ RepositoryProvider( create: (context) => NumberRepository(), ), RepositoryProvider( create: (context) => SubjectRepository(), ), ], child: Text('Example'), ), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocConvertToMultiRepositoryProviderIntentionAction/before.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: RepositoryProvider( create: (context) => NumberRepository(), child: Text('Example'), ), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocConvertToMultiRepositoryProviderIntentionAction/description.html ================================================ Converts RepositoryProvider widget to MultiRepositoryProvider. ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithBlocBuilderIntentionAction/after.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: BlocBuilder( builder: (context, state) { return Text('Example'); }, ), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithBlocBuilderIntentionAction/before.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Text('Example'), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithBlocBuilderIntentionAction/description.html ================================================ Wraps the current widget in a BlocBuilder. ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithBlocConsumerIntentionAction/after.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: BlocConsumer( listener: (context, state) { // TODO: implement listener }, builder: (context, state) { return Text('Example'); }, ), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithBlocConsumerIntentionAction/before.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Text('Example'), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithBlocConsumerIntentionAction/description.html ================================================ Wraps the current widget in a BlocConsumer. ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithBlocListenerIntentionAction/after.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: BlocListener( listener: (context, state) { // TODO: implement listener} }, child: Text('Example'), ), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithBlocListenerIntentionAction/before.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Text('Example'), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithBlocListenerIntentionAction/description.html ================================================ Wraps the current widget in a BlocListener. ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithBlocProviderIntentionAction/after.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: BlocProvider( create: (context) => SubjectBloc(), child: Text('Example'), ), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithBlocProviderIntentionAction/before.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Text('Example'), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithBlocProviderIntentionAction/description.html ================================================ Wraps the current widget in a BlocProvider. ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithBlocSelectorIntentionAction/after.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: BlocSelector( selector: (state) { // TODO: return selected state }, builder: (state) { return Text('Example'); }, ), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithBlocSelectorIntentionAction/before.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Text('Example'), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithBlocSelectorIntentionAction/description.html ================================================ Wraps the current widget in a BlocSelector. ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithRepositoryProviderIntentionAction/after.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: RepositoryProvider( create: (context) => SubjectRepository(), child: Text('Example'), ), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithRepositoryProviderIntentionAction/before.java.template ================================================ class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Text('Example'), ), ); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/intentionDescriptions/BlocWrapWithRepositoryProviderIntentionAction/description.html ================================================ Wraps the current widget in a RepositoryProvider. ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/liveTemplates/Bloc.xml ================================================ ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/templates/bloc_basic/bloc.dart.template ================================================ import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part '{{bloc_snake_case}}_event.dart'; part '{{bloc_snake_case}}_state.dart'; class {{bloc_pascal_case}}Bloc extends Bloc<{{bloc_pascal_case}}Event, {{bloc_pascal_case}}State> { {{bloc_pascal_case}}Bloc() : super({{bloc_pascal_case}}Initial()) { on<{{bloc_pascal_case}}Event>((event, emit) { // TODO: implement event handler }); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/templates/bloc_basic/bloc_event.dart.template ================================================ part of '{{bloc_snake_case}}_bloc.dart'; @immutable sealed class {{bloc_pascal_case}}Event {} ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/templates/bloc_basic/bloc_state.dart.template ================================================ part of '{{bloc_snake_case}}_bloc.dart'; @immutable sealed class {{bloc_pascal_case}}State {} final class {{bloc_pascal_case}}Initial extends {{bloc_pascal_case}}State {} ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/templates/bloc_equatable/bloc.dart.template ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; part '{{bloc_snake_case}}_event.dart'; part '{{bloc_snake_case}}_state.dart'; class {{bloc_pascal_case}}Bloc extends Bloc<{{bloc_pascal_case}}Event, {{bloc_pascal_case}}State> { {{bloc_pascal_case}}Bloc() : super({{bloc_pascal_case}}Initial()) { on<{{bloc_pascal_case}}Event>((event, emit) { // TODO: implement event handler }); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/templates/bloc_equatable/bloc_event.dart.template ================================================ part of '{{bloc_snake_case}}_bloc.dart'; sealed class {{bloc_pascal_case}}Event extends Equatable { const {{bloc_pascal_case}}Event(); } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/templates/bloc_equatable/bloc_state.dart.template ================================================ part of '{{bloc_snake_case}}_bloc.dart'; sealed class {{bloc_pascal_case}}State extends Equatable { const {{bloc_pascal_case}}State(); } final class {{bloc_pascal_case}}Initial extends {{bloc_pascal_case}}State { @override List get props => []; } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/templates/bloc_freezed/bloc.dart.template ================================================ import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part '{{bloc_snake_case}}_event.dart'; part '{{bloc_snake_case}}_state.dart'; part '{{bloc_snake_case}}_bloc.freezed.dart'; class {{bloc_pascal_case}}Bloc extends Bloc<{{bloc_pascal_case}}Event, {{bloc_pascal_case}}State> { {{bloc_pascal_case}}Bloc() : super(const {{bloc_pascal_case}}State.initial()) { on<{{bloc_pascal_case}}Event>((event, emit) { // TODO: implement event handler }); } } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/templates/bloc_freezed/bloc_event.dart.template ================================================ part of '{{bloc_snake_case}}_bloc.dart'; @freezed class {{bloc_pascal_case}}Event with _${{bloc_pascal_case}}Event { const factory {{bloc_pascal_case}}Event.started() = _Started; } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/templates/bloc_freezed/bloc_state.dart.template ================================================ part of '{{bloc_snake_case}}_bloc.dart'; @freezed class {{bloc_pascal_case}}State with _${{bloc_pascal_case}}State { const factory {{bloc_pascal_case}}State.initial() = _Initial; } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/templates/cubit_basic/cubit.dart.template ================================================ import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part '{{cubit_snake_case}}_state.dart'; class {{cubit_pascal_case}}Cubit extends Cubit<{{cubit_pascal_case}}State> { {{cubit_pascal_case}}Cubit() : super({{cubit_pascal_case}}Initial()); } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/templates/cubit_basic/cubit_state.dart.template ================================================ part of '{{cubit_snake_case}}_cubit.dart'; @immutable sealed class {{cubit_pascal_case}}State {} final class {{cubit_pascal_case}}Initial extends {{cubit_pascal_case}}State {} ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/templates/cubit_equatable/cubit.dart.template ================================================ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; part '{{cubit_snake_case}}_state.dart'; class {{cubit_pascal_case}}Cubit extends Cubit<{{cubit_pascal_case}}State> { {{cubit_pascal_case}}Cubit() : super({{cubit_pascal_case}}Initial()); } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/templates/cubit_equatable/cubit_state.dart.template ================================================ part of '{{cubit_snake_case}}_cubit.dart'; sealed class {{cubit_pascal_case}}State extends Equatable { const {{cubit_pascal_case}}State(); } final class {{cubit_pascal_case}}Initial extends {{cubit_pascal_case}}State { @override List get props => []; } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/templates/cubit_freezed/cubit.dart.template ================================================ import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part '{{cubit_snake_case}}_state.dart'; part '{{cubit_snake_case}}_cubit.freezed.dart'; class {{cubit_pascal_case}}Cubit extends Cubit<{{cubit_pascal_case}}State> { {{cubit_pascal_case}}Cubit() : super(const {{cubit_pascal_case}}State.initial()); } ================================================ FILE: extensions/intellij/intellij_generator_plugin/src/main/resources/templates/cubit_freezed/cubit_state.dart.template ================================================ part of '{{cubit_snake_case}}_cubit.dart'; @freezed class {{cubit_pascal_case}}State with _${{cubit_pascal_case}}State { const factory {{cubit_pascal_case}}State.initial() = _Initial; } ================================================ FILE: extensions/vscode/.gitignore ================================================ node_modules out/ dist/ *.vsix ================================================ FILE: extensions/vscode/.vscode/launch.json ================================================ // A launch configuration that compiles the extension and then opens it inside a new window // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { "version": "0.2.0", "configurations": [ { "name": "Run Extension", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceFolder}"], "outFiles": ["${workspaceFolder}/dist/**/*.js"], "preLaunchTask": "npm: compile" } ] } ================================================ FILE: extensions/vscode/.vscodeignore ================================================ .vscode/** .vscode-test/** node_modules out/** src/** .gitignore vsc-extension-quickstart.md webpack.config.js **/tsconfig.json **/tslint.json **/*.map **/*.ts ================================================ FILE: extensions/vscode/CHANGELOG.md ================================================ # 6.8.13 - deps: upgrade to `bloc_tools: 0.1.0-dev.21` - deps: various dependency upgrades # 6.8.12 - deps: upgrade to `bloc_tools: 0.1.0-dev.20` # 6.8.11 - deps: upgrade to `bloc_tools: 0.1.0-dev.19` - deps: various dependency upgrades # 6.8.10 - deps: upgrade to `bloc_tools: 0.1.0-dev.18` - deps: various dependency upgrades - docs: fix broken link in `README` # 6.8.9 - deps: upgrade to `bloc_tools: 0.1.0-dev.17` # 6.8.8 - deps: upgrade to `bloc_tools: 0.1.0-dev.16` - fix: increased CPU usage when using `fvm` - deps: various dependency upgrades # 6.8.7 - deps: upgrade to `bloc_tools: 0.1.0-dev.15` # 6.8.6 - fix: support for x64 and arm64 `bloc_tools` executables # 6.8.5 - fix: install precompiled `bloc_tools` executable - deps: upgrade to `bloc_tools: 0.1.0-dev.13` # 6.8.4 - deps: upgrade to `bloc_tools: 0.1.0-dev.12` # 6.8.3 - fix: add Dart SDK constraint for language server # 6.8.2 - deps: upgrade to `bloc_tools: 0.1.0-dev.11` # 6.8.1 - feat: improve bloc tools install/upgrade ux - fix: various language server bugs on windows - deps: upgrade to `bloc_tools: 0.1.0-dev.10` # 6.8.0 - feat: add language server # 6.7.0 - deps: upgrade vscode engine to ^1.75.0 - deps: upgrade webpack and braces # 6.6.6 - fix: wrap with preserves escaping - fix: wrap with preserves `const` - docs: update docs site references - chore: update copyright year # 6.6.5 - fix: show context menu conditionally # 6.6.4 - feat: add `useSealedClasses` to extension settings - docs: fix `README` badges - deps: upgrade various dependencies # 6.6.3 - fix: wrap with interpolation - deps: upgrade various dependencies # 6.6.2 - fix: rename element to bloc in `context.select` snippet # 6.6.1 - fix: bloc and cubit template casing # 6.6.0 - feat: add private mock snippets - `_mockbloc` - `_mockcubit` - `_fake` - `_mock` # 6.5.1 - fix: reduce bundle size # 6.5.0 - fix: update `BlocObserver` snippet to resolve Dart analyzer warning - feat: determine versions via `pubspec.lock` - feat: add `Mock` snippet - chore: upgrade dependencies # 6.4.0 - feat: add `_onevent` snippet for creating an internal event handler - feat: improve `onevent` snippet to infer default event type - feat: add new extension settings: - `bloc.newBlocTemplate.createDirectory` (defaults to true) - `bloc.newCubitTemplate.createDirectory` (defaults to true) - refactor: remove deprecated snippets: - `feventwhen` - `feventmap` # 6.3.0 - feat: add snippets for: - `MockBloc` - `MockCubit` - `Fake` # 6.2.0 - feat: query latest package versions from pub.dev # 6.1.0 - feat: update to latest packages - bloc -> ^7.2.1 - flutter_bloc -> ^7.3.3 - bloc_test -> ^8.5.0 - fix:(vscode): escape $ in "wrap with" and "convert to" # 6.0.1 - chore: remove unnecessary abstract keyword from freezed template # 6.0.0 - **BREAKING**: update to bloc ^7.2.0 - update snippets to use `on` instead of deprecated `mapEventToState` - feat: add `onevent` snippet to register a new `EventHandler` - feat: update to latest packages - bloc -> ^7.2.0 - flutter_bloc -> ^7.3.0 - angular_bloc -> ^7.1.0 - bloc_test -> ^8.2.0 - replay_bloc -> ^0.1.0 - bloc_concurrency -> ^0.1.0 - sealed_flutter_bloc -> ^7.1.0 # 5.8.0 - feat: add "Convert to..." Multi-Widget Actions - `Convert to MultiBlocListener` - `Convert to MultiBlocProvider` - `Convert to MultiRepositoryProvider` # 5.7.0 - feat: add snippets for `BlocSelector` - feat: add `Wrap with BlocSelector` action - feat: update to latest packages - bloc_test -> ^8.1.0 - equatable -> ^2.0.3 - flutter_bloc -> ^7.1.0 - hydrated_bloc -> ^7.0.1 - sealed_flutter_bloc -> ^7.0.0 # 5.6.2 - fix: "Wrap with..." selection issues # 5.6.1 - feat: add snippets for `bloc_test` - feat: add snippets for importing `bloc`, `bloc_test` and `flutter_bloc` - fix: show code actions in refactorings menu # 5.6.0 - feat: freezed classes no longer require [abstract](https://pub.dev/packages/freezed#the-abstract-keyword) keyword when using freezed >= 0.14.0 - feat: update `BlocObserver` snippets to support null safety and bloc v7.0.0 - feat: update to latest packages - bloc -> ^7.0.0 - bloc_test -> ^8.0.0 - equatable -> ^2.0.0 - flutter_bloc -> ^7.0.0 - hydrated_bloc -> ^7.0.0 # 5.5.1 - feat: improve `context.select` extension snippet # 5.5.0 - feat: add `checkForUpdates` configuration in extension settings # 5.4.0 - feat: add snippets for: - `context.read` - `context.select` - `context.watch` - feat: update to latest packages - angular_bloc -> ^6.0.1 - bloc -> ^6.1.0 - bloc_test -> ^7.1.0 - equatable -> ^1.2.5 - flutter_bloc -> ^6.1.0 - hydrated_bloc -> ^6.0.3 - sealed_flutter_bloc -> ^6.0.0 # 5.3.2 - fix: update dependencies to fix potential security vulnerabilities # 5.3.1 - fix: freezed cubit template typo # 5.3.0 - feat: make templates configurable via workspace settings - feat: improve cubit state equatable template - fix: remove unused dependency in freezed bloc template # 5.2.0 - feat: updates to snippets - `contextbloc` -> `ctxbloc` - `contextrepository` -> `ctxrepo` - `repositoryof` -> `repoof` - `repositoryprovider` -> `repoprovider` - `multirepositoryprovider` -> `multirepoprovider` - `blocstate` (new) - `blocevent` (new) # 5.1.1 - fix: freezed template typo # 5.1.0 - feat: add new bloc/cubit support for `package:freezed` - feat: add snippet support for `package:freezed` - `fstate`: new freezed state - `fevent`: new freezed event - `feventwhen`: new freezed event.when helper function - `feventmap`: new freezed event.map helper function - feat: add bloc code actions - Wrap with `BlocBuilder` - Wrap with `BlocListener` - Wrap with `BlocConsumer` - Wrap with `BlocProvider` - Wrap with `RepositoryProvider` # 5.0.0 - **BREAKING**: update to latest bloc packages - bloc -> ^6.0.0 - bloc_test -> ^7.0.0 - flutter_bloc -> ^6.0.0 - hydrated_bloc -> ^6.0.0 - fix: remove error dialog when no `pubspec.yaml` found in root # 4.2.2 - fix: Equatable not being recognized on Windows # 4.2.1 - fix: `CubitObserver` snippet fixes - fix: `BlocObserver` snippet fixes # 4.2.0 - feat: Add Open Migration Guide for `bloc`, `flutter_bloc`, and `hydrated_bloc` # 4.1.1 - fix: package version analysis on dev_dependencies # 4.1.0 - Include "Cubit: New Cubit" command to generate a cubit and state - Include Cubit Ecosystem when performing package version analysis - Infer Equatable Usage - Update snippets to support - `Cubit` - `CubitBuilder` - `CubitListener` - `MultiCubitListener` - `CubitConsumer` - `CubitProvider` - `MultiCubitProvider` - `CubitObserver` - `context.cubit()` - `CubitProvider.of()` # 4.0.0 Update latest package versions: - equatable -> ^1.2.0 - bloc -> ^5.0.0 - bloc_test -> ^6.0.0 - flutter_bloc -> ^5.0.0 - hydrated_bloc -> ^5.0.0 # 3.6.0 Update latest package versions: - bloc_test -> ^5.1.0 - hydrated_bloc -> ^4.0.0 - sealed_flutter_bloc -> ^4.0.0 # 3.5.0 Update latest package versions: - bloc_test -> ^5.0.0 - equatable -> ^1.1.1 - angular_bloc -> ^4.0.0 - bloc -> ^4.0.0 - flutter_bloc -> ^4.0.0 # 3.4.0 Update snippets to support - `context.bloc()` - `context.repository()` # 3.3.0 Update latest package versions: - bloc_test -> ^4.0.0 - equatable -> ^1.1.0 - flutter_bloc -> ^3.2.0 # 3.2.0 Update bloc generator to use `parts` (removes barrel file) Update dependency analyzer to handle `any` version # 3.1.0 Update to support flutter_bloc `v3.1.0` Update Snippets for: - `BlocConsumer` (blocconsumer) - `BlocProvider.of` (blocof) - `RepositoryProvider.of` (repositoryof) # 3.0.1 Hotfix for `command 'extension.new-bloc' not found` # 3.0.0 Update to support bloc `v3.0.0` # 2.2.0 Update snippets to support flutter_bloc `v2.1.0` # 2.1.0 Update to support equatable `v1.0.0` # 2.0.0 Update to support bloc `v2.0.0` # 1.0.0 Update to support bloc `v1.0.0` # 0.13.0 Update Snippets to support changes in bloc `v0.16.0` # 0.12.2 Add Update Action to automatically update outdated dependencies # 0.12.1 Add detection for outdated dependencies in workspace. # 0.12.0 Update Snippets and New Bloc to support changes in equatable `v0.6.0` # 0.11.1 `Equatable` enhancement to address `implicit-dynamic` warning ([#463](https://github.com/felangel/bloc/pull/463)). # 0.11.0 Update Snippets to support changes in flutter_bloc `v0.20.0` # 0.10.1 - Minor Documentation Updates # 0.10.0 Update Snippets for: - `RepositoryProvider` - `MultiRepositoryProvider` - `MultiBlocProvider` - `MultiBlocListener` to support changes in flutter_bloc `v0.19.0` # 0.9.0 Update Snippets for: - `ImmutableProvider` - `ImmutableProviderTree` to support changes in flutter_bloc `v0.18.0` # 0.8.0 Update Snippets for: - `BlocProvider` - `BlocProviderTree` to support changes in flutter_bloc `v0.17.0` # 0.7.0 Update Snippets for: - `BlocProvider` - `BlocProviderTree` to support changes in flutter_bloc `v0.16.0` # 0.6.2 Updated `BlocDelegate` snippet to support changes in bloc `v0.13.0` # 0.6.1 Bloc generation does not overwrite existing bloc files; it attempts to merge instead. # 0.6.0 Added Snippets for: - `BlocListenerTree` # 0.5.0 Added Snippets for: - `BlocListener` # 0.4.2 Update BlocDelegate Snippet to include calls to `super` # 0.4.1 Added Snippets for: - `BlocEvent` - `BlocState` # 0.4.0 Support for `bloc v0.11.0` # 0.3.2 Added `@immutable` decorator to bloc events and states # 0.3.1 Added Snippet for `BlocDelegate` # 0.3.0 Added Snippets for `BlocBuilder`, `BlocProvider`, and `BlocProviderTree` # 0.2.1 Update `Equatable` usage to be "advanced" # 0.2.0 Updated to include "Bloc: New Bloc" to generate full bloc directory structure. # 0.1.3 Minor Documentation Updates # 0.1.2 Minor Documentation Updates # 0.1.1 Minor Documentation Updates # 0.1.0 Initial Version of the Extension. - Includes the ability to create a Bloc snippet ================================================ FILE: extensions/vscode/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2026 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: extensions/vscode/README.md ================================================

    Bloc

    build codecov Star on Github

    Version Downloads Installs Ratings Flutter Website Awesome Flutter Flutter Samples License: MIT Discord Bloc Library

    --- ## Overview [VSCode](https://code.visualstudio.com/) support for the [Bloc Library](https://bloclibrary.dev) and provides tools for effectively creating [Blocs](https://github.com/felangel/bloc) and [Cubits](https://github.com/felangel/cubit) for both [Flutter](https://flutter.dev/) and [AngularDart](https://angulardart.dev/) apps. ## Installation Bloc can be installed from the [VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) or by [searching within VSCode](https://code.visualstudio.com/docs/editor/extension-gallery#_search-for-an-extension). ## Language Server By default, the bloc language server started which enables custom bloc diagnotic reporting. See [the official documentation](https://bloclibrary.dev/lint) for more information about configuring the linter and the various supported lint rules. ## Commands | Command | Description | | ------------------ | -------------------- | | `Bloc: New Bloc` | Generate a new Bloc | | `Cubit: New Cubit` | Generate a new Cubit | You can activate the commands by launching the command palette (View -> Command Palette) and running entering the command name or you can right click on the directory in which you'd like to create the bloc/cubit and select the command from the context menu. ![demo](https://raw.githubusercontent.com/felangel/bloc/master/extensions/vscode/assets/new-bloc-usage.gif) ## Code Actions | Action | Description | | ------------------------------------ | ---------------------------------------------------------------------- | | `Convert to MultiBlocListener` | Converts current `BlocListener` into a `MultiBlocListener` | | `Convert to MultiBlocProvider` | Converts current `BlocProvider` into a `MultiBlocProvider` | | `Convert to MultiRepositoryProvider` | Converts current `RepositoryProvider` into a `MultiRepositoryProvider` | | `Wrap with BlocBuilder` | Wraps current widget in a `BlocBuilder` | | `Wrap with BlocSelector` | Wraps current widget in a `BlocSelector` | | `Wrap with BlocListener` | Wraps current widget in a `BlocListener` | | `Wrap with BlocConsumer` | Wraps current widget in a `BlocConsumer` | | `Wrap with BlocProvider` | Wraps current widget in a `BlocProvider` | | `Wrap with RepositoryProvider` | Wraps current widget in a `RepositoryProvider` | ![demo](https://raw.githubusercontent.com/felangel/bloc/master/extensions/vscode/assets/wrap-with-usage.gif) ## Snippets ### Bloc | Shortcut | Description | | ------------------- | --------------------------------------------- | | `importbloc` | Imports `package:bloc` | | `importflutterbloc` | Imports `package:flutter_bloc` | | `importbloctest` | Imports `package:bloc_test` | | `bloc` | Creates a bloc class | | `cubit` | Creates a cubit class | | `onevent` | Register a new `EventHandler` | | `_onevent` | Define a new `EventHandler` | | `blocobserver` | Creates a `BlocObserver` class | | `blocprovider` | Creates a `BlocProvider` widget | | `multiblocprovider` | Creates a `MultiBlocProvider` widget | | `repoprovider` | Creates a `RepositoryProvider` widget | | `multirepoprovider` | Creates a `MultiRepositoryProvider` widget | | `blocbuilder` | Creates a `BlocBuilder` widget | | `blocselector` | Creates a `BlocSelector` widget | | `bloclistener` | Creates a `BlocListener` widget | | `multibloclistener` | Creates a `MultiBlocListener` widget | | `blocconsumer` | Creates a `BlocConsumer` widget | | `blocof` | Shortcut for `BlocProvider.of()` | | `repoof` | Shortcut for `RepositoryProvider.of()` | | `read` | Shortcut for `context.read()` | | `watch` | Shortcut for `context.watch()` | | `select` | Shortcut for `context.select()` | | `blocstate` | Creates a state class | | `blocevent` | Creates an event class | | `bloctest` | Creates a `blocTest` | | `mockbloc` | Creates a class extending `MockBloc` | | `_mockbloc` | Creates a private class extending `MockBloc` | | `mockcubit` | Creates a class extending `MockCubit` | | `_mockcubit` | Creates a private class extending `MockCubit` | | `fake` | Creates a class extending `Fake` | | `_fake` | Creates a private class extending `Fake` | | `mock` | Creates a class extending `Mock` | | `_mock` | Creates a private class extending `Mock` | ### Freezed Bloc | Shortcut | Description | | -------- | ----------------------- | | `fstate` | Creates a freezed state | | `fevent` | Creates a freezed event | ================================================ FILE: extensions/vscode/package.json ================================================ { "name": "bloc", "displayName": "bloc", "description": "Support for the bloc library and provides tools for effectively creating blocs for both Flutter and AngularDart apps.", "version": "6.8.13", "publisher": "FelixAngelov", "bugs": { "url": "https://github.com/felangel/bloc/issues", "email": "felangelov@gmail.com" }, "repository": { "type": "git", "url": "https://github.com/felangel/bloc" }, "homepage": "https://bloclibrary.dev", "engines": { "vscode": "^1.75.0" }, "categories": [ "Snippets", "Programming Languages" ], "keywords": [ "dart", "flutter", "angulardart", "bloc", "state-management", "language-server" ], "icon": "assets/logo.png", "activationEvents": [ "workspaceContains:**/pubspec.yaml" ], "main": "./dist/extension", "contributes": { "configuration": [ { "title": "Bloc", "properties": { "bloc.checkForUpdates": { "type": "boolean", "default": true, "description": "Whether to check if you are using the latest package versions at startup." }, "bloc.languageServer.enabled": { "type": "boolean", "default": true, "description": "Whether to run the bloc language server at startup." }, "bloc.newBlocTemplate.type": { "type": "string", "default": "auto", "enum": [ "auto", "equatable", "freezed", "simple" ], "enumDescriptions": [ "automatically pick template based on dependencies", "always use equatable template", "always use freezed template", "always use simple template" ] }, "bloc.newBlocTemplate.createDirectory": { "type": "boolean", "default": true, "description": "Whether to create a bloc directory when creating a new bloc." }, "bloc.newBlocTemplate.useSealedClasses": { "type": "boolean", "default": true, "description": "Whether to use sealed classes when creating a new bloc." }, "bloc.newCubitTemplate.type": { "type": "string", "default": "auto", "enum": [ "auto", "equatable", "freezed", "simple" ], "enumDescriptions": [ "automatically pick template based on dependencies", "always use equatable template", "always use freezed template", "always use simple template" ] }, "bloc.newCubitTemplate.createDirectory": { "type": "boolean", "default": true, "description": "Whether to create a cubit directory when creating a new cubit." }, "bloc.newCubitTemplate.useSealedClasses": { "type": "boolean", "default": true, "description": "Whether to use sealed classes when creating a new cubit." } } } ], "commands": [ { "command": "extension.new-bloc", "title": "Bloc: New Bloc", "icon": "assets/logo.png" }, { "command": "extension.new-cubit", "title": "Bloc: New Cubit", "icon": "assets/logo.png" }, { "command": "extension.convert-multibloclistener", "title": "Convert to MultiBlocListener" }, { "command": "extension.convert-multiblocprovider", "title": "Convert to MultiBlocProvider" }, { "command": "extension.convert-multirepositoryprovider", "title": "Convert to MultiRepositoryProvider" }, { "command": "extension.wrap-blocbuilder", "title": "Wrap with BlocBuilder" }, { "command": "extension.wrap-blocselector", "title": "Wrap with BlocSelector" }, { "command": "extension.wrap-bloclistener", "title": "Wrap with BlocListener" }, { "command": "extension.wrap-blocconsumer", "title": "Wrap with BlocConsumer" }, { "command": "extension.wrap-blocprovider", "title": "Wrap with BlocProvider" }, { "command": "extension.wrap-repositoryprovider", "title": "Wrap with RepositoryProvider" } ], "menus": { "explorer/context": [ { "command": "extension.new-bloc", "group": "blocGroup@1", "when": "explorerResourceIsFolder && bloc.showContextMenu" }, { "command": "extension.new-cubit", "group": "blocGroup@1", "when": "explorerResourceIsFolder && bloc.showContextMenu" } ], "commandPalette": [ { "command": "extension.convert-multibloclistener", "when": "editorLangId == dart" }, { "command": "extension.convert-multiblocprovider", "when": "editorLangId == dart" }, { "command": "extension.convert-multirepositoryprovider", "when": "editorLangId == dart" }, { "command": "extension.wrap-blocbuilder", "when": "editorLangId == dart" }, { "command": "extension.wrap-blocselector", "when": "editorLangId == dart" }, { "command": "extension.wrap-bloclistener", "when": "editorLangId == dart" }, { "command": "extension.wrap-blocconsumer", "when": "editorLangId == dart" }, { "command": "extension.wrap-blocprovider", "when": "editorLangId == dart" }, { "command": "extension.wrap-repositoryprovider", "when": "editorLangId == dart" } ] }, "snippets": [ { "language": "dart", "path": "./snippets/bloc.json" }, { "language": "dart", "path": "./snippets/bloc_test.json" }, { "language": "dart", "path": "./snippets/flutter_bloc.json" }, { "language": "dart", "path": "./snippets/freezed_bloc.json" } ] }, "scripts": { "vscode:prepublish": "webpack --mode production", "webpack": "webpack --mode development", "webpack-dev": "webpack --mode development --watch", "test-compile": "tsc -p ./", "compile": "tsc -p ./", "watch": "tsc -watch -p ./" }, "devDependencies": { "@types/change-case": "^2.3.1", "@types/js-yaml": "^3.12.1", "@types/lodash": "^4.14.121", "@types/mkdirp": "^0.5.2", "@types/mocha": "^2.2.42", "@types/node": "^12.7.8", "@types/node-fetch": "^2.0.0", "@types/rimraf": "^2.0.2", "@types/semver": "^6.0.2", "@types/uuid": "^8.3.4", "@types/vscode": "^1.56.0", "ts-loader": "^9.4.2", "tslint": "^5.12.1", "typescript": "^5.0.0", "webpack": "^5.105.0", "webpack-cli": "^5.0.1" }, "dependencies": { "axios": "^1.13.5", "change-case": "^4.1.2", "js-yaml": "^4.1.1", "lodash": "^4.17.23", "mkdirp": "^0.5.1", "node-fetch": "^2.0.0", "rimraf": "^3.0.2", "semver": "^6.3.1", "uuid": "^8.3.2", "vscode-languageclient": "^7.0.0" } } ================================================ FILE: extensions/vscode/snippets/bloc.json ================================================ { "Bloc": { "prefix": "bloc", "body": [ "class ${1:Subject}Bloc extends Bloc<$1Event, $1State> {", "\t${1:Subject}Bloc() : super(${1:Subject}Initial()) {", "\t\ton<$1Event>((event, emit) {", "\t\t\t${2:// TODO: implement event handler}", "\t\t});", "\t}", "}" ] }, "Cubit": { "prefix": "cubit", "body": [ "class ${1:Subject}Cubit extends Cubit<$1State> {", "\t${1:Subject}Cubit() : super(${1:Subject}Initial());", "}" ] }, "BlocObserver": { "prefix": "blocobserver", "body": [ "import 'package:bloc/bloc.dart';", "", "class ${1:My}BlocObserver extends BlocObserver {", "\t@override", "\tvoid onEvent(Bloc bloc, Object? event) {", "\t\tsuper.onEvent(bloc, event);", "\t\t${2:// TODO: implement onEvent}", "\t}", "", "\t@override", "\tvoid onError(BlocBase bloc, Object error, StackTrace stackTrace) {", "\t\t${3:// TODO: implement onError}", "\t\tsuper.onError(bloc, error, stackTrace);", "\t}", "", "\t@override", "\tvoid onChange(BlocBase bloc, Change change) {", "\t\tsuper.onChange(bloc, change);", "\t\t${4:// TODO: implement onChange}", "\t}", "", "\t@override", "\tvoid onTransition(Bloc bloc, Transition transition) {", "\t\tsuper.onTransition(bloc, transition);", "\t\t${4:// TODO: implement onTransition}", "\t}", "}" ] }, "Bloc State": { "prefix": "blocstate", "body": [ "class ${1:Subject}${2:Verb}${3:State} extends $1State {", "\tconst $1$2$3($5);", "", "\t$4", "", "\t@override", "\tList get props => [$6];", "}" ], "description": "Subject + Verb (action) + State" }, "Bloc Event": { "prefix": "blocevent", "body": [ "class ${1:Subject}${2:Noun}${3:Verb} extends $1Event {", "\tconst $1$2$3($5);", "", "\t$4", "", "\t@override", "\tList get props => [$6];", "}" ], "description": "Subject + Noun (optional) + Verb (event)" }, "Import package:bloc": { "prefix": "importbloc", "body": "import 'package:bloc/bloc.dart';", "description": "import package:bloc/bloc.dart;" }, "Register Event Handler": { "prefix": "onevent", "body": [ "on<${1:${TM_FILENAME_BASE/(.*)(?=_bloc)(_bloc)/${1:/pascalcase}/g}Event}>((event, emit) {", "\t${2:// TODO: implement event handler}", "});" ], "description": "Register a new EventHandler" }, "Define Event Handler": { "prefix": "_onevent", "body": [ "${1|void,Future|} _on${2:Event}(", "\t$2 event,", "\tEmitter<${3:${TM_FILENAME_BASE/(.*)(?=_bloc)(_bloc)/${1:/pascalcase}/g}State}> emit,", ") ${4:async} {", "\t${5:// TODO: implement event handler}", "}" ], "description": "Define a new EventHandler" } } ================================================ FILE: extensions/vscode/snippets/bloc_test.json ================================================ { "BlocTest": { "prefix": "bloctest", "body": [ "blocTest<${1:Subject}${2|Bloc,Cubit|}, $1State>(", "\t'emits [${3:MyState}] when ${4:MyEvent} is added.',", "\tbuild: () => ${1:Subject}${2|Bloc,Cubit|}(),", "\tact: (bloc) => bloc.add(${4:MyEvent()}),", "\texpect: () => const <$1State>[${3:MyState()}],", ");" ], "description": "create a new blocTest" }, "Import package:bloc_test": { "prefix": "importbloctest", "body": "import 'package:bloc_test/bloc_test.dart';", "description": "import package:bloc_test/bloc_test.dart;" }, "MockBloc": { "prefix": "mockbloc", "body": [ "class Mock${1:Subject}Bloc extends MockBloc<${1}Event, ${1}State> implements ${1}Bloc {}" ], "description": "create a mock bloc" }, "_MockBloc": { "prefix": "_mockbloc", "body": [ "class _Mock${1:Subject}Bloc extends MockBloc<${1}Event, ${1}State> implements ${1}Bloc {}" ], "description": "create a private mock bloc" }, "MockCubit": { "prefix": "mockcubit", "body": [ "class Mock${1:Subject}Cubit extends MockCubit<${1}State> implements ${1}Cubit {}" ], "description": "create a mock cubit" }, "_MockCubit": { "prefix": "_mockcubit", "body": [ "class _Mock${1:Subject}Cubit extends MockCubit<${1}State> implements ${1}Cubit {}" ], "description": "create a private mock cubit" }, "Fake": { "prefix": "fake", "body": ["class Fake${1:Subject} extends Fake implements ${1} {}"], "description": "create a fake object" }, "_Fake": { "prefix": "_fake", "body": ["class _Fake${1:Subject} extends Fake implements ${1} {}"], "description": "create a private fake object" }, "Mock": { "prefix": "mock", "body": ["class Mock${1:Subject} extends Mock implements ${1} {}"], "description": "create a mock object" }, "_Mock": { "prefix": "_mock", "body": ["class _Mock${1:Subject} extends Mock implements ${1} {}"], "description": "create a private mock object" } } ================================================ FILE: extensions/vscode/snippets/flutter_bloc.json ================================================ { "BlocProvider": { "prefix": "blocprovider", "body": [ "BlocProvider(", "\tcreate: (context) => ${1:Subject}${2|Bloc,Cubit|}(),", "\tchild: ${3:Container()},", ")" ] }, "MultiBlocProvider": { "prefix": "multiblocprovider", "body": [ "MultiBlocProvider(", "\tproviders: [", "\t\tBlocProvider(", "\t\t\tcreate: (context) => ${1:Subject}${2|Bloc,Cubit|}(),", "\t\t),", "\t\tBlocProvider(", "\t\t\tcreate: (context) => ${3:Subject}${4|Bloc,Cubit|}(),", "\t\t),", "\t],", "\tchild: ${5:Container()},", ")" ] }, "RepositoryProvider": { "prefix": "repoprovider", "body": [ "RepositoryProvider(", "\tcreate: (context) => ${1:Subject}Repository(),", "\tchild: ${2:Container()},", ")" ] }, "MultiRepositoryProvider": { "prefix": "multirepoprovider", "body": [ "MultiRepositoryProvider(", "\tproviders: [", "\t\tRepositoryProvider(", "\t\t\tcreate: (context) => ${1:Subject}Repository(),", "\t\t),", "\t\tRepositoryProvider(", "\t\t\tcreate: (context) => ${2:Subject}Repository(),", "\t\t),", "\t],", "\tchild: ${3:Container()},", ")" ] }, "BlocBuilder": { "prefix": "blocbuilder", "body": [ "BlocBuilder<${1:Subject}${2|Bloc,Cubit|}, $1State>(", "\tbuilder: (context, state) {", "\t\treturn ${3:Container()};", "\t},", ")" ] }, "BlocSelector": { "prefix": "blocselector", "body": [ "BlocSelector<${1:Subject}${2|Bloc,Cubit|}, $1State, ${3:Selected}>(", "\tselector: (state) {", "\t\treturn ${4:state};", "\t},", "\tbuilder: (context, state) {", "\t\treturn ${5:Container()};", "\t},", ")" ] }, "BlocListener": { "prefix": "bloclistener", "body": [ "BlocListener<${1:Subject}${2|Bloc,Cubit|}, $1State>(", "\tlistener: (context, state) {", "\t\t${3:// TODO: implement listener}", "\t},", "\tchild: ${4:Container()},", ")" ] }, "MultiBlocListener": { "prefix": "multibloclistener", "body": [ "MultiBlocListener(", "\tlisteners: [", "\t\tBlocListener<${1:Subject}${2|Bloc,Cubit|}, $1State>(", "\t\t\tlistener: (context, state) {", "\t\t\t\t${3:// TODO: implement listener}", "\t\t\t},", "\t\t),", "\t\tBlocListener<${4:Subject}${5|Bloc,Cubit|}, $4State>(", "\t\t\tlistener: (context, state) {", "\t\t\t\t${6:// TODO: implement listener}", "\t\t\t},", "\t\t),", "\t],", "\tchild: ${7:Container()},", ")" ] }, "BlocConsumer": { "prefix": "blocconsumer", "body": [ "BlocConsumer<${1:Subject}${2|Bloc,Cubit|}, $1State>(", "\tlistener: (context, state) {", "\t\t${3:// TODO: implement listener}", "\t},", "\tbuilder: (context, state) {", "\t\treturn ${4:Container()};", "\t},", ")" ] }, "BlocProvider.of()": { "prefix": "blocof", "body": "BlocProvider.of<${1:Subject}${2|Bloc,Cubit|}>(context)" }, "RepositoryProvider.of()": { "prefix": "repoof", "body": "RepositoryProvider.of<${1:Subject}Repository>(context)" }, "context.read()": { "prefix": "read", "body": "context.read<${1:Subject}${2|Bloc,Cubit,Repository|}>()" }, "context.select()": { "prefix": "select", "body": "context.select((${1:Subject}${2|Bloc,Cubit|} ${3:bloc}) => $3$4)" }, "context.watch()": { "prefix": "watch", "body": "context.watch<${1:Subject}${2|Bloc,Cubit,Repository|}>()" }, "Import package:flutter_bloc": { "prefix": "importflutterbloc", "body": "import 'package:flutter_bloc/flutter_bloc.dart';", "description": "import package:flutter_bloc/flutter_bloc.dart;" } } ================================================ FILE: extensions/vscode/snippets/freezed_bloc.json ================================================ { "New freezed state": { "prefix": "fstate", "body": "const factory ${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/g}.${1:stateName}($2) = _${1/(.*)/${1:/capitalize}/};\n$3" }, "New freezed event": { "prefix": "fevent", "body": "const factory ${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/g}.${1:eventName}($2) = _${1/(.*)/${1:/capitalize}/};\n$3" } } ================================================ FILE: extensions/vscode/src/code-actions/bloc-code-action-provider.ts ================================================ import { window, CodeAction, CodeActionProvider, CodeActionKind } from "vscode"; import { getSelectedText } from "../utils"; const blocListenerRegExp = new RegExp("^BlocListener(\\<.*\\>)*\\(.*\\)", "ms"); const blocProviderRegExp = new RegExp( "^BlocProvider(\\<.*\\>)*(\\.value)*\\(.*\\)", "ms" ); const repositoryProviderRegExp = new RegExp( "^RepositoryProvider(\\<.*\\>)*(\\.value)*\\(.*\\)", "ms" ); export class BlocCodeActionProvider implements CodeActionProvider { public provideCodeActions(): CodeAction[] { const editor = window.activeTextEditor; if (!editor) return []; const selectedText = editor.document.getText(getSelectedText(editor)); if (selectedText === "") return []; const isBlocListener = blocListenerRegExp.test(selectedText); const isBlocProvider = blocProviderRegExp.test(selectedText); const isRepositoryProvider = repositoryProviderRegExp.test(selectedText); return [ ...(isBlocListener ? [ { command: "extension.convert-multibloclistener", title: "Convert to MultiBlocListener", }, ] : []), ...(isBlocProvider ? [ { command: "extension.convert-multiblocprovider", title: "Convert to MultiBlocProvider", }, ] : []), ...(isRepositoryProvider ? [ { command: "extension.convert-multirepositoryprovider", title: "Convert to MultiRepositoryProvider", }, ] : []), { command: "extension.wrap-blocbuilder", title: "Wrap with BlocBuilder", }, { command: "extension.wrap-blocselector", title: "Wrap with BlocSelector", }, { command: "extension.wrap-bloclistener", title: "Wrap with BlocListener", }, { command: "extension.wrap-blocconsumer", title: "Wrap with BlocConsumer", }, { command: "extension.wrap-blocprovider", title: "Wrap with BlocProvider", }, { command: "extension.wrap-repositoryprovider", title: "Wrap with RepositoryProvider", }, ].map((c) => { let action = new CodeAction(c.title, CodeActionKind.Refactor); action.command = { command: c.command, title: c.title, }; return action; }); } } ================================================ FILE: extensions/vscode/src/code-actions/index.ts ================================================ export * from "./bloc-code-action-provider"; ================================================ FILE: extensions/vscode/src/commands/convert-to.command.ts ================================================ import { convertTo } from "../utils"; const multiBlocProviderSnippet = (widget: string, child: string) => { return `MultiBlocProvider( providers: [ ${widget}, BlocProvider( create: (context) => \${1:Subject}\${2|Bloc,Cubit|}(), ), ], ${child} )`; }; const multiBlocListenerSnippet = (widget: string, child: string) => { return `MultiBlocListener( listeners: [ ${widget}, BlocListener<\${1:Subject}\${2|Bloc,Cubit|}, \$1State>( listener: (context, state) { \${4:// TODO: implement listener} }, ), ], ${child} )`; }; const multiRepositoryProviderSnippet = (widget: string, child: string) => { return `MultiRepositoryProvider( providers: [ ${widget}, RepositoryProvider( create: (context) => \${1:Subject}Repository(), ), ], ${child} )`; }; export const convertToMultiBlocProvider = async () => convertTo(multiBlocProviderSnippet); export const convertToMultiBlocListener = async () => convertTo(multiBlocListenerSnippet); export const convertToMultiRepositoryProvider = async () => convertTo(multiRepositoryProviderSnippet); ================================================ FILE: extensions/vscode/src/commands/index.ts ================================================ export * from "./convert-to.command"; export * from "./new-bloc.command"; export * from "./new-cubit.command"; export * from "./wrap-with.command"; ================================================ FILE: extensions/vscode/src/commands/new-bloc.command.ts ================================================ import * as _ from "lodash"; import * as changeCase from "change-case"; import * as mkdirp from "mkdirp"; import { InputBoxOptions, OpenDialogOptions, Uri, window, workspace, } from "vscode"; import { existsSync, lstatSync, writeFile } from "fs"; import { getBlocEventTemplate, getBlocStateTemplate, getBlocTemplate, } from "../templates"; import { getBlocType, BlocType, TemplateType } from "../utils"; export const newBloc = async (uri: Uri) => { const blocName = await promptForBlocName(); if (_.isNil(blocName) || blocName.trim() === "") { window.showErrorMessage("The bloc name must not be empty"); return; } let targetDirectory; if (_.isNil(_.get(uri, "fsPath")) || !lstatSync(uri.fsPath).isDirectory()) { targetDirectory = await promptForTargetDirectory(); if (_.isNil(targetDirectory)) { window.showErrorMessage("Please select a valid directory"); return; } } else { targetDirectory = uri.fsPath; } const blocType = await getBlocType(TemplateType.Bloc); const pascalCaseBlocName = changeCase.pascalCase(blocName); try { await generateBlocCode(blocName, targetDirectory, blocType); window.showInformationMessage( `Successfully Generated ${pascalCaseBlocName} Bloc` ); } catch (error) { window.showErrorMessage( `Error: ${error instanceof Error ? error.message : JSON.stringify(error)}` ); } }; function promptForBlocName(): Thenable { const blocNamePromptOptions: InputBoxOptions = { prompt: "Bloc Name", placeHolder: "counter", }; return window.showInputBox(blocNamePromptOptions); } async function promptForTargetDirectory(): Promise { const options: OpenDialogOptions = { canSelectMany: false, openLabel: "Select a folder to create the bloc in", canSelectFolders: true, }; return window.showOpenDialog(options).then((uri) => { if (_.isNil(uri) || _.isEmpty(uri)) { return undefined; } return uri[0].fsPath; }); } async function generateBlocCode( blocName: string, targetDirectory: string, type: BlocType ) { const shouldCreateDirectory = workspace .getConfiguration("bloc") .get("newBlocTemplate.createDirectory"); const blocDirectoryPath = shouldCreateDirectory ? `${targetDirectory}/bloc` : targetDirectory; if (!existsSync(blocDirectoryPath)) { await createDirectory(blocDirectoryPath); } const useSealedClasses = workspace .getConfiguration("bloc") .get("newBlocTemplate.useSealedClasses", true); await Promise.all([ createBlocEventTemplate( blocName, blocDirectoryPath, type, useSealedClasses ), createBlocStateTemplate( blocName, blocDirectoryPath, type, useSealedClasses ), createBlocTemplate(blocName, blocDirectoryPath, type), ]); } function createDirectory(targetDirectory: string): Promise { return new Promise((resolve, reject) => { mkdirp(targetDirectory, (error) => { if (error) { return reject(error); } resolve(); }); }); } function createBlocEventTemplate( blocName: string, targetDirectory: string, type: BlocType, useSealedClasses: boolean ) { const snakeCaseBlocName = changeCase.snakeCase(blocName); const targetPath = `${targetDirectory}/${snakeCaseBlocName}_event.dart`; if (existsSync(targetPath)) { throw Error(`${snakeCaseBlocName}_event.dart already exists`); } return new Promise(async (resolve, reject) => { writeFile( targetPath, getBlocEventTemplate(blocName, type, useSealedClasses), "utf8", (error) => { if (error) { reject(error); return; } resolve(); } ); }); } function createBlocStateTemplate( blocName: string, targetDirectory: string, type: BlocType, useSealedClasses: boolean ) { const snakeCaseBlocName = changeCase.snakeCase(blocName); const targetPath = `${targetDirectory}/${snakeCaseBlocName}_state.dart`; if (existsSync(targetPath)) { throw Error(`${snakeCaseBlocName}_state.dart already exists`); } return new Promise(async (resolve, reject) => { writeFile( targetPath, getBlocStateTemplate(blocName, type, useSealedClasses), "utf8", (error) => { if (error) { reject(error); return; } resolve(); } ); }); } function createBlocTemplate( blocName: string, targetDirectory: string, type: BlocType ) { const snakeCaseBlocName = changeCase.snakeCase(blocName); const targetPath = `${targetDirectory}/${snakeCaseBlocName}_bloc.dart`; if (existsSync(targetPath)) { throw Error(`${snakeCaseBlocName}_bloc.dart already exists`); } return new Promise(async (resolve, reject) => { writeFile(targetPath, getBlocTemplate(blocName, type), "utf8", (error) => { if (error) { reject(error); return; } resolve(); }); }); } ================================================ FILE: extensions/vscode/src/commands/new-cubit.command.ts ================================================ import * as _ from "lodash"; import * as changeCase from "change-case"; import * as mkdirp from "mkdirp"; import { InputBoxOptions, OpenDialogOptions, Uri, window, workspace, } from "vscode"; import { existsSync, lstatSync, writeFile } from "fs"; import { getCubitStateTemplate, getCubitTemplate } from "../templates"; import { getBlocType, BlocType, TemplateType } from "../utils"; export const newCubit = async (uri: Uri) => { const cubitName = await promptForCubitName(); if (_.isNil(cubitName) || cubitName.trim() === "") { window.showErrorMessage("The cubit name must not be empty"); return; } let targetDirectory; if (_.isNil(_.get(uri, "fsPath")) || !lstatSync(uri.fsPath).isDirectory()) { targetDirectory = await promptForTargetDirectory(); if (_.isNil(targetDirectory)) { window.showErrorMessage("Please select a valid directory"); return; } } else { targetDirectory = uri.fsPath; } const blocType = await getBlocType(TemplateType.Cubit); const pascalCaseCubitName = changeCase.pascalCase(cubitName); try { await generateCubitCode(cubitName, targetDirectory, blocType); window.showInformationMessage( `Successfully Generated ${pascalCaseCubitName} Cubit` ); } catch (error) { window.showErrorMessage( `Error: ${error instanceof Error ? error.message : JSON.stringify(error)}` ); } }; function promptForCubitName(): Thenable { const cubitNamePromptOptions: InputBoxOptions = { prompt: "Cubit Name", placeHolder: "counter", }; return window.showInputBox(cubitNamePromptOptions); } async function promptForTargetDirectory(): Promise { const options: OpenDialogOptions = { canSelectMany: false, openLabel: "Select a folder to create the cubit in", canSelectFolders: true, }; return window.showOpenDialog(options).then((uri) => { if (_.isNil(uri) || _.isEmpty(uri)) { return undefined; } return uri[0].fsPath; }); } async function generateCubitCode( cubitName: string, targetDirectory: string, type: BlocType ) { const shouldCreateDirectory = workspace .getConfiguration("bloc") .get("newCubitTemplate.createDirectory"); const cubitDirectoryPath = shouldCreateDirectory ? `${targetDirectory}/cubit` : targetDirectory; if (!existsSync(cubitDirectoryPath)) { await createDirectory(cubitDirectoryPath); } const useSealedClasses = workspace .getConfiguration("bloc") .get("newCubitTemplate.useSealedClasses", true); await Promise.all([ createCubitStateTemplate(cubitName, cubitDirectoryPath, type, useSealedClasses), createCubitTemplate(cubitName, cubitDirectoryPath, type), ]); } function createDirectory(targetDirectory: string): Promise { return new Promise((resolve, reject) => { mkdirp(targetDirectory, (error) => { if (error) { return reject(error); } resolve(); }); }); } function createCubitStateTemplate( cubitName: string, targetDirectory: string, type: BlocType, useSealedClasses: boolean ) { const snakeCaseCubitName = changeCase.snakeCase(cubitName); const targetPath = `${targetDirectory}/${snakeCaseCubitName}_state.dart`; if (existsSync(targetPath)) { throw Error(`${snakeCaseCubitName}_state.dart already exists`); } return new Promise(async (resolve, reject) => { writeFile( targetPath, getCubitStateTemplate(cubitName, type, useSealedClasses), "utf8", (error) => { if (error) { reject(error); return; } resolve(); } ); }); } function createCubitTemplate( cubitName: string, targetDirectory: string, type: BlocType ) { const snakeCaseCubitName = changeCase.snakeCase(cubitName); const targetPath = `${targetDirectory}/${snakeCaseCubitName}_cubit.dart`; if (existsSync(targetPath)) { throw Error(`${snakeCaseCubitName}_cubit.dart already exists`); } return new Promise(async (resolve, reject) => { writeFile( targetPath, getCubitTemplate(cubitName, type), "utf8", (error) => { if (error) { reject(error); return; } resolve(); } ); }); } ================================================ FILE: extensions/vscode/src/commands/wrap-with.command.ts ================================================ import { wrapWith } from "../utils"; const blocBuilderSnippet = (widget: string) => { return `BlocBuilder<\${1:Subject}\${2|Bloc,Cubit|}, $1State>( builder: (context, state) { return ${widget}; }, )`; }; const blocSelectorSnippet = (widget: string) => { return `BlocSelector<\${1:Subject}\${2|Bloc,Cubit|}, $1State, \${3:SelectedState}>( selector: (state) { return \${4:state}; }, builder: (context, state) { return ${widget}; }, )`; }; const blocListenerSnippet = (widget: string) => { return `BlocListener<\${1:Subject}\${2|Bloc,Cubit|}, $1State>( listener: (context, state) { \${3:// TODO: implement listener} }, child: ${widget}, )`; }; const blocProviderSnippet = (widget: string) => { return `BlocProvider( create: (context) => \${1:Subject}\${2|Bloc,Cubit|}(), child: ${widget}, )`; }; const blocConsumerSnippet = (widget: string) => { return `BlocConsumer<\${1:Subject}\${2|Bloc,Cubit|}, $1State>( listener: (context, state) { \${3:// TODO: implement listener} }, builder: (context, state) { return ${widget}; }, )`; }; const repositoryProviderSnippet = (widget: string) => { return `RepositoryProvider( create: (context) => \${1:Subject}Repository(), child: ${widget}, )`; }; export const wrapWithBlocBuilder = async () => wrapWith(blocBuilderSnippet); export const wrapWithBlocSelector = async () => wrapWith(blocSelectorSnippet); export const wrapWithBlocListener = async () => wrapWith(blocListenerSnippet); export const wrapWithBlocConsumer = async () => wrapWith(blocConsumerSnippet); export const wrapWithBlocProvider = async () => wrapWith(blocProviderSnippet); export const wrapWithRepositoryProvider = async () => wrapWith(repositoryProviderSnippet); ================================================ FILE: extensions/vscode/src/extension.ts ================================================ import * as _ from "lodash"; import { commands, ExtensionContext, languages, window, workspace, } from "vscode"; import { BlocCodeActionProvider } from "./code-actions"; import { convertToMultiBlocListener, convertToMultiBlocProvider, convertToMultiRepositoryProvider, newBloc, newCubit, wrapWithBlocBuilder, wrapWithBlocConsumer, wrapWithBlocListener, wrapWithBlocProvider, wrapWithBlocSelector, wrapWithRepositoryProvider, } from "./commands"; import { analyzeDependencies, setShowContextMenu } from "./utils"; import { client, DART_FILE, tryStartLanguageServer } from "./language-server"; export function activate(context: ExtensionContext) { if ( workspace.getConfiguration("bloc").get("languageServer.enabled") ) { tryStartLanguageServer(context); } if (workspace.getConfiguration("bloc").get("checkForUpdates")) { analyzeDependencies(); } setShowContextMenu(); context.subscriptions.push( window.onDidChangeActiveTextEditor((_) => setShowContextMenu()), workspace.onDidChangeWorkspaceFolders((_) => setShowContextMenu()), workspace.onDidChangeTextDocument(async function (event) { if (event.document.uri.fsPath.endsWith("pubspec.yaml")) { setShowContextMenu(event.document.uri); } }), commands.registerCommand("extension.new-bloc", newBloc), commands.registerCommand("extension.new-cubit", newCubit), commands.registerCommand( "extension.convert-multibloclistener", convertToMultiBlocListener ), commands.registerCommand( "extension.convert-multiblocprovider", convertToMultiBlocProvider ), commands.registerCommand( "extension.convert-multirepositoryprovider", convertToMultiRepositoryProvider ), commands.registerCommand("extension.wrap-blocbuilder", wrapWithBlocBuilder), commands.registerCommand( "extension.wrap-blocselector", wrapWithBlocSelector ), commands.registerCommand( "extension.wrap-bloclistener", wrapWithBlocListener ), commands.registerCommand( "extension.wrap-blocconsumer", wrapWithBlocConsumer ), commands.registerCommand( "extension.wrap-blocprovider", wrapWithBlocProvider ), commands.registerCommand( "extension.wrap-repositoryprovider", wrapWithRepositoryProvider ), languages.registerCodeActionsProvider( DART_FILE, new BlocCodeActionProvider() ) ); } export function deactivate(): Thenable | undefined { if (!client) return undefined; return client.stop(); } ================================================ FILE: extensions/vscode/src/language-server/index.ts ================================================ export * from "./language-server"; export * from "./selectors"; ================================================ FILE: extensions/vscode/src/language-server/language-server.ts ================================================ import { ExtensionContext, ProgressLocation, window } from "vscode"; import { LanguageClient, LanguageClientOptions, RevealOutputChannelOn, ServerOptions, TransportKind, } from "vscode-languageclient/node"; import { getBlocToolsExecutable, installBlocTools } from "../utils"; let client: LanguageClient; const DART_FILE = { language: "dart", scheme: "file" }; const ANALYSIS_OPTIONS_FILE = { pattern: "**/analysis_options.yaml", scheme: "file", }; async function startLanguageServer(executable: string) { const serverOptions: ServerOptions = { command: `"${executable}"`, args: ["language-server"], options: { env: process.env, shell: true, }, transport: TransportKind.stdio, }; const clientOptions: LanguageClientOptions = { revealOutputChannelOn: RevealOutputChannelOn.Info, documentSelector: [DART_FILE, ANALYSIS_OPTIONS_FILE], }; client = new LanguageClient( "blocAnalysisLSP", "Bloc Analysis Server", serverOptions, clientOptions ); return client.start(); } async function startLanguageServerWithProgress(executable: string) { window.withProgress( { location: ProgressLocation.Window, title: "Bloc Analysis Server", }, async () => { try { await startLanguageServer(executable); window.setStatusBarMessage("✓ Bloc Analysis Server", 3000); } catch (err) { window.showErrorMessage(`${err}`); } } ); } async function tryStartLanguageServer( context: ExtensionContext ): Promise { const executable = await getBlocToolsExecutable(context); if (executable) return startLanguageServerWithProgress(executable); await window.withProgress( { location: ProgressLocation.Notification, title: "Installing the Bloc Language Server", }, async () => { try { await installBlocTools(context); window.setStatusBarMessage("Bloc Language Server installed", 3000); } catch (err) { window.showErrorMessage(`${err}`); } } ); const installedExecutable = await getBlocToolsExecutable(context); if (!installedExecutable) { window.showErrorMessage("Failed to install the Bloc Language Server"); return; } return startLanguageServerWithProgress(installedExecutable); } export { client, tryStartLanguageServer }; ================================================ FILE: extensions/vscode/src/language-server/selectors.ts ================================================ const ANALYSIS_OPTIONS_FILE = { pattern: "**/analysis_options.yaml", scheme: "file", }; const DART_FILE = { language: "dart", scheme: "file" }; export { ANALYSIS_OPTIONS_FILE, DART_FILE }; ================================================ FILE: extensions/vscode/src/templates/bloc-event.template.ts ================================================ import * as changeCase from "change-case"; import { BlocType } from "../utils"; export function getBlocEventTemplate( blocName: string, type: BlocType, useSealedClasses: boolean ): string { switch (type) { case BlocType.Freezed: return getFreezedBlocEvent(blocName); case BlocType.Equatable: return getEquatableBlocEventTemplate(blocName, useSealedClasses); default: return getDefaultBlocEventTemplate(blocName, useSealedClasses); } } function getEquatableBlocEventTemplate( blocName: string, useSealedClasses: boolean ): string { const classPrefix = useSealedClasses ? "sealed" : "abstract"; const pascalCaseBlocName = changeCase.pascalCase(blocName); const snakeCaseBlocName = changeCase.snakeCase(blocName); return `part of '${snakeCaseBlocName}_bloc.dart'; ${classPrefix} class ${pascalCaseBlocName}Event extends Equatable { const ${pascalCaseBlocName}Event(); @override List get props => []; } `; } function getDefaultBlocEventTemplate( blocName: string, useSealedClasses: boolean ): string { const classPrefix = useSealedClasses ? "sealed" : "abstract"; const pascalCaseBlocName = changeCase.pascalCase(blocName); const snakeCaseBlocName = changeCase.snakeCase(blocName); return `part of '${snakeCaseBlocName}_bloc.dart'; @immutable ${classPrefix} class ${pascalCaseBlocName}Event {} `; } function getFreezedBlocEvent(blocName: string): string { const pascalCaseBlocName = changeCase.pascalCase(blocName) + "Event"; const snakeCaseBlocName = changeCase.snakeCase(blocName); return `part of '${snakeCaseBlocName}_bloc.dart'; @freezed class ${pascalCaseBlocName} with _\$${pascalCaseBlocName} { const factory ${pascalCaseBlocName}.started() = _Started; }`; } ================================================ FILE: extensions/vscode/src/templates/bloc-state.template.ts ================================================ import * as changeCase from "change-case"; import { BlocType } from "../utils"; export function getBlocStateTemplate( blocName: string, type: BlocType, useSealedClasses: boolean ): string { switch (type) { case BlocType.Freezed: return getFreezedBlocStateTemplate(blocName); case BlocType.Equatable: return getEquatableBlocStateTemplate(blocName, useSealedClasses); default: return getDefaultBlocStateTemplate(blocName, useSealedClasses); } } function getEquatableBlocStateTemplate( blocName: string, useSealedClasses: boolean ): string { const classPrefix = useSealedClasses ? "sealed" : "abstract"; const subclassPrefix = useSealedClasses ? "final " : ""; const pascalCaseBlocName = changeCase.pascalCase(blocName); const snakeCaseBlocName = changeCase.snakeCase(blocName); return `part of '${snakeCaseBlocName}_bloc.dart'; ${classPrefix} class ${pascalCaseBlocName}State extends Equatable { const ${pascalCaseBlocName}State(); @override List get props => []; } ${subclassPrefix}class ${pascalCaseBlocName}Initial extends ${pascalCaseBlocName}State {} `; } function getDefaultBlocStateTemplate( blocName: string, useSealedClasses: boolean ): string { const classPrefix = useSealedClasses ? "sealed" : "abstract"; const subclassPrefix = useSealedClasses ? "final " : ""; const pascalCaseBlocName = changeCase.pascalCase(blocName); const snakeCaseBlocName = changeCase.snakeCase(blocName); return `part of '${snakeCaseBlocName}_bloc.dart'; @immutable ${classPrefix} class ${pascalCaseBlocName}State {} ${subclassPrefix}class ${pascalCaseBlocName}Initial extends ${pascalCaseBlocName}State {} `; } function getFreezedBlocStateTemplate(blocName: string): string { const pascalCaseBlocName = changeCase.pascalCase(blocName) + "State"; const snakeCaseBlocName = changeCase.snakeCase(blocName); return `part of '${snakeCaseBlocName}_bloc.dart'; @freezed class ${pascalCaseBlocName} with _\$${pascalCaseBlocName} { const factory ${pascalCaseBlocName}.initial() = _Initial; } `; } ================================================ FILE: extensions/vscode/src/templates/bloc.template.ts ================================================ import * as changeCase from "change-case"; import { BlocType } from "../utils"; export function getBlocTemplate(blocName: string, type: BlocType): string { switch (type) { case BlocType.Freezed: return getFreezedBlocTemplate(blocName); case BlocType.Equatable: return getEquatableBlocTemplate(blocName); default: return getDefaultBlocTemplate(blocName); } } function getEquatableBlocTemplate(blocName: string) { const pascalCaseBlocName = changeCase.pascalCase(blocName); const snakeCaseBlocName = changeCase.snakeCase(blocName); const blocState = `${pascalCaseBlocName}State`; const blocEvent = `${pascalCaseBlocName}Event`; return `import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; part '${snakeCaseBlocName}_event.dart'; part '${snakeCaseBlocName}_state.dart'; class ${pascalCaseBlocName}Bloc extends Bloc<${blocEvent}, ${blocState}> { ${pascalCaseBlocName}Bloc() : super(${pascalCaseBlocName}Initial()) { on<${blocEvent}>((event, emit) { // TODO: implement event handler }); } } `; } function getDefaultBlocTemplate(blocName: string) { const pascalCaseBlocName = changeCase.pascalCase(blocName); const snakeCaseBlocName = changeCase.snakeCase(blocName); const blocState = `${pascalCaseBlocName}State`; const blocEvent = `${pascalCaseBlocName}Event`; return `import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part '${snakeCaseBlocName}_event.dart'; part '${snakeCaseBlocName}_state.dart'; class ${pascalCaseBlocName}Bloc extends Bloc<${blocEvent}, ${blocState}> { ${pascalCaseBlocName}Bloc() : super(${pascalCaseBlocName}Initial()) { on<${blocEvent}>((event, emit) { // TODO: implement event handler }); } } `; } export function getFreezedBlocTemplate(blocName: string) { const pascalCaseBlocName = changeCase.pascalCase(blocName); const snakeCaseBlocName = changeCase.snakeCase(blocName); const blocState = `${pascalCaseBlocName}State`; const blocEvent = `${pascalCaseBlocName}Event`; return `import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part '${snakeCaseBlocName}_event.dart'; part '${snakeCaseBlocName}_state.dart'; part '${snakeCaseBlocName}_bloc.freezed.dart'; class ${pascalCaseBlocName}Bloc extends Bloc<${blocEvent}, ${blocState}> { ${pascalCaseBlocName}Bloc() : super(_Initial()) { on<${blocEvent}>((event, emit) { // TODO: implement event handler }); } } `; } ================================================ FILE: extensions/vscode/src/templates/cubit-state.template.ts ================================================ import * as changeCase from "change-case"; import { BlocType } from "../utils"; export function getCubitStateTemplate( cubitName: string, type: BlocType, useSealedClasses: boolean ): string { switch (type) { case BlocType.Freezed: return getFreezedCubitStateTemplate(cubitName); case BlocType.Equatable: return getEquatableCubitStateTemplate(cubitName, useSealedClasses); default: return getDefaultCubitStateTemplate(cubitName, useSealedClasses); } } function getEquatableCubitStateTemplate( cubitName: string, useSealedClasses: boolean ): string { const classPrefix = useSealedClasses ? "sealed" : "abstract"; const subclassPrefix = useSealedClasses ? "final " : ""; const pascalCaseCubitName = changeCase.pascalCase(cubitName); const snakeCaseCubitName = changeCase.snakeCase(cubitName); return `part of '${snakeCaseCubitName}_cubit.dart'; ${classPrefix} class ${pascalCaseCubitName}State extends Equatable { const ${pascalCaseCubitName}State(); @override List get props => []; } ${subclassPrefix}class ${pascalCaseCubitName}Initial extends ${pascalCaseCubitName}State {} `; } function getDefaultCubitStateTemplate( cubitName: string, useSealedClasses: boolean ): string { const classPrefix = useSealedClasses ? "sealed" : "abstract"; const subclassPrefix = useSealedClasses ? "final " : ""; const pascalCaseCubitName = changeCase.pascalCase(cubitName); const snakeCaseCubitName = changeCase.snakeCase(cubitName); return `part of '${snakeCaseCubitName}_cubit.dart'; @immutable ${classPrefix} class ${pascalCaseCubitName}State {} ${subclassPrefix}class ${pascalCaseCubitName}Initial extends ${pascalCaseCubitName}State {} `; } function getFreezedCubitStateTemplate(cubitName: string): string { const pascalCaseCubitName = changeCase.pascalCase(cubitName); const snakeCaseCubitName = changeCase.snakeCase(cubitName); return `part of '${snakeCaseCubitName}_cubit.dart'; @freezed class ${pascalCaseCubitName}State with _\$${pascalCaseCubitName}State { const factory ${pascalCaseCubitName}State.initial() = _Initial; } `; } ================================================ FILE: extensions/vscode/src/templates/cubit.template.ts ================================================ import * as changeCase from "change-case"; import { BlocType } from "../utils"; export function getCubitTemplate(cubitName: string, type: BlocType): string { switch (type) { case BlocType.Freezed: return getFreezedCubitTemplate(cubitName); case BlocType.Equatable: return getEquatableCubitTemplate(cubitName); default: return getDefaultCubitTemplate(cubitName); } } function getEquatableCubitTemplate(cubitName: string) { const pascalCaseCubitName = changeCase.pascalCase(cubitName); const snakeCaseCubitName = changeCase.snakeCase(cubitName); const cubitState = `${pascalCaseCubitName}State`; return `import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; part '${snakeCaseCubitName}_state.dart'; class ${pascalCaseCubitName}Cubit extends Cubit<${cubitState}> { ${pascalCaseCubitName}Cubit() : super(${pascalCaseCubitName}Initial()); } `; } function getDefaultCubitTemplate(cubitName: string) { const pascalCaseCubitName = changeCase.pascalCase(cubitName); const snakeCaseCubitName = changeCase.snakeCase(cubitName); const cubitState = `${pascalCaseCubitName}State`; return `import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part '${snakeCaseCubitName}_state.dart'; class ${pascalCaseCubitName}Cubit extends Cubit<${cubitState}> { ${pascalCaseCubitName}Cubit() : super(${pascalCaseCubitName}Initial()); } `; } export function getFreezedCubitTemplate(cubitName: string) { const pascalCaseCubitName = changeCase.pascalCase(cubitName); const snakeCaseCubitName = changeCase.snakeCase(cubitName); const cubitState = `${pascalCaseCubitName}State`; return `import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part '${snakeCaseCubitName}_state.dart'; part '${snakeCaseCubitName}_cubit.freezed.dart'; class ${pascalCaseCubitName}Cubit extends Cubit<${cubitState}> { ${pascalCaseCubitName}Cubit() : super(${pascalCaseCubitName}State.initial()); } `; } ================================================ FILE: extensions/vscode/src/templates/index.ts ================================================ export * from "./bloc-event.template"; export * from "./bloc-state.template"; export * from "./bloc.template"; export * from "./cubit-state.template"; export * from "./cubit.template"; ================================================ FILE: extensions/vscode/src/utils/analyze-dependencies.ts ================================================ import * as _ from "lodash"; import * as semver from "semver"; import { window, env, Uri } from "vscode"; import { getLatestPackageVersion, getPubspecLock } from "."; import { updatePubspecDependency } from "./update-pubspec-dependency"; const DEFAULT_VERSION_VALUE = "0.0.0"; interface Dependency { name: string; version: string; actions: Action[]; } interface Action { name: string; callback: Function; } const openBlocMigrationGuide = { name: "Open Migration Guide", callback: () => { env.openExternal(Uri.parse("https://bloclibrary.dev/migration")); }, }; const openEquatableMigrationGuide = { name: "Open Migration Guide", callback: () => { env.openExternal( Uri.parse( "https://github.com/felangel/equatable/blob/master/doc/migration_guides/migration-0.6.0.md" ) ); }, }; const deps = [ { name: "angular_bloc", actions: [openBlocMigrationGuide] }, { name: "bloc", actions: [openBlocMigrationGuide] }, { name: "bloc_concurrency", actions: [openBlocMigrationGuide] }, { name: "equatable", actions: [openEquatableMigrationGuide] }, { name: "flutter_bloc", actions: [openBlocMigrationGuide] }, { name: "hydrated_bloc", actions: [openBlocMigrationGuide] }, { name: "replay_bloc", actions: [openBlocMigrationGuide] }, { name: "sealed_flutter_bloc", actions: [openBlocMigrationGuide] }, ]; const devDeps = [{ name: "bloc_test", actions: [openBlocMigrationGuide] }]; export async function analyzeDependencies() { const dependencies = await getDependencies(deps); const devDependencies = await getDependencies(devDeps); const pubspecLock = await getPubspecLock(); const pubspecLockDependencies = _.get(pubspecLock, "packages", {}); checkForUpgrades(dependencies, pubspecLockDependencies); checkForUpgrades(devDependencies, pubspecLockDependencies); } function checkForUpgrades( dependencies: Dependency[], pubspecDependencies: object[] ) { for (let i = 0; i < dependencies.length; i++) { const dependency = dependencies[i]; if (_.isEmpty(dependency.version)) continue; if (_.has(pubspecDependencies, dependency.name)) { const currentDependencyVersion = _.get( pubspecDependencies, dependency.name, ).version; const hasLatestVersion = currentDependencyVersion === dependency.version; if (hasLatestVersion) continue; showUpdateMessage(dependency, currentDependencyVersion); } } } function showUpdateMessage(dependency : Dependency, dependencyVersion : string) { const minVersion = _.get( semver.minVersion(dependencyVersion), "version", DEFAULT_VERSION_VALUE ); if (!semver.satisfies(minVersion, dependency.version)) { window .showWarningMessage( `This workspace contains an outdated version of ${dependency.name}. Please update to ${dependency.version}.`, ...dependency.actions.map((action) => action.name).concat("Update") ) .then((invokedAction) => { if (invokedAction === "Update") { return updatePubspecDependency({ name: dependency.name, latestVersion: `^${dependency.version}`, currentVersion: dependencyVersion, }); } const action = dependency.actions.find( (action) => action.name === invokedAction ); if (!_.isNil(action)) { action.callback(); } }); } } async function getDependencies( dependencies: { name: string; actions: Action[] }[] ): Promise { const futures: Promise[] = dependencies.map( async (dependency) => { return { name: dependency.name, actions: dependency.actions, version: await getLatestPackageVersion(dependency.name), }; } ); return Promise.all(futures); } ================================================ FILE: extensions/vscode/src/utils/convert-to.ts ================================================ import { window, commands, SnippetString } from "vscode"; import { getSelectedText } from "../utils"; const childRegExp = new RegExp("[^S\r\n]*child: .*,s*", "ms"); export const convertTo = async ( snippet: (widget: string, child: string) => string ) => { let editor = window.activeTextEditor; if (!editor) return; const selection = getSelectedText(editor); const rawWidget = editor.document.getText(selection).replace("$", "//$"); const match = rawWidget.match(childRegExp); if (!match || !match.length) return; const child = match[0]; if (!child) return; const widget = rawWidget.replace(childRegExp, ""); editor.insertSnippet(new SnippetString(snippet(widget, child)), selection); await commands.executeCommand("editor.action.formatDocument"); }; ================================================ FILE: extensions/vscode/src/utils/downloader.ts ================================================ import axios, { AxiosRequestConfig } from "axios"; import * as fs from "fs"; import * as path from "path"; import * as rimraf from "rimraf"; import { pipeline, Readable } from "stream"; import { promisify } from "util"; import { v4 as uuid } from "uuid"; import { ExtensionContext, Uri } from "vscode"; import { retry } from "./retry"; const DEFAULT_TIMEOUT_MS = 5000; const DEFAULT_RETRY_COUNT = 5; const DEFAULT_RETRY_DELAY_MS = 100; const pipelineAsync = promisify(pipeline); const rmAsync = promisify(rimraf); export async function downloadFile( url: Uri, filename: string, context: ExtensionContext ): Promise { if (url.scheme !== `http` && url.scheme !== `https`) { throw new Error( `Unsupported URI scheme in url. Supported schemes are http and https.` ); } const downloadsStoragePath: string = downloadsPath(context); const tempFileDownloadPath: string = path.join(downloadsStoragePath, uuid()); const fileDownloadPath: string = path.join(downloadsStoragePath, filename); await fs.promises.mkdir(downloadsStoragePath, { recursive: true }); let progress = 0; let progressTimerId: any; try { progressTimerId = setInterval(() => { if (progress > 100) clearInterval(progressTimerId); }, 1500); const downloadStream: Readable = await get( url.toString(), DEFAULT_TIMEOUT_MS, DEFAULT_RETRY_COUNT, DEFAULT_RETRY_DELAY_MS ); const writeStream = fs.createWriteStream(tempFileDownloadPath); await Promise.all([ pipelineAsync([downloadStream, writeStream]), new Promise((resolve) => writeStream.on(`close`, resolve)), ]); } catch (error) { if (progressTimerId != null) clearInterval(progressTimerId); throw error; } await fs.promises.chmod(tempFileDownloadPath, 0o744); // make executable await rmAsync(fileDownloadPath); const renameDownloadedFile = async (): Promise => { await fs.promises.rename(tempFileDownloadPath, fileDownloadPath); return Uri.file(fileDownloadPath); }; return retry( renameDownloadedFile, renameDownloadedFile.name, DEFAULT_RETRY_COUNT, DEFAULT_RETRY_DELAY_MS ); } export async function tryGetDownload( filename: string, context: ExtensionContext ): Promise { try { return await getDownload(filename, context); } catch { return undefined; } } function downloadsPath(context: ExtensionContext): string { return path.join(context.globalStorageUri.fsPath, `downloads`); } async function listDownloads(context: ExtensionContext): Promise { const downloadsStoragePath = downloadsPath(context); try { const filePaths: string[] = await fs.promises.readdir(downloadsStoragePath); return filePaths.map((filePath) => Uri.file(path.join(downloadsStoragePath, filePath)) ); } catch (error) { return []; } } async function getDownload( filename: string, context: ExtensionContext ): Promise { const filePaths = await listDownloads(context); const matchingUris = filePaths.filter( (uri) => uri.path.split(`/`).pop() === filename.replace(`/`, ``) ); switch (matchingUris.length) { case 1: return matchingUris[0]; case 0: throw new Error( `Download not found: ${path.join(downloadsPath(context), filename)}` ); default: throw new Error( `Multiple downloads found: ${filePaths.map((uri) => uri.toString())}` ); } } async function get( url: string, timeoutInMs: number, retries: number, retryDelayInMs: number ): Promise { const body = () => getAsStream(url, timeoutInMs); const onError = (error: Error) => { const statusCode = (error as any)?.response?.status; if (statusCode != null && 400 <= statusCode && statusCode < 500) { throw error; } }; return retry(body, "getAsStream", retries, retryDelayInMs, onError); } async function getAsStream( url: string, timeoutInMs: number ): Promise { const options: AxiosRequestConfig = { timeout: timeoutInMs, responseType: `stream`, proxy: false, // Disabling axios proxy in order to use VSCode proxy settings. }; const response = await axios.get(url, options); if (response === undefined) { throw new Error( `Undefined response received when downloading from '${url}'` ); } return response.data; } ================================================ FILE: extensions/vscode/src/utils/exec.ts ================================================ import * as cp from "child_process"; export interface ExecOptions { cwd?: string | undefined; } export const exec = (cmd: string, options?: ExecOptions) => new Promise((resolve, reject) => { cp.exec(cmd, { cwd: options?.cwd }, (err, output) => { if (err) { return reject(err); } return resolve(output); }); }); ================================================ FILE: extensions/vscode/src/utils/get-bloc-tools-executable.ts ================================================ import { BLOC_TOOLS_VERSION, tryGetDownload } from "."; import { ExtensionContext } from "vscode"; export const getBlocToolsExecutable = async ( context: ExtensionContext ): Promise => { try { const executable = await tryGetDownload( `bloc_${BLOC_TOOLS_VERSION}`, context ); if (!executable) return null; return executable.fsPath; } catch (_) { return null; } }; ================================================ FILE: extensions/vscode/src/utils/get-bloc-type.ts ================================================ import { hasDependency } from "./has-dependency"; import { TemplateType, getTemplateSetting, TemplateSetting, } from "./get-template-setting"; const equatable = "equatable"; const freezed_annotation = "freezed_annotation"; export const enum BlocType { Simple, Equatable, Freezed, } export async function getBlocType(type: TemplateType): Promise { const setting = getTemplateSetting(type); switch (setting) { case TemplateSetting.Freezed: return BlocType.Freezed; case TemplateSetting.Equatable: return BlocType.Equatable; case TemplateSetting.Simple: return BlocType.Simple; case TemplateSetting.Auto: default: return getDefaultDependency(); } } async function getDefaultDependency(): Promise { if (await hasDependency(freezed_annotation)) { return BlocType.Freezed; } else if (await hasDependency(equatable)) { return BlocType.Equatable; } else { return BlocType.Simple; } } ================================================ FILE: extensions/vscode/src/utils/get-dart-version.ts ================================================ import { exec } from "./exec"; import * as semver from "semver"; export const getDartVersion = async (): Promise => { try { const result = await exec("dart --version"); // Dart SDK version: 3.7.2 (stable) (Tue Mar 11 04:27:50 2025 -0700) on "macos_arm64" const output = result.trim(); // Parse "major.minor.patch" const regexp = new RegExp(/\d+\.\d+\.\d+/); const versionString = output.match(regexp)?.[0] ?? null; return semver.parse(versionString); } catch (_) { return null; } }; ================================================ FILE: extensions/vscode/src/utils/get-latest-package-version.ts ================================================ import * as _ from "lodash"; import fetch from "node-fetch"; export async function getLatestPackageVersion(name: string): Promise { try { const url = `https://pub.dev/api/packages/${name}`; const response = await fetch(url); const body = await response.json(); return _.get(body, "latest.version", ""); } catch (_) { return ""; } } ================================================ FILE: extensions/vscode/src/utils/get-pubspec-path.ts ================================================ import { workspace } from "vscode"; import * as path from "path"; const PUBSPEC_FILE_NAME = "pubspec.yaml"; const PUBSPEC_LOCK_FILE_NAME = "pubspec.lock"; export function getPubspecPath(): string | undefined { return getWorkspacePath(PUBSPEC_FILE_NAME); } export function getPubspecLockPath(): string | undefined { return getWorkspacePath(PUBSPEC_LOCK_FILE_NAME); } function getWorkspacePath(fileName: string): string | undefined { if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) { return path.join( `${workspace.workspaceFolders[0].uri.path}`, fileName ); } } ================================================ FILE: extensions/vscode/src/utils/get-pubspec.ts ================================================ import * as yaml from "js-yaml"; import { getPubspecPath, getPubspecLockPath } from "./get-pubspec-path"; import { workspace, Uri } from "vscode"; export async function getPubspec(): Promise | undefined> { const pubspecPath = getPubspecPath(); return getYAMLFileContent(pubspecPath); } export async function getPubspecLock(): Promise | undefined> { const pubspecLockPath = getPubspecLockPath(); return getYAMLFileContent(pubspecLockPath); } async function getYAMLFileContent(path: string | undefined): Promise | undefined> { if (path) { try { let content = await workspace.fs.readFile(Uri.file(path)); return yaml.load(content.toString()); } catch (_) {} } } ================================================ FILE: extensions/vscode/src/utils/get-selected-text.ts ================================================ import { TextEditor, Selection, Position } from "vscode"; const openBracket = "("; const closeBracket = ")"; export const getSelectedText = (editor: TextEditor): Selection => { const emptySelection = new Selection( editor.document.positionAt(0), editor.document.positionAt(0) ); const language = editor.document.languageId; if (language != "dart") return emptySelection; const line = editor.document.lineAt(editor.selection.start); const lineText = line.text; const openBracketIndex = line.text.indexOf( openBracket, editor.selection.anchor.character ); let widgetStartIndex = openBracketIndex > 1 ? openBracketIndex - 1 : editor.selection.anchor.character; for (widgetStartIndex; widgetStartIndex > 0; widgetStartIndex--) { const currentChar = lineText.charAt(widgetStartIndex); const isBeginningOfWidget = currentChar === openBracket || (currentChar === " " && lineText.charAt(widgetStartIndex - 1) !== "," && lineText.substring(widgetStartIndex - 5, widgetStartIndex) != "const"); if (isBeginningOfWidget) break; } widgetStartIndex++; if (openBracketIndex < 0) { const commaIndex = lineText.indexOf(",", widgetStartIndex); const bracketIndex = lineText.indexOf(closeBracket, widgetStartIndex); const endIndex = commaIndex >= 0 ? commaIndex : bracketIndex >= 0 ? bracketIndex : lineText.length; return new Selection( new Position(line.lineNumber, widgetStartIndex), new Position(line.lineNumber, endIndex) ); } let bracketCount = 1; for (let l = line.lineNumber; l < editor.document.lineCount; l++) { const currentLine = editor.document.lineAt(l); let c = l === line.lineNumber ? openBracketIndex + 1 : 0; for (c; c < currentLine.text.length; c++) { const currentChar = currentLine.text.charAt(c); if (currentChar === openBracket) bracketCount++; if (currentChar === closeBracket) bracketCount--; if (bracketCount === 0) { return new Selection( new Position(line.lineNumber, widgetStartIndex), new Position(l, c + 1) ); } } } return emptySelection; }; ================================================ FILE: extensions/vscode/src/utils/get-template-setting.ts ================================================ import { workspace } from "vscode"; export const enum TemplateSetting { Auto, Equatable, Freezed, Simple, } export const enum TemplateType { Bloc, Cubit, } export function getTemplateSetting(type: TemplateType): TemplateSetting { let config: string | undefined; switch (type) { case TemplateType.Bloc: config = workspace.getConfiguration("bloc").get("newBlocTemplate.type"); break; case TemplateType.Cubit: config = workspace.getConfiguration("bloc").get("newCubitTemplate.type"); break; default: return TemplateSetting.Auto; } switch (config) { case "freezed": return TemplateSetting.Freezed; case "equatable": return TemplateSetting.Equatable; case "simple": return TemplateSetting.Simple; case "auto": default: return TemplateSetting.Auto; } } ================================================ FILE: extensions/vscode/src/utils/has-dependency.ts ================================================ import * as _ from "lodash"; import { getPubspec } from "."; export async function hasDependency(dependency: string) { const pubspec = await getPubspec(); const dependencies = _.get(pubspec, "dependencies", {}); return _.has(dependencies, dependency); } ================================================ FILE: extensions/vscode/src/utils/index.ts ================================================ export * from "./analyze-dependencies"; export * from "./convert-to"; export * from "./downloader"; export * from "./exec"; export * from "./get-bloc-tools-executable"; export * from "./get-bloc-type"; export * from "./get-dart-version"; export * from "./get-latest-package-version"; export * from "./get-pubspec"; export * from "./get-pubspec-path"; export * from "./get-selected-text"; export * from "./get-template-setting"; export * from "./has-dependency"; export * from "./install-bloc-tools"; export * from "./retry"; export * from "./set-show-context-menu"; export * from "./update-pubspec-dependency"; export * from "./wrap-with"; ================================================ FILE: extensions/vscode/src/utils/install-bloc-tools.ts ================================================ import { arch, type } from "node:os"; import { ExtensionContext, Uri } from "vscode"; import { downloadFile } from "."; export const BLOC_TOOLS_VERSION = "0.1.0-dev.21"; export const installBlocTools = async ( context: ExtensionContext ): Promise => { try { const os = getOS(); if (os === OperatingSystem.unknown) return false; const arch = getArch(); if (arch == Architecture.unknown) return false; await downloadFile( Uri.parse( `https://github.com/felangel/bloc/releases/download/bloc_tools-v${BLOC_TOOLS_VERSION}/bloc_${os}_${arch}` ), `bloc_${BLOC_TOOLS_VERSION}`, context ); return true; } catch (_) { return false; } }; function getOS(): OperatingSystem { const hostOS = type(); switch (hostOS) { case "Linux": return OperatingSystem.linux; case "Darwin": return OperatingSystem.macos; case "Windows_NT": return OperatingSystem.windows; } return OperatingSystem.unknown; } function getArch(): Architecture { const hostArch = arch(); switch (hostArch) { case "arm64": return Architecture.arm64; case "x64": return Architecture.x64; } return Architecture.unknown; } enum OperatingSystem { linux = "linux", macos = "macos", windows = "windows", unknown = "-", } enum Architecture { arm64 = "arm64", x64 = "x64", unknown = "-", } ================================================ FILE: extensions/vscode/src/utils/retry.ts ================================================ export async function retry( callback: () => Promise, label: string, retryCount: number, delay: number, onError?: (error: Error) => void ): Promise { try { return await callback(); } catch (error) { if (error instanceof Error) { if (retryCount === 0) throw new Error( `Maximum retry count exceeded.'${label}' failed with error: ${ error.message }. ${JSON.stringify(error)}` ); if (onError != null) onError(error); } await new Promise((resolve): void => { setTimeout(resolve, delay); }); return retry(callback, label, retryCount - 1, delay * 2, onError); } } ================================================ FILE: extensions/vscode/src/utils/set-show-context-menu.ts ================================================ import * as yaml from "js-yaml"; import * as _ from "lodash"; import { commands, Uri, workspace } from "vscode"; export async function setShowContextMenu( pubspec?: Uri | undefined, ): Promise { async function pubspecIncludesBloc(pubspec: Uri): Promise { try { const content = await workspace.fs.readFile(pubspec); const yamlContent = yaml.load(content.toString()); const dependencies = _.get(yamlContent, "dependencies", {}); return [ "angular_bloc", "bloc", "flutter_bloc", "hydrated_bloc", "replay_bloc", ].some((d) => dependencies.hasOwnProperty(d)); } catch (_) {} return false; } async function workspaceIncludesBloc(): Promise { try { const pubspecs = await workspace.findFiles("**/**/pubspec.yaml"); for (const pubspec of pubspecs) { if (await pubspecIncludesBloc(pubspec)) { return true; } } } catch (_) {} return false; } commands.executeCommand( "setContext", "bloc.showContextMenu", pubspec ? await pubspecIncludesBloc(pubspec) : await workspaceIncludesBloc(), ); } ================================================ FILE: extensions/vscode/src/utils/update-pubspec-dependency.ts ================================================ import * as _ from "lodash"; import * as fs from "fs"; import { workspace } from "vscode"; import { getPubspecPath } from "./get-pubspec-path"; export function updatePubspecDependency(dependency: { name: string; latestVersion: string; currentVersion: string; }) { if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) { const pubspecPath = getPubspecPath(); if (pubspecPath) { try { fs.writeFileSync( pubspecPath, fs .readFileSync(pubspecPath, "utf8") .replace( `${dependency.name}: ${dependency.currentVersion}`, `${dependency.name}: ${dependency.latestVersion}` ) ); } catch (_) {} } } } ================================================ FILE: extensions/vscode/src/utils/wrap-with.ts ================================================ import { commands, SnippetString, window } from "vscode"; import { getSelectedText } from "../utils"; const interpolatedVarRegExp = /[$]/g; const escapedCharacterRegExp = /[\\]/g; export const wrapWith = async (snippet: (widget: string) => string) => { let editor = window.activeTextEditor; if (!editor) return; const selection = getSelectedText(editor); const widget = editor.document .getText(selection) .replace(escapedCharacterRegExp, "\\\\") .replace(interpolatedVarRegExp, "\\$"); editor.insertSnippet(new SnippetString(snippet(widget)), selection); await commands.executeCommand("editor.action.formatDocument"); }; ================================================ FILE: extensions/vscode/tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es6", "outDir": "dist", "lib": ["es6", "DOM"], "sourceMap": true, "rootDir": "src", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true }, "exclude": ["node_modules", ".vscode-test"] } ================================================ FILE: extensions/vscode/tslint.json ================================================ { "rules": { "no-string-throw": true, "no-unused-expression": true, "no-duplicate-variable": true, "curly": true, "class-name": true, "semicolon": [true, "always"], "triple-equals": true }, "defaultSeverity": "warning" } ================================================ FILE: extensions/vscode/webpack.config.js ================================================ 'use strict'; const path = require('path'); /**@type {import('webpack').Configuration}*/ const config = { target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ output: { // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ path: path.resolve(__dirname, 'dist'), filename: 'extension.js', libraryTarget: 'commonjs2', devtoolModuleFilenameTemplate: '../[resource-path]' }, devtool: 'source-map', externals: { vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ }, resolve: { // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader extensions: ['.ts', '.js'] }, module: { rules: [ { test: /\.ts$/, exclude: /node_modules/, use: [ { loader: 'ts-loader' } ] } ] } }; module.exports = config; ================================================ FILE: extensions/zed/.gitignore ================================================ # Generated by Cargo # will have compiled files and executables debug target # These are backup files generated by rustfmt **/*.rs.bk # MSVC Windows builds of rustc generate these, which store debugging information *.pdb # Generated by cargo mutants # Contains mutation testing data **/mutants.out*/ # RustRover # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ ================================================ FILE: extensions/zed/CHANGELOG.md ================================================ # 0.1.0 - feat: initial extension release - includes snippets and language server ================================================ FILE: extensions/zed/Cargo.toml ================================================ [package] name = "bloc-zed" version = "0.1.0" edition = "2024" license = "MIT" [lib] crate-type = ["cdylib"] [dependencies] zed_extension_api = "0.7.0" ================================================ FILE: extensions/zed/LICENSE ================================================ MIT License Copyright (c) 2026 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: extensions/zed/README.md ================================================

    Bloc

    build Star on Github License: MIT Discord

    --- ## Overview [Zed](https://zed.dev) support for the [Bloc Library](https://bloclibrary.dev) providing snippets and the Bloc language server for [Dart](https://dart.dev/) and [Flutter](https://flutter.dev/) apps. ## Installation Install from the Zed Extensions panel by searching for "Bloc", or install as a dev extension for local development: 1. Open Zed 2. Open the command palette and run `zed: install dev extension` 3. Select the `extensions/zed` directory ## Language Server The Bloc language server provides custom diagnostic reporting for bloc-related lint rules. See [the official documentation](https://bloclibrary.dev/lint) for more information about configuring the linter and supported lint rules. The language server binary (`bloc_tools`) is automatically downloaded from [GitHub releases](https://github.com/felangel/bloc/releases). ## Snippets ### Bloc | Shortcut | Description | | ------------------- | --------------------------------------------- | | `importbloc` | Imports `package:bloc` | | `importflutterbloc` | Imports `package:flutter_bloc` | | `importbloctest` | Imports `package:bloc_test` | | `bloc` | Creates a Bloc class | | `cubit` | Creates a Cubit class | | `onevent` | Register a new `EventHandler` | | `_onevent` | Define a new `EventHandler` | | `blocobserver` | Creates a `BlocObserver` class | | `blocprovider` | Creates a `BlocProvider` widget | | `multiblocprovider` | Creates a `MultiBlocProvider` widget | | `repoprovider` | Creates a `RepositoryProvider` widget | | `multirepoprovider` | Creates a `MultiRepositoryProvider` widget | | `blocbuilder` | Creates a `BlocBuilder` widget | | `blocselector` | Creates a `BlocSelector` widget | | `bloclistener` | Creates a `BlocListener` widget | | `multibloclistener` | Creates a `MultiBlocListener` widget | | `blocconsumer` | Creates a `BlocConsumer` widget | | `blocof` | Shortcut for `BlocProvider.of()` | | `repoof` | Shortcut for `RepositoryProvider.of()` | | `read` | Shortcut for `context.read()` | | `watch` | Shortcut for `context.watch()` | | `select` | Shortcut for `context.select()` | | `blocstate` | Creates a state class | | `blocevent` | Creates an event class | | `bloctest` | Creates a `blocTest` | | `mockbloc` | Creates a class extending `MockBloc` | | `_mockbloc` | Creates a private class extending `MockBloc` | | `mockcubit` | Creates a class extending `MockCubit` | | `_mockcubit` | Creates a private class extending `MockCubit` | | `fake` | Creates a class extending `Fake` | | `_fake` | Creates a private class extending `Fake` | | `mock` | Creates a class extending `Mock` | | `_mock` | Creates a private class extending `Mock` | ### Freezed Bloc | Shortcut | Description | | -------- | ----------------------- | | `fstate` | Creates a freezed state | | `fevent` | Creates a freezed event | ================================================ FILE: extensions/zed/extension.toml ================================================ id = "bloc" name = "Bloc" description = "Support for the Bloc state management library for Dart and Flutter. Provides snippets and the Bloc language server." version = "0.1.0" schema_version = 1 authors = ["Felix Angelov ", "Johannes Naylor "] repository = "https://github.com/felangel/bloc" snippets = ["snippets/dart.json"] [language_servers.bloc] name = "Bloc Language Server" languages = ["Dart"] ================================================ FILE: extensions/zed/snippets/dart.json ================================================ { "Bloc": { "prefix": "bloc", "body": [ "class ${1:Subject}Bloc extends Bloc<$1Event, $1State> {", "\t${1:Subject}Bloc() : super(${1:Subject}Initial()) {", "\t\ton<$1Event>((event, emit) {", "\t\t\t$0", "\t\t});", "\t}", "}" ], "description": "Creates a Bloc class" }, "Cubit": { "prefix": "cubit", "body": [ "class ${1:Subject}Cubit extends Cubit<$1State> {", "\t${1:Subject}Cubit() : super(${1:Subject}Initial());", "}" ], "description": "Creates a Cubit class" }, "BlocObserver": { "prefix": "blocobserver", "body": [ "import 'package:bloc/bloc.dart';", "", "class ${1:My}BlocObserver extends BlocObserver {", "\t@override", "\tvoid onEvent(Bloc bloc, Object? event) {", "\t\tsuper.onEvent(bloc, event);", "\t\t$0", "\t}", "", "\t@override", "\tvoid onError(BlocBase bloc, Object error, StackTrace stackTrace) {", "\t\tsuper.onError(bloc, error, stackTrace);", "\t}", "", "\t@override", "\tvoid onChange(BlocBase bloc, Change change) {", "\t\tsuper.onChange(bloc, change);", "\t}", "", "\t@override", "\tvoid onTransition(Bloc bloc, Transition transition) {", "\t\tsuper.onTransition(bloc, transition);", "\t}", "}" ], "description": "Creates a BlocObserver class" }, "Bloc State": { "prefix": "blocstate", "body": [ "class ${1:Subject}${2:Verb}${3:State} extends $1State {", "\tconst $1$2$3($5);", "", "\t$4", "", "\t@override", "\tList get props => [$6];", "}" ], "description": "Creates a Bloc state class (Subject + Verb + State)" }, "Bloc Event": { "prefix": "blocevent", "body": [ "class ${1:Subject}${2:Noun}${3:Verb} extends $1Event {", "\tconst $1$2$3($5);", "", "\t$4", "", "\t@override", "\tList get props => [$6];", "}" ], "description": "Creates a Bloc event class (Subject + Noun + Verb)" }, "Import package:bloc": { "prefix": "importbloc", "body": "import 'package:bloc/bloc.dart';", "description": "Import package:bloc/bloc.dart" }, "Register Event Handler": { "prefix": "onevent", "body": [ "on<${1:Subject}Event>((event, emit) {", "\t$0", "});" ], "description": "Register a new EventHandler" }, "Define Event Handler": { "prefix": "_onevent", "body": [ "${1:void} _on${2:Event}(", "\t$2 event,", "\tEmitter<${3:Subject}State> emit,", ") {", "\t$0", "}" ], "description": "Define a new EventHandler" }, "BlocTest": { "prefix": "bloctest", "body": [ "blocTest<${1:Subject}${2:Bloc}, $1State>(", "\t'emits [${3:MyState}] when ${4:MyEvent} is added.',", "\tbuild: () => $1$2(),", "\tact: (bloc) => bloc.add($4()),", "\texpect: () => const <$1State>[$3()],", ");" ], "description": "Creates a blocTest" }, "Import package:bloc_test": { "prefix": "importbloctest", "body": "import 'package:bloc_test/bloc_test.dart';", "description": "Import package:bloc_test/bloc_test.dart" }, "MockBloc": { "prefix": "mockbloc", "body": "class Mock${1:Subject}Bloc extends MockBloc<${1}Event, ${1}State> implements ${1}Bloc {}", "description": "Creates a MockBloc class" }, "_MockBloc": { "prefix": "_mockbloc", "body": "class _Mock${1:Subject}Bloc extends MockBloc<${1}Event, ${1}State> implements ${1}Bloc {}", "description": "Creates a private MockBloc class" }, "MockCubit": { "prefix": "mockcubit", "body": "class Mock${1:Subject}Cubit extends MockCubit<${1}State> implements ${1}Cubit {}", "description": "Creates a MockCubit class" }, "_MockCubit": { "prefix": "_mockcubit", "body": "class _Mock${1:Subject}Cubit extends MockCubit<${1}State> implements ${1}Cubit {}", "description": "Creates a private MockCubit class" }, "Fake": { "prefix": "fake", "body": "class Fake${1:Subject} extends Fake implements ${1} {}", "description": "Creates a Fake class" }, "_Fake": { "prefix": "_fake", "body": "class _Fake${1:Subject} extends Fake implements ${1} {}", "description": "Creates a private Fake class" }, "Mock": { "prefix": "mock", "body": "class Mock${1:Subject} extends Mock implements ${1} {}", "description": "Creates a Mock class" }, "_Mock": { "prefix": "_mock", "body": "class _Mock${1:Subject} extends Mock implements ${1} {}", "description": "Creates a private Mock class" }, "BlocProvider": { "prefix": "blocprovider", "body": [ "BlocProvider(", "\tcreate: (context) => ${1:Subject}${2:Bloc}(),", "\tchild: ${3:Container()},", ")" ], "description": "Creates a BlocProvider widget" }, "MultiBlocProvider": { "prefix": "multiblocprovider", "body": [ "MultiBlocProvider(", "\tproviders: [", "\t\tBlocProvider(", "\t\t\tcreate: (context) => ${1:Subject}${2:Bloc}(),", "\t\t),", "\t\tBlocProvider(", "\t\t\tcreate: (context) => ${3:Subject}${4:Bloc}(),", "\t\t),", "\t],", "\tchild: ${5:Container()},", ")" ], "description": "Creates a MultiBlocProvider widget" }, "RepositoryProvider": { "prefix": "repoprovider", "body": [ "RepositoryProvider(", "\tcreate: (context) => ${1:Subject}Repository(),", "\tchild: ${2:Container()},", ")" ], "description": "Creates a RepositoryProvider widget" }, "MultiRepositoryProvider": { "prefix": "multirepoprovider", "body": [ "MultiRepositoryProvider(", "\tproviders: [", "\t\tRepositoryProvider(", "\t\t\tcreate: (context) => ${1:Subject}Repository(),", "\t\t),", "\t\tRepositoryProvider(", "\t\t\tcreate: (context) => ${2:Subject}Repository(),", "\t\t),", "\t],", "\tchild: ${3:Container()},", ")" ], "description": "Creates a MultiRepositoryProvider widget" }, "BlocBuilder": { "prefix": "blocbuilder", "body": [ "BlocBuilder<${1:Subject}${2:Bloc}, $1State>(", "\tbuilder: (context, state) {", "\t\treturn ${3:Container()};", "\t},", ")" ], "description": "Creates a BlocBuilder widget" }, "BlocSelector": { "prefix": "blocselector", "body": [ "BlocSelector<${1:Subject}${2:Bloc}, $1State, ${3:Selected}>(", "\tselector: (state) {", "\t\treturn ${4:state};", "\t},", "\tbuilder: (context, state) {", "\t\treturn ${5:Container()};", "\t},", ")" ], "description": "Creates a BlocSelector widget" }, "BlocListener": { "prefix": "bloclistener", "body": [ "BlocListener<${1:Subject}${2:Bloc}, $1State>(", "\tlistener: (context, state) {", "\t\t$0", "\t},", "\tchild: ${3:Container()},", ")" ], "description": "Creates a BlocListener widget" }, "MultiBlocListener": { "prefix": "multibloclistener", "body": [ "MultiBlocListener(", "\tlisteners: [", "\t\tBlocListener<${1:Subject}${2:Bloc}, $1State>(", "\t\t\tlistener: (context, state) {", "\t\t\t\t$0", "\t\t\t},", "\t\t),", "\t],", "\tchild: ${3:Container()},", ")" ], "description": "Creates a MultiBlocListener widget" }, "BlocConsumer": { "prefix": "blocconsumer", "body": [ "BlocConsumer<${1:Subject}${2:Bloc}, $1State>(", "\tlistener: (context, state) {", "\t\t$0", "\t},", "\tbuilder: (context, state) {", "\t\treturn ${3:Container()};", "\t},", ")" ], "description": "Creates a BlocConsumer widget" }, "BlocProvider.of()": { "prefix": "blocof", "body": "BlocProvider.of<${1:Subject}${2:Bloc}>(context)", "description": "Shortcut for BlocProvider.of()" }, "RepositoryProvider.of()": { "prefix": "repoof", "body": "RepositoryProvider.of<${1:Subject}Repository>(context)", "description": "Shortcut for RepositoryProvider.of()" }, "context.read()": { "prefix": "read", "body": "context.read<${1:Subject}${2:Bloc}>()", "description": "Shortcut for context.read()" }, "context.select()": { "prefix": "select", "body": "context.select((${1:Subject}${2:Bloc} ${3:bloc}) => $3$0)", "description": "Shortcut for context.select()" }, "context.watch()": { "prefix": "watch", "body": "context.watch<${1:Subject}${2:Bloc}>()", "description": "Shortcut for context.watch()" }, "Import package:flutter_bloc": { "prefix": "importflutterbloc", "body": "import 'package:flutter_bloc/flutter_bloc.dart';", "description": "Import package:flutter_bloc/flutter_bloc.dart" }, "New freezed state": { "prefix": "fstate", "body": "const factory ${1:ClassName}.${2:stateName}($3) = _${4:StateName};\n$0", "description": "Creates a freezed state" }, "New freezed event": { "prefix": "fevent", "body": "const factory ${1:ClassName}.${2:eventName}($3) = _${4:EventName};\n$0", "description": "Creates a freezed event" } } ================================================ FILE: extensions/zed/src/lib.rs ================================================ use std::fs; use zed_extension_api::{self as zed, LanguageServerId, Result, settings::LspSettings}; const BLOC_TOOLS_REPO: &str = "felangel/bloc"; const BLOC_TOOLS_RELEASE_TAG: &str = "bloc_tools-v0.1.0-dev.22"; struct BlocExtension { cached_binary_path: Option, } impl BlocExtension { fn language_server_binary_path( &mut self, language_server_id: &LanguageServerId, worktree: &zed::Worktree, ) -> Result { let binary_settings = LspSettings::for_worktree("bloc", worktree) .ok() .and_then(|s| s.binary); if let Some(path) = binary_settings.as_ref().and_then(|s| s.path.clone()) { return Ok(path); } if let Some(path) = &self.cached_binary_path { if fs::metadata(path).map_or(false, |m| m.is_file()) { return Ok(path.clone()); } } zed::set_language_server_installation_status( language_server_id, &zed::LanguageServerInstallationStatus::CheckingForUpdate, ); let release = match zed::github_release_by_tag_name(BLOC_TOOLS_REPO, BLOC_TOOLS_RELEASE_TAG) { Ok(release) => release, Err(e) => { let url = format!( "https://api.github.com/repos/{BLOC_TOOLS_REPO}/releases/tags/{BLOC_TOOLS_RELEASE_TAG}" ); return Err(format!("Failed to fetch release from {url}: {e}")); } }; let (os, arch) = zed::current_platform(); let asset_name = format!( "bloc_{os}_{arch}", os = match os { zed::Os::Mac => "macos", zed::Os::Linux => "linux", zed::Os::Windows => "windows", }, arch = match arch { zed::Architecture::Aarch64 => "arm64", zed::Architecture::X8664 => "x64", zed::Architecture::X86 => "x64", }, ); let asset = release .assets .iter() .find(|a| a.name == asset_name) .ok_or_else(|| { format!( "No compatible binary found for this platform (looked for '{}')", asset_name ) })?; let version_dir = format!("bloc_tools-{}", release.version); let binary_path = format!("{version_dir}/{asset_name}"); if !fs::metadata(&binary_path).map_or(false, |m| m.is_file()) { zed::set_language_server_installation_status( language_server_id, &zed::LanguageServerInstallationStatus::Downloading, ); fs::create_dir_all(&version_dir) .map_err(|e| format!("Failed to create directory '{version_dir}': {e}"))?; zed::download_file( &asset.download_url, &binary_path, zed::DownloadedFileType::Uncompressed, ) .map_err(|e| format!("Failed to download bloc_tools: {e}"))?; zed::make_file_executable(&binary_path)?; // Clean up old versions if let Ok(entries) = fs::read_dir(".") { for entry in entries.flatten() { if let Some(name) = entry.file_name().to_str() { if name.starts_with("bloc_tools-") && name != version_dir { fs::remove_dir_all(entry.path()).ok(); } } } } } self.cached_binary_path = Some(binary_path.clone()); Ok(binary_path) } } impl zed::Extension for BlocExtension { fn new() -> Self { BlocExtension { cached_binary_path: None, } } fn language_server_command( &mut self, language_server_id: &LanguageServerId, worktree: &zed::Worktree, ) -> Result { let binary_path = self.language_server_binary_path(language_server_id, worktree)?; let args = LspSettings::for_worktree("bloc", worktree) .ok() .and_then(|s| s.binary) .and_then(|b| b.arguments) .unwrap_or_else(|| vec!["language-server".to_string()]); Ok(zed::Command { command: binary_path, args, env: Default::default(), }) } } zed::register_extension!(BlocExtension); ================================================ FILE: packages/angular_bloc/CHANGELOG.md ================================================ # 10.0.0-dev.5 - chore(deps): upgrade to `package:bloc v9.0.0` - chore: update sponsors # 10.0.0-dev.4 - chore: update sponsors table in `README` - chore: add `funding` to `pubspec.yaml` # 10.0.0-dev.3 - chore: update copyright year - chore: update sponsors # 10.0.0-dev.2 - chore(deps): upgrade to `package:ngdart v8.0.0-dev.4` - chore: add `topics` to `pubspec.yaml` - chore(deps): upgrade to `package:mocktail v1.0.0` - chore: update sponsors # 10.0.0-dev.1 - docs: upgrade to Dart 3 - refactor: standardize analysis_options - refactor: update sdk constraints and fix analysis warnings # 10.0.0-dev.0 - **BREAKING**: refactor: upgrade `ngdart` to v8 pre-release # 9.0.0 - **BREAKING**: feat: upgrade to `ngdart v7.0.0` - refactor: remove deprecated `invariant_booleans` lint rule - docs: update example to follow naming conventions - chore: add screenshots to `pubspec.yaml` # 8.0.0 - **BREAKING**: feat: upgrade to `bloc v8.0.0` # 8.0.0-dev.3 - **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.5` # 8.0.0-dev.2 - **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.3` # 8.0.0-dev.1 - **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.2` # 7.1.0 - feat: upgrade to `bloc ^7.2.0` # 7.0.0 - **BREAKING**: refactor: upgrade to `angular ^7.0.0` - **BREAKING**: refactor: upgrade to `bloc ^7.0.0` - **BREAKING**: refactor upgrade to null safety (`dart >= 2.12.0`) # 6.0.1 - Fix CHANGELOG formatting # 6.0.0 - Update to `angular ^6.0.1` - Update to `bloc ^6.1.0` - Export `package:bloc` # 6.0.0-dev.8 - Update to `bloc ^6.0.0` # 6.0.0-dev.7 - Update to `bloc ^6.0.0-dev.1` - Remove external dependency on package:angular_cubit - Inline documentation updates - README updates - Example application updates # 6.0.0-dev.6 - Update to `bloc ^5.0.1` - Update to `angular_cubit ^0.1.0-dev.4` # 6.0.0-dev.5 - Update to `bloc ^5.0.0` - Update to `angular_cubit ^0.1.0-dev.1` # 6.0.0-dev.4 - Update to `bloc ^5.0.0-dev.11` - Updates to documentation # 6.0.0-dev.3 - Update to `bloc ^5.0.0-dev.10` # 6.0.0-dev.2 - Update to `bloc ^5.0.0-dev.7` # 6.0.0-dev.1 - Update to `bloc ^5.0.0-dev.6` - Update to extend [angular_cubit](https://pub.dev/packages/angular_cubit) - Update documentation and static assets. # 5.0.0-dev.1 - Update to `angular_dart: ^6.0.0-alpha+1` # 4.0.0 - Update to `bloc: ^4.0.0` # 4.0.0-dev.4 - Update to `bloc: ^4.0.0-dev.4` # 4.0.0-dev.3 - Update to `bloc: ^4.0.0-dev.3` # 4.0.0-dev.2 - Update to `bloc: ^4.0.0-dev.2` # 4.0.0-dev.1 - Update to `bloc: ^4.0.0-dev.1` # 3.0.0 - Update to `bloc: ^3.0.0` and Minor Updates to Documentation # 3.0.0-dev.1 - Update to `bloc: ^3.0.0-dev.1` # 2.0.0 - Update to `bloc: ^2.0.0` and Documentation Updates - Adhere to [effective dart](https://dart.dev/guides/language/effective-dart) ([#561](https://github.com/felangel/bloc/issues/561)) # 1.0.0 - Update to `bloc: ^1.0.0` and Minor Updates to Documentation # 0.11.0 - Update to `bloc: ^0.16.0` and Minor Updates to Documentation # 0.10.0 - Update to `bloc: ^0.15.0` and Minor Updates to Documentation # 0.9.0 - Update to `bloc: ^0.14.0` and Minor Updates to Documentation # 0.8.0 - Update to `bloc: ^0.13.0` and Minor Updates to Documentation # 0.7.0 - Update to `bloc: ^0.12.0` and Minor Updates to Documentation # 0.6.0 - Update to `bloc: ^0.11.0` and Minor Updates to Documentation # 0.5.0 - Update to `bloc: ^0.10.0` and Minor Updates to Documentation # 0.4.4 - Additional Minor Updates to Documentation # 0.4.3 - Update to `bloc:^0.9.3` and Minor Updates to Documentation # 0.4.2 - Additional Minor Updates to Documentation # 0.4.1 - Minor Updates to Documentation # 0.4.0 - Update to `bloc: ^0.9.0` # 0.3.3 - Additional Minor Updates to Documentation # 0.3.2 - Additional Minor Updates to Documentation # 0.3.1 - Minor Updates to Documentation # 0.3.0 - Update to `bloc: ^0.8.0` # 0.2.5 - Additional Minor Updates to Documentation # 0.2.4 - Additional Minor Updates to Documentation # 0.2.3 - Updates to Documentation and Examples # 0.2.2 - Additional Minor Updates to Documentation # 0.2.1 - Minor Updates to Documentation # 0.2.0 - Update to `bloc: ^0.7.0` # 0.1.2 - Minor Updates to Documentation # 0.1.1 - Minor Updates to Documentation # 0.1.0 - Initial Version of the library. - Includes the ability to connect presentation layer to `Bloc` by using the `BlocPipe` Component. ================================================ FILE: packages/angular_bloc/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2026 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/angular_bloc/README.md ================================================

    Angular Bloc Package

    Pub build codecov Star on Github style: bloc lint Flutter Website Awesome Flutter Flutter Samples License: MIT Discord Bloc Library

    --- A Dart package that helps implement the BLoC pattern in [AngularDart](https://pub.dev/packages/ngdart). Built to work with [package:bloc](https://pub.dev/packages/bloc). **Learn more at [bloclibrary.dev](https://bloclibrary.dev)!** --- ## Sponsors Our top sponsors are shown below! [[Become a Sponsor](https://github.com/sponsors/felangel)]
    --- ## Angular Components **BlocPipe** is an Angular pipe which helps bind `Bloc` state changes to the presentation layer. `BlocPipe` handles rendering the html element in response to new states. `BlocPipe` is very similar to `AsyncPipe` but is designed specifically for blocs. ## Cubit Usage Lets take a look at how to use `BlocPipe` to hook up a `CounterPage` html template to a `CounterCubit`. ### `counter_cubit.dart` ```dart import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); void increment() => emit(state + 1); void decrement() => emit(state — 1); } ``` ### `counter_page_component.dart` ```dart import 'package:angular/angular.dart'; import 'package:angular_bloc/angular_bloc.dart'; import './counter_cubit.dart'; @Component( selector: 'counter-page', templateUrl: 'counter_page_component.html', pipes: [BlocPipe], ) class CounterPageComponent implements OnInit, OnDestroy { late final CounterCubit counterCubit; @override void ngOnInit() { counterCubit = CounterCubit(); } @override void ngOnDestroy() { counterCubit.close(); } } ``` ### `counter_page_component.html` ```html

    Counter App

    Current Count: {{ $pipe.bloc(counterCubit) }}

    ``` ## Bloc Usage Lets take a look at how to use `BlocPipe` to hook up a `CounterPage` html template to a `CounterBloc`. ### `counter_bloc.dart` ```dart import 'package:bloc/bloc.dart'; sealed class CounterEvent {} final class CounterIncrementPressed extends CounterEvent {} final class CounterDecrementPressed extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); on((event, emit) => emit(state - 1)); } } ``` ### `counter_page_component.dart` ```dart import 'package:angular/angular.dart'; import 'package:angular_bloc/angular_bloc.dart'; import './counter_bloc.dart'; @Component( selector: 'counter-page', templateUrl: 'counter_page_component.html', pipes: [BlocPipe], ) class CounterPageComponent implements OnInit, OnDestroy { late final CounterBloc counterBloc; @override void ngOnInit() { counterBloc = CounterBloc(); } @override void ngOnDestroy() { counterBloc.close(); } void increment() => counterBloc.add(CounterIncrementPressed()); void decrement() => counterBloc.add(CounterDecrementPressed()); } ``` ### `counter_page_component.html` ```html

    Counter App

    Current Count: {{ $pipe.bloc(counterBloc) }}

    ``` At this point we have successfully separated our presentational layer from our business logic layer! ## Dart Versions - Dart 2: >= 2.12.0 ## Examples - [Counter](https://github.com/felangel/bloc/tree/master/examples/angular_counter) - a complete example of how to create a `CounterBloc` and hook it up to an AngularDart app. - [Github Search](https://github.com/felangel/bloc/tree/master/examples/github_search/angular_github_search) - an example of how to create a Github Search Application using the `bloc` and `angular_bloc` packages. ## Maintainers - [Felix Angelov](https://github.com/felangel) ================================================ FILE: packages/angular_bloc/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml ================================================ FILE: packages/angular_bloc/dart_test.yaml ================================================ # As of test: ^0.12.32, we can use a common/base dart_test.yaml, which this is. # # See documentation: # * https://github.com/dart-lang/test/blob/master/doc/configuration.md # * https://github.com/dart-lang/test/blob/master/doc/configuration.md#include presets: # When run with -P travis, we have different settings/options. # # 1: We don't use Chrome --headless: # 2: We use --reporter expanded # 3: We skip anything tagged "fails-on-travis". travis: override_platforms: chrome: settings: # Disable some security options, since this is only on Travis. # https://chromium.googlesource.com/chromium/src/+/master/docs/design/sandbox.md # https://docs.travis-ci.com/user/chrome#Sandboxing arguments: --no-sandbox # TODO(https://github.com/dart-lang/test/issues/772) headless: false # Don't run any tests that are tagged ["fails-on-travis"]. exclude_tags: "fails-on-travis" # https://github.com/dart-lang/test/blob/master/doc/configuration.md#reporter reporter: expanded # This repository has only web tests. platforms: - chrome ================================================ FILE: packages/angular_bloc/example/example.dart ================================================ // This example is one-pager for the [angular_bloc](https://github.com/felangel/bloc/tree/master/examples/angular_counter) example. import 'package:angular_bloc/angular_bloc.dart'; import 'package:ngdart/angular.dart'; @Component( selector: 'my-app', template: '', directives: [CounterPageComponent], ) class AppComponent {} abstract class CounterEvent {} class CounterIncrementPressed extends CounterEvent {} class CounterDecrementPressed extends CounterEvent {} // ignore: prefer_file_naming_conventions class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); on((event, emit) => emit(state - 1)); } @override void onTransition(Transition transition) { super.onTransition(transition); // ignore: avoid_print print(transition); } } const String template = r'''

    Counter App

    Current Count: {{ $pipe.bloc(counterBloc) }}

    '''; @Component( selector: 'counter-page', pipes: [BlocPipe], template: template, ) class CounterPageComponent implements OnInit, OnDestroy { late final CounterBloc counterBloc; @override void ngOnInit() { counterBloc = CounterBloc(); } @override void ngOnDestroy() { counterBloc.close(); } void increment() => counterBloc.add(CounterIncrementPressed()); void decrement() => counterBloc.add(CounterDecrementPressed()); } ================================================ FILE: packages/angular_bloc/lib/angular_bloc.dart ================================================ /// Angular components that make it easy to implement the BLoC design pattern. /// Built to be used with the [bloc state management package](https://pub.dev/packages/bloc). /// /// Get started at [bloclibrary.dev](https://bloclibrary.dev) 🚀 library angular_dart; export 'package:bloc/bloc.dart'; export './src/pipes/pipes.dart'; ================================================ FILE: packages/angular_bloc/lib/src/pipes/bloc_pipe.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:ngdart/angular.dart' show ChangeDetectorRef, OnDestroy, Pipe; /// {@template bloc_pipe} /// A `pipe` which helps bind [BlocBase] ([Bloc] and [Cubit]) /// state changes to the presentation layer. /// /// [BlocPipe] handles rendering the html element in response to new states. /// [BlocPipe] is very similar to `AsyncPipe` but is designed /// specifically for blocs. /// /// ```html ///

    Current Count: {{ $pipe.bloc(counterBloc) }}

    /// ``` /// /// See also: /// /// * [Bloc] for more information about how to make and use blocs. /// * [Cubit] for more information about how to make and use cubits. /// /// {@endtemplate} @Pipe('bloc', pure: false) class BlocPipe implements OnDestroy { /// {@macro bloc_pipe} BlocPipe(this._ref); final ChangeDetectorRef _ref; BlocBase? _bloc; Object? _latestValue; StreamSubscription? _subscription; @override void ngOnDestroy() { if (_subscription != null) { _dispose(); } } /// Angular invokes the [transform] method with the value of a binding as the /// first argument, and any parameters as the second argument in list form. dynamic transform(BlocBase? bloc) { if (_bloc == null) { if (bloc != null) { _subscribe(bloc); } } else if (!_maybeStreamIdentical(bloc, _bloc)) { _dispose(); return transform(bloc); } if (bloc == null) { return null; } return _latestValue ?? bloc.state; } void _subscribe(BlocBase bloc) { _bloc = bloc; _subscription = bloc.stream.listen( (dynamic value) => _updateLatestValue(bloc, value), ); } void _updateLatestValue(dynamic async, Object? value) { if (identical(async, _bloc)) { _latestValue = value; _ref.markForCheck(); } } void _dispose() { _subscription?.cancel(); _latestValue = null; _subscription = null; _bloc = null; } // StreamController.stream getter always returns new Stream instance, // operator== check is also needed. See // https://github.com/dart-lang/angular2/issues/260 static bool _maybeStreamIdentical(dynamic a, dynamic b) { if (!identical(a, b)) { return a is Stream && b is Stream && a == b; } return true; } } ================================================ FILE: packages/angular_bloc/lib/src/pipes/pipes.dart ================================================ export './bloc_pipe.dart' show BlocPipe; ================================================ FILE: packages/angular_bloc/pubspec.yaml ================================================ name: angular_bloc description: Angular Components that make it easy to implement the BLoC (Business Logic Component) design pattern. Built to be used with the bloc state management package. version: 10.0.0-dev.5 repository: https://github.com/felangel/bloc/tree/master/packages/angular_bloc issue_tracker: https://github.com/felangel/bloc/issues homepage: https://bloclibrary.dev documentation: https://bloclibrary.dev/getting-started topics: [bloc, state-management] funding: [https://github.com/sponsors/felangel] environment: sdk: ">=2.19.0 <4.0.0" dependencies: bloc: ^9.0.0 ngdart: ^8.0.0-dev.4 dev_dependencies: bloc_lint: ^0.3.0 build_runner: ^2.0.0 build_test: ^2.0.0 build_web_compilers: ^4.0.0 meta: ^1.8.0 mocktail: ^1.0.0 ngtest: ^5.0.0-dev.3 test: ^1.16.0 screenshots: - description: The angular bloc package logo. path: screenshots/logo.png ================================================ FILE: packages/angular_bloc/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../bloc ================================================ FILE: packages/angular_bloc/test/bloc_pipe_test.dart ================================================ @TestOn('browser') library angular_bloc_test; import 'dart:async'; import 'package:angular_bloc/angular_bloc.dart'; import 'package:mocktail/mocktail.dart'; import 'package:ngdart/angular.dart' show ChangeDetectorRef; import 'package:test/test.dart'; class MockChangeDetectorRef extends Mock implements ChangeDetectorRef {} abstract class CounterEvent {} class Increment extends CounterEvent {} class Decrement extends CounterEvent {} // ignore: prefer_file_naming_conventions class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); on((event, emit) => emit(state - 1)); } } void main() { group('Stream', () { late Bloc bloc; late BlocPipe pipe; late ChangeDetectorRef ref; setUp(() { bloc = CounterBloc(); ref = MockChangeDetectorRef(); pipe = BlocPipe(ref); }); group('transform', () { test('should return initialState when subscribing to an bloc', () { expect(pipe.transform(bloc), 0); }); test('should return the latest available value', () async { pipe.transform(bloc); bloc.add(Increment()); Timer.run( expectAsync0(() { final dynamic res = pipe.transform(bloc); expect(res, 1); }), ); }); test( 'should return same value when nothing has changed ' 'since the last call', () async { pipe.transform(bloc); bloc.add(Increment()); Timer.run( expectAsync0(() { pipe.transform(bloc); expect(pipe.transform(bloc), 1); }), ); }); test( 'should dispose of the existing subscription when ' 'subscribing to a new bloc', () async { pipe.transform(bloc); final newBloc = CounterBloc(); expect(pipe.transform(newBloc), 0); // this should not affect the pipe bloc.add(Increment()); Timer.run( expectAsync0(() { expect(pipe.transform(newBloc), 0); }), ); }); test('should not dispose of existing subscription when Streams are equal', () async { // See https://github.com/dart-lang/angular2/issues/260 final bloc = CounterBloc(); expect(pipe.transform(bloc), 0); bloc.add(Increment()); Timer.run( expectAsync0(() { expect(pipe.transform(bloc), 1); }), ); }); test('should request a change detection check upon receiving a new value', () async { pipe.transform(bloc); bloc.add(Increment()); Timer( const Duration(milliseconds: 10), expectAsync0(() { verify(() => ref.markForCheck()).called(1); }), ); }); }); group('ngOnDestroy', () { test('should do nothing when no subscription and not throw exception', () { pipe.ngOnDestroy(); }); test('should dispose of the existing subscription', () async { pipe ..transform(bloc) ..ngOnDestroy(); bloc.add(Increment()); Timer.run( expectAsync0(() { expect(pipe.transform(bloc), 1); }), ); }); }); }); } ================================================ FILE: packages/bloc/CHANGELOG.md ================================================ # 9.2.0 - feat: add `MultiBlocObserver` ([#4714](https://github.com/felangel/bloc/pull/4714)) - docs: minor `README.md` improvements ([#4657](https://github.com/felangel/bloc/pull/4657)) # 9.1.0 - docs: add `onDone` to `README` and `example` ([#4641](https://github.com/felangel/bloc/pull/4641)) - feat: add `onDone` callback ([#4633](https://github.com/felangel/bloc/pull/4633)) - refactor: add `pkg:bloc_lint` ([#4620](https://github.com/felangel/bloc/pull/4620)) # 9.0.1 - refactor: analysis options updates ([#4616](https://github.com/felangel/bloc/pull/4616)) - docs: update build status badge ([#4502](https://github.com/felangel/bloc/pull/4502)) - docs: update sponsors ([#4418](https://github.com/felangel/bloc/pull/4418)) - docs: update minimum Dart SDK version in `README.md` # 9.0.0 - **BREAKING** refactor!: introduce `EmittableStateStreamableSource` ([#4311](https://github.com/felangel/bloc/pull/4311)) - `BlocBase` implements `EmittableStateStreamableSource` - **BREAKING** refactor!: remove deprecated `BlocOverrides` ([#4137](https://github.com/felangel/bloc/pull/4137)) - refactor: use `Object.hashAll` internally ([#4310](https://github.com/felangel/bloc/pull/4310)) - bumps minimum Dart SDK to 2.14 - chore: update sponsors # 8.1.4 - docs: improve diagrams - chore: update copyright year - chore: update sponsors # 8.1.3 - chore: update sponsors ([#4054](https://github.com/felangel/bloc/pull/4054)) - chore: fix `require_trailing_commas` ([#3977](https://github.com/felangel/bloc/pull/3977)) - chore(deps): upgrade to `package:mocktail v1.0.0` ([#3919](https://github.com/felangel/bloc/pull/3919)) - chore: add `topics` to `pubspec.yaml` ([#3914](https://github.com/felangel/bloc/pull/3914)) # 8.1.2 - docs: upgrade README snippets to Dart 3 ([#3826](https://github.com/felangel/bloc/pull/3826)) - refactor: standardize analysis options and resolve warnings ([#3826](https://github.com/felangel/bloc/pull/3826)) - docs: remove superfluous word from inline docs ([#3734](https://github.com/felangel/bloc/pull/3734)) # 8.1.1 - chore: add screenshots to `pubspec.yaml` ([#3708](https://github.com/felangel/bloc/pull/3708)) - refactor: `const` constructor support for `BlocObserver` ([#3704](https://github.com/felangel/bloc/pull/3704)) - refactor: upgrade to Dart 2.19 ([#3699](https://github.com/felangel/bloc/pull/3699)) - remove deprecated `invariant_booleans` lint rule # 8.1.0 - feat: reintroduce `Bloc.observer` and `Bloc.transformer` ([#3469](https://github.com/felangel/bloc/pull/3469)) - deprecate: `BlocOverrides` - fix: remove unnecessary `async` from `Emitter.onEach` ([#3392](https://github.com/felangel/bloc/pull/3392)) - chore: upgrade to `mocktail ^0.3.0` ([#3477](https://github.com/felangel/bloc/pull/3477)) # 8.0.3 - refactor: resolve analysis warnings ([#3189](https://github.com/felangel/bloc/pull/3189)) - docs: fix inline doc comment ([#3167](https://github.com/felangel/bloc/pull/3167)) - docs: update GetStream utm tags ([#3136](https://github.com/felangel/bloc/pull/3136)) - docs: update VGV sponsors logo ([#3125](https://github.com/felangel/bloc/pull/3125)) # 8.0.2 - fix: make `onChange` and `addError` protected ([#3071](https://github.com/felangel/bloc/pull/3071)) - refactor: use `late` keyword for internal state controller ([#3100](https://github.com/felangel/bloc/pull/3100)) - refactor: add `isClosed` to `Closable` ([#3066](https://github.com/felangel/bloc/pull/3066)) - refactor: add core interfaces ([#3012](https://github.com/felangel/bloc/pull/3012)) - refactor: internal reorganization ([#3011](https://github.com/felangel/bloc/pull/3011)) - docs: update example to follow naming conventions ([#3029](https://github.com/felangel/bloc/pull/3029)) # 8.0.1 - fix: allow `emit` usage within tests ([#2982](https://github.com/felangel/bloc/pull/2982)) # 8.0.0 - **BREAKING**: feat: introduce `BlocOverrides` API ([#2932](https://github.com/felangel/bloc/pull/2932)) - `Bloc.observer` removed in favor of `BlocOverrides.runZoned` and `BlocOverrides.current.blocObserver` - `Bloc.transformer` removed in favor of `BlocOverrides.runZoned` and `BlocOverrides.current.eventTransformer` - **BREAKING**: refactor: make `BlocObserver` an abstract class - **BREAKING**: feat: `add` throws `StateError` when bloc is closed ([#2912](https://github.com/felangel/bloc/pull/2912)) - **BREAKING**: feat: `emit` throws `StateError` when bloc is closed ([#2913](https://github.com/felangel/bloc/pull/2913)) - **BREAKING**: feat: improve error handling/reporting - `BlocUnhandledErrorException` is removed - Uncaught exceptions are always reported to `onError` and rethrown - `addError` reports error to `onError` but does not propagate as an uncaught exception - **BREAKING**: feat: restrict scope of `emit` in `Bloc` and `Cubit` - In `Cubit`, `emit` is `protected` so it can only be used within the `Cubit` instance. - In `Bloc`, `emit` is `internal` so it cannot be used outside of the internal package implementation. - **BREAKING**: refactor: remove deprecated `TransitionFunction` - **BREAKING**: refactor: remove deprecated `transformEvents` - **BREAKING**: refactor: remove deprecated `mapEventToState` - **BREAKING**: refactor: remove deprecated `transformTransitions` - **BREAKING**: refactor: remove deprecated `listen` on `BlocBase` - feat: throw `StateError` if an event is added without a registered event handler # 8.0.0-dev.5 - **BREAKING**: feat: introduce `BlocOverrides` API ([#2932](https://github.com/felangel/bloc/pull/2932)) - `Bloc.observer` removed in favor of `BlocOverrides.runZoned` and `BlocOverrides.current.blocObserver` - `Bloc.transformer` removed in favor of `BlocOverrides.runZoned` and `BlocOverrides.current.eventTransformer` - **BREAKING**: refactor: make `BlocObserver` an abstract class - **BREAKING**: feat: `add` throws `StateError` when bloc is closed ([#2912](https://github.com/felangel/bloc/pull/2912)) - **BREAKING**: feat: `emit` throws `StateError` when bloc is closed ([#2913](https://github.com/felangel/bloc/pull/2913)) # 8.0.0-dev.4 - **BREAKING**: feat: improve error handling/reporting - `BlocUnhandledErrorException` is removed - Uncaught exceptions are always reported to `onError` and rethrown - `addError` reports error to `onError` but does not propagate as an uncaught exception # 8.0.0-dev.3 - **BREAKING**: feat: restrict scope of `emit` in `Bloc` and `Cubit` - In `Cubit`, `emit` is `protected` so it can only be used within the `Cubit` instance. - In `Bloc`, `emit` is `internal` so it cannot be used outside of the internal package implementation. # 8.0.0-dev.2 - **BREAKING**: refactor: remove deprecated `listen` on `BlocBase` # 8.0.0-dev.1 - **BREAKING**: refactor: remove deprecated `TransitionFunction` - **BREAKING**: refactor: remove deprecated `transformEvents` - **BREAKING**: refactor: remove deprecated `mapEventToState` - **BREAKING**: refactor: remove deprecated `transformTransitions` - feat: throw `StateError` if an event is added without a registered event handler # 7.2.1 - fix: `on` should have an `EventTransformer` instead of `EventTransformer` # 7.2.0 - feat: introduce `on` API to register event handlers - by default events are processed concurrently - feat: introduce `Bloc.transformer` API to configure the default `EventTransformer` - feat: introduce `Emitter` to trigger state changes - `call` to trigger a state change (alignment with `Cubit`) - `forEach` as an analogue for `await for` - `onEach` to simplify subscription management - `isDone` to abort expensive async operations - feat: throw `StateError` if `mapEventToState` is used in conjunction with `on` - feat: throw `StateError` if duplicate event handlers are registered - feat: throw `AssertionError` when `emit` is called in a completed `EventHandler` - feat: throw `AssertionError` when `emit.onEach` and `emit.forEach` are unawaited - **DEPRECATE**: fix: `mapEventToState` deprecated in favor of `on` - **DEPRECATE**: fix: `transformEvents` deprecated in favor of `EventTransformer` - use a built in `EventTransformer` or define your own - **DEPRECATE**: fix: `transformTransitions` deprecated - override `Stream get stream` to modify the outbound stream # 7.2.0-dev.3 - **BREAKING**: refactor: require `emit.forEach` `onData` to be synchronous - refactor: minor internal optimizations in `on` implementation # 7.2.0-dev.2 - **BREAKING**: refactor!: make `onData` callback in `emit.onEach` and `emit.forEach` named - **BREAKING**: feat!: rename `emit.isCanceled` to `emit.isDone` to encapsulate completion and cancelation - feat: introduce optional `onError` in `emit.onEach` and `emit.forEach` - feat: throw `AssertionError` when `emit` is called in a completed `EventHandler` - feat: throw `AssertionError` when `emit.onEach` and `emit.forEach` are unawaited - fix: `emit.onEach` and `emit.forEach` error propagation when stream emits an error # 7.2.0-dev.1 - feat: introduce `on` API to register event handlers - by default events are processed concurrently - feat: introduce `Bloc.transformer` API to configure the default `EventTransformer` - feat: introduce `Emitter` to trigger state changes - `call` to trigger a state change (alignment with `Cubit`) - `forEach` as an analogue for `await for` - `onEach` to simplify subscription management - `isCanceled` to abort expensive async operations - feat: throw `StateError` if `mapEventToState` is used in conjunction with `on` - feat: throw `StateError` if duplicate event handlers are registered - **DEPRECATE**: fix: `mapEventToState` deprecated in favor of `on` - **DEPRECATE**: fix: `transformEvents` deprecated in favor of `EventTransformer` - use a built in `EventTransformer` or define your own - **DEPRECATE**: fix: `transformTransitions` deprecated - override `Stream get stream` to modify the outbound stream # 7.1.0 - feat: expose `isClosed` getter on `BlocBase` - refactor: simplify internal event controller initialization - docs: update `onChange` description in README - docs: update GetStream sponsorship urls # 7.0.0 - **BREAKING**: refactor: `Bloc` and `Cubit` extend `BlocBase` - refactor: `void onError(Cubit cubit, Object error, StackTrace stackTrace)` -> `void onError(BlocBase bloc, Object error, StackTrace stackTrace)` - refactor: `void onCreate(Cubit cubit)` -> `void onCreate(BlocBase bloc)` - refactor: `void onClose(Cubit cubit)` -> `void onClose(BlocBase bloc)` - **BREAKING**: refactor: `Bloc` and `Cubit` do not extend `Stream` and implement `Sink` - refactor: use `bloc.stream` or `cubit.stream` to access `Stream` - `myBloc.map(...)` -> `myBloc.stream.map(...)` - refactor: deprecate `bloc.listen` in favor of `bloc.stream.listen` - **BREAKING**: refactor: `CubitUnhandledErrorException` -> `BlocUnhandledErrorException` - **BREAKING**: opt into null safety - feat!: upgrade Dart SDK constraints to `>=2.12.0-0 <3.0.0` - fix: `transformEvents` multiple subscriptions issue - test: improve testing for advanced `transformEvents` behavior - chore: bump to `meta: ^1.3.0` # 7.0.0-nullsafety.4 - **BREAKING**: refactor: `Bloc` and `Cubit` extend `BlocBase` - refactor: `void onError(Bloc bloc, Object error, StackTrace stackTrace)` -> `void onError(BlocBase bloc, Object error, StackTrace stackTrace)` - refactor: `void onCreate(Bloc bloc)` -> `void onCreate(BlocBase bloc)` - refactor: `void onClose(Bloc bloc)` -> `void onClose(BlocBase bloc)` - **BREAKING**: refactor: `Bloc` and `Cubit` do not extend `Stream` and implement `Sink` - refactor: use `bloc.stream` or `cubit.stream` to access `Stream` - `myBloc.map(...)` -> `myBloc.stream.map(...)` - refactor: deprecate `bloc.listen` in favor of `bloc.stream.listen` - **BREAKING**: revert: refactor: `Change` and `onChange` removed in favor of `Transition` and `onTransition` # 7.0.0-nullsafety.3 - fix: `transformEvents` multiple subscriptions issue - test: improve testing for advanced `transformEvents` behavior # 7.0.0-nullsafety.2 - chore: bump to `meta: ^1.3.0` # 7.0.0-nullsafety.1 - **BREAKING**: refactor: `Cubit` extends `Bloc` - refactor: `Change` and `onChange` removed in favor of `Transition` and `onTransition` - refactor: `void onError(Cubit cubit, Object error, StackTrace stackTrace)` -> `void onError(Bloc bloc, Object error, StackTrace stackTrace)` - refactor: `void onCreate(Cubit cubit)` -> `void onCreate(Bloc bloc)` - refactor: `void onClose(Cubit cubit)` -> `void onClose(Bloc bloc)` - refactor: `CubitUnhandledErrorException` -> `BlocUnhandledErrorException` # 7.0.0-nullsafety.0 - **BREAKING**: opt into null safety - feat!: upgrade Dart SDK constraints to `>=2.12.0-0 <3.0.0` # 6.1.3 - fix: `transformEvents` multiple subscriptions issue due to `v6.1.2` # 6.1.2 - fix: bloc memory leak due to internal event stream being a broadcast stream # 6.1.1 - fix: `close` should always emit done # 6.1.0 - feat: add `onCreate` and `onClose` to `BlocObserver` # 6.0.3 - docs: README updates to include flow diagrams for `Bloc` and `Cubit`. # 6.0.2 - refactor: cubit internal memory and performance optimizations # 6.0.1 - docs: minor documentation fixes and improvements # 6.0.0 - **BREAKING**: do not emit current state on subscription - **BREAKING**: `onError` in `BlocObserver` takes a `Cubit` as first parameter - **BREAKING**: allow blocs and cubits to emit the initial state - feat: include `cubit` and remove external dependency on [package:cubit](https://pub.dev/packages/cubit) - exports class `Cubit` - exports class `Change` (`Transition` for `Cubit`) - feat: `onChange` added to `BlocObserver` - refactor: apply additional lint rules - fix: add `@visibleForTesting` to `emit` on class `Cubit` - docs: fix inline documentation references # 6.0.0-dev.2 - fix: add `@visibleForTesting` to `emit` on class `Cubit` # 6.0.0-dev.1 - **BREAKING**: do not emit current state on subscription - **BREAKING**: `onError` in `BlocObserver` takes a `Cubit` as first parameter - **BREAKING**: allow blocs and cubits to emit the initial state - feat: include `cubit` and remove external dependency on [package:cubit](https://pub.dev/packages/cubit) - exports class `Cubit` - exports class `Change` (`Transition` for `Cubit`) - feat: `onChange` added to `BlocObserver` - refactor: apply additional lint rules - docs: fix inline documentation references # 5.0.1 - fix: upgrade to `cubit ^0.1.2` - docs: minor documentation updates # 5.0.0 - **BREAKING**: remove `initialState` override in favor of providing the initial state via super ([#1304](https://github.com/felangel/bloc/issues/1304)). - **BREAKING**: Remove `BlocSupervisor` and rename `BlocDelegate` to `BlocObserver`. - feat: support `null` states ([#1312](https://github.com/felangel/bloc/issues/1312)). - refactor: bloc to extend [cubit](https://pub.dev/packages/cubit) rather than `Stream`. - feat: ignore newly added events after bloc is closed ([#1236](https://github.com/felangel/bloc/issues/1236)). - feat: add `addError` to conform to `EventSink` interface. - feat: mark `onError`, `onTransition`, `onEvent` as `protected`. - docs: documentation improvements - docs: logo updates # 5.0.0-dev.11 - feat: add `addError` to conform to `EventSink` interface. - feat: mark `onError`, `onTransition`, `onEvent` as `protected`. # 5.0.0-dev.10 - docs: additional minor improvement to bloc logo alignment # 5.0.0-dev.9 - docs: minor improvement to bloc logo alignment # 5.0.0-dev.8 - **BREAKING**: Remove `BlocSupervisor` and rename `BlocDelegate` to `BlocObserver`. # 5.0.0-dev.7 - Ignore newly added events after bloc is closed ([#1236](https://github.com/felangel/bloc/issues/1236)). # 5.0.0-dev.6 - Additional documentation optimizations. # 5.0.0-dev.5 - Optimize documentation assets for smaller viewports. # 5.0.0-dev.4 - Update to [cubit](https://pub.dev/packages/cubit) `^0.0.13` - Update documentation and static assets. # 5.0.0-dev.3 - Update documentation and static assets. # 5.0.0-dev.2 - **BREAKING**: update `initialState` to be a required positional parameter ([related issue](https://github.com/dart-lang/sdk/issues/42438)). # 5.0.0-dev.1 - **BREAKING**: remove `initialState` override in favor of providing the initial state via super ([#1304](https://github.com/felangel/bloc/issues/1304)). - feat: support `null` states ([#1312](https://github.com/felangel/bloc/issues/1312)). - refactor: bloc to extend [cubit](https://pub.dev/packages/cubit) rather than `Stream`. # 4.0.0 - Remove `rxdart` dependency ([#821](https://github.com/felangel/bloc/pull/821)) - Replace `transformStates` with `transformTransitions` ([#840](https://github.com/felangel/bloc/pull/840)) - Fix null `stackTrace` in `onError` ([#963](https://github.com/felangel/bloc/pull/963)) - Fix remove duplicate terminating state - Add `mustCallSuper` to `onEvent`, `onTransition`, and `onError` - Surface Unhandled Bloc Errors in Debug Mode - Internal testing improvements # 4.0.0-dev.4 - Surface Unhandled Bloc Errors in Debug Mode - Internal testing improvements # 4.0.0-dev.3 - Add `mustCallSuper` to `onEvent`, `onTransition`, and `onError` # 4.0.0-dev.2 - Fix remove duplicate terminating state # 4.0.0-dev.1 - Remove `rxdart` dependency ([#821](https://github.com/felangel/bloc/pull/821)) - Replace `transformStates` with `transformTransitions` ([#840](https://github.com/felangel/bloc/pull/840)) - Fix null `stackTrace` in `onError` ([#963](https://github.com/felangel/bloc/pull/963)) # 3.0.0 - Upgrade to `rxdart ^0.23.0` - Upgrade to `Dart >= 2.6.0` # 3.0.0-dev.1 - Upgrade to `rxdart ^0.23.0` # 2.0.0 - Allow blocs to finish processing pending events on `close` ([#639](https://github.com/felangel/bloc/issues/639)) - Documentation Updates # 1.0.1 - Bugfix: Exceptions thrown in `onTransition` are passed to `onError` and should not break bloc functionality ([#641](https://github.com/felangel/bloc/issues/641)) - Adhere to [effective dart](https://dart.dev/guides/language/effective-dart) ([#561](https://github.com/felangel/bloc/issues/561)) - Documentation and Example Updates # 1.0.0 - `dispatch` and `dispose` removed - Documentation Updates # 0.16.1 - Minor Documentation Updates # 0.16.0 - Bloc extends `Stream` ([#558](https://github.com/felangel/bloc/issues/558)) - `bloc.state.listen` -> `bloc.listen` - `bloc.currentState` -> `bloc.state` - Bloc implements `Sink` ([#558](https://github.com/felangel/bloc/issues/558)) - `dispatch` deprecated in favor of `add` - `dispose` deprecated in favor of `close` - Documentation and Example Updates # 0.15.0 - Removed Bloc `event` Stream ([#326](https://github.com/felangel/bloc/issues/326)) - Renamed `transform` to `transformEvents` - Added `transformStates` ([#382](https://github.com/felangel/bloc/issues/382)) # 0.14.4 Additional Dependency and Documentation Updates. # 0.14.3 Dependency and Documentation Updates. # 0.14.2 - Deprecated Bloc `event` Stream ([#326](https://github.com/felangel/bloc/issues/326)) - Documentation Updates # 0.14.1 Internal `BlocDelegate` update and Documentation Updates. # 0.14.0 `BlocDelegate` initialization improvements and Documentation Updates. - `BlocSupervisor().delegate = ...` is now `BlocSupervisor.delegate = ...` ([#304](https://github.com/felangel/bloc/issues/304)). # 0.13.0 `Bloc` and `BlocDelegate` Improvements, new Features, and Documentation Updates. - Improved `dispose` to ignore pending events ([#257](https://github.com/felangel/bloc/issues/257)). - Exposed `event` stream on `Bloc` similar to `state` stream to expose a `Stream` of `dispatched` events ([#259](https://github.com/felangel/bloc/issues/259)). - Update to use `rxdart` version `^0.22.0` ([#265](https://github.com/felangel/bloc/issues/265)). - `BlocDelegate` methods include a reference to the `Bloc` instance ([#259](https://github.com/felangel/bloc/issues/259)). - Added `onEvent` to `Bloc` and `BlocDelegate` ([#259](https://github.com/felangel/bloc/issues/259)). # 0.12.0 Updated `transform` to enable advanced event filtering and processing and Documentation Updates. # 0.11.2 Added `BlocDelegate` `onError` and `onTransition` mustCallSuper and Documentation Updates # 0.11.1 Added `dispose` mustCallSuper and Documentation Updates # 0.11.0 Update `mapEventToState` to remove unnecessary argument for `currentState` - `Stream mapEventToState(S currentState, E event)` -> `Stream mapEventToState(E event)` - Documentation Updates - Example Updates # 0.10.0 Updated to `rxdart ^0.21.0` and Documentation Updates # 0.9.5 Minor Enhancements to Code Style and Documentation. # 0.9.4 Calls to `dispatch` after `dispose` has been called trigger `onError` in the `Bloc` and `BlocDelegate`. # 0.9.3 Restrict `rxdart` to `">=0.18.1 <0.21.0"` due to breaking changes. # 0.9.2 Additional Minor Updates to Documentation # 0.9.1 Minor Updates to Documentation # 0.9.0 `Bloc` and `BlocDelegate` Error Handling - Added `onError` to `Bloc` for local error handling. - Added `onError` to `BlocDelegate` for global error handling. # 0.8.4 Blocs handle exceptions thrown in `mapEventToState` and documentation updates. # 0.8.3 Minor Internal Improvements and Documentation Updates # 0.8.2 Additional Minor Updates to Documentation # 0.8.1 Minor Updates to Documentation # 0.8.0 Blocs ignore duplicate states # 0.7.8 Additional Minor Updates to Documentation # 0.7.7 Additional Minor Updates to Documentation # 0.7.6 Minor Updates to Documentation # 0.7.5 Exposed `currentState` in `Bloc` - Updates to Documentation. # 0.7.4 Updated `mapEventToState` parameter name - `Stream mapEventToState(S state, E event)` -> `Stream mapEventToState(S currentState, E event)` - Updates to Documentation. - Updates to Example. # 0.7.3 Minor Updates to Documentation # 0.7.2 `Transition` Fix - `Bloc` with `mapEventToState` which returns multiple states per event will now correctly report the `Transitions`. # 0.7.1 Improvements to `Bloc` usage in pure Dart applications. - `Bloc` state is seeded with `initialState` automatically # 0.7.0 Added `BlocSupervisor` and `BlocDelegate`. - `BlocSupervisor` notifies `BlocDelegate` of `Transitions` - `BlocDelegate` exposes `onTransition` which is invoked for all `Bloc` `Transitions`. # 0.6.0 `Transitions` and `initialState` updates. - Added `Transition`s and `onTransition` - Made `initialState` required # 0.5.2 Additional minor Updates to Documentation. # 0.5.1 Minor Updates to Documentation # 0.5.0 Moved Flutter Widgets to flutter_bloc package # 0.4.2 Additional minor Updates to Documentation. # 0.4.1 Minor Updates to Documentation. # 0.4.0 Added `BlocProvider`. - `BlocProvider.of(context)` - Updates to Documentation. - Updates to Example. # 0.3.0 Updated `mapEventToState` to take current state as an argument. - `Stream mapEventToState(E event)` -> `Stream mapEventToState(S state, E event)` - Updates to Documentation. - Updates to Example. # 0.2.5 Additional Minor Updates to Documentation. # 0.2.4 Additional Minor Updates to Documentation. # 0.2.3 Additional Minor Updates to Documentation. # 0.2.2 Additional Minor Updates to Documentation. # 0.2.1 Minor Updates to Documentation. # 0.2.0 Added Support for Stream Transformation - Includes `Stream transform(Stream events)` - Updates to Documentation # 0.1.2 Additional Minor Updates to Documentation. # 0.1.1 Minor Updates to Documentation. # 0.1.0 Initial Version of the library. - Includes the ability to create a custom Bloc by extending `Bloc` class. - Includes the ability to connect presentation layer to `Bloc` by using the `BlocBuilder` Widget. ================================================ FILE: packages/bloc/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2026 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/bloc/README.md ================================================

    build

    Bloc

    Pub build codecov Star on Github style: bloc lint Flutter Website Awesome Flutter Flutter Samples License: MIT Discord Bloc Library

    --- A predictable state management library that helps implement the BLoC (Business Logic Component) design pattern. **Learn more at [bloclibrary.dev](https://bloclibrary.dev)!** This package is built to work with: - [flutter_bloc](https://pub.dev/packages/flutter_bloc) - [angular_bloc](https://pub.dev/packages/angular_bloc) - [bloc_concurrency](https://pub.dev/packages/bloc_concurrency) - [bloc_test](https://pub.dev/packages/bloc_test) - [hydrated_bloc](https://pub.dev/packages/hydrated_bloc) - [replay_bloc](https://pub.dev/packages/replay_bloc) --- ## Sponsors Our top sponsors are shown below! [[Become a Sponsor](https://github.com/sponsors/felangel)]
    --- ## Overview The goal of this package is to make it easy to implement the `BLoC` Design Pattern (Business Logic Component). This design pattern helps to separate _presentation_ from _business logic_. Following the BLoC pattern facilitates testability and reusability. This package abstracts reactive aspects of the pattern allowing developers to focus on writing the business logic. ### Cubit ![Cubit Architecture](https://raw.githubusercontent.com/felangel/bloc/master/assets/diagrams/cubit_architecture_full.png) A `Cubit` is a class which extends `BlocBase` and can be extended to manage any type of state. `Cubit` requires an initial state which will be the state before `emit` has been called. The current state of a `cubit` can be accessed via the `state` getter and the state of the `cubit` can be updated by calling `emit` with a new `state`. ![Cubit Flow](https://raw.githubusercontent.com/felangel/bloc/master/assets/diagrams//cubit_flow.png) State changes in cubit begin with predefined function calls which can use the `emit` method to output new states. `onChange` is called right before a state change occurs and contains the current and next state. #### Creating a Cubit ```dart /// A `CounterCubit` which manages an `int` as its state. class CounterCubit extends Cubit { /// The initial state of the `CounterCubit` is 0. CounterCubit() : super(0); /// When increment is called, the current state /// of the cubit is accessed via `state` and /// a new `state` is emitted via `emit`. void increment() => emit(state + 1); } ``` #### Using a Cubit ```dart void main() { /// Create a `CounterCubit` instance. final cubit = CounterCubit(); /// Access the state of the `cubit` via `state`. print(cubit.state); // 0 /// Interact with the `cubit` to trigger `state` changes. cubit.increment(); /// Access the new `state`. print(cubit.state); // 1 /// Close the `cubit` when it is no longer needed. cubit.close(); } ``` #### Observing a Cubit `onChange` can be overridden to observe state changes for a single `cubit`. `onError` can be overridden to observe errors for a single `cubit`. ```dart class CounterCubit extends Cubit { CounterCubit() : super(0); void increment() => emit(state + 1); @override void onChange(Change change) { super.onChange(change); print(change); } @override void onError(Object error, StackTrace stackTrace) { print('$error, $stackTrace'); super.onError(error, stackTrace); } } ``` `BlocObserver` can be used to observe all `cubits`. ```dart class MyBlocObserver extends BlocObserver { @override void onCreate(BlocBase bloc) { super.onCreate(bloc); print('onCreate -- ${bloc.runtimeType}'); } @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); print('onChange -- ${bloc.runtimeType}, $change'); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { print('onError -- ${bloc.runtimeType}, $error'); super.onError(bloc, error, stackTrace); } @override void onClose(BlocBase bloc) { super.onClose(bloc); print('onClose -- ${bloc.runtimeType}'); } } ``` ```dart void main() { Bloc.observer = MyBlocObserver(); // Use cubits... } ``` To register multiple `BlocObserver` instances, use `MultiBlocObserver`: ```dart Bloc.observer = MultiBlocObserver( observers: [ MyLoggingObserver(), MyErrorObserver(), MyPerformanceObserver(), ], ); ``` ### Bloc ![Bloc Architecture](https://raw.githubusercontent.com/felangel/bloc/master/assets/diagrams/bloc_architecture_full.png) A `Bloc` is a more advanced class which relies on `events` to trigger `state` changes rather than functions. `Bloc` also extends `BlocBase` which means it has a similar public API as `Cubit`. However, rather than calling a `function` on a `Bloc` and directly emitting a new `state`, `Blocs` receive `events` and convert the incoming `events` into outgoing `states`. ![Bloc Flow](https://raw.githubusercontent.com/felangel/bloc/master/assets/diagrams/bloc_flow.png) State changes in bloc begin when events are added which trigger `onEvent`. The events are then funnelled through an `EventTransformer`. By default, each event is processed concurrently but a custom `EventTransformer` can be provided to manipulate the incoming event stream. All registered `EventHandlers` for that event type are then invoked with the incoming event. Each `EventHandler` is responsible for emitting zero or more states in response to the event. Lastly, `onTransition` is called just before the state is updated and contains the current state, event, and next state. #### Creating a Bloc ```dart /// The events which `CounterBloc` will react to. sealed class CounterEvent {} /// Notifies bloc to increment state. final class CounterIncrementPressed extends CounterEvent {} /// A `CounterBloc` which handles converting `CounterEvent`s into `int`s. class CounterBloc extends Bloc { /// The initial state of the `CounterBloc` is 0. CounterBloc() : super(0) { /// When a `CounterIncrementPressed` event is added, /// the current `state` of the bloc is accessed via the `state` property /// and a new state is emitted via `emit`. on((event, emit) => emit(state + 1)); } } ``` #### Using a Bloc ```dart Future main() async { /// Create a `CounterBloc` instance. final bloc = CounterBloc(); /// Access the state of the `bloc` via `state`. print(bloc.state); // 0 /// Interact with the `bloc` to trigger `state` changes. bloc.add(CounterIncrementPressed()); /// Wait for next iteration of the event-loop /// to ensure event has been processed. await Future.delayed(Duration.zero); /// Access the new `state`. print(bloc.state); // 1 /// Close the `bloc` when it is no longer needed. await bloc.close(); } ``` #### Observing a Bloc Since all `Blocs` extend `BlocBase` just like `Cubit`, `onChange` and `onError` can be overridden in a `Bloc` as well. In addition, `Blocs` can also override `onEvent`, `onTransition`, and `onDone`. `onEvent` is called any time a new `event` is added to the `Bloc`. `onTransition` is similar to `onChange`, however, it contains the `event` which triggered the state change in addition to the `currentState` and `nextState`. `onDone` is called whenever the event handler for a given `event` has completed. ```dart sealed class CounterEvent {} final class CounterIncrementPressed extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } @override void onEvent(CounterEvent event) { super.onEvent(event); print(event); } @override void onChange(Change change) { super.onChange(change); print(change); } @override void onTransition(Transition transition) { super.onTransition(transition); print(transition); } @override void onDone(CounterEvent event, [Object? error, StackTrace? stackTrace]) { super.onDone(event, error, stackTrace); print('$event, $error'); } @override void onError(Object error, StackTrace stackTrace) { print('$error, $stackTrace'); super.onError(error, stackTrace); } } ``` `BlocObserver` can be used to observe all `blocs` as well. ```dart class MyBlocObserver extends BlocObserver { @override void onCreate(BlocBase bloc) { super.onCreate(bloc); print('onCreate -- ${bloc.runtimeType}'); } @override void onEvent(Bloc bloc, Object? event) { super.onEvent(bloc, event); print('onEvent -- ${bloc.runtimeType}, $event'); } @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); print('onChange -- ${bloc.runtimeType}, $change'); } @override void onTransition(Bloc bloc, Transition transition) { super.onTransition(bloc, transition); print('onTransition -- ${bloc.runtimeType}, $transition'); } @override void onDone(Bloc bloc, Object? event, [Object? error, StackTrace? stackTrace]) { super.onDone(bloc, event, error, stackTrace); print('onDone -- ${bloc.runtimeType}, $event, $error'); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { print('onError -- ${bloc.runtimeType}, $error'); super.onError(bloc, error, stackTrace); } @override void onClose(BlocBase bloc) { super.onClose(bloc); print('onClose -- ${bloc.runtimeType}'); } } ``` ```dart void main() { Bloc.observer = MyBlocObserver(); // Use blocs... } ``` Just as with `Cubit`, to register multiple `BlocObserver` instances use `MultiBlocObserver`: ```dart Bloc.observer = MultiBlocObserver( observers: [ MyLoggingObserver(), MyErrorObserver(), MyPerformanceObserver(), ], ); ``` ## Dart Versions - Dart 2: >= 2.14 ## Examples - [Counter](https://github.com/felangel/bloc/tree/master/packages/bloc/example) - an example of how to create a `CounterBloc` in a pure Dart app. ## Maintainers - [Felix Angelov](https://github.com/felangel) ================================================ FILE: packages/bloc/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml ================================================ FILE: packages/bloc/example/main.dart ================================================ // ignore_for_file: avoid_print import 'dart:async'; import 'package:bloc/bloc.dart'; class SimpleBlocObserver extends BlocObserver { const SimpleBlocObserver(); @override void onCreate(BlocBase bloc) { super.onCreate(bloc); print('onCreate -- bloc: ${bloc.runtimeType}'); } @override void onEvent(Bloc bloc, Object? event) { super.onEvent(bloc, event); print('onEvent -- bloc: ${bloc.runtimeType}, event: $event'); } @override void onChange(BlocBase bloc, Change change) { super.onChange(bloc, change); print('onChange -- bloc: ${bloc.runtimeType}, change: $change'); } @override void onTransition( Bloc bloc, Transition transition, ) { super.onTransition(bloc, transition); print('onTransition -- bloc: ${bloc.runtimeType}, transition: $transition'); } @override void onDone( Bloc bloc, Object? event, [ Object? error, StackTrace? stackTrace, ]) { super.onDone(bloc, event, error, stackTrace); print('onDone -- bloc: ${bloc.runtimeType}, event: $event, error: $error'); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { print('onError -- bloc: ${bloc.runtimeType}, error: $error'); super.onError(bloc, error, stackTrace); } @override void onClose(BlocBase bloc) { super.onClose(bloc); print('onClose -- bloc: ${bloc.runtimeType}'); } } void main() { Bloc.observer = const SimpleBlocObserver(); cubitMain(); blocMain(); } void cubitMain() { print('----------CUBIT----------'); /// Create a `CounterCubit` instance. final cubit = CounterCubit(); /// Access the state of the `cubit` via `state`. print(cubit.state); // 0 /// Interact with the `cubit` to trigger `state` changes. cubit.increment(); /// Access the new `state`. print(cubit.state); // 1 /// Close the `cubit` when it is no longer needed. cubit.close(); } Future blocMain() async { print('----------BLOC----------'); /// Create a `CounterBloc` instance. final bloc = CounterBloc(); /// Access the state of the `bloc` via `state`. print(bloc.state); /// Interact with the `bloc` to trigger `state` changes. bloc.add(CounterIncrementPressed()); /// Wait for next iteration of the event-loop /// to ensure event has been processed. await Future.delayed(Duration.zero); /// Access the new `state`. print(bloc.state); /// Close the `bloc` when it is no longer needed. await bloc.close(); } /// A `CounterCubit` which manages an `int` as its state. class CounterCubit extends Cubit { /// The initial state of the `CounterCubit` is 0. CounterCubit() : super(0); /// When increment is called, the current state /// of the cubit is accessed via `state` and /// a new `state` is emitted via `emit`. void increment() => emit(state + 1); } /// The events which `CounterBloc` will react to. abstract class CounterEvent {} /// Notifies bloc to increment state. class CounterIncrementPressed extends CounterEvent {} /// A `CounterBloc` which handles converting `CounterEvent`s into `int`s. class CounterBloc extends Bloc { /// The initial state of the `CounterBloc` is 0. CounterBloc() : super(0) { /// When a `CounterIncrementPressed` event is added, /// the current `state` of the bloc is accessed via the `state` property /// and a new state is emitted via `emit`. on((event, emit) => emit(state + 1)); } } ================================================ FILE: packages/bloc/lib/bloc.dart ================================================ /// A predictable state management library for [Dart](https://dart.dev). /// /// Get started at [bloclibrary.dev](https://bloclibrary.dev) 🚀 library bloc; export 'src/bloc.dart'; export 'src/bloc_observer.dart'; export 'src/change.dart'; export 'src/cubit.dart'; export 'src/transition.dart'; ================================================ FILE: packages/bloc/lib/src/bloc.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part 'bloc_base.dart'; part 'emitter.dart'; /// An [ErrorSink] that supports adding events. /// /// Multiple events can be reported to the sink via `add`. abstract class BlocEventSink implements ErrorSink { /// Adds an [event] to the sink. /// /// Must not be called on a closed sink. void add(Event event); } /// An event handler is responsible for reacting to an incoming [Event] /// and can emit zero or more states via the [Emitter]. typedef EventHandler = FutureOr Function( Event event, Emitter emit, ); /// Signature for a function which converts an incoming event /// into an outbound stream of events. /// Used when defining custom [EventTransformer]s. typedef EventMapper = Stream Function(Event event); /// Used to change how events are processed. /// By default events are processed concurrently. typedef EventTransformer = Stream Function( Stream events, EventMapper mapper, ); /// {@template bloc} /// Takes a `Stream` of `Events` as input /// and transforms them into a `Stream` of `States` as output. /// {@endtemplate} abstract class Bloc extends BlocBase implements BlocEventSink { /// {@macro bloc} Bloc(State initialState) : super(initialState); /// The current [BlocObserver] instance. static BlocObserver observer = const _DefaultBlocObserver(); /// The default [EventTransformer] used for all event handlers. /// By default all events are processed concurrently. /// /// If a custom transformer is specified for a particular event handler, /// it will take precendence over the global transformer. /// /// See also: /// /// * [package:bloc_concurrency](https://pub.dev/packages/bloc_concurrency) for an /// opinionated set of event transformers. /// static EventTransformer transformer = (events, mapper) { return events .map(mapper) .transform(const _FlatMapStreamTransformer()); }; final _eventController = StreamController.broadcast(); final _subscriptions = >[]; final _handlers = <_Handler>[]; final _emitters = <_Emitter>[]; final _eventTransformer = Bloc.transformer; /// Notifies the [Bloc] of a new [event] which triggers /// all corresponding [EventHandler] instances. /// /// * A [StateError] will be thrown if there is no event handler /// registered for the incoming [event]. /// /// * A [StateError] will be thrown if the bloc is closed and the /// [event] will not be processed. @override void add(Event event) { // ignore: prefer_asserts_with_message assert(() { final handlerExists = _handlers.any((handler) => handler.isType(event)); if (!handlerExists) { final eventType = event.runtimeType; throw StateError( '''add($eventType) was called without a registered event handler.\n''' '''Make sure to register a handler via on<$eventType>((event, emit) {...})''', ); } return true; }()); try { onEvent(event); _eventController.add(event); } catch (error, stackTrace) { onError(error, stackTrace); rethrow; } } /// Called whenever an [event] is [add]ed to the [Bloc]. /// A great spot to add logging/analytics at the individual [Bloc] level. /// /// **Note: `super.onEvent` should always be called first.** /// ```dart /// @override /// void onEvent(Event event) { /// // Always call super.onEvent with the current event /// super.onEvent(event); /// /// // Custom onEvent logic goes here /// } /// ``` /// /// See also: /// /// * [BlocObserver.onEvent] for observing events globally. /// @protected @mustCallSuper void onEvent(Event event) { // ignore: invalid_use_of_protected_member _blocObserver.onEvent(this, event); } /// {@template emit} /// **[emit] is only for internal use and should never be called directly /// outside of tests. The [Emitter] instance provided to each [EventHandler] /// should be used instead.** /// /// ```dart /// class MyBloc extends Bloc { /// MyBloc() : super(MyInitialState()) { /// on((event, emit) { /// // use `emit` to update the state. /// emit(MyOtherState()); /// }); /// } /// } /// ``` /// /// Updates the state of the bloc to the provided [state]. /// A bloc's state should only be updated by `emitting` a new `state` /// from an [EventHandler] in response to an incoming event. /// {@endtemplate} @visibleForTesting @override void emit(State state) => super.emit(state); /// Register event handler for an event of type `E`. /// There should only ever be one event handler per event type `E`. /// /// ```dart /// abstract class CounterEvent {} /// class CounterIncrementPressed extends CounterEvent {} /// /// class CounterBloc extends Bloc { /// CounterBloc() : super(0) { /// on((event, emit) => emit(state + 1)); /// } /// } /// ``` /// /// * A [StateError] will be thrown if there are multiple event handlers /// registered for the same type `E`. /// /// By default, events will be processed concurrently. /// /// See also: /// /// * [EventTransformer] to customize how events are processed. /// * [package:bloc_concurrency](https://pub.dev/packages/bloc_concurrency) for an /// opinionated set of event transformers. /// void on( EventHandler handler, { EventTransformer? transformer, }) { // ignore: prefer_asserts_with_message assert(() { final handlerExists = _handlers.any((handler) => handler.type == E); if (handlerExists) { throw StateError( 'on<$E> was called multiple times. ' 'There should only be a single event handler per event type.', ); } _handlers.add(_Handler(isType: (dynamic e) => e is E, type: E)); return true; }()); final subscription = (transformer ?? _eventTransformer)( _eventController.stream.where((event) => event is E).cast(), (dynamic event) { void onEmit(State state) { if (isClosed) return; if (this.state == state && _emitted) return; onTransition( Transition( currentState: this.state, event: event as E, nextState: state, ), ); emit(state); } final emitter = _Emitter(onEmit); final controller = StreamController.broadcast( sync: true, onCancel: emitter.cancel, ); Future handleEvent() async { void tearDown() { emitter.complete(); _emitters.remove(emitter); if (!controller.isClosed) controller.close(); } try { _emitters.add(emitter); await handler(event as E, emitter); onDone(event); } catch (error, stackTrace) { onError(error, stackTrace); onDone(event as E, error, stackTrace); rethrow; } finally { tearDown(); } } handleEvent(); return controller.stream; }, ).listen(null); _subscriptions.add(subscription); } /// Called whenever a [transition] occurs with the given [transition]. /// A [transition] occurs when a new `event` is added /// and a new state is `emitted` from a corresponding [EventHandler]. /// /// [onTransition] is called before a [Bloc]'s [state] has been updated. /// A great spot to add logging/analytics at the individual [Bloc] level. /// /// **Note: `super.onTransition` should always be called first.** /// ```dart /// @override /// void onTransition(Transition transition) { /// // Always call super.onTransition with the current transition /// super.onTransition(transition); /// /// // Custom onTransition logic goes here /// } /// ``` /// /// See also: /// /// * [BlocObserver.onTransition] for observing transitions globally. /// @protected @mustCallSuper void onTransition(Transition transition) { // ignore: invalid_use_of_protected_member _blocObserver.onTransition(this, transition); } /// Called whenever an [event] handler for a specific [Bloc] has completed. /// This may include an [error] and [stackTrace] if an uncaught exception /// occurred within the event handler. /// /// [onDone] is called right after the event handler has completed. /// A great spot to add logging/analytics at the individual [Bloc] level. /// /// **Note: `super.onDone` should always be called first.** /// ```dart /// @override /// void onDone(Event event, [Object? error, StackTrace? stackTrace]) { /// // Always call super.onDone with the respective event. /// super.onDone(event, error, stackTrace); /// /// // Custom onDone logic goes here /// } /// ``` /// /// See also: /// /// * [BlocObserver.onDone] for observing event handler completions globally. /// @protected @mustCallSuper void onDone(Event event, [Object? error, StackTrace? stackTrace]) { // ignore: invalid_use_of_protected_member _blocObserver.onDone(this, event, error, stackTrace); } /// Closes the `event` and `state` `Streams`. /// This method should be called when a [Bloc] is no longer needed. /// Once [close] is called, `events` that are [add]ed will not be /// processed. /// In addition, if [close] is called while `events` are still being /// processed, the [Bloc] will finish processing the pending `events`. @mustCallSuper @override Future close() async { await _eventController.close(); for (final emitter in _emitters) { emitter.cancel(); } await Future.wait(_emitters.map((e) => e.future)); await Future.wait(_subscriptions.map((s) => s.cancel())); return super.close(); } } class _Handler { const _Handler({required this.isType, required this.type}); final bool Function(dynamic value) isType; final Type type; } class _DefaultBlocObserver extends BlocObserver { const _DefaultBlocObserver(); } class _FlatMapStreamTransformer extends StreamTransformerBase, T> { const _FlatMapStreamTransformer(); @override Stream bind(Stream> stream) { final controller = StreamController.broadcast(sync: true); controller.onListen = () { final subscriptions = >[]; final outerSubscription = stream.listen( (inner) { final subscription = inner.listen( controller.add, onError: controller.addError, ); subscription.onDone(() { subscriptions.remove(subscription); if (subscriptions.isEmpty) controller.close(); }); subscriptions.add(subscription); }, onError: controller.addError, ); outerSubscription.onDone(() { subscriptions.remove(outerSubscription); if (subscriptions.isEmpty) controller.close(); }); subscriptions.add(outerSubscription); controller.onCancel = () { if (subscriptions.isEmpty) return null; final cancels = [for (final s in subscriptions) s.cancel()]; return Future.wait(cancels).then((_) {}); }; }; return controller.stream; } } ================================================ FILE: packages/bloc/lib/src/bloc_base.dart ================================================ part of 'bloc.dart'; /// An object that provides access to a stream of states over time. abstract class Streamable { /// The current [stream] of states. Stream get stream; } /// A [Streamable] that provides synchronous access to the current [state]. abstract class StateStreamable implements Streamable { /// The current [state]. State get state; } /// A [StateStreamable] that must be closed when no longer in use. abstract class StateStreamableSource implements StateStreamable, Closable {} /// An object that must be closed when no longer in use. abstract class Closable { /// Closes the current instance. /// The returned future completes when the instance has been closed. FutureOr close(); /// Whether the object is closed. /// /// An object is considered closed once [close] is called. bool get isClosed; } /// An object that can emit new states. // ignore: one_member_abstracts abstract class Emittable { /// Emits a new [state]. void emit(State state); } /// A generic destination for errors. /// /// Multiple errors can be reported to the sink via `addError`. abstract class ErrorSink implements Closable { /// Adds an [error] to the sink with an optional [stackTrace]. /// /// Must not be called on a closed sink. void addError(Object error, [StackTrace? stackTrace]); } /// A [StateStreamableSource] that can emit new states. abstract class EmittableStateStreamableSource implements StateStreamableSource, Emittable {} /// {@template bloc_base} /// An interface for the core functionality implemented by /// both [Bloc] and [Cubit]. /// {@endtemplate} abstract class BlocBase implements EmittableStateStreamableSource, ErrorSink { /// {@macro bloc_base} BlocBase(this._state) { // ignore: invalid_use_of_protected_member _blocObserver.onCreate(this); } final _blocObserver = Bloc.observer; late final _stateController = StreamController.broadcast(); State _state; bool _emitted = false; @override State get state => _state; @override Stream get stream => _stateController.stream; /// Whether the bloc is closed. /// /// A bloc is considered closed once [close] is called. /// Subsequent state changes cannot occur within a closed bloc. @override bool get isClosed => _stateController.isClosed; /// Updates the [state] to the provided [state]. /// [emit] does nothing if the [state] being emitted /// is equal to the current [state]. /// /// To allow for the possibility of notifying listeners of the initial state, /// emitting a state which is equal to the initial state is allowed as long /// as it is the first thing emitted by the instance. /// /// * Throws a [StateError] if the bloc is closed. @protected @visibleForTesting @override void emit(State state) { try { if (isClosed) { throw StateError('Cannot emit new states after calling close'); } if (state == _state && _emitted) return; onChange(Change(currentState: this.state, nextState: state)); _state = state; _stateController.add(_state); _emitted = true; } catch (error, stackTrace) { onError(error, stackTrace); rethrow; } } /// Called whenever a [change] occurs with the given [change]. /// A [change] occurs when a new `state` is emitted. /// [onChange] is called before the `state` of the `cubit` is updated. /// [onChange] is a great spot to add logging/analytics for a specific `cubit`. /// /// **Note: `super.onChange` should always be called first.** /// ```dart /// @override /// void onChange(Change change) { /// // Always call super.onChange with the current change /// super.onChange(change); /// /// // Custom onChange logic goes here /// } /// ``` /// /// See also: /// /// * [BlocObserver] for observing [Cubit] behavior globally. /// @protected @mustCallSuper void onChange(Change change) { // ignore: invalid_use_of_protected_member _blocObserver.onChange(this, change); } /// Reports an [error] which triggers [onError] with an optional [StackTrace]. @protected @mustCallSuper @override void addError(Object error, [StackTrace? stackTrace]) { onError(error, stackTrace ?? StackTrace.current); } /// Called whenever an [error] occurs and notifies [BlocObserver.onError]. /// /// **Note: `super.onError` should always be called last.** /// /// ```dart /// @override /// void onError(Object error, StackTrace stackTrace) { /// // Custom onError logic goes here /// /// // Always call super.onError with the current error and stackTrace /// super.onError(error, stackTrace); /// } /// ``` @protected @mustCallSuper void onError(Object error, StackTrace stackTrace) { // ignore: invalid_use_of_protected_member _blocObserver.onError(this, error, stackTrace); } /// Closes the instance. /// This method should be called when the instance is no longer needed. /// Once [close] is called, the instance can no longer be used. @mustCallSuper @override Future close() async { // ignore: invalid_use_of_protected_member _blocObserver.onClose(this); await _stateController.close(); } } ================================================ FILE: packages/bloc/lib/src/bloc_observer.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; /// {@template bloc_observer} /// An interface for observing the behavior of [Bloc] instances. /// {@endtemplate} abstract class BlocObserver { /// {@macro bloc_observer} const BlocObserver(); /// Called whenever a [Bloc] is instantiated. /// In many cases, a cubit may be lazily instantiated and /// [onCreate] can be used to observe exactly when the cubit /// instance is created. @protected @mustCallSuper void onCreate(BlocBase bloc) {} /// Called whenever an [event] is `added` to any [bloc] with the given [bloc] /// and [event]. @protected @mustCallSuper void onEvent(Bloc bloc, Object? event) {} /// Called whenever a [Change] occurs in any [bloc] /// A [change] occurs when a new state is emitted. /// [onChange] is called before a bloc's state has been updated. @protected @mustCallSuper void onChange(BlocBase bloc, Change change) {} /// Called whenever a transition occurs in any [bloc] with the given [bloc] /// and [transition]. /// A [transition] occurs when a new `event` is added /// and a new state is `emitted` from a corresponding [EventHandler]. /// [onTransition] is called before a [bloc]'s state has been updated. @protected @mustCallSuper void onTransition( Bloc bloc, Transition transition, ) {} /// Called whenever an [error] is thrown in any [Bloc] or [Cubit]. /// The [stackTrace] argument may be [StackTrace.empty] if an error /// was received without a stack trace. @protected @mustCallSuper void onError(BlocBase bloc, Object error, StackTrace stackTrace) {} /// Called whenever an [event] handler for a specific [bloc] has completed. /// This may include an [error] and [stackTrace] if an uncaught exception /// occurred within the event handler. @protected @mustCallSuper void onDone( Bloc bloc, Object? event, [ Object? error, StackTrace? stackTrace, ]) {} /// Called whenever a [Bloc] is closed. /// [onClose] is called just before the [Bloc] is closed /// and indicates that the particular instance will no longer /// emit new states. @protected @mustCallSuper void onClose(BlocBase bloc) {} } /// {@template multi_bloc_observer} /// A [BlocObserver] which supports registering multiple [BlocObserver] /// instances. This is useful when maintaining multiple [BlocObserver] instances /// for different functions e.g. `LoggingBlocObserver`, /// `ErrorReportingBlocObserver`. /// /// ```dart /// Bloc.observer = MultiBlocObserver( /// observers: [ /// LoggingObserver(), /// ErrorObserver(), /// PerformanceObserver(), /// ], /// ); /// ``` /// {@endtemplate} class MultiBlocObserver implements BlocObserver { /// {@macro multi_bloc_observer} const MultiBlocObserver({required this.observers}); /// The list of [BlocObserver] instances that will be registered. Observers /// are notified of all lifecycle events in the same order that they are /// specified. final List observers; @override void onCreate(BlocBase bloc) { for (final observer in observers) { observer.onCreate(bloc); } } @override void onEvent(Bloc bloc, Object? event) { for (final observer in observers) { observer.onEvent(bloc, event); } } @override void onChange(BlocBase bloc, Change change) { for (final observer in observers) { observer.onChange(bloc, change); } } @override void onTransition( Bloc bloc, Transition transition, ) { for (final observer in observers) { observer.onTransition(bloc, transition); } } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { for (final observer in observers) { observer.onError(bloc, error, stackTrace); } } @override void onDone( Bloc bloc, Object? event, [ Object? error, StackTrace? stackTrace, ]) { for (final observer in observers) { observer.onDone(bloc, event, error, stackTrace); } } @override void onClose(BlocBase bloc) { for (final observer in observers) { observer.onClose(bloc); } } } ================================================ FILE: packages/bloc/lib/src/change.dart ================================================ import 'package:meta/meta.dart'; /// {@template change} /// A [Change] represents the change from one [State] to another. /// A [Change] consists of the [currentState] and [nextState]. /// {@endtemplate} @immutable class Change { /// {@macro change} const Change({required this.currentState, required this.nextState}); /// The current [State] at the time of the [Change]. final State currentState; /// The next [State] at the time of the [Change]. final State nextState; @override bool operator ==(Object other) => identical(this, other) || other is Change && runtimeType == other.runtimeType && currentState == other.currentState && nextState == other.nextState; @override int get hashCode => Object.hashAll([currentState, nextState]); @override String toString() { return 'Change { currentState: $currentState, nextState: $nextState }'; } } ================================================ FILE: packages/bloc/lib/src/cubit.dart ================================================ import 'package:bloc/bloc.dart'; /// {@template cubit} /// A [Cubit] is similar to [Bloc] but has no notion of events /// and relies on methods to [emit] new states. /// /// Every [Cubit] requires an initial state which will be the /// state of the [Cubit] before [emit] has been called. /// /// The current state of a [Cubit] can be accessed via the [state] getter. /// /// ```dart /// class CounterCubit extends Cubit { /// CounterCubit() : super(0); /// /// void increment() => emit(state + 1); /// } /// ``` /// /// {@endtemplate} abstract class Cubit extends BlocBase { /// {@macro cubit} Cubit(State initialState) : super(initialState); } ================================================ FILE: packages/bloc/lib/src/emitter.dart ================================================ part of 'bloc.dart'; /// {@template emitter} /// An [Emitter] is a class which is capable of emitting new states. /// /// See also: /// /// * [EventHandler] which has access to an [Emitter]. /// /// {@endtemplate} abstract class Emitter { /// Subscribes to the provided [stream] and invokes the [onData] callback /// when the [stream] emits new data. /// /// [onEach] completes when the event handler is cancelled or when /// the provided [stream] has ended. /// /// If [onError] is omitted, any errors on this [stream] /// are considered unhandled, and will be thrown by [onEach]. /// As a result, the internal subscription to the [stream] will be canceled. /// /// If [onError] is provided, any errors on this [stream] will be passed on to /// [onError] and will not result in unhandled exceptions or cancelations to /// the internal stream subscription. /// /// **Note**: The stack trace argument may be [StackTrace.empty] /// if the [stream] received an error without a stack trace. Future onEach( Stream stream, { required void Function(T data) onData, void Function(Object error, StackTrace stackTrace)? onError, }); /// Subscribes to the provided [stream] and invokes the [onData] callback /// when the [stream] emits new data and the result of [onData] is emitted. /// /// [forEach] completes when the event handler is cancelled or when /// the provided [stream] has ended. /// /// If [onError] is omitted, any errors on this [stream] /// are considered unhandled, and will be thrown by [forEach]. /// As a result, the internal subscription to the [stream] will be canceled. /// /// If [onError] is provided, any errors on this [stream] will be passed on to /// [onError] and will not result in unhandled exceptions or cancelations to /// the internal stream subscription. /// /// **Note**: The stack trace argument may be [StackTrace.empty] /// if the [stream] received an error without a stack trace. Future forEach( Stream stream, { required State Function(T data) onData, State Function(Object error, StackTrace stackTrace)? onError, }); /// Whether the [EventHandler] associated with this [Emitter] /// has been completed or canceled. bool get isDone; /// Emits the provided [state]. void call(State state); } class _Emitter implements Emitter { _Emitter(this._emit); final void Function(State state) _emit; final _completer = Completer(); final _disposables = Function()>[]; var _isCanceled = false; var _isCompleted = false; @override Future onEach( Stream stream, { required void Function(T data) onData, void Function(Object error, StackTrace stackTrace)? onError, }) { final completer = Completer(); final subscription = stream.listen( onData, onDone: completer.complete, onError: onError ?? completer.completeError, cancelOnError: onError == null, ); _disposables.add(subscription.cancel); return Future.any([future, completer.future]).whenComplete(() { subscription.cancel(); _disposables.remove(subscription.cancel); }); } @override Future forEach( Stream stream, { required State Function(T data) onData, State Function(Object error, StackTrace stackTrace)? onError, }) { return onEach( stream, onData: (data) => call(onData(data)), onError: onError != null ? (Object error, StackTrace stackTrace) { call(onError(error, stackTrace)); } : null, ); } @override void call(State state) { assert( !_isCompleted, ''' emit was called after an event handler completed normally. This is usually due to an unawaited future in an event handler. Please make sure to await all asynchronous operations with event handlers and use emit.isDone after asynchronous operations before calling emit() to ensure the event handler has not completed. **BAD** on((event, emit) { future.whenComplete(() => emit(...)); }); **GOOD** on((event, emit) async { await future.whenComplete(() => emit(...)); }); ''', ); if (!_isCanceled) _emit(state); } @override bool get isDone => _isCanceled || _isCompleted; void cancel() { if (isDone) return; _isCanceled = true; _close(); } void complete() { if (isDone) return; assert( _disposables.isEmpty, ''' An event handler completed but left pending subscriptions behind. This is most likely due to an unawaited emit.forEach or emit.onEach. Please make sure to await all asynchronous operations within event handlers. **BAD** on((event, emit) { emit.forEach(...); }); **GOOD** on((event, emit) async { await emit.forEach(...); }); **GOOD** on((event, emit) { return emit.forEach(...); }); **GOOD** on((event, emit) => emit.forEach(...)); ''', ); _isCompleted = true; _close(); } void _close() { for (final disposable in _disposables) { disposable.call(); } _disposables.clear(); if (!_completer.isCompleted) _completer.complete(); } Future get future => _completer.future; } ================================================ FILE: packages/bloc/lib/src/transition.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; /// {@template transition} /// A [Transition] is the change from one state to another. /// Consists of the [currentState], an [event], and the [nextState]. /// {@endtemplate} @immutable class Transition extends Change { /// {@macro transition} const Transition({ required State currentState, required this.event, required State nextState, }) : super(currentState: currentState, nextState: nextState); /// The [Event] which triggered the current [Transition]. final Event event; @override bool operator ==(Object other) => identical(this, other) || other is Transition && runtimeType == other.runtimeType && currentState == other.currentState && event == other.event && nextState == other.nextState; @override int get hashCode => Object.hashAll([currentState, event, nextState]); @override String toString() { return '''Transition { currentState: $currentState, event: $event, nextState: $nextState }'''; } } ================================================ FILE: packages/bloc/pubspec.yaml ================================================ name: bloc description: A predictable state management library that helps implement the BLoC (Business Logic Component) design pattern. version: 9.2.0 repository: https://github.com/felangel/bloc/tree/master/packages/bloc issue_tracker: https://github.com/felangel/bloc/issues homepage: https://github.com/felangel/bloc documentation: https://bloclibrary.dev topics: [bloc, state-management] funding: [https://github.com/sponsors/felangel] environment: sdk: ">=2.14.0 <4.0.0" dependencies: meta: ^1.3.0 dev_dependencies: bloc_lint: ^0.3.2 mocktail: ^1.0.0 stream_transform: ^2.0.0 test: ^1.18.2 screenshots: - description: The bloc package logo. path: screenshots/logo.png ================================================ FILE: packages/bloc/test/bloc_event_transformer_test.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import 'package:test/test.dart'; @immutable abstract class CounterEvent {} class Increment extends CounterEvent { @override bool operator ==(Object value) { if (identical(this, value)) return true; return value is Increment; } @override int get hashCode => 0; } const delay = Duration(milliseconds: 30); Future wait() => Future.delayed(delay); Future tick() => Future.delayed(Duration.zero); class CounterBloc extends Bloc { CounterBloc({EventTransformer? incrementTransformer}) : super(0) { on( (event, emit) { onCalls.add(event); return Future.delayed(delay, () { if (emit.isDone) return; onEmitCalls.add(event); emit(state + 1); }); }, transformer: incrementTransformer, ); } final onCalls = []; final onEmitCalls = []; } void main() { late EventTransformer transformer; setUp(() { transformer = Bloc.transformer; }); tearDown(() { Bloc.transformer = transformer; }); test('processes events concurrently by default', () async { final states = []; final bloc = CounterBloc() ..stream.listen(states.add) ..add(Increment()) ..add(Increment()) ..add(Increment()); await tick(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); await bloc.close(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); }); test( 'when processing events concurrently ' 'all subscriptions are canceled on close', () async { final states = []; final bloc = CounterBloc() ..stream.listen(states.add) ..add(Increment()) ..add(Increment()) ..add(Increment()); await tick(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); await bloc.close(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); expect(bloc.onEmitCalls, isEmpty); expect(states, isEmpty); }); test( 'processes events sequentially when ' 'transformer is overridden.', () async { EventTransformer incrementTransformer() { return (events, mapper) => events.asyncExpand(mapper); } final states = []; final bloc = CounterBloc(incrementTransformer: incrementTransformer()) ..stream.listen(states.add) ..add(Increment()) ..add(Increment()) ..add(Increment()); await tick(); expect( bloc.onCalls, equals([Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment()]), ); expect(states, equals([1])); await tick(); expect( bloc.onCalls, equals([Increment(), Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment()]), ); expect(states, equals([1, 2])); await tick(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); await bloc.close(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); }); test( 'processes events sequentially when ' 'Bloc.transformer is overridden.', () async { Bloc.transformer = (events, mapper) => events.asyncExpand(mapper); final states = []; final bloc = CounterBloc() ..stream.listen(states.add) ..add(Increment()) ..add(Increment()) ..add(Increment()); await tick(); expect( bloc.onCalls, equals([Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment()]), ); expect(states, equals([1])); await tick(); expect( bloc.onCalls, equals([Increment(), Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment()]), ); expect(states, equals([1, 2])); await tick(); expect( bloc.onCalls, equals([ Increment(), Increment(), Increment(), ]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); await bloc.close(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); }); } ================================================ FILE: packages/bloc/test/bloc_event_transformer_test_legacy.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import 'package:test/test.dart'; @immutable abstract class CounterEvent {} class Increment extends CounterEvent { @override bool operator ==(Object value) { if (identical(this, value)) return true; return value is Increment; } @override int get hashCode => 0; } const delay = Duration(milliseconds: 30); Future wait() => Future.delayed(delay); Future tick() => Future.delayed(Duration.zero); class CounterBloc extends Bloc { CounterBloc({EventTransformer? incrementTransformer}) : super(0) { on( (event, emit) { onCalls.add(event); return Future.delayed(delay, () { if (emit.isDone) return; onEmitCalls.add(event); emit(state + 1); }); }, transformer: incrementTransformer, ); } final onCalls = []; final onEmitCalls = []; } void main() { test('processes events concurrently by default', () async { final states = []; final bloc = CounterBloc() ..stream.listen(states.add) ..add(Increment()) ..add(Increment()) ..add(Increment()); await tick(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); await bloc.close(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); }); test( 'when processing events concurrently ' 'all subscriptions are canceled on close', () async { final states = []; final bloc = CounterBloc() ..stream.listen(states.add) ..add(Increment()) ..add(Increment()) ..add(Increment()); await tick(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); await bloc.close(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); expect(bloc.onEmitCalls, isEmpty); expect(states, isEmpty); }); test( 'processes events sequentially when ' 'transformer is overridden.', () async { EventTransformer incrementTransformer() { return (events, mapper) => events.asyncExpand(mapper); } final states = []; final bloc = CounterBloc(incrementTransformer: incrementTransformer()) ..stream.listen(states.add) ..add(Increment()) ..add(Increment()) ..add(Increment()); await tick(); expect( bloc.onCalls, equals([Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment()]), ); expect(states, equals([1])); await tick(); expect( bloc.onCalls, equals([Increment(), Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment()]), ); expect(states, equals([1, 2])); await tick(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); await bloc.close(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); }); test( 'processes events sequentially when ' 'Bloc.transformer is overridden.', () async { final defaultTransformer = Bloc.transformer; addTearDown(() => Bloc.transformer = defaultTransformer); Bloc.transformer = (events, mapper) => events.asyncExpand(mapper); final states = []; final bloc = CounterBloc() ..stream.listen(states.add) ..add(Increment()) ..add(Increment()) ..add(Increment()); await tick(); expect(bloc.onCalls, equals([Increment()])); await wait(); expect(bloc.onEmitCalls, equals([Increment()])); expect(states, equals([1])); await tick(); expect(bloc.onCalls, equals([Increment(), Increment()])); await wait(); expect(bloc.onEmitCalls, equals([Increment(), Increment()])); expect(states, equals([1, 2])); await tick(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); await bloc.close(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); }); } ================================================ FILE: packages/bloc/test/bloc_observer_test.dart ================================================ // ignore_for_file: invalid_use_of_protected_member import 'package:bloc/bloc.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; import 'blocs/blocs.dart'; class _MockBlocObserver extends Mock implements BlocObserver {} class DefaultBlocObserver extends BlocObserver { const DefaultBlocObserver(); } void main() { final bloc = CounterBloc(); final error = Exception(); const stackTrace = StackTrace.empty; const event = CounterEvent.increment; const change = Change(currentState: 0, nextState: 1); const transition = Transition( currentState: 0, event: CounterEvent.increment, nextState: 1, ); group(BlocObserver, () { group('onCreate', () { test('does nothing by default', () { const DefaultBlocObserver().onCreate(bloc); }); }); group('onEvent', () { test('does nothing by default', () { const DefaultBlocObserver().onEvent(bloc, event); }); }); group('onChange', () { test('does nothing by default', () { const DefaultBlocObserver().onChange(bloc, change); }); }); group('onTransition', () { test('does nothing by default', () { const DefaultBlocObserver().onTransition(bloc, transition); }); }); group('onDone', () { test('does nothing by default', () { const DefaultBlocObserver().onDone(bloc, event); }); }); group('onError', () { test('does nothing by default', () { const DefaultBlocObserver().onError(bloc, error, stackTrace); }); }); group('onClose', () { test('does nothing by default', () { const DefaultBlocObserver().onClose(bloc); }); }); }); group(MultiBlocObserver, () { late MultiBlocObserver observer; late List observers; setUp(() { observers = [_MockBlocObserver(), _MockBlocObserver()]; observer = MultiBlocObserver(observers: observers); }); group('onCreate', () { test('notifies all registered observers', () { observer.onCreate(bloc); for (final observer in observers) { verify(() => observer.onCreate(bloc)).called(1); } }); }); group('onEvent', () { test('notifies all registered observers', () { observer.onEvent(bloc, event); for (final observer in observers) { verify(() => observer.onEvent(bloc, event)).called(1); } }); }); group('onChange', () { test('notifies all registered observers', () { observer.onChange(bloc, change); for (final observer in observers) { verify(() => observer.onChange(bloc, change)).called(1); } }); }); group('onTransition', () { test('notifies all registered observers', () { observer.onTransition(bloc, transition); for (final observer in observers) { verify(() => observer.onTransition(bloc, transition)).called(1); } }); }); group('onDone', () { test('notifies all registered observers', () { observer.onDone(bloc, event); for (final observer in observers) { verify(() => observer.onDone(bloc, event)).called(1); } }); }); group('onError', () { test('notifies all registered observers', () { observer.onError(bloc, error, stackTrace); for (final observer in observers) { verify(() => observer.onError(bloc, error, stackTrace)).called(1); } }); }); group('onClose', () { test('notifies all registered observers', () { observer.onClose(bloc); for (final observer in observers) { verify(() => observer.onClose(bloc)).called(1); } }); }); }); } ================================================ FILE: packages/bloc/test/bloc_on_test.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:test/test.dart'; abstract class TestEvent {} class TestEventA extends TestEvent {} class TestEventAA extends TestEventA {} class TestEventB extends TestEvent {} class TestEventBA extends TestEventB {} class TestState {} typedef OnEvent = void Function(E event, Emitter emit); void defaultOnEvent(E event, Emitter emit) {} class TestBloc extends Bloc { TestBloc({ this.onTestEvent, this.onTestEventA, this.onTestEventB, this.onTestEventAA, this.onTestEventBA, }) : super(TestState()) { on(onTestEventA ?? defaultOnEvent); on(onTestEventB ?? defaultOnEvent); on(onTestEventAA ?? defaultOnEvent); on(onTestEventBA ?? defaultOnEvent); on(onTestEvent ?? defaultOnEvent); } final OnEvent? onTestEvent; final OnEvent? onTestEventA; final OnEvent? onTestEventAA; final OnEvent? onTestEventB; final OnEvent? onTestEventBA; } class DuplicateHandlerBloc extends Bloc { DuplicateHandlerBloc() : super(TestState()) { on(defaultOnEvent); on(defaultOnEvent); } } class MissingHandlerBloc extends Bloc { MissingHandlerBloc() : super(TestState()); } void main() { group('on', () { test('throws StateError when handler is registered more than once', () { const expectedMessage = 'on was called multiple times. ' 'There should only be a single event handler per event type.'; final expected = throwsA( isA().having((e) => e.message, 'message', expectedMessage), ); expect(() => DuplicateHandlerBloc(), expected); }); test('throws StateError when handler is missing', () { const expectedMessage = '''add(TestEventA) was called without a registered event handler.\n''' '''Make sure to register a handler via on((event, emit) {...})'''; final expected = throwsA( isA().having((e) => e.message, 'message', expectedMessage), ); expect(() => MissingHandlerBloc().add(TestEventA()), expected); }); test('invokes all on when event E is added where E is T', () async { var onEventCallCount = 0; var onACallCount = 0; var onBCallCount = 0; var onAACallCount = 0; var onBACallCount = 0; final bloc = TestBloc( onTestEvent: (_, __) => onEventCallCount++, onTestEventA: (_, __) => onACallCount++, onTestEventB: (_, __) => onBCallCount++, onTestEventAA: (_, __) => onAACallCount++, onTestEventBA: (_, __) => onBACallCount++, )..add(TestEventA()); await Future.delayed(Duration.zero); expect(onEventCallCount, equals(1)); expect(onACallCount, equals(1)); expect(onBCallCount, equals(0)); expect(onAACallCount, equals(0)); expect(onBACallCount, equals(0)); bloc.add(TestEventAA()); await Future.delayed(Duration.zero); expect(onEventCallCount, equals(2)); expect(onACallCount, equals(2)); expect(onBCallCount, equals(0)); expect(onAACallCount, equals(1)); expect(onBACallCount, equals(0)); bloc.add(TestEventB()); await Future.delayed(Duration.zero); expect(onEventCallCount, equals(3)); expect(onACallCount, equals(2)); expect(onBCallCount, equals(1)); expect(onAACallCount, equals(1)); expect(onBACallCount, equals(0)); bloc.add(TestEventBA()); await Future.delayed(Duration.zero); expect(onEventCallCount, equals(4)); expect(onACallCount, equals(2)); expect(onBCallCount, equals(2)); expect(onAACallCount, equals(1)); expect(onBACallCount, equals(1)); await bloc.close(); }); }); } ================================================ FILE: packages/bloc/test/bloc_test.dart ================================================ // ignore_for_file: invalid_use_of_protected_member import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; import 'blocs/blocs.dart'; Future tick() => Future.delayed(Duration.zero); class MockBlocObserver extends Mock implements BlocObserver {} class FakeBlocBase extends Fake implements BlocBase {} void main() { group('Bloc Tests', () { group('Simple Bloc', () { late SimpleBloc simpleBloc; late MockBlocObserver observer; setUp(() { simpleBloc = SimpleBloc(); observer = MockBlocObserver(); Bloc.observer = observer; }); test('triggers onCreate on observer when instantiated', () { final bloc = SimpleBloc(); verify(() => observer.onCreate(bloc)).called(1); }); test('triggers onClose on observer when closed', () async { final bloc = SimpleBloc(); await bloc.close(); verify(() => observer.onClose(bloc)).called(1); }); test('close does not emit new states over the state stream', () async { final expectedStates = [emitsDone]; unawaited(expectLater(simpleBloc.stream, emitsInOrder(expectedStates))); await simpleBloc.close(); }); test('state returns correct value initially', () { expect(simpleBloc.state, ''); }); test('should map single event to correct state', () { final expectedStates = ['data', emitsDone]; final simpleBloc = SimpleBloc(); expectLater( simpleBloc.stream, emitsInOrder(expectedStates), ).then((dynamic _) { verifyInOrder([ () => observer.onCreate(simpleBloc), () => observer.onEvent(simpleBloc, 'event'), () => observer.onTransition( simpleBloc, const Transition( currentState: '', event: 'event', nextState: 'data', ), ), () => observer.onChange( simpleBloc, const Change(currentState: '', nextState: 'data'), ), () => observer.onDone(simpleBloc, 'event'), () => observer.onClose(simpleBloc), ]); expect(simpleBloc.state, 'data'); }); simpleBloc ..add('event') ..close(); }); test('should map multiple events to correct states', () { final expectedStates = ['data', emitsDone]; final simpleBloc = SimpleBloc(); expectLater( simpleBloc.stream, emitsInOrder(expectedStates), ).then((dynamic _) { verifyInOrder([ () => observer.onCreate(simpleBloc), () => observer.onEvent(simpleBloc, 'event1'), () => observer.onEvent(simpleBloc, 'event2'), () => observer.onEvent(simpleBloc, 'event3'), () => observer.onTransition( simpleBloc, const Transition( currentState: '', event: 'event1', nextState: 'data', ), ), () => observer.onChange( simpleBloc, const Change(currentState: '', nextState: 'data'), ), () => observer.onDone(simpleBloc, 'event1'), () => observer.onClose(simpleBloc), ]); expect(simpleBloc.state, 'data'); }); simpleBloc ..add('event1') ..add('event2') ..add('event3') ..close(); }); test('is a broadcast stream', () { final expectedStates = ['data', emitsDone]; expect(simpleBloc.stream.isBroadcast, isTrue); expectLater(simpleBloc.stream, emitsInOrder(expectedStates)); expectLater(simpleBloc.stream, emitsInOrder(expectedStates)); simpleBloc ..add('event') ..close(); }); test('multiple subscribers receive the latest state', () { const expectedStates = ['data']; expectLater(simpleBloc.stream, emitsInOrder(expectedStates)); expectLater(simpleBloc.stream, emitsInOrder(expectedStates)); expectLater(simpleBloc.stream, emitsInOrder(expectedStates)); simpleBloc.add('event'); }); }); group('Complex Bloc', () { late ComplexBloc complexBloc; late MockBlocObserver observer; setUp(() { complexBloc = ComplexBloc(); observer = MockBlocObserver(); Bloc.observer = observer; }); test('close does not emit new states over the state stream', () async { final expectedStates = [emitsDone]; unawaited( expectLater(complexBloc.stream, emitsInOrder(expectedStates)), ); await complexBloc.close(); }); test('state returns correct value initially', () { expect(complexBloc.state, ComplexStateA()); }); test('should map single event to correct state', () { final expectedStates = [ComplexStateB()]; final complexBloc = ComplexBloc(); expectLater( complexBloc.stream, emitsInOrder(expectedStates), ).then((dynamic _) { verifyInOrder([ () => observer.onCreate(complexBloc), () => observer.onEvent(complexBloc, ComplexEventB()), () => observer.onTransition( complexBloc, Transition( currentState: ComplexStateA(), event: ComplexEventB(), nextState: ComplexStateB(), ), ), () => observer.onChange( complexBloc, Change( currentState: ComplexStateA(), nextState: ComplexStateB(), ), ), () => observer.onDone(complexBloc, ComplexEventB()), () => observer.onClose(complexBloc), ]); expect(complexBloc.state, ComplexStateB()); }); complexBloc ..add(ComplexEventB()) ..close(); }); test('should map multiple events to correct states', () async { final expectedStates = [ ComplexStateB(), ComplexStateD(), ComplexStateA(), ComplexStateC(), ]; unawaited( expectLater(complexBloc.stream, emitsInOrder(expectedStates)), ); complexBloc.add(ComplexEventA()); await Future.delayed(const Duration(milliseconds: 20)); complexBloc.add(ComplexEventB()); await Future.delayed(const Duration(milliseconds: 20)); complexBloc.add(ComplexEventC()); await Future.delayed(const Duration(milliseconds: 20)); complexBloc.add(ComplexEventD()); await Future.delayed(const Duration(milliseconds: 200)); complexBloc ..add(ComplexEventC()) ..add(ComplexEventA()); await Future.delayed(const Duration(milliseconds: 120)); complexBloc.add(ComplexEventC()); }); test('is a broadcast stream', () { final expectedStates = [ComplexStateB()]; expect(complexBloc.stream.isBroadcast, isTrue); expectLater(complexBloc.stream, emitsInOrder(expectedStates)); expectLater(complexBloc.stream, emitsInOrder(expectedStates)); complexBloc.add(ComplexEventB()); }); test('multiple subscribers receive the latest state', () { final expected = [ComplexStateB()]; expectLater(complexBloc.stream, emitsInOrder(expected)); expectLater(complexBloc.stream, emitsInOrder(expected)); expectLater(complexBloc.stream, emitsInOrder(expected)); complexBloc.add(ComplexEventB()); }); }); group('CounterBloc', () { late CounterBloc counterBloc; late MockBlocObserver observer; late List transitions; late List events; setUp(() { events = []; transitions = []; counterBloc = CounterBloc( onEventCallback: events.add, onTransitionCallback: (transition) { transitions.add(transition.toString()); }, ); observer = MockBlocObserver(); Bloc.observer = observer; }); test('state returns correct value initially', () { expect(counterBloc.state, 0); expect(events.isEmpty, true); expect(transitions.isEmpty, true); }); test('single Increment event updates state to 1', () { final expectedStates = [1, emitsDone]; final expectedTransitions = [ '''Transition { currentState: 0, event: CounterEvent.increment, nextState: 1 }''', ]; final counterBloc = CounterBloc( onEventCallback: events.add, onTransitionCallback: (transition) { transitions.add(transition.toString()); }, ); expectLater( counterBloc.stream, emitsInOrder(expectedStates), ).then((dynamic _) { expectLater(transitions, expectedTransitions); verifyInOrder([ () => observer.onCreate(counterBloc), () => observer.onEvent(counterBloc, CounterEvent.increment), () => observer.onTransition( counterBloc, const Transition( currentState: 0, event: CounterEvent.increment, nextState: 1, ), ), () => observer.onChange( counterBloc, const Change(currentState: 0, nextState: 1), ), () => observer.onDone(counterBloc, CounterEvent.increment), () => observer.onClose(counterBloc), ]); expect(counterBloc.state, 1); }); counterBloc ..add(CounterEvent.increment) ..close(); }); test('multiple Increment event updates state to 3', () { final expectedStates = [1, 2, 3, emitsDone]; final expectedTransitions = [ '''Transition { currentState: 0, event: CounterEvent.increment, nextState: 1 }''', '''Transition { currentState: 1, event: CounterEvent.increment, nextState: 2 }''', '''Transition { currentState: 2, event: CounterEvent.increment, nextState: 3 }''', ]; final counterBloc = CounterBloc( onEventCallback: events.add, onTransitionCallback: (transition) { transitions.add(transition.toString()); }, ); expectLater( counterBloc.stream, emitsInOrder(expectedStates), ).then((dynamic _) { expect(transitions, expectedTransitions); verifyInOrder([ () => observer.onCreate(counterBloc), () => observer.onEvent(counterBloc, CounterEvent.increment), () => observer.onEvent(counterBloc, CounterEvent.increment), () => observer.onEvent(counterBloc, CounterEvent.increment), () => observer.onTransition( counterBloc, const Transition( currentState: 0, event: CounterEvent.increment, nextState: 1, ), ), () => observer.onChange( counterBloc, const Change(currentState: 0, nextState: 1), ), () => observer.onDone(counterBloc, CounterEvent.increment), () => observer.onTransition( counterBloc, const Transition( currentState: 1, event: CounterEvent.increment, nextState: 2, ), ), () => observer.onChange( counterBloc, const Change(currentState: 1, nextState: 2), ), () => observer.onTransition( counterBloc, const Transition( currentState: 2, event: CounterEvent.increment, nextState: 3, ), ), () => observer.onChange( counterBloc, const Change(currentState: 2, nextState: 3), ), () => observer.onDone(counterBloc, CounterEvent.increment), () => observer.onClose(counterBloc), ]); expect(counterBloc.state, equals(3)); }); counterBloc ..add(CounterEvent.increment) ..add(CounterEvent.increment) ..add(CounterEvent.increment) ..close(); }); test('is a broadcast stream', () { final expectedStates = [1, emitsDone]; expect(counterBloc.stream.isBroadcast, isTrue); expectLater(counterBloc.stream, emitsInOrder(expectedStates)); expectLater(counterBloc.stream, emitsInOrder(expectedStates)); counterBloc ..add(CounterEvent.increment) ..close(); }); test('multiple subscribers receive the latest state', () { const expected = [1]; expectLater(counterBloc.stream, emitsInOrder(expected)); expectLater(counterBloc.stream, emitsInOrder(expected)); expectLater(counterBloc.stream, emitsInOrder(expected)); counterBloc.add(CounterEvent.increment); }); test('maintains correct transition composition', () { final expectedTransitions = >[ const Transition( currentState: 0, event: CounterEvent.decrement, nextState: -1, ), const Transition( currentState: -1, event: CounterEvent.increment, nextState: 0, ), ]; final expectedStates = [-1, 0, emitsDone]; final transitions = >[]; final counterBloc = CounterBloc(onTransitionCallback: transitions.add); expectLater( counterBloc.stream, emitsInOrder(expectedStates), ).then((dynamic _) { expect(transitions, expectedTransitions); }); counterBloc ..add(CounterEvent.decrement) ..add(CounterEvent.increment) ..close(); }); test('events are processed asynchronously', () async { expect(counterBloc.state, 0); expect(events.isEmpty, true); expect(transitions.isEmpty, true); counterBloc.add(CounterEvent.increment); expect(counterBloc.state, 0); expect(events, [CounterEvent.increment]); expect(transitions.isEmpty, true); await tick(); expect(counterBloc.state, 1); expect(events, [CounterEvent.increment]); expect( transitions, const [ '''Transition { currentState: 0, event: CounterEvent.increment, nextState: 1 }''', ], ); }); }); group('Async Bloc', () { late AsyncBloc asyncBloc; late MockBlocObserver observer; setUpAll(() { registerFallbackValue(FakeBlocBase()); registerFallbackValue(StackTrace.empty); }); setUp(() { asyncBloc = AsyncBloc(); observer = MockBlocObserver(); Bloc.observer = observer; }); test('close does not emit new states over the state stream', () async { final expectedStates = [emitsDone]; unawaited(expectLater(asyncBloc.stream, emitsInOrder(expectedStates))); await asyncBloc.close(); }); test( 'close while events are pending finishes processing pending events ' 'and does not trigger onError', () async { final expectedStates = [ AsyncState.initial().copyWith(isLoading: true), AsyncState.initial().copyWith(isSuccess: true), ]; final states = []; final asyncBloc = AsyncBloc() ..stream.listen(states.add) ..add(AsyncEvent()); await asyncBloc.close(); expect(states, expectedStates); verify(() => observer.onDone(asyncBloc, AsyncEvent())).called(1); verifyNever(() => observer.onError(any(), any(), any())); }); test('state returns correct value initially', () { expect(asyncBloc.state, AsyncState.initial()); }); test('should map single event to correct state', () { final expectedStates = [ const AsyncState(isLoading: true, hasError: false, isSuccess: false), const AsyncState(isLoading: false, hasError: false, isSuccess: true), emitsDone, ]; final asyncBloc = AsyncBloc(); expectLater( asyncBloc.stream, emitsInOrder(expectedStates), ).then((dynamic _) { verifyInOrder([ () => observer.onCreate(asyncBloc), () => observer.onEvent(asyncBloc, AsyncEvent()), () => observer.onTransition( asyncBloc, Transition( currentState: const AsyncState( isLoading: false, hasError: false, isSuccess: false, ), event: AsyncEvent(), nextState: const AsyncState( isLoading: true, hasError: false, isSuccess: false, ), ), ), () => observer.onChange( asyncBloc, const Change( currentState: AsyncState( isLoading: false, hasError: false, isSuccess: false, ), nextState: AsyncState( isLoading: true, hasError: false, isSuccess: false, ), ), ), () => observer.onTransition( asyncBloc, Transition( currentState: const AsyncState( isLoading: true, hasError: false, isSuccess: false, ), event: AsyncEvent(), nextState: const AsyncState( isLoading: false, hasError: false, isSuccess: true, ), ), ), () => observer.onChange( asyncBloc, const Change( currentState: AsyncState( isLoading: true, hasError: false, isSuccess: false, ), nextState: AsyncState( isLoading: false, hasError: false, isSuccess: true, ), ), ), () => observer.onDone(asyncBloc, AsyncEvent()), () => observer.onClose(asyncBloc), ]); expect( asyncBloc.state, const AsyncState( isLoading: false, hasError: false, isSuccess: true, ), ); }); asyncBloc ..add(AsyncEvent()) ..close(); }); test('should map multiple events to correct states', () { final expectedStates = [ const AsyncState(isLoading: true, hasError: false, isSuccess: false), const AsyncState(isLoading: false, hasError: false, isSuccess: true), const AsyncState(isLoading: true, hasError: false, isSuccess: false), const AsyncState(isLoading: false, hasError: false, isSuccess: true), emitsDone, ]; final asyncBloc = AsyncBloc(); expectLater( asyncBloc.stream, emitsInOrder(expectedStates), ).then((dynamic _) { verifyInOrder([ () => observer.onCreate(asyncBloc), () => observer.onEvent(asyncBloc, AsyncEvent()), () => observer.onEvent(asyncBloc, AsyncEvent()), () => observer.onTransition( asyncBloc, Transition( currentState: const AsyncState( isLoading: false, hasError: false, isSuccess: false, ), event: AsyncEvent(), nextState: const AsyncState( isLoading: true, hasError: false, isSuccess: false, ), ), ), () => observer.onChange( asyncBloc, const Change( currentState: AsyncState( isLoading: false, hasError: false, isSuccess: false, ), nextState: AsyncState( isLoading: true, hasError: false, isSuccess: false, ), ), ), () => observer.onTransition( asyncBloc, Transition( currentState: const AsyncState( isLoading: true, hasError: false, isSuccess: false, ), event: AsyncEvent(), nextState: const AsyncState( isLoading: false, hasError: false, isSuccess: true, ), ), ), () => observer.onChange( asyncBloc, const Change( currentState: AsyncState( isLoading: true, hasError: false, isSuccess: false, ), nextState: AsyncState( isLoading: false, hasError: false, isSuccess: true, ), ), ), () => observer.onDone(asyncBloc, AsyncEvent()), () => observer.onTransition( asyncBloc, Transition( currentState: const AsyncState( isLoading: false, hasError: false, isSuccess: true, ), event: AsyncEvent(), nextState: const AsyncState( isLoading: true, hasError: false, isSuccess: false, ), ), ), () => observer.onChange( asyncBloc, const Change( currentState: AsyncState( isLoading: false, hasError: false, isSuccess: true, ), nextState: AsyncState( isLoading: true, hasError: false, isSuccess: false, ), ), ), () => observer.onTransition( asyncBloc, Transition( currentState: const AsyncState( isLoading: true, hasError: false, isSuccess: false, ), event: AsyncEvent(), nextState: const AsyncState( isLoading: false, hasError: false, isSuccess: true, ), ), ), () => observer.onChange( asyncBloc, const Change( currentState: AsyncState( isLoading: true, hasError: false, isSuccess: false, ), nextState: AsyncState( isLoading: false, hasError: false, isSuccess: true, ), ), ), () => observer.onDone(asyncBloc, AsyncEvent()), () => observer.onClose(asyncBloc), ]); expect( asyncBloc.state, const AsyncState( isLoading: false, hasError: false, isSuccess: true, ), ); }); asyncBloc ..add(AsyncEvent()) ..add(AsyncEvent()) ..close(); }); test('is a broadcast stream', () { final expectedStates = [ const AsyncState(isLoading: true, hasError: false, isSuccess: false), const AsyncState(isLoading: false, hasError: false, isSuccess: true), emitsDone, ]; expect(asyncBloc.stream.isBroadcast, isTrue); expectLater(asyncBloc.stream, emitsInOrder(expectedStates)); expectLater(asyncBloc.stream, emitsInOrder(expectedStates)); asyncBloc ..add(AsyncEvent()) ..close(); }); test('multiple subscribers receive the latest state', () { final expected = [ const AsyncState(isLoading: true, hasError: false, isSuccess: false), const AsyncState(isLoading: false, hasError: false, isSuccess: true), ]; expectLater(asyncBloc.stream, emitsInOrder(expected)); expectLater(asyncBloc.stream, emitsInOrder(expected)); expectLater(asyncBloc.stream, emitsInOrder(expected)); asyncBloc.add(AsyncEvent()); }); }); group('MergeBloc', () { test('maintains correct transition composition', () { final expectedTransitions = >[ const Transition( currentState: 0, event: CounterEvent.increment, nextState: 1, ), const Transition( currentState: 1, event: CounterEvent.decrement, nextState: 0, ), const Transition( currentState: 0, event: CounterEvent.decrement, nextState: -1, ), ]; final expectedStates = [1, 0, -1, emitsDone]; final transitions = >[]; final bloc = MergeBloc( onTransitionCallback: transitions.add, ); expectLater( bloc.stream, emitsInOrder(expectedStates), ).then((dynamic _) { expect(transitions, expectedTransitions); }); bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment) ..add(CounterEvent.decrement) ..add(CounterEvent.decrement) ..close(); }); }); group('SeededBloc', () { test('does not emit repeated states', () { final seededBloc = SeededBloc(seed: 0, states: [1, 2, 1, 1]); final expectedStates = [1, 2, 1, emitsDone]; expectLater(seededBloc.stream, emitsInOrder(expectedStates)); seededBloc ..add('event') ..close(); }); test('can emit initial state only once', () { final seededBloc = SeededBloc(seed: 0, states: [0, 0]); final expectedStates = [0, emitsDone]; expectLater(seededBloc.stream, emitsInOrder(expectedStates)); seededBloc ..add('event') ..close(); }); test( 'can emit initial state and ' 'continue emitting distinct states', () { final seededBloc = SeededBloc(seed: 0, states: [0, 0, 1]); final expectedStates = [0, 1, emitsDone]; expectLater(seededBloc.stream, emitsInOrder(expectedStates)); seededBloc ..add('event') ..close(); }); test('discards subsequent duplicate states (distinct events)', () { final seededBloc = SeededBloc(seed: 0, states: [1, 1]); final expectedStates = [1, emitsDone]; expectLater(seededBloc.stream, emitsInOrder(expectedStates)); seededBloc ..add('eventA') ..add('eventB') ..add('eventC') ..close(); }); test('discards subsequent duplicate states (same event)', () { final seededBloc = SeededBloc(seed: 0, states: [1, 1]); final expectedStates = [1, emitsDone]; expectLater(seededBloc.stream, emitsInOrder(expectedStates)); seededBloc ..add('event') ..add('event') ..add('event') ..close(); }); }); group('StreamBloc', () { test('cancels subscriptions correctly', () async { const expectedStates = [0, 1, 2, 3, 4]; final states = []; final controller = StreamController.broadcast(); final bloc = StreamBloc(controller.stream) ..stream.listen(states.add) ..add(Subscribe()); await tick(); controller ..add(0) ..add(1) ..add(2); await tick(); bloc.add(Subscribe()); await tick(); controller ..add(3) ..add(4); await Future.delayed(const Duration(milliseconds: 300)); await bloc.close(); expect(states, equals(expectedStates)); }); }); group('RestartableStreamBloc', () { test('unawaited forEach throws AssertionError', () async { late final Object blocError; await runZonedGuarded(() async { final controller = StreamController.broadcast(); final bloc = RestartableStreamBloc(controller.stream) ..add(UnawaitedForEach()); await tick(); controller.add(0); await tick(); await Future.delayed(const Duration(milliseconds: 300)); await bloc.close(); }, (error, stackTrace) { blocError = error; }); expect( blocError, isA().having( (e) => e.message, 'message', contains( '''An event handler completed but left pending subscriptions behind.''', ), ), ); }); test('forEach cancels subscriptions correctly', () async { const expectedStates = [0, 1, 2, 3, 4]; final states = []; final controller = StreamController.broadcast(); final bloc = RestartableStreamBloc(controller.stream) ..stream.listen(states.add) ..add(ForEach()); await tick(); controller ..add(0) ..add(1) ..add(2); await tick(); bloc.add(ForEach()); await tick(); controller ..add(3) ..add(4); await bloc.close(); expect(states, equals(expectedStates)); }); test( 'forEach with onError handles errors emitted by stream ' 'and does not cancel the subscription', () async { const expectedStates = [1, 2, 3, -1, 4, 5, 6]; final error = Exception('oops'); final states = []; final controller = StreamController.broadcast(); final bloc = RestartableStreamBloc(controller.stream) ..stream.listen(states.add) ..add(ForEachOnError()); await tick(); controller ..add(1) ..add(2) ..add(3); await tick(); controller ..addError(error) ..add(4) ..add(5) ..add(6); await tick(); expect(states, equals(expectedStates)); await bloc.close(); }); test('forEach with try/catch handles errors emitted by stream', () async { const expectedStates = [1, 2, 3, -1]; final error = Exception('oops'); final states = []; final controller = StreamController.broadcast(); final bloc = RestartableStreamBloc(controller.stream) ..stream.listen(states.add) ..add(ForEachTryCatch()); await tick(); controller ..add(1) ..add(2) ..add(3); await tick(); controller.addError(error); await tick(); expect(states, equals(expectedStates)); await bloc.close(); }); test( 'forEach with catchError ' 'handles errors emitted by stream', () async { const expectedStates = [1, 2, 3, -1]; final error = Exception('oops'); final states = []; final controller = StreamController.broadcast(); final bloc = RestartableStreamBloc(controller.stream) ..stream.listen(states.add) ..add(ForEachCatchError()); await tick(); controller ..add(1) ..add(2) ..add(3); await tick(); controller.addError(error); await tick(); expect(states, equals(expectedStates)); await bloc.close(); }); test('forEach throws when stream emits error', () async { const expectedStates = [1, 2, 3]; final error = Exception('oops'); final states = []; late final dynamic uncaughtError; await runZonedGuarded( () async { final controller = StreamController.broadcast(); final bloc = RestartableStreamBloc(controller.stream) ..stream.listen(states.add) ..add(ForEach()); await tick(); controller ..add(1) ..add(2) ..add(3); await tick(); controller ..addError(error) ..add(3) ..add(4) ..add(5); await bloc.close(); }, (error, stackTrace) => uncaughtError = error, ); expect(states, equals(expectedStates)); expect(uncaughtError, equals(error)); }); test('unawaited onEach throws AssertionError', () async { late final Object blocError; await runZonedGuarded(() async { final controller = StreamController.broadcast(); final bloc = RestartableStreamBloc(controller.stream) ..add(UnawaitedOnEach()); await bloc.close(); }, (error, stackTrace) { blocError = error; }); expect( blocError, isA().having( (e) => e.message, 'message', contains( '''An event handler completed but left pending subscriptions behind.''', ), ), ); }); test( 'onEach with onError handles errors emitted by stream ' 'and does not cancel subscription', () async { const expectedStates = [1, 2, 3, -1, 4, 5, 6]; final error = Exception('oops'); final states = []; final controller = StreamController.broadcast(); final bloc = RestartableStreamBloc(controller.stream) ..stream.listen(states.add) ..add(OnEachOnError()); await tick(); controller ..add(1) ..add(2) ..add(3); await tick(); await Future.delayed(const Duration(milliseconds: 300)); controller ..addError(error) ..add(4) ..add(5) ..add(6); await tick(); await Future.delayed(const Duration(milliseconds: 300)); expect(states, equals(expectedStates)); await bloc.close(); }); test('onEach with try/catch handles errors emitted by stream', () async { const expectedStates = [1, 2, 3, -1]; final error = Exception('oops'); final states = []; final controller = StreamController.broadcast(); final bloc = RestartableStreamBloc(controller.stream) ..stream.listen(states.add) ..add(OnEachTryCatch()); await tick(); controller ..add(1) ..add(2) ..add(3); await tick(); await Future.delayed(const Duration(milliseconds: 300)); controller.addError(error); await tick(); expect(states, equals(expectedStates)); await bloc.close(); }); test( 'onEach with try/catch handles errors ' 'emitted by stream and cancels delayed emits', () async { const expectedStates = [-1]; final error = Exception('oops'); final states = []; final controller = StreamController.broadcast(); final bloc = RestartableStreamBloc(controller.stream) ..stream.listen(states.add) ..add(OnEachTryCatchAbort()); await tick(); controller ..add(1) ..add(2) ..add(3) ..addError(error); await tick(); await Future.delayed(const Duration(milliseconds: 300)); expect(states, equals(expectedStates)); await bloc.close(); }); test( 'onEach with catchError ' 'handles errors emitted by stream', () async { const expectedStates = [1, 2, 3, -1]; final error = Exception('oops'); final states = []; final controller = StreamController.broadcast(); final bloc = RestartableStreamBloc(controller.stream) ..stream.listen(states.add) ..add(OnEachCatchError()); await tick(); controller ..add(1) ..add(2) ..add(3); await tick(); await Future.delayed(const Duration(milliseconds: 300)); controller.addError(error); await tick(); expect(states, equals(expectedStates)); await bloc.close(); }); test('onEach cancels subscriptions correctly', () async { const expectedStates = [3, 4]; final states = []; final controller = StreamController.broadcast(); final bloc = RestartableStreamBloc(controller.stream) ..stream.listen(states.add) ..add(OnEach()); await tick(); controller ..add(0) ..add(1) ..add(2); bloc.add(OnEach()); await tick(); controller ..add(3) ..add(4); await Future.delayed(const Duration(milliseconds: 300)); await bloc.close(); expect(states, equals(expectedStates)); }); test('onEach throws when stream emits error', () async { const expectedStates = [1, 2, 3]; final error = Exception('oops'); final states = []; late final dynamic uncaughtError; await runZonedGuarded( () async { final controller = StreamController.broadcast(); final bloc = RestartableStreamBloc(controller.stream) ..stream.listen(states.add) ..add(OnEach()); await tick(); controller ..add(1) ..add(2) ..add(3); await tick(); await Future.delayed(const Duration(milliseconds: 300)); controller ..addError(error) ..add(4) ..add(5) ..add(6); await tick(); await Future.delayed(const Duration(milliseconds: 300)); await bloc.close(); }, (error, stack) => uncaughtError = error, ); expect(states, equals(expectedStates)); expect(uncaughtError, equals(error)); }); }); group('UnawaitedBloc', () { test( 'throws AssertionError when emit is called ' 'after the event handler completed normally', () async { late final Object blocError; await runZonedGuarded( () async { final completer = Completer(); final bloc = UnawaitedBloc(completer.future)..add(UnawaitedEvent()); await tick(); completer.complete(); await tick(); await bloc.close(); }, (error, stackTrace) => blocError = error, ); expect( blocError, isA().having( (e) => e.message, 'message', contains( 'emit was called after an event handler completed normally.', ), ), ); }); }); group('Exception', () { test('does not break stream', () { runZonedGuarded(() { final expectedStates = [-1, emitsDone]; final counterBloc = CounterExceptionBloc(); expectLater(counterBloc.stream, emitsInOrder(expectedStates)); counterBloc ..add(CounterEvent.increment) ..add(CounterEvent.decrement) ..close(); }, (Object error, StackTrace stackTrace) { expect(error.toString(), equals('Exception: fatal exception')); expect(stackTrace, isNotNull); expect(stackTrace, isNot(StackTrace.empty)); }); }); test('addError triggers onError', () async { final expectedError = Exception('fatal exception'); runZonedGuarded(() { OnExceptionBloc( exception: expectedError, onErrorCallback: (Object _, StackTrace __) {}, ).addError(expectedError, StackTrace.current); }, (Object error, StackTrace stackTrace) { expect(error, equals(expectedError)); expect(stackTrace, isNotNull); expect(stackTrace, isNot(StackTrace.empty)); }); }); test('triggers onError from on', () { final exception = Exception('fatal exception'); runZonedGuarded(() { Object? expectedError; StackTrace? expectedStacktrace; final onExceptionBloc = OnExceptionBloc( exception: exception, onErrorCallback: (Object error, StackTrace stackTrace) { expectedError = error; expectedStacktrace = stackTrace; }, ); expectLater( onExceptionBloc.stream, emitsInOrder([emitsDone]), ).then((dynamic _) { expect(expectedError, exception); expect(expectedStacktrace, isNotNull); expect(expectedStacktrace, isNot(StackTrace.empty)); }); onExceptionBloc ..add(CounterEvent.increment) ..close(); }, (Object error, StackTrace stackTrace) { expect(error, equals(exception)); expect(stackTrace, isNotNull); expect(stackTrace, isNot(StackTrace.empty)); }); }); test('triggers onError from onEvent', () { final exception = Exception('fatal exception'); runZonedGuarded(() { OnEventErrorBloc(exception: exception) ..add(CounterEvent.increment) ..close(); }, (Object error, StackTrace stackTrace) { expect(error, equals(exception)); expect(stackTrace, isNotNull); expect(stackTrace, isNot(StackTrace.empty)); }); }); test( 'add throws StateError and triggers onError ' 'when bloc is closed', () { Object? capturedError; StackTrace? capturedStacktrace; var didThrow = false; runZonedGuarded(() { final counterBloc = CounterBloc( onErrorCallback: (error, stackTrace) { capturedError = error; capturedStacktrace = stackTrace; }, ); expectLater( counterBloc.stream, emitsInOrder([emitsDone]), ); counterBloc ..close() ..add(CounterEvent.increment); }, (Object error, StackTrace stackTrace) { didThrow = true; expect(error, equals(capturedError)); expect(stackTrace, equals(capturedStacktrace)); }); expect(didThrow, isTrue); expect( capturedError, isA().having( (e) => e.message, 'message', 'Cannot add new events after calling close', ), ); expect(capturedStacktrace, isNotNull); }); }); group('Error', () { test('does not break stream', () { runZonedGuarded( () { final expectedStates = [-1, emitsDone]; final counterBloc = CounterErrorBloc(); expectLater(counterBloc.stream, emitsInOrder(expectedStates)); counterBloc ..add(CounterEvent.increment) ..add(CounterEvent.decrement) ..close(); }, (Object _, StackTrace __) {}, ); }); test('triggers onError from event handler', () { runZonedGuarded( () { final error = Error(); Object? expectedError; StackTrace? expectedStacktrace; final onErrorBloc = OnErrorBloc( error: error, onErrorCallback: (Object error, StackTrace stackTrace) { expectedError = error; expectedStacktrace = stackTrace; }, ); expectLater( onErrorBloc.stream, emitsInOrder([emitsDone]), ).then((dynamic _) { expect(expectedError, error); expect(expectedStacktrace, isNotNull); }); onErrorBloc ..add(CounterEvent.increment) ..close(); }, (Object _, StackTrace __) {}, ); }); test('triggers onError from onTransition', () { runZonedGuarded( () { final error = Error(); Object? expectedError; StackTrace? expectedStacktrace; final onTransitionErrorBloc = OnTransitionErrorBloc( error: error, onErrorCallback: (Object error, StackTrace stackTrace) { expectedError = error; expectedStacktrace = stackTrace; }, ); expectLater( onTransitionErrorBloc.stream, emitsInOrder([emitsDone]), ).then((dynamic _) { expect(expectedError, error); expect(expectedStacktrace, isNotNull); expect(onTransitionErrorBloc.state, 0); }); onTransitionErrorBloc ..add(CounterEvent.increment) ..close(); }, (Object _, StackTrace __) {}, ); }); }); group('emit', () { test('updates the state', () async { final counterBloc = CounterBloc(); unawaited( expectLater(counterBloc.stream, emitsInOrder(const [42])), ); counterBloc.emit(42); expect(counterBloc.state, 42); await counterBloc.close(); }); test( 'throws StateError and triggers onError ' 'when bloc is closed', () async { Object? capturedError; StackTrace? capturedStacktrace; final states = []; final expectedStateError = isA().having( (e) => e.message, 'message', 'Cannot emit new states after calling close', ); final counterBloc = CounterBloc( onErrorCallback: (error, stackTrace) { capturedError = error; capturedStacktrace = stackTrace; }, )..stream.listen(states.add); await counterBloc.close(); expect(counterBloc.isClosed, isTrue); expect(counterBloc.state, equals(0)); expect(states, isEmpty); expect(() => counterBloc.emit(1), throwsA(expectedStateError)); expect(counterBloc.state, equals(0)); expect(states, isEmpty); expect(capturedError, expectedStateError); expect(capturedStacktrace, isNotNull); }); }); group('close', () { test('emits done (sync)', () { final bloc = CounterBloc()..close(); expect(bloc.stream, emitsDone); }); test('emits done (async)', () async { final bloc = CounterBloc(); await bloc.close(); expect(bloc.stream, emitsDone); }); }); group('isClosed', () { test('returns true after bloc is closed', () async { final bloc = CounterBloc(); expect(bloc.isClosed, isFalse); await bloc.close(); expect(bloc.isClosed, isTrue); }); }); }); } void unawaited(Future future) {} ================================================ FILE: packages/bloc/test/blocs/async/async_bloc.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; part 'async_event.dart'; part 'async_state.dart'; class AsyncBloc extends Bloc { AsyncBloc() : super(AsyncState.initial()) { on( (event, emit) async { emit(state.copyWith(isLoading: true, isSuccess: false)); await Future.delayed(Duration.zero); emit(state.copyWith(isLoading: false, isSuccess: true)); }, transformer: (events, mapper) => events.asyncExpand(mapper), ); } } ================================================ FILE: packages/bloc/test/blocs/async/async_event.dart ================================================ part of 'async_bloc.dart'; @immutable class AsyncEvent { @override bool operator ==( Object other, ) => identical( this, other, ) || other is AsyncEvent && runtimeType == other.runtimeType; @override int get hashCode => Object.hashAll([runtimeType]); @override String toString() => 'AsyncEvent'; } ================================================ FILE: packages/bloc/test/blocs/async/async_state.dart ================================================ part of 'async_bloc.dart'; @immutable class AsyncState { const AsyncState({ required this.isLoading, required this.hasError, required this.isSuccess, }); factory AsyncState.initial() { return const AsyncState( isLoading: false, hasError: false, isSuccess: false, ); } final bool isLoading; final bool hasError; final bool isSuccess; AsyncState copyWith({bool? isLoading, bool? hasError, bool? isSuccess}) { return AsyncState( isLoading: isLoading ?? this.isLoading, hasError: hasError ?? this.hasError, isSuccess: isSuccess ?? this.isSuccess, ); } @override bool operator ==( Object other, ) => identical( this, other, ) || other is AsyncState && runtimeType == other.runtimeType && isLoading == other.isLoading && hasError == other.hasError && isSuccess == other.isSuccess; @override int get hashCode => Object.hashAll([isLoading, hasError, isSuccess]); @override String toString() => 'AsyncState { isLoading: $isLoading, hasError: $hasError, ' 'isSuccess: $isSuccess }'; } ================================================ FILE: packages/bloc/test/blocs/blocs.dart ================================================ export './async/async_bloc.dart'; export './complex/complex_bloc.dart'; export './counter/counter.dart'; export './seeded/seeded_bloc.dart'; export './simple/simple_bloc.dart'; export './stream/stream.dart'; export './unawaited/unawaited_bloc.dart'; ================================================ FILE: packages/bloc/test/blocs/complex/complex_bloc.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import 'package:stream_transform/stream_transform.dart'; part 'complex_event.dart'; part 'complex_state.dart'; const _delay = Duration(milliseconds: 100); class ComplexBloc extends Bloc { ComplexBloc() : super(ComplexStateA()) { on((_, emit) => emit(ComplexStateA())); on((_, emit) => emit(ComplexStateB())); on( (_, emit) => Future.delayed(_delay, () => emit(ComplexStateC())), ); on( (_, emit) => Future.delayed(_delay, () => emit(ComplexStateD())), ); } @override Stream get stream { return super.stream.debounce(const Duration(milliseconds: 50)); } } ================================================ FILE: packages/bloc/test/blocs/complex/complex_event.dart ================================================ part of 'complex_bloc.dart'; @immutable abstract class ComplexEvent {} class ComplexEventA extends ComplexEvent { @override bool operator ==( Object other, ) => identical( this, other, ) || other is ComplexEventA && runtimeType == other.runtimeType; @override int get hashCode => 0; } class ComplexEventB extends ComplexEvent { @override bool operator ==( Object other, ) => identical( this, other, ) || other is ComplexEventB && runtimeType == other.runtimeType; @override int get hashCode => 1; } class ComplexEventC extends ComplexEvent { @override bool operator ==( Object other, ) => identical( this, other, ) || other is ComplexEventC && runtimeType == other.runtimeType; @override int get hashCode => 2; } class ComplexEventD extends ComplexEvent { @override bool operator ==( Object other, ) => identical( this, other, ) || other is ComplexEventD && runtimeType == other.runtimeType; @override int get hashCode => 3; } ================================================ FILE: packages/bloc/test/blocs/complex/complex_state.dart ================================================ part of 'complex_bloc.dart'; @immutable abstract class ComplexState {} class ComplexStateA extends ComplexState { @override bool operator ==( Object other, ) => identical( this, other, ) || other is ComplexStateA && runtimeType == other.runtimeType; @override int get hashCode => 0; } class ComplexStateB extends ComplexState { @override bool operator ==( Object other, ) => identical( this, other, ) || other is ComplexStateB && runtimeType == other.runtimeType; @override int get hashCode => 1; } class ComplexStateC extends ComplexState { @override bool operator ==( Object other, ) => identical( this, other, ) || other is ComplexStateC && runtimeType == other.runtimeType; @override int get hashCode => 2; } class ComplexStateD extends ComplexState { @override bool operator ==( Object other, ) => identical( this, other, ) || other is ComplexStateD && runtimeType == other.runtimeType; @override int get hashCode => 3; } ================================================ FILE: packages/bloc/test/blocs/counter/counter.dart ================================================ export './counter_bloc.dart'; export './counter_error_bloc.dart'; export './counter_exception_bloc.dart'; export './merge_bloc.dart'; export './on_error_bloc.dart'; export './on_event_error_bloc.dart'; export './on_exception_bloc.dart'; export './on_transition_error_bloc.dart'; ================================================ FILE: packages/bloc/test/blocs/counter/counter_bloc.dart ================================================ import 'package:bloc/bloc.dart'; typedef OnEventCallback = void Function(CounterEvent); typedef OnTransitionCallback = void Function(Transition); typedef OnErrorCallback = void Function(Object error, StackTrace? stackTrace); enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc({ this.onEventCallback, this.onTransitionCallback, this.onErrorCallback, }) : super(0) { on(_onCounterEvent); } final OnEventCallback? onEventCallback; final OnTransitionCallback? onTransitionCallback; final OnErrorCallback? onErrorCallback; @override void onEvent(CounterEvent event) { super.onEvent(event); onEventCallback?.call(event); } @override void onTransition(Transition transition) { super.onTransition(transition); onTransitionCallback?.call(transition); } @override void onError(Object error, StackTrace stackTrace) { onErrorCallback?.call(error, stackTrace); super.onError(error, stackTrace); } void _onCounterEvent(CounterEvent event, Emitter emit) { switch (event) { case CounterEvent.increment: return emit(state + 1); case CounterEvent.decrement: return emit(state - 1); } } } ================================================ FILE: packages/bloc/test/blocs/counter/counter_error_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import '../counter/counter_bloc.dart'; class CounterErrorBloc extends Bloc { CounterErrorBloc() : super(0) { on(_onCounterEvent); } void _onCounterEvent(CounterEvent event, Emitter emit) { switch (event) { case CounterEvent.decrement: return emit(state - 1); case CounterEvent.increment: throw Error(); } } } ================================================ FILE: packages/bloc/test/blocs/counter/counter_exception_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import '../counter/counter_bloc.dart'; class CounterExceptionBloc extends Bloc { CounterExceptionBloc() : super(0) { on(_onCounterEvent); } void _onCounterEvent(CounterEvent event, Emitter emit) { switch (event) { case CounterEvent.decrement: return emit(state - 1); case CounterEvent.increment: throw Exception('fatal exception'); } } } ================================================ FILE: packages/bloc/test/blocs/counter/merge_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:stream_transform/stream_transform.dart'; import '../blocs.dart'; EventTransformer customTransformer() { return (events, mapper) { final nonDebounceStream = events.where((event) => event != CounterEvent.increment); final debounceStream = events .where((event) => event == CounterEvent.increment) .throttle(const Duration(milliseconds: 100)); return nonDebounceStream .merge(debounceStream) .concurrentAsyncExpand(mapper); }; } class MergeBloc extends Bloc { MergeBloc({this.onTransitionCallback}) : super(0) { on(_onCounterEvent, transformer: customTransformer()); } final void Function(Transition)? onTransitionCallback; @override void onTransition(Transition transition) { super.onTransition(transition); onTransitionCallback?.call(transition); } void _onCounterEvent(CounterEvent event, Emitter emit) { switch (event) { case CounterEvent.increment: return emit(state + 1); case CounterEvent.decrement: return emit(state - 1); } } } ================================================ FILE: packages/bloc/test/blocs/counter/on_error_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import '../counter/counter_bloc.dart'; class OnErrorBloc extends Bloc { OnErrorBloc({required this.error, required this.onErrorCallback}) : super(0) { on(_onCounterEvent); } final void Function(Object, StackTrace) onErrorCallback; final Error error; @override void onError(Object error, StackTrace stackTrace) { onErrorCallback(error, stackTrace); super.onError(error, stackTrace); } void _onCounterEvent(CounterEvent event, Emitter emit) { throw error; } } ================================================ FILE: packages/bloc/test/blocs/counter/on_event_error_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import '../counter/counter_bloc.dart'; class OnEventErrorBloc extends Bloc { OnEventErrorBloc({required this.exception}) : super(0) { on((_, __) {}); } final Exception exception; @override // ignore: must_call_super void onEvent(CounterEvent event) { throw exception; } } ================================================ FILE: packages/bloc/test/blocs/counter/on_exception_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import '../counter/counter_bloc.dart'; class OnExceptionBloc extends Bloc { OnExceptionBloc({ required this.exception, required this.onErrorCallback, }) : super(0) { on(_onCounterEvent); } final void Function(Object, StackTrace) onErrorCallback; final Exception exception; @override void onError(Object error, StackTrace stackTrace) { onErrorCallback(error, stackTrace); super.onError(error, stackTrace); } void _onCounterEvent(CounterEvent event, Emitter emit) { throw exception; } } ================================================ FILE: packages/bloc/test/blocs/counter/on_transition_error_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import '../counter/counter_bloc.dart'; class OnTransitionErrorBloc extends Bloc { OnTransitionErrorBloc({ required this.error, required this.onErrorCallback, }) : super(0) { on(_onCounterEvent); } final void Function(Object, StackTrace) onErrorCallback; final Error error; @override void onError(Object error, StackTrace stackTrace) { onErrorCallback(error, stackTrace); super.onError(error, stackTrace); } @override void onTransition(Transition transition) { super.onTransition(transition); throw error; } void _onCounterEvent(CounterEvent event, Emitter emit) { switch (event) { case CounterEvent.increment: return emit(state + 1); case CounterEvent.decrement: return emit(state - 1); } } } ================================================ FILE: packages/bloc/test/blocs/seeded/seeded_bloc.dart ================================================ import 'package:bloc/bloc.dart'; class SeededBloc extends Bloc { SeededBloc({required this.seed, required this.states}) : super(seed) { on((event, emit) { states.forEach(emit.call); }); } final List states; final int seed; } ================================================ FILE: packages/bloc/test/blocs/simple/simple_bloc.dart ================================================ import 'package:bloc/bloc.dart'; class SimpleBloc extends Bloc { SimpleBloc() : super('') { on((_, emit) => emit('data')); } } ================================================ FILE: packages/bloc/test/blocs/stream/restartable_stream_bloc.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:stream_transform/stream_transform.dart'; abstract class RestartableStreamEvent {} class ForEach extends RestartableStreamEvent {} class ForEachOnError extends RestartableStreamEvent {} class ForEachTryCatch extends RestartableStreamEvent {} class ForEachCatchError extends RestartableStreamEvent {} class UnawaitedForEach extends RestartableStreamEvent {} class OnEach extends RestartableStreamEvent {} class OnEachOnError extends RestartableStreamEvent {} class OnEachTryCatch extends RestartableStreamEvent {} class OnEachTryCatchAbort extends RestartableStreamEvent {} class OnEachCatchError extends RestartableStreamEvent {} class UnawaitedOnEach extends RestartableStreamEvent {} const _delay = Duration(milliseconds: 100); class RestartableStreamBloc extends Bloc { RestartableStreamBloc(Stream stream) : super(0) { on( (_, emit) async { await emit.forEach( stream, onData: (i) => i, ); }, transformer: (events, mapper) => events.switchMap(mapper), ); on( (_, emit) async { try { await emit.forEach( stream, onData: (i) => i, onError: (_, __) => -1, ); } catch (_) { emit(-1); } }, transformer: (events, mapper) => events.switchMap(mapper), ); on( (_, emit) async { try { await emit.forEach( stream, onData: (i) => i, ); } catch (_) { emit(-1); } }, transformer: (events, mapper) => events.switchMap(mapper), ); on( (_, emit) => emit .forEach( stream, onData: (i) => i, ) .catchError((dynamic _) => emit(-1)), transformer: (events, mapper) => events.switchMap(mapper), ); on( (_, emit) { emit.forEach( stream, onData: (i) => i, ); }, transformer: (events, mapper) => events.switchMap(mapper), ); on( (_, emit) async { await emit.onEach( stream, onData: (i) => Future.delayed(_delay, () => emit(i)), ); }, transformer: (events, mapper) => events.switchMap(mapper), ); on( (_, emit) async { await emit.onEach( stream, onData: (i) => Future.delayed(_delay, () => emit(i)), onError: (_, __) => emit(-1), ); }, transformer: (events, mapper) => events.switchMap(mapper), ); on( (_, emit) async { try { await emit.onEach( stream, onData: (i) => Future.delayed(_delay, () => emit(i)), ); } catch (_) { emit(-1); } }, transformer: (events, mapper) => events.switchMap(mapper), ); on( (_, emit) async { try { await emit.onEach( stream, onData: (i) => Future.delayed(_delay, () { if (emit.isDone) return; emit(i); }), ); } catch (_) { emit(-1); } }, transformer: (events, mapper) => events.switchMap(mapper), ); on( (_, emit) => emit .onEach( stream, onData: (i) => Future.delayed(_delay, () => emit(i)), ) .catchError((dynamic _) => emit(-1)), transformer: (events, mapper) => events.switchMap(mapper), ); on( (_, emit) { emit.onEach( stream, onData: (i) => Future.delayed(_delay, () => emit(i)), ); }, transformer: (events, mapper) => events.switchMap(mapper), ); } } ================================================ FILE: packages/bloc/test/blocs/stream/stream.dart ================================================ export 'restartable_stream_bloc.dart'; export 'stream_bloc.dart'; ================================================ FILE: packages/bloc/test/blocs/stream/stream_bloc.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; abstract class StreamEvent {} class Subscribe extends StreamEvent {} class OnData extends StreamEvent { OnData(this.data); final int data; } class StreamBloc extends Bloc { StreamBloc(Stream stream) : super(0) { on((_, emit) { _subscription?.cancel(); _subscription = stream.listen((i) { Future.delayed( const Duration(milliseconds: 100), () => add(OnData(i)), ); }); }); on((event, emit) => emit(event.data)); } StreamSubscription? _subscription; @override Future close() { _subscription?.cancel(); return super.close(); } } ================================================ FILE: packages/bloc/test/blocs/unawaited/unawaited_bloc.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; class UnawaitedEvent {} class UnawaitedState {} class UnawaitedBloc extends Bloc { UnawaitedBloc(Future future) : super(UnawaitedState()) { on((event, emit) { future.whenComplete(() => emit(UnawaitedState())); }); } } ================================================ FILE: packages/bloc/test/cubit_test.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; import 'cubits/cubits.dart'; class MockBlocObserver extends Mock implements BlocObserver {} class FakeBlocBase extends Fake implements BlocBase {} class FakeChange extends Fake implements Change {} void main() { group('Cubit', () { group('constructor', () { late BlocObserver observer; setUp(() { observer = MockBlocObserver(); Bloc.observer = observer; }); test('triggers onCreate on observer', () { final cubit = CounterCubit(); // ignore: invalid_use_of_protected_member verify(() => observer.onCreate(cubit)).called(1); }); }); group('initial state', () { test('is correct', () { expect(CounterCubit().state, 0); }); }); group('addError', () { late BlocObserver observer; setUp(() { observer = MockBlocObserver(); Bloc.observer = observer; }); test('triggers onError', () async { final expectedError = Exception('fatal exception'); final expectedStackTrace = StackTrace.current; final errors = []; final stackTraces = []; final cubit = CounterCubit( onErrorCallback: (error, stackTrace) { errors.add(error); stackTraces.add(stackTrace); }, // ignore: invalid_use_of_protected_member )..addError(expectedError, expectedStackTrace); expect(errors.length, equals(1)); expect(errors.first, equals(expectedError)); expect(stackTraces.length, equals(1)); expect(stackTraces.first, isNotNull); expect(stackTraces.first, isNot(StackTrace.empty)); verify( // ignore: invalid_use_of_protected_member () => observer.onError(cubit, expectedError, expectedStackTrace), ).called(1); }); }); group('onChange', () { late BlocObserver observer; setUpAll(() { registerFallbackValue(FakeBlocBase()); registerFallbackValue(FakeChange()); }); setUp(() { observer = MockBlocObserver(); Bloc.observer = observer; }); test('is not called for the initial state', () async { final changes = >[]; final cubit = CounterCubit(onChangeCallback: changes.add); await cubit.close(); expect(changes, isEmpty); // ignore: invalid_use_of_protected_member verifyNever(() => observer.onChange(any(), any())); }); test('is called with correct change for a single state change', () async { final changes = >[]; final cubit = CounterCubit(onChangeCallback: changes.add)..increment(); await cubit.close(); expect( changes, const [Change(currentState: 0, nextState: 1)], ); verify( // ignore: invalid_use_of_protected_member () => observer.onChange( cubit, const Change(currentState: 0, nextState: 1), ), ).called(1); }); test('is called with correct changes for multiple state changes', () async { final changes = >[]; final cubit = CounterCubit(onChangeCallback: changes.add) ..increment() ..increment(); await cubit.close(); expect( changes, const [ Change(currentState: 0, nextState: 1), Change(currentState: 1, nextState: 2), ], ); verify( // ignore: invalid_use_of_protected_member () => observer.onChange( cubit, const Change(currentState: 0, nextState: 1), ), ).called(1); verify( // ignore: invalid_use_of_protected_member () => observer.onChange( cubit, const Change(currentState: 1, nextState: 2), ), ).called(1); }); }); group('emit', () { test('throws StateError if cubit is closed', () { var didThrow = false; runZonedGuarded(() { final cubit = CounterCubit(); expectLater( cubit.stream, emitsInOrder([equals(1), emitsDone]), ); cubit ..increment() ..close() ..increment(); }, (error, _) { didThrow = true; expect( error, isA().having( (e) => e.message, 'message', 'Cannot emit new states after calling close', ), ); }); expect(didThrow, isTrue); }); test('emits states in the correct order', () async { final states = []; final cubit = CounterCubit(); final subscription = cubit.stream.listen(states.add); cubit.increment(); await cubit.close(); await subscription.cancel(); expect(states, [1]); }); test('can emit initial state only once', () async { final states = []; final cubit = SeededCubit(initialState: 0); final subscription = cubit.stream.listen(states.add); cubit ..emitState(0) ..emitState(0); await cubit.close(); await subscription.cancel(); expect(states, [0]); }); test( 'can emit initial state and ' 'continue emitting distinct states', () async { final states = []; final cubit = SeededCubit(initialState: 0); final subscription = cubit.stream.listen(states.add); cubit ..emitState(0) ..emitState(1); await cubit.close(); await subscription.cancel(); expect(states, [0, 1]); }); test('does not emit duplicate states', () async { final states = []; final cubit = SeededCubit(initialState: 0); final subscription = cubit.stream.listen(states.add); cubit ..emitState(1) ..emitState(1) ..emitState(2) ..emitState(2) ..emitState(3) ..emitState(3); await cubit.close(); await subscription.cancel(); expect(states, [1, 2, 3]); }); }); group('listen', () { test('returns a StreamSubscription', () { final cubit = CounterCubit(); final subscription = cubit.stream.listen((_) {}); expect(subscription, isA>()); subscription.cancel(); cubit.close(); }); test('does not receive current state upon subscribing', () async { final states = []; final cubit = CounterCubit()..stream.listen(states.add); await cubit.close(); expect(states, isEmpty); }); test('receives single async state', () async { final states = []; final cubit = FakeAsyncCounterCubit()..stream.listen(states.add); await cubit.increment(); await cubit.close(); expect(states, [equals(1)]); }); test('receives multiple async states', () async { final states = []; final cubit = FakeAsyncCounterCubit()..stream.listen(states.add); await cubit.increment(); await cubit.increment(); await cubit.increment(); await cubit.close(); expect(states, [equals(1), equals(2), equals(3)]); }); test('can call listen multiple times', () async { final states = []; final cubit = CounterCubit() ..stream.listen(states.add) ..stream.listen(states.add) ..increment(); await cubit.close(); expect(states, [equals(1), equals(1)]); }); }); group('close', () { late MockBlocObserver observer; setUp(() { observer = MockBlocObserver(); Bloc.observer = observer; }); test('triggers onClose on observer', () async { final cubit = CounterCubit(); await cubit.close(); // ignore: invalid_use_of_protected_member verify(() => observer.onClose(cubit)).called(1); }); test('emits done (sync)', () { final cubit = CounterCubit()..close(); expect(cubit.stream, emitsDone); }); test('emits done (async)', () async { final cubit = CounterCubit(); await cubit.close(); expect(cubit.stream, emitsDone); }); }); group('isClosed', () { test('returns true after cubit is closed', () async { final cubit = CounterCubit(); expect(cubit.isClosed, isFalse); await cubit.close(); expect(cubit.isClosed, isTrue); }); }); }); } ================================================ FILE: packages/bloc/test/cubits/counter_cubit.dart ================================================ import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit({this.onChangeCallback, this.onErrorCallback}) : super(0); final void Function(Change)? onChangeCallback; final void Function(Object error, StackTrace stackTrace)? onErrorCallback; void increment() => emit(state + 1); void decrement() => emit(state - 1); @override void onChange(Change change) { super.onChange(change); onChangeCallback?.call(change); } @override void onError(Object error, StackTrace stackTrace) { onErrorCallback?.call(error, stackTrace); super.onError(error, stackTrace); } } ================================================ FILE: packages/bloc/test/cubits/cubits.dart ================================================ export 'counter_cubit.dart'; export 'fake_async_cubit.dart'; export 'seeded_cubit.dart'; ================================================ FILE: packages/bloc/test/cubits/fake_async_cubit.dart ================================================ import 'package:bloc/bloc.dart'; class FakeAsyncCounterCubit extends Cubit { FakeAsyncCounterCubit() : super(0); Future increment() async { final nextState = await _increment(state); emit(nextState); } Future _increment(int value) async { return value + 1; } } ================================================ FILE: packages/bloc/test/cubits/seeded_cubit.dart ================================================ import 'package:bloc/bloc.dart'; class SeededCubit extends Cubit { SeededCubit({required T initialState}) : super(initialState); void emitState(T state) => emit(state); } ================================================ FILE: packages/bloc/test/transition_test.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import 'package:test/test.dart'; @immutable abstract class TransitionEvent {} @immutable abstract class TransitionState {} class SimpleTransitionEvent extends TransitionEvent {} class SimpleTransitionState extends TransitionState {} class CounterEvent extends TransitionEvent { CounterEvent(this.eventData); final String eventData; @override bool operator ==(Object other) => identical(this, other) || other is CounterEvent && runtimeType == other.runtimeType && eventData == other.eventData; @override int get hashCode => Object.hashAll([eventData]); } class CounterState extends TransitionState { CounterState(this.count); final int count; @override bool operator ==(Object other) => identical(this, other) || other is CounterState && runtimeType == other.runtimeType && count == other.count; @override int get hashCode => Object.hashAll([count]); } void main() { group('Change Tests', () { group('constructor', () { test( 'should return normally when initialized with ' 'all required parameters', () { expect( () => const Change(currentState: 0, nextState: 1), returnsNormally, ); }); }); group('== operator', () { test('should return true if 2 Changes are equal', () { const changeA = Change(currentState: 0, nextState: 1); const changeB = Change(currentState: 0, nextState: 1); expect(changeA == changeB, isTrue); }); test('should return false if 2 Changes are not equal', () { const changeA = Change(currentState: 0, nextState: 1); const changeB = Change(currentState: 0, nextState: -1); expect(changeA == changeB, isFalse); }); }); group('hashCode', () { test('should return correct hashCode', () { const change = Change(currentState: 0, nextState: 1); expect( change.hashCode, Object.hashAll([change.currentState, change.nextState]), ); }); }); group('toString', () { test('should return correct string representation of Change', () { const change = Change(currentState: 0, nextState: 1); expect( change.toString(), 'Change { currentState: ${change.currentState}, ' 'nextState: ${change.nextState} }', ); }); }); }); group('Transition Tests', () { group('constructor', () { test( 'should not throw assertion error when initialized ' 'with a null currentState', () { expect( () => Transition( currentState: null, event: SimpleTransitionEvent(), nextState: SimpleTransitionState(), ), isNot(throwsA(isA())), ); }); test( 'should not throw assertion error when initialized with a null event', () { expect( () => Transition( currentState: SimpleTransitionState(), event: null, nextState: SimpleTransitionState(), ), isNot(throwsA(isA())), ); }); test( 'should not throw assertion error ' 'when initialized with a null nextState', () { expect( () => Transition( currentState: SimpleTransitionState(), event: SimpleTransitionEvent(), nextState: null, ), isNot(throwsA(isA())), ); }); test( 'should not throw assertion error when initialized with ' 'all required parameters', () { try { Transition( currentState: SimpleTransitionState(), event: SimpleTransitionEvent(), nextState: SimpleTransitionState(), ); } catch (_) { fail( 'should not throw error when initialized ' 'with all required parameters', ); } }); }); group('== operator', () { test('should return true if 2 Transitions are equal', () { final transitionA = Transition( currentState: CounterState(0), event: CounterEvent('increment'), nextState: CounterState(1), ); final transitionB = Transition( currentState: CounterState(0), event: CounterEvent('increment'), nextState: CounterState(1), ); expect(transitionA == transitionB, true); }); test('should return false if 2 Transitions are not equal', () { final transitionA = Transition( currentState: CounterState(0), event: CounterEvent('increment'), nextState: CounterState(1), ); final transitionB = Transition( currentState: CounterState(1), event: CounterEvent('decrement'), nextState: CounterState(0), ); expect(transitionA == transitionB, false); }); }); group('hashCode', () { test('should return correct hashCode', () { final transition = Transition( currentState: CounterState(0), event: CounterEvent('increment'), nextState: CounterState(1), ); expect( transition.hashCode, Object.hashAll([ transition.currentState, transition.event, transition.nextState, ]), ); }); }); group('toString', () { test('should return correct string representation for Transition', () { final transition = Transition( currentState: CounterState(0), event: CounterEvent('increment'), nextState: CounterState(1), ); expect( transition.toString(), 'Transition { currentState: ${transition.currentState}, ' 'event: ${transition.event}, ' 'nextState: ${transition.nextState} }'); }); }); }); } ================================================ FILE: packages/bloc_concurrency/.gitignore ================================================ # Files and directories created by pub .dart_tool/ .packages pubspec.lock # Conventional directory for build outputs build/ # Directory created by dartdoc doc/api/ # Temporary Files .tmp/ # Files generated during tests .test_coverage.dart coverage/ ================================================ FILE: packages/bloc_concurrency/CHANGELOG.md ================================================ # 0.3.0 - chore(deps): upgrade to `package:bloc v9.0.0` - chore: update sponsors - chore: add `funding` to `pubspec.yaml` # 0.2.5 - docs: improve diagrams - chore: update copyright year - chore: update sponsors # 0.2.4 - chore: update sponsors # 0.2.3 - chore: fix `require_trailing_commas` - chore(deps): upgrade to `package:mocktail v1.0.0` - chore: add `topics` to `pubspec.yaml` # 0.2.2 - docs: upgrade to Dart 3 - refactor: standardize analysis_options # 0.2.1 - chore: add screenshots to `pubspec.yaml` - refactor: upgrade to Dart 2.19 - deps: upgrade to `very_good_analysis 3.1.0` - docs: update example to follow naming conventions # 0.2.0 - feat: upgrade to `bloc: ^8.0.0` # 0.2.0-dev.2 - feat: upgrade to `bloc: ^8.0.0-dev.5` # 0.2.0-dev.1 - feat: upgrade to `bloc: ^8.0.0-dev.3` # 0.1.0 - feat: upgrade to `bloc: ^7.2.0` # 0.1.0-dev.2 - feat: upgrade to `bloc: ^7.2.0-dev.2` # 0.1.0-dev.1 - feat: initial development release - includes `EventTransformer` options: - `concurrent`: process events concurrently - `sequential`: process events sequentially - `droppable`: ignore any events added while an event is processing - `restartable`: process only the latest event and cancel previous handlers ================================================ FILE: packages/bloc_concurrency/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2026 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/bloc_concurrency/README.md ================================================

    Bloc Concurrency

    Pub build codecov Star on Github style: bloc lint Flutter Website Awesome Flutter Flutter Samples License: MIT Discord Bloc Library

    --- A Dart package that exposes custom event transformers inspired by [ember concurrency](https://github.com/machty/ember-concurrency). Built to work with [bloc](https://pub.dev/packages/bloc). **Learn more at [bloclibrary.dev](https://bloclibrary.dev)!** --- ## Sponsors Our top sponsors are shown below! [[Become a Sponsor](https://github.com/sponsors/felangel)]
    --- ## Event Transformers ![Event Transformers](https://raw.githubusercontent.com/felangel/bloc/master/assets/diagrams/bloc_concurrency.png) `bloc_concurrency` provides an opinionated set of event transformers: - `concurrent` - process events concurrently - `sequential` - process events sequentially - `droppable` - ignore any events added while an event is processing - `restartable` - process only the latest event and cancel previous event handlers ## Usage ```dart import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; sealed class CounterEvent {} final class CounterIncrementPressed extends CounterEvent {} class CounterBloc extends Bloc { CounterBloc() : super(0) { on( (event, emit) async { await Future.delayed(Duration(seconds: 1)); emit(state + 1); }, /// Specify a custom event transformer from `package:bloc_concurrency` /// in this case events will be processed sequentially. transformer: sequential(), ); } } ``` ## Dart Versions - Dart 2: >= 2.14 ## Maintainers - [Felix Angelov](https://github.com/felangel) ================================================ FILE: packages/bloc_concurrency/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml analyzer: exclude: - "**/version.dart" ================================================ FILE: packages/bloc_concurrency/example/main.dart ================================================ // ignore_for_file: avoid_print, prefer_file_naming_conventions import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; Future tick() => Future.delayed(Duration.zero); Future main() async { /// Create a `CounterBloc` instance. final bloc = CounterBloc(); /// Subscribe to state changes and print each state. final subscription = bloc.stream.listen(print); /// Interact with the `bloc` to trigger `state` changes. bloc.add(CounterIncrementPressed()); await tick(); bloc.add(CounterIncrementPressed()); await tick(); bloc.add(CounterIncrementPressed()); await tick(); /// Wait 1 second... await Future.delayed(const Duration(seconds: 1)); /// Close the `bloc` when it is no longer needed. await bloc.close(); /// Cancel the subscription. await subscription.cancel(); } /// The events which `CounterBloc` will react to. abstract class CounterEvent {} /// Notifies bloc to increment state. class CounterIncrementPressed extends CounterEvent {} /// A `CounterBloc` which handles converting `CounterEvent`s into `int`s. class CounterBloc extends Bloc { /// The initial state of the `CounterBloc` is 0. CounterBloc() : super(0) { /// When a `CounterIncrementPressed` event is added, /// the current `state` of the bloc is accessed via the `state` property /// and a new state is emitted via `emit`. on( (event, emit) async { await Future.delayed(const Duration(seconds: 1)); emit(state + 1); }, /// Specify a custom event transformer /// in this case events will be processed sequentially. transformer: sequential(), ); } } ================================================ FILE: packages/bloc_concurrency/lib/bloc_concurrency.dart ================================================ /// Custom event transformers inspired by ember concurrency. /// Built to be used with the [bloc](https://pub.dev/packages/bloc) state management package. /// /// Get started at [bloclibrary.dev](https://bloclibrary.dev) 🚀 library bloc_concurrency; export 'src/concurrent.dart'; export 'src/droppable.dart'; export 'src/restartable.dart'; export 'src/sequential.dart'; ================================================ FILE: packages/bloc_concurrency/lib/src/concurrent.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:stream_transform/stream_transform.dart'; /// Process events concurrently. /// /// **Note**: there may be event handler overlap and state changes will occur /// as soon as they are emitted. This means that states may be emitted in /// an order that does not match the order in which the corresponding events /// were added. EventTransformer concurrent() { return (events, mapper) => events.concurrentAsyncExpand(mapper); } ================================================ FILE: packages/bloc_concurrency/lib/src/droppable.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; /// Process only one event and ignore (drop) any new events /// until the current event is done. /// /// **Note**: dropped events never trigger the event handler. EventTransformer droppable() { return (events, mapper) { return events.transform(_ExhaustMapStreamTransformer(mapper)); }; } class _ExhaustMapStreamTransformer extends StreamTransformerBase { _ExhaustMapStreamTransformer(this.mapper); final EventMapper mapper; @override Stream bind(Stream stream) { late StreamSubscription subscription; StreamSubscription? mappedSubscription; final controller = StreamController( onCancel: () async { await mappedSubscription?.cancel(); return subscription.cancel(); }, sync: true, ); subscription = stream.listen( (data) { if (mappedSubscription != null) return; final Stream mappedStream; mappedStream = mapper(data); mappedSubscription = mappedStream.listen( controller.add, onError: controller.addError, onDone: () => mappedSubscription = null, ); }, onError: controller.addError, onDone: () => mappedSubscription ?? controller.close(), ); return controller.stream; } } ================================================ FILE: packages/bloc_concurrency/lib/src/restartable.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:stream_transform/stream_transform.dart'; /// Process only one event by cancelling any pending events and /// processing the new event immediately. /// /// Avoid using [restartable] if you expect an event to have /// immediate results -- it should only be used with asynchronous APIs. /// /// **Note**: there is no event handler overlap and any currently running tasks /// will be aborted if a new event is added before a prior one completes. EventTransformer restartable() { return (events, mapper) => events.switchMap(mapper); } ================================================ FILE: packages/bloc_concurrency/lib/src/sequential.dart ================================================ import 'package:bloc/bloc.dart'; /// Process events one at a time by maintaining a queue of added events /// and processing the events sequentially. /// /// **Note**: there is no event handler overlap and every event is guaranteed /// to be handled in the order it was received. EventTransformer sequential() { return (events, mapper) => events.asyncExpand(mapper); } ================================================ FILE: packages/bloc_concurrency/pubspec.yaml ================================================ name: bloc_concurrency description: Custom event transformers inspired by ember concurrency. Built to be used with the bloc state management package. version: 0.3.0 repository: https://github.com/felangel/bloc/tree/master/packages/bloc_concurrency issue_tracker: https://github.com/felangel/bloc/issues homepage: https://github.com/felangel/bloc documentation: https://bloclibrary.dev topics: [bloc, concurrency, state-management] funding: [https://github.com/sponsors/felangel] environment: sdk: ">=2.14.0 <4.0.0" dependencies: bloc: ^9.0.0 stream_transform: ^2.0.0 dev_dependencies: bloc_lint: ^0.3.2 mocktail: ^1.0.0 test: ^1.17.0 screenshots: - description: The bloc concurrency package logo. path: screenshots/logo.png ================================================ FILE: packages/bloc_concurrency/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../bloc ================================================ FILE: packages/bloc_concurrency/test/src/concurrent_test.dart ================================================ import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:test/test.dart'; import 'helpers.dart'; void main() { group('concurrent', () { test('processes events concurrently by default', () async { final states = []; final bloc = CounterBloc(concurrent()) ..stream.listen(states.add) ..add(Increment()) ..add(Increment()) ..add(Increment()); await tick(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); await bloc.close(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); }); }); } ================================================ FILE: packages/bloc_concurrency/test/src/droppable_test.dart ================================================ import 'dart:async'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:test/test.dart'; import 'helpers.dart'; void main() { group('droppable', () { test('processes only the current event and ignores remaining', () async { final states = []; final bloc = CounterBloc(droppable()) ..stream.listen(states.add) ..add(Increment()) ..add(Increment()) ..add(Increment()); await tick(); expect(bloc.onCalls, equals([Increment()])); await wait(); expect(bloc.onEmitCalls, equals([Increment()])); expect(states, equals([1])); bloc ..add(Increment()) ..add(Increment()) ..add(Increment()); await tick(); expect( bloc.onCalls, equals([Increment(), Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment()]), ); expect(states, equals([1, 2])); bloc ..add(Increment()) ..add(Increment()) ..add(Increment()); await tick(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); await bloc.close(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); }); test('cancels the mapped subscription when it is active.', () async { final states = []; final controller = StreamController.broadcast(); final stream = droppable()(controller.stream, (x) async* { await wait(); yield x; }); final subscription = stream.listen(states.add); controller.add(0); await wait(); expect(states, isEmpty); expect(controller.hasListener, isTrue); await subscription.cancel(); expect(states, isEmpty); expect(controller.hasListener, isFalse); await controller.close(); expect(states, isEmpty); }); }); } ================================================ FILE: packages/bloc_concurrency/test/src/helpers.dart ================================================ // ignore_for_file: avoid_equals_and_hash_code_on_mutable_classes, lines_longer_than_80_chars, prefer_file_naming_conventions, avoid_public_fields import 'package:bloc/bloc.dart'; abstract class CounterEvent {} class Increment extends CounterEvent { @override bool operator ==(Object value) { if (identical(this, value)) return true; return value is Increment; } @override int get hashCode => 0; } const delay = Duration(milliseconds: 30); Future wait() => Future.delayed(delay); Future tick() => Future.delayed(Duration.zero); class CounterBloc extends Bloc { CounterBloc(EventTransformer transformer) : super(0) { on( (event, emit) { onCalls.add(event); return Future.delayed(delay, () { if (emit.isDone) return; onEmitCalls.add(event); emit(state + 1); }); }, transformer: transformer, ); } final onCalls = []; final onEmitCalls = []; } ================================================ FILE: packages/bloc_concurrency/test/src/restartable_test.dart ================================================ import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:test/test.dart'; import 'helpers.dart'; void main() { group('restartable', () { test('processes only the latest event and cancels remaining', () async { final states = []; final bloc = CounterBloc(restartable())..stream.listen(states.add); Future addEvents() async { const spacer = Duration(milliseconds: 10); await Future.delayed(spacer); bloc.add(Increment()); await Future.delayed(spacer); bloc.add(Increment()); await Future.delayed(spacer); bloc.add(Increment()); await Future.delayed(spacer); } await tick(); await addEvents(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); await wait(); expect(bloc.onEmitCalls, equals([Increment()])); expect(states, equals([1])); await tick(); await addEvents(); expect( bloc.onCalls, equals([ Increment(), Increment(), Increment(), Increment(), Increment(), Increment(), ]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment()]), ); expect(states, equals([1, 2])); await tick(); await addEvents(); expect( bloc.onCalls, equals([ Increment(), Increment(), Increment(), Increment(), Increment(), Increment(), Increment(), Increment(), Increment(), ]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); await bloc.close(); expect( bloc.onCalls, equals([ Increment(), Increment(), Increment(), Increment(), Increment(), Increment(), Increment(), Increment(), Increment(), ]), ); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); }); }); } ================================================ FILE: packages/bloc_concurrency/test/src/sequential_test.dart ================================================ import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:test/test.dart'; import 'helpers.dart'; void main() { group('sequential', () { test('processes events one at a time', () async { final states = []; final bloc = CounterBloc(sequential()) ..stream.listen(states.add) ..add(Increment()) ..add(Increment()) ..add(Increment()); await tick(); expect( bloc.onCalls, equals([Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment()]), ); expect(states, equals([1])); await tick(); expect( bloc.onCalls, equals([Increment(), Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment()]), ); expect(states, equals([1, 2])); await tick(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); await wait(); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); await bloc.close(); expect( bloc.onCalls, equals([Increment(), Increment(), Increment()]), ); expect( bloc.onEmitCalls, equals([Increment(), Increment(), Increment()]), ); expect(states, equals([1, 2, 3])); }); }); } ================================================ FILE: packages/bloc_lint/CHANGELOG.md ================================================ # 0.4.0 - feat: upgrade to latest analyzer frontend # 0.3.7 - fix: `analysis_options.yaml` resolution in pub workspace - deps: support latest stable Flutter version. # 0.3.6 - feat: upgrade to latest analyzer frontend # 0.3.5 - fix: adjust upper bound for `_fe_analyzer_shared` # 0.3.4 - chore: various dependency upgrades # 0.3.3 - fix: ignore whitespace in ignore comments - docs: add `bloc_lint` badge to `README` # 0.3.2 - feat: add [prefer_build_context_extensions](https://bloclibrary.dev/lint-rules/prefer_build_context_extensions) # 0.3.1 - fix: adjust lower bound for `_fe_analyzer_shared` # 0.3.0 - fix: widen supported version ranges for `_fe_analyzer_shared` - feat: add [avoid_build_context_extensions](https://bloclibrary.dev/lint-rules/avoid_build_context_extensions) - feat: add [prefer_file_naming_conventions](https://bloclibrary.dev/lint-rules/prefer_file_naming_conventions) # 0.2.1 - feat: ignore dot directories - fix: include bloc/cubit instances defined in any file - chore: various dependency upgrades # 0.2.0 - chore: stable `0.2.0` release 🎉 - chore: various dependency upgrades - fix: ignore `.fvm` # 0.2.0-dev.6 - fix: `prefer_void_public_cubit_methods` false positive when using switch expressions - fix: `avoid_public_bloc_methods` false positive when using switch expressions # 0.2.0-dev.5 - fix: `avoid_public_bloc_methods` false positive when using switch expressions # 0.2.0-dev.4 - fix: various bug fixes for Windows # 0.2.0-dev.3 - feat: add support for `// ignore` - feat: add support for `// ignore_for_file` # 0.2.0-dev.2 - feat: add [prefer_void_public_cubit_methods](https://bloclibrary.dev/lint-rules/prefer_void_public_cubit_methods) # 0.2.0-dev.1 - fix: package resolution in `include` on windows - fix: uri resolution on windows - docs: improvements to `README.md` # 0.2.0-dev.0 - Full rewrite of `pkg:bloc_lint` - Supported Lint Rules - [avoid_flutter_imports](https://bloclibrary.dev/lint-rules/avoid_flutter_imports) - [avoid_public_bloc_methods](https://bloclibrary.dev/lint-rules/avoid_public_bloc_methods) - [avoid_public_fields](https://bloclibrary.dev/lint-rules/avoid_public_fields) - [prefer_bloc](https://bloclibrary.dev/lint-rules/prefer_bloc) - [prefer_cubit](https://bloclibrary.dev/lint-rules/prefer_cubit) # 0.1.0 - Initial experimental community release using `pkg:custom_lint` ================================================ FILE: packages/bloc_lint/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2026 Felix Angelov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/bloc_lint/README.md ================================================

    Bloc

    Pub build codecov Star on Github style: bloc lint Flutter Website Awesome Flutter Flutter Samples License: MIT Discord Bloc Library

    --- Official lint rules for development when using the bloc state management library. **Learn more at [bloclibrary.dev](https://bloclibrary.dev)!** This package is built to work with: - [bloc](https://pub.dev/packages/bloc) - [bloc_tools](https://pub.dev/packages/bloc_tools) - [flutter_bloc](https://pub.dev/packages/flutter_bloc) - [angular_bloc](https://pub.dev/packages/angular_bloc) - [bloc_concurrency](https://pub.dev/packages/bloc_concurrency) - [bloc_test](https://pub.dev/packages/bloc_test) - [hydrated_bloc](https://pub.dev/packages/hydrated_bloc) - [replay_bloc](https://pub.dev/packages/replay_bloc) --- ## Sponsors Our top sponsors are shown below! [[Become a Sponsor](https://github.com/sponsors/felangel)]
    --- ## Quick Start 1. Install the [bloc command-line tools](https://pub.dev/packages/bloc_tools) ```sh dart pub global activate bloc_tools ``` 2. Install the [bloc_lint](https://pub.dev/packages/bloc_lint) package ```sh dart pub add --dev bloc_lint ``` 3. Add an `analysis_options.yaml` to the root of your project with the recommended rules ```yaml include: package:bloc_lint/recommended.yaml ``` 4. Run the linter ```sh bloc lint . ``` For more information, check out the [official documentation](https://bloclibrary.dev/lint) ## Recommended Lint Rules - [avoid_flutter_imports](https://bloclibrary.dev/lint-rules/avoid_flutter_imports) - [avoid_public_bloc_methods](https://bloclibrary.dev/lint-rules/avoid_public_bloc_methods) - [avoid_public_fields](https://bloclibrary.dev/lint-rules/avoid_public_fields) - [prefer_file_naming_conventions](https://bloclibrary.dev/lint-rules/prefer_file_naming_conventions) - [prefer_void_public_cubit_methods](https://bloclibrary.dev/lint-rules/prefer_void_public_cubit_methods) ## All Lint Rules - [avoid_build_context_extensions](https://bloclibrary.dev/lint-rules/avoid_build_context_extensions) - [avoid_flutter_imports](https://bloclibrary.dev/lint-rules/avoid_flutter_imports) - [avoid_public_bloc_methods](https://bloclibrary.dev/lint-rules/avoid_public_bloc_methods) - [avoid_public_fields](https://bloclibrary.dev/lint-rules/avoid_public_fields) - [prefer_bloc](https://bloclibrary.dev/lint-rules/prefer_bloc) - [prefer_build_context_extensions](https://bloclibrary.dev/lint-rules/prefer_build_context_extensions) - [prefer_cubit](https://bloclibrary.dev/lint-rules/prefer_cubit) - [prefer_file_naming_conventions](https://bloclibrary.dev/lint-rules/prefer_file_naming_conventions) - [prefer_void_public_cubit_methods](https://bloclibrary.dev/lint-rules/prefer_void_public_cubit_methods) ## Dart Versions - Dart 3: >= 3.7.0 ## Maintainers - [Felix Angelov](https://github.com/felangel) ================================================ FILE: packages/bloc_lint/analysis_options.yaml ================================================ include: ../../analysis_options.yaml ================================================ FILE: packages/bloc_lint/build.yaml ================================================ # See https://github.com/dart-lang/build/tree/master/build_web_compilers#configuration targets: $default: builders: source_gen|combining_builder: options: ignore_for_file: - implicit_dynamic_parameter - require_trailing_commas - cast_nullable_to_non_nullable - lines_longer_than_80_chars - strict_raw_type json_serializable: options: any_map: true disallow_unrecognized_keys: false field_rename: kebab include_if_null: false checked: true explicit_to_json: true create_to_json: true ================================================ FILE: packages/bloc_lint/example/main.dart ================================================ // ignore_for_file: unused_local_variable import 'package:bloc_lint/bloc_lint.dart'; // Usage: dart run main.dart ./path/to/analyze void main(List args) { // Analyze the provided file or directory and report all diagnostics. final diagnostics = const Linter().analyze(uri: Uri.parse(args.first)); } ================================================ FILE: packages/bloc_lint/lib/all.yaml ================================================ bloc: rules: - avoid_build_context_extensions - avoid_flutter_imports - avoid_public_bloc_methods - avoid_public_fields - prefer_bloc - prefer_build_context_extensions - prefer_cubit - prefer_file_naming_conventions - prefer_void_public_cubit_methods ================================================ FILE: packages/bloc_lint/lib/bloc_lint.dart ================================================ export 'package:_fe_analyzer_shared/src/parser/parser.dart' show DeclarationKind, IdentifierContext, Listener; export 'package:_fe_analyzer_shared/src/scanner/token.dart' show BeginToken, Keyword, Token, TokenType; export 'src/diagnostic.dart' show Diagnostic, Severity; export 'src/lint_rule.dart' show LintRule, LintRuleBuilder; export 'src/linter.dart' show LintContext, Linter; export 'src/rules/rules.dart' show AvoidBuildContextExtensions, AvoidFlutterImports, AvoidPublicBlocMethods, AvoidPublicFields, PreferBloc, PreferBuildContextExtensions, PreferCubit, PreferFileNamingConventions, PreferVoidPublicCubitMethods; export 'src/text_document.dart' show Position, Range, TextDocument, TextDocumentType, TextDocumentTypeX, TextDocumentX; ================================================ FILE: packages/bloc_lint/lib/recommended.yaml ================================================ bloc: rules: - avoid_flutter_imports - avoid_public_bloc_methods - avoid_public_fields - prefer_file_naming_conventions - prefer_void_public_cubit_methods ================================================ FILE: packages/bloc_lint/lib/src/analysis_options.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:bloc_lint/src/diagnostic.dart'; import 'package:bloc_lint/src/env.dart'; import 'package:checked_yaml/checked_yaml.dart'; import 'package:collection/collection.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:path/path.dart' as p; part 'analysis_options.g.dart'; /// {@template analysis_options} /// The analysis_options object which contains the parsed /// yaml as well as the file path. /// {@endtemplate} class AnalysisOptions { /// {@macro analysis_options} const AnalysisOptions({required this.file, required this.yaml}); /// Parse the [file] as an [AnalysisOptions]. factory AnalysisOptions.parse(File file) { final content = file.readAsStringSync(); final yaml = checkedYamlDecode( content, (m) => AnalysisOptionsYaml.fromJson(m!), ); return AnalysisOptions(file: file, yaml: yaml); } /// Resolve the provided options by recursively joining all includes. factory AnalysisOptions.resolve(File file) { AnalysisOptions recursiveResolver( File file, Directory root, Map yaml, ) { final options = AnalysisOptions.parse(file); for (final include in options.yaml.include ?? []) { final isPackageInclude = include.startsWith('package:'); final parsedOptions = isPackageInclude ? AnalysisOptions.tryInclude(include, cwd: root) : AnalysisOptions.tryParse( File(p.normalize(p.join(options.file.parent.path, include))), ); if (parsedOptions == null) continue; final resolved = recursiveResolver( parsedOptions.file, isPackageInclude ? root : file.parent, yaml, ); // Accumulate the merged analysis_options yaml content // ignore: parameter_assignments yaml = _merge(yaml, resolved.yaml.toJson()); } return AnalysisOptions( file: options.file, yaml: AnalysisOptionsYaml.fromJson(_merge(yaml, options.yaml.toJson())), ); } return recursiveResolver(file, file.parent, {}); } /// Try to parse [file] and return `null` if parsing fails. static AnalysisOptions? tryParse(File file) { try { return AnalysisOptions.parse(file); } on Exception { return null; } } /// Try to resolve [file] and return `null` if resolving fails. static AnalysisOptions? tryResolve(File file) { try { return AnalysisOptions.resolve(file); } on Exception { return null; } } /// Try to parse an analysis_options yaml referenced by the [include]. static AnalysisOptions? tryInclude(String include, {required Directory cwd}) { final packagePrefix = include.split('/').first; final packageName = packagePrefix.split('package:').last; final packageConfigFile = findPackageConfigFile(cwd); if (packageConfigFile == null) return null; final packageConfig = json.decode(packageConfigFile.readAsStringSync()) as Map; final packages = packageConfig['packages'] as List; final package = packages.cast>().firstWhereOrNull( (entry) => entry['name'] == packageName, ); if (package == null) return null; final fullUri = Uri.tryParse( p.join(package['rootUri'] as String, package['packageUri'] as String), ); if (fullUri == null) return null; var path = include.split(packagePrefix).last; if (path.startsWith(p.separator)) path = path.substring(1); var resolvedPath = p.fromUri(fullUri) + path; if (!p.isAbsolute(resolvedPath)) { resolvedPath = p.join(packageConfigFile.parent.path, resolvedPath); } return AnalysisOptions.tryParse(File(p.normalize(resolvedPath))); } /// The `analysis_options.yaml` file. final File file; /// The parsed yaml contents. final AnalysisOptionsYaml yaml; } /// {@template analysis_options_yaml} /// The `analysis_options.yaml` configuration. /// {@endtemplate} @JsonSerializable() class AnalysisOptionsYaml { /// {@macro analysis_options_yaml} AnalysisOptionsYaml({this.include, this.analyzer, this.bloc}); /// Converts [Map] to [AnalysisOptionsYaml] factory AnalysisOptionsYaml.fromJson(Map json) => _$AnalysisOptionsYamlFromJson(json); /// Converts [AnalysisOptionsYaml] to [Map]. Map toJson() => _$AnalysisOptionsYamlToJson(this); /// The list of shared analysis options. @IncludeConverter() final List? include; /// The dart analyzer options. final AnalyzerOptions? analyzer; /// The bloc lint options. final BlocLintOptions? bloc; } /// {@template analyzer_options} /// Dart analyzer options. /// {@endtemplate} @JsonSerializable() class AnalyzerOptions { /// {@macro analyzer_options} const AnalyzerOptions({this.exclude = const []}); /// Converts [Map] to [AnalyzerOptions] factory AnalyzerOptions.fromJson(Map json) => _$AnalyzerOptionsFromJson(json); /// Converts [AnalyzerOptions] to [Map]. Map toJson() => _$AnalyzerOptionsToJson(this); /// List of files, directories, or globs to exclude. final List exclude; } /// {@template bloc_lint_options} /// Bloc-specific lint options. /// {@endtemplate} @JsonSerializable() class BlocLintOptions { /// {@macro bloc_lint_options} const BlocLintOptions({required this.rules}); /// Converts [Map] to [BlocLintOptions]. factory BlocLintOptions.fromJson(Map json) => _$BlocLintOptionsFromJson(json); /// Converts [BlocLintOptions] to [Map]. Map toJson() => _$BlocLintOptionsToJson(this); /// The configured bloc lint rules. @RulesConverter() final Map? rules; } @JsonEnum(valueField: 'value') /// The state of a given linter rule. enum LinterRuleState { /// The rule is enabled with a default severity. enabled('true'), /// The rule is disabled. disabled('false'), /// The rule is enabled with a severity of info. info('info'), /// The rule is enabled with a severity of error. error('error'), /// The rule is enabled with a severity of warning. warning('warning'), /// The rule is enabled with a severity of hint. hint('hint'); const LinterRuleState(this.value); /// The underlying value. final String value; /// Parse the provided [value] as a [LinterRuleState]. static LinterRuleState fromJson(String value) { return LinterRuleState.values.firstWhere((v) => v.value == value); } /// Converts the [LinterRuleState] to a json encoded value. String toJson() => value; } /// {@template include_converter} /// Json Converter for analysis_options includes (`List`). /// {@endtemplate} class IncludeConverter implements JsonConverter?, dynamic> { /// {@macro include_converter} const IncludeConverter(); @override dynamic toJson(List? value) => value; @override List? fromJson(dynamic value) { if (value is String) return [value]; if (value is List) return value.cast(); return null; } } /// {@template rules_converter} /// Json Converter for lint rules (`Map`). /// {@endtemplate} class RulesConverter implements JsonConverter?, dynamic> { /// {@macro rules_converter} const RulesConverter(); @override dynamic toJson(Map? value) { return value?.map( (key, value) => MapEntry(key, value.toJson()), ); } @override Map? fromJson(dynamic value) { final dynamic decoded = value is String ? json.decode(value) : value; if (decoded is List) { return { for (final v in decoded) v as String: LinterRuleState.enabled, }; } if (decoded is Map) { return decoded.map( (dynamic key, dynamic value) => MapEntry( key as String, LinterRuleState.fromJson(decoded[key].toString()), ), ); } return null; } } /// Extension methods on [LinterRuleState]. extension LinterRuleStateX on LinterRuleState { /// Whether the rule is enabled. bool get isEnabled => this != LinterRuleState.disabled; /// Whether the rule is disabled. bool get isDisabled => !isEnabled; /// Converts the rule to a [Severity] using the [fallback]. Severity? toSeverity({Severity? fallback}) { return switch (this) { LinterRuleState.enabled => fallback, LinterRuleState.disabled => null, LinterRuleState.info => Severity.info, LinterRuleState.error => Severity.error, LinterRuleState.warning => Severity.warning, LinterRuleState.hint => Severity.hint, }; } } /// Merge two maps recursively. Map _merge( Map mapA, Map mapB, ) { return mergeMaps( mapA, mapB, value: (p0, p1) { if (p0 is Map && p1 is Map) { return _merge(p0, p1); } else if (p0 is List && p1 is List) { return {...p0, ...p1}.toList(); } else { return p1; } }, ); } ================================================ FILE: packages/bloc_lint/lib/src/analysis_options.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: implicit_dynamic_parameter, require_trailing_commas, cast_nullable_to_non_nullable, lines_longer_than_80_chars, strict_raw_type part of 'analysis_options.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** AnalysisOptionsYaml _$AnalysisOptionsYamlFromJson(Map json) => $checkedCreate('AnalysisOptionsYaml', json, ($checkedConvert) { final val = AnalysisOptionsYaml( include: $checkedConvert( 'include', (v) => const IncludeConverter().fromJson(v), ), analyzer: $checkedConvert( 'analyzer', (v) => v == null ? null : AnalyzerOptions.fromJson(v as Map), ), bloc: $checkedConvert( 'bloc', (v) => v == null ? null : BlocLintOptions.fromJson(v as Map), ), ); return val; }); Map _$AnalysisOptionsYamlToJson( AnalysisOptionsYaml instance, ) => { if (const IncludeConverter().toJson(instance.include) case final value?) 'include': value, if (instance.analyzer?.toJson() case final value?) 'analyzer': value, if (instance.bloc?.toJson() case final value?) 'bloc': value, }; AnalyzerOptions _$AnalyzerOptionsFromJson(Map json) => $checkedCreate('AnalyzerOptions', json, ($checkedConvert) { final val = AnalyzerOptions( exclude: $checkedConvert( 'exclude', (v) => (v as List?)?.map((e) => e as String).toList() ?? const [], ), ); return val; }); Map _$AnalyzerOptionsToJson(AnalyzerOptions instance) => {'exclude': instance.exclude}; BlocLintOptions _$BlocLintOptionsFromJson(Map json) => $checkedCreate('BlocLintOptions', json, ($checkedConvert) { final val = BlocLintOptions( rules: $checkedConvert( 'rules', (v) => const RulesConverter().fromJson(v), ), ); return val; }); Map _$BlocLintOptionsToJson(BlocLintOptions instance) => { if (const RulesConverter().toJson(instance.rules) case final value?) 'rules': value, }; ================================================ FILE: packages/bloc_lint/lib/src/diagnostic.dart ================================================ import 'package:bloc_lint/bloc_lint.dart'; /// The severity of the reported lint rule. enum Severity { /// The diagnostic is reported as an error. error, /// The diagnostic is reported as an warning. warning, /// The diagnostic is reported as info. info, /// The diagnostic is reported as a hint. hint, } /// {@template diagnostic} /// A diagnostic which is reported by a lint rule. /// {@endtemplate} class Diagnostic { /// {@macro diagnostic} const Diagnostic({ required this.range, required this.source, required this.message, required this.description, required this.code, required this.severity, this.hint = '', }); /// The affected range. final Range range; /// The source of the lint rule (e.g. who reported it). final String source; /// The message associated with the lint. final String message; /// An optional property to describe the error code. final String description; /// A hint or recommendation usually presented to the user. final String hint; /// The diagnostic's code, which usually appear in the user interface. final String code; /// The severity level of the lint. final Severity severity; /// Converts a [Diagnostic] to a [Map]. Map toJson() { return { 'range': range.toJson(), 'source': source, 'message': message, 'description': description, 'hint': hint, 'code': code, 'severity': severity.name, }; } } ================================================ FILE: packages/bloc_lint/lib/src/env.dart ================================================ import 'dart:core'; import 'dart:io'; import 'package:bloc_lint/src/analysis_options.dart'; import 'package:path/path.dart' as p; import 'package:pubspec_lock_parse/pubspec_lock_parse.dart'; /// The `package_config.json` file path for this project. String packageConfigPath(Directory cwd) { return p.join(cwd.path, '.dart_tool', 'package_config.json'); } /// The `pubspec.yaml` file path for this project. String pubspecYamlPath(Directory cwd) { return p.join(cwd.path, 'pubspec.yaml'); } /// The `pubspec.lock` file path for this project. String pubspecLockPath(Directory cwd) { return p.join(cwd.path, 'pubspec.lock'); } /// The `analysis_options.yaml` file path for this project. String analysisOptionsPath(Directory cwd) { return p.join(cwd.path, 'analysis_options.yaml'); } /// The `package_config.json` file for this package. /// /// Returns `null` if the file does not exist or is invalid. File? findPackageConfigFile(Directory cwd) { final root = findProjectRoot(cwd); if (root == null) return null; final file = File(packageConfigPath(root)); if (!file.existsSync()) return null; return file; } /// The `analysis_options.yaml` file for this package. /// /// Returns `null` if the file does not exist or is invalid. File? findAnalysisOptionsFile(Directory cwd) { final root = findPackageRoot(cwd); if (root == null) return null; final file = File(analysisOptionsPath(root)); if (!file.existsSync()) return null; return file; } /// The resolved `analysis_options.yaml` file for this project. /// /// Returns `null` if the file does not exist or is invalid. AnalysisOptions? findAnalysisOptions(Directory cwd) { final file = findAnalysisOptionsFile(cwd); if (file == null) return null; return AnalysisOptions.tryResolve(file); } /// The `pubspec.lock` file for this project, parsed into a [PubspecLock] /// object. /// /// Returns `null` if the file does not exist or is invalid. PubspecLock? findPubspecLock(Directory cwd) { final root = findProjectRoot(cwd); if (root == null) return null; final file = File(pubspecLockPath(root)); if (!file.existsSync()) return null; try { return PubspecLock.parse(file.readAsStringSync()); } on Exception { return null; } } /// Returns the root directory of the nearest package. Directory? findPackageRoot(Directory cwd) { final file = findNearestAncestor( where: (path) => File(pubspecYamlPath(Directory(path))), cwd: cwd, ); if (file == null) return null; return Directory(p.dirname(file.path)); } /// Returns the root directory of the nearest project. Directory? findProjectRoot(Directory cwd) { final file = findNearestAncestor( where: (path) => File(pubspecLockPath(Directory(path))), cwd: cwd, ); if (file == null) return null; return Directory(p.dirname(file.path)); } /// Finds nearest ancestor file relative to the [cwd] that satisfies [where]. File? findNearestAncestor({ required File? Function(String path) where, required Directory cwd, }) { Directory? prev; var dir = cwd; while (prev?.path != dir.path) { final file = where(dir.path); if (file?.existsSync() ?? false) return file; prev = dir; dir = dir.parent; } return null; } ================================================ FILE: packages/bloc_lint/lib/src/lint_rule.dart ================================================ import 'package:bloc_lint/bloc_lint.dart'; /// Signature for a method that builds a [LintRule] with an optional [severity]. typedef LintRuleBuilder = LintRule Function([Severity? severity]); /// {@template lint_rule} /// An individual lint rule. /// {@endtemplate} abstract class LintRule { /// {@macro lint_rule} LintRule({required this.name, required this.severity}); /// The unique name of the rule. final String name; /// The severity of the rule. final Severity severity; /// Method that must be implemented which returns a listener /// given a [LintContext]. Listener? create(LintContext context); } ================================================ FILE: packages/bloc_lint/lib/src/linter.dart ================================================ import 'dart:convert'; import 'dart:io'; // Use the experimental features from the shared frontend. // ignore: implementation_imports import 'package:_fe_analyzer_shared/src/parser/experimental_features.dart' show DefaultExperimentalFeatures; // Use the parser from the shared frontend. // ignore: implementation_imports import 'package:_fe_analyzer_shared/src/parser/parser.dart' show Parser; // Use the scanner from the shared frontend. // ignore: implementation_imports import 'package:_fe_analyzer_shared/src/scanner/scanner.dart' show scan; import 'package:bloc_lint/bloc_lint.dart'; import 'package:bloc_lint/src/analysis_options.dart'; import 'package:bloc_lint/src/env.dart'; import 'package:collection/collection.dart'; import 'package:glob/glob.dart'; import 'package:path/path.dart' as p; /// All supported lint rules. final allRules = { AvoidBuildContextExtensions.rule: AvoidBuildContextExtensions.new, AvoidFlutterImports.rule: AvoidFlutterImports.new, AvoidPublicBlocMethods.rule: AvoidPublicBlocMethods.new, AvoidPublicFields.rule: AvoidPublicFields.new, PreferBloc.rule: PreferBloc.new, PreferBuildContextExtensions.rule: PreferBuildContextExtensions.new, PreferCubit.rule: PreferCubit.new, PreferFileNamingConventions.rule: PreferFileNamingConventions.new, PreferVoidPublicCubitMethods.rule: PreferVoidPublicCubitMethods.new, }; /// {@template linter} /// A class that is able to analyze files and directories and /// report diagnostics based on a registered set of lint rules. /// {@endtemplate} class Linter { /// {@macro linter} const Linter(); /// Analyzes the provided [uri] and returns all reported diagnostics. If /// [content] is provided, it will be explicitly analyzed, otherwise the [uri] /// will be analyzed (both single files and directories are supported). Map> analyze({required Uri uri, String? content}) { final path = uri.canonicalizedPath.toLongPath(); final directory = Directory(path); if (directory.existsSync()) return _analyzeDirectory(directory); final file = File(path); if (file.existsSync() && file.isLintableDartFile) { if (content != null) return _analyzeContent(uri, content); return _analyzeFile(file); } return {}; } Map> _analyzeDirectory(Directory directory) { final files = directory .listSync(recursive: true) .where((e) => e.isLintableDartFile) .cast(); return files .map(_analyzeFile) .fold(>{}, (prev, cur) => {...prev, ...cur}); } Map> _analyzeFile(File file) { return _analyzeContent(file.uri, file.readAsStringSync()); } Map> _analyzeContent(Uri uri, String content) { final diagnostics = []; final canonicalizedPath = uri.canonicalizedPath; final results = {canonicalizedPath: diagnostics}; final path = canonicalizedPath.toLongPath(); final cwd = File(path).parent; final pubspecLock = findPubspecLock(cwd); if (pubspecLock == null) return results; if (!pubspecLock.packages.keys.contains('bloc')) return results; final analysisOptions = findAnalysisOptions(cwd); if (analysisOptions == null) return results; final relativePath = p .relative(path, from: analysisOptions.file.parent.path) .replaceAll(r'\', '/'); if (analysisOptions.excludes.any((e) => e.matches(relativePath))) { return results; } final document = TextDocument(uri: uri, content: content); final ignoreForFile = document.ignoreForFile; if (ignoreForFile.containsTypeLint) return results; final enabledRules = {...analysisOptions.lintRules} ..removeWhere((rule) => ignoreForFile.contains(rule.name)); final tokens = scan(utf8.encode(content)).tokens; for (final rule in enabledRules) { final context = LintContext._(rule: rule, document: document); final listener = rule.create(context); if (listener == null) continue; Parser( listener, experimentalFeatures: const DefaultExperimentalFeatures(), ).parseUnit(tokens); diagnostics.addAll(context.diagnostics); } return results; } } extension on AnalysisOptions { /// Gets the list of [Glob] patterns to be excluded for this project. List get excludes { final excludes = yaml.analyzer?.exclude ?? []; final context = p.Context(current: file.parent.path); return excludes.map((e) => Glob(e, context: context)).toList(); } /// Gets the list of [LintRule] for this project. List get lintRules { final blocLintOptions = yaml.bloc; if (blocLintOptions == null) return []; final rules = blocLintOptions.rules; if (rules == null) return []; return rules.entries .map((analysisEntry) { final rule = analysisEntry.key; final state = analysisEntry.value; if (state.isDisabled) return null; final entry = allRules.entries.firstWhereOrNull((e) => e.key == rule); if (entry == null) return null; final builder = entry.value; final severity = state.toSeverity(fallback: builder().severity); return builder(severity); }) .whereType() .toList(); } } extension on FileSystemEntity { bool get isLintableDartFile { return this is File && p.extension(path) == '.dart' && !p.basename(path).endsWith('.g.dart') && !p.split(path).any((segment) => segment.startsWith('.')); } } extension on String { String toLongPath() { // Support long file paths on Windows // https://github.com/dart-lang/sdk/issues/27825 if (Platform.isWindows) return r'\\?\' + this; return this; } } extension on Uri { String get canonicalizedPath { final path = isScheme('file') ? p.fromUri(this) : this.path; return p.normalize(p.absolute(path)); } } /// {@template lint_context} /// A context object that is provided to each [LintRule] and /// provides APIs for reporting diagnostics and accessing the /// current [TextDocument]. /// {@endtemplate} class LintContext { /// {@macro lint_context} LintContext._({required LintRule rule, required this.document}) : _rule = rule; final LintRule _rule; /// The current [document] being analyzed. final TextDocument document; final List _diagnostics = []; /// The list of reported diagnostics. List get diagnostics => _diagnostics; /// Reports a lint at the provided [range]. void report({ required Range range, required String message, String hint = '', }) { final ignore = document.ignoreForLine(range: range); if (ignore.containsTypeLint || ignore.contains(_rule.name)) return; _diagnostics.add( Diagnostic( range: range, source: 'bloc', message: message, hint: hint, code: _rule.name, description: 'https://bloclibrary.dev/lint-rules/${_rule.name}', severity: _rule.severity, ), ); } /// Reports a lint from [beginToken] to [endToken]. void reportTokenRange({ required Token beginToken, required Token endToken, required String message, String hint = '', }) { report( range: Range( start: document.positionAt(beginToken.offset), end: document.positionAt(endToken.offset + endToken.length), ), message: message, hint: hint, ); } /// Reports a lint at the specified [token]. void reportToken({ required Token token, required String message, String hint = '', }) { report( range: Range( start: document.positionAt(token.offset), end: document.positionAt(token.offset + token.length), ), message: message, hint: hint, ); } } extension on Set { /// Whether the set of strings contains `type=lint`. bool get containsTypeLint => contains('type=lint'); } ================================================ FILE: packages/bloc_lint/lib/src/rules/avoid_build_context_extensions.dart ================================================ import 'package:bloc_lint/bloc_lint.dart'; import 'package:collection/collection.dart'; /// {@template avoid_build_context_extensions} /// The avoid_build_context_extensions lint rule. /// {@endtemplate} class AvoidBuildContextExtensions extends LintRule { /// {@macro avoid_build_context_extensions} AvoidBuildContextExtensions([Severity? severity]) : super(name: rule, severity: severity ?? Severity.warning); /// The name of the lint rule. static const rule = 'avoid_build_context_extensions'; @override Listener create(LintContext context) => _Listener(context); } // Supported `BuildContext` extensions methods. enum _ContextMethod { read, watch, select } class _Listener extends Listener { _Listener(this.context); final LintContext context; static const _declarationKeywords = {'final', 'const', 'var', 'late'}; /// Whether the type is implicitly a bloc type. bool _isImplicitBlocType = false; @override void beginInitializedIdentifier(Token nameToken) { final prev = nameToken.previous; if (prev != null && prev.type == TokenType.IDENTIFIER && !_declarationKeywords.contains(prev.lexeme)) { _isImplicitBlocType = prev.isBlocType; } super.beginInitializedIdentifier(nameToken); } @override void endInitializedIdentifier(Token nameToken) { _isImplicitBlocType = false; super.endInitializedIdentifier(nameToken); } @override void handleIdentifier(Token token, IdentifierContext _) { final method = _ContextMethod.values.firstWhereOrNull( (value) => value.name == token.lexeme, ); if (method == null) return; final prev = token.previous; if (prev == null || prev.type != TokenType.PERIOD) return; final target = prev.previous; if (target == null) return; if (target.lexeme != 'context') return; // Case 1: implicit type // e.g. final MyBloc bloc = context.read(); if (_isImplicitBlocType) return _report(method, target, token); // Case 2: explicit type // e.g. final bloc = context.read(); final openBracketToken = token.next; if (openBracketToken == null) return; if (openBracketToken.type != TokenType.LT) return; final typeToken = openBracketToken.next; if (typeToken == null) return; if (typeToken.type != TokenType.IDENTIFIER) return; if (!typeToken.isBlocType) return; return _report(method, target, token); } void _report(_ContextMethod method, Token beginToken, Token endToken) { context.reportTokenRange( beginToken: beginToken, endToken: endToken, message: 'Avoid using BuildContext extensions.', hint: 'Prefer using ${method.alternative} instead.', ); } } extension on _ContextMethod { String get alternative { switch (this) { case _ContextMethod.read: return 'BlocProvider.of(context, listen: false)'; case _ContextMethod.watch: return '''BlocBuilder(...) or BlocProvider.of(context)'''; case _ContextMethod.select: return 'BlocSelector(...)'; } } } extension on Token { bool get isBlocType { return lexeme.endsWith('Bloc') || lexeme.endsWith('Cubit'); } } ================================================ FILE: packages/bloc_lint/lib/src/rules/avoid_flutter_imports.dart ================================================ import 'package:bloc_lint/bloc_lint.dart'; /// {@template avoid_flutter_imports} /// The avoid_flutter_imports lint rule. /// {@endtemplate} class AvoidFlutterImports extends LintRule { /// {@macro avoid_flutter_imports} AvoidFlutterImports([Severity? severity]) : super(name: rule, severity: severity ?? Severity.warning); /// The name of the lint rule. static const rule = 'avoid_flutter_imports'; @override Listener? create(LintContext context) { if (context.document.type.isOther) return null; return _Listener(context); } } class _Listener extends Listener { _Listener(this.context); static const flutterImport = 'package:flutter/'; final LintContext context; @override void beginImport(Token importKeyword) { final package = importKeyword.next; if (package == null) return; if (!package.lexeme.substring(1).startsWith(flutterImport)) return; final instance = context.document.type.isBloc ? 'Bloc' : 'Cubit'; context.reportToken( token: package, message: '''Avoid importing Flutter within ${instance.toLowerCase()} instances.''', hint: '${instance}s should be decoupled from Flutter.', ); } } ================================================ FILE: packages/bloc_lint/lib/src/rules/avoid_public_bloc_methods.dart ================================================ import 'package:bloc_lint/bloc_lint.dart'; /// {@template avoid_public_bloc_methods} /// The avoid_public_bloc_methods lint rule. /// {@endtemplate} class AvoidPublicBlocMethods extends LintRule { /// {@macro avoid_public_bloc_methods} AvoidPublicBlocMethods([Severity? severity]) : super(name: rule, severity: severity ?? Severity.warning); /// The name of the lint rule. static const rule = 'avoid_public_bloc_methods'; @override Listener? create(LintContext context) => _Listener(context); } class _Listener extends Listener { _Listener(this.context); static const allowedMethods = [ 'add', 'addError', 'close', 'emit', 'state', 'stream', 'on', 'onChange', 'onError', 'onEvent', 'onTransition', 'toString', ]; final LintContext context; var _isOverride = false; @override void beginMetadata(Token token) { _isOverride = token.next?.lexeme == 'override'; } @override void beginMethod( DeclarationKind declarationKind, Token? augmentToken, Token? externalToken, Token? staticToken, Token? covariantToken, Token? varFinalOrConst, Token? getOrSet, Token name, String? enclosingDeclarationName, ) { if (declarationKind != DeclarationKind.Class) return; if (_isOverride || staticToken != null) return; if (!(enclosingDeclarationName?.endsWith('Bloc') ?? false)) { return; } if (name.previous?.type == Keyword.SWITCH) return; final methodName = name.lexeme; if (enclosingDeclarationName == methodName) return; if (allowedMethods.contains(methodName)) return; if (methodName.startsWith('_')) return; context.reportToken( token: name, message: 'Avoid public methods on bloc instances.', hint: 'Prefer notifying bloc instances via `add`.', ); } } ================================================ FILE: packages/bloc_lint/lib/src/rules/avoid_public_fields.dart ================================================ import 'package:bloc_lint/bloc_lint.dart'; /// {@template avoid_public_fields} /// The avoid_public_fields lint rule. /// {@endtemplate} class AvoidPublicFields extends LintRule { /// {@macro avoid_public_fields} AvoidPublicFields([Severity? severity]) : super(name: rule, severity: severity ?? Severity.warning); /// The name of the lint rule. static const rule = 'avoid_public_fields'; @override Listener? create(LintContext context) => _Listener(context); } class _Listener extends Listener { _Listener(this.context); final LintContext context; bool _isRelevantEnclosingClass = false; @override void beginClassDeclaration( Token begin, Token? abstractToken, Token? macroToken, Token? sealedToken, Token? baseToken, Token? interfaceToken, Token? finalToken, Token? augmentToken, Token? mixinToken, Token name, ) { _isRelevantEnclosingClass = false; final extendz = name.next; if (extendz == null || extendz.kind != Keyword.EXTENDS.kind) return; final superclazz = extendz.next; if (superclazz == null) return; if (superclazz.lexeme.endsWith('Bloc') || superclazz.lexeme.endsWith('Cubit')) { _isRelevantEnclosingClass = true; } return; } @override void endFields( DeclarationKind kind, Token? abstractToken, Token? augmentToken, Token? externalToken, Token? staticToken, Token? covariantToken, Token? lateToken, Token? varFinalOrConst, int count, Token beginToken, Token endToken, ) { if (!_isRelevantEnclosingClass) return; if (staticToken != null) return; final fieldName = _getFieldName(beginToken, endToken); if (fieldName.lexeme.startsWith('_')) return; context.reportTokenRange( beginToken: beginToken, endToken: endToken, message: 'Avoid public fields.', hint: 'Prefer using the `state` to hold all public fields.', ); } } List _getTokens(Token begin, Token end) { final tokens = []; Token? token = begin; while (token != null && token != end) { tokens.add(token); token = token.next; } return tokens; } Token _getFieldName(Token begin, Token end) { final tokens = _getTokens(begin, end); final equalsIndex = tokens.indexWhere((token) => token.type == TokenType.EQ); if (equalsIndex != -1) return tokens.elementAt(equalsIndex).previous!; return end.previous!; } ================================================ FILE: packages/bloc_lint/lib/src/rules/prefer_bloc.dart ================================================ import 'package:bloc_lint/bloc_lint.dart'; /// {@template prefer_bloc} /// The prefer_bloc lint rule. /// {@endtemplate} class PreferBloc extends LintRule { /// {@macro prefer_bloc} PreferBloc([Severity? severity]) : super(name: rule, severity: severity ?? Severity.info); /// The name of the lint rule. static const rule = 'prefer_bloc'; @override Listener create(LintContext context) => _Listener(context); } class _Listener extends Listener { _Listener(this.context); final LintContext context; @override void beginClassDeclaration( Token begin, Token? abstractToken, Token? macroToken, Token? sealedToken, Token? baseToken, Token? interfaceToken, Token? finalToken, Token? augmentToken, Token? mixinToken, Token name, ) { final extendz = name.next; if (extendz == null || extendz.kind != Keyword.EXTENDS.kind) return; final superclazz = extendz.next; if (superclazz == null) return; if (superclazz.lexeme.endsWith('Cubit')) { final prefix = superclazz.lexeme.split('Cubit').first; context.reportToken( token: name, message: 'Avoid extending ${prefix}Cubit.', hint: 'Prefer extending ${prefix}Bloc instead.', ); } } } ================================================ FILE: packages/bloc_lint/lib/src/rules/prefer_build_context_extensions.dart ================================================ import 'package:bloc_lint/bloc_lint.dart'; /// {@template prefer_build_context_extensions} /// The prefer_build_context_extensions lint rule. /// {@endtemplate} class PreferBuildContextExtensions extends LintRule { /// {@macro prefer_build_context_extensions} PreferBuildContextExtensions([Severity? severity]) : super(name: rule, severity: severity ?? Severity.warning); /// The name of the lint rule. static const rule = 'prefer_build_context_extensions'; @override Listener create(LintContext context) => _Listener(context); } class _Listener extends Listener { _Listener(this.context); final LintContext context; static const _message = 'Prefer using BuildContext extensions.'; @override void handleIdentifier(Token token, IdentifierContext _) { final provider = tryParseProvider(token); if (provider != null) { return context.reportTokenRange( beginToken: provider, endToken: token, message: _message, hint: ''' Avoid using ${provider.lexeme}.of. Prefer using context.read or context.watch instead.''', ); } final blocBuilder = tryParseBlocBuilder(token); if (blocBuilder != null) { return context.reportTokenRange( beginToken: blocBuilder, endToken: token, message: _message, hint: ''' Avoid using ${blocBuilder.lexeme}. Prefer using context.watch instead.''', ); } final blocSelector = tryParseBlocSelector(token); if (blocSelector != null) { return context.reportTokenRange( beginToken: blocSelector, endToken: token, message: _message, hint: ''' Avoid using ${blocSelector.lexeme}. Prefer using context.select instead.''', ); } } Token? tryParseProvider(Token token) { if (token.lexeme != 'of') return null; final prev = token.previous; if (prev == null) return null; if (prev.type != TokenType.PERIOD) return null; final next = token.next; if (next == null) return null; final target = prev.previous; if (target == null) return null; const providers = {'BlocProvider', 'RepositoryProvider'}; return providers.contains(target.lexeme) ? target : null; } Token? tryParseBlocBuilder(Token token) { if (token.lexeme != 'BlocBuilder') return null; return (token.next is BeginToken) ? token : null; } Token? tryParseBlocSelector(Token token) { if (token.lexeme != 'BlocSelector') return null; return (token.next is BeginToken) ? token : null; } } ================================================ FILE: packages/bloc_lint/lib/src/rules/prefer_cubit.dart ================================================ import 'package:bloc_lint/bloc_lint.dart'; /// {@template prefer_cubit} /// The prefer_cubit lint rule. /// {@endtemplate} class PreferCubit extends LintRule { /// {@macro prefer_cubit} PreferCubit([Severity? severity]) : super(name: rule, severity: severity ?? Severity.info); /// The name of the lint rule. static const rule = 'prefer_cubit'; @override Listener create(LintContext context) => _Listener(context); } class _Listener extends Listener { _Listener(this.context); final LintContext context; @override void beginClassDeclaration( Token begin, Token? abstractToken, Token? macroToken, Token? sealedToken, Token? baseToken, Token? interfaceToken, Token? finalToken, Token? augmentToken, Token? mixinToken, Token name, ) { final extendz = name.next; if (extendz == null || extendz.kind != Keyword.EXTENDS.kind) return; final superclazz = extendz.next; if (superclazz == null) return; if (superclazz.lexeme.endsWith('Bloc')) { final prefix = superclazz.lexeme.split('Bloc').first; context.reportToken( token: name, message: 'Avoid extending ${prefix}Bloc.', hint: 'Prefer extending ${prefix}Cubit instead.', ); } } } ================================================ FILE: packages/bloc_lint/lib/src/rules/prefer_file_naming_conventions.dart ================================================ import 'package:bloc_lint/bloc_lint.dart'; import 'package:bloc_lint/src/string_case.dart'; import 'package:path/path.dart' as path; /// {@template prefer_file_naming_conventions} /// The prefer_file_naming_conventions lint rule. /// {@endtemplate} class PreferFileNamingConventions extends LintRule { /// {@macro prefer_file_naming_conventions} PreferFileNamingConventions([Severity? severity]) : super(name: rule, severity: severity ?? Severity.info); /// The name of the lint rule. static const rule = 'prefer_file_naming_conventions'; @override Listener create(LintContext context) => _Listener(context); } class _Listener extends Listener { _Listener(this.context); final LintContext context; @override void beginClassDeclaration( Token begin, Token? abstractToken, Token? macroToken, Token? sealedToken, Token? baseToken, Token? interfaceToken, Token? finalToken, Token? augmentToken, Token? mixinToken, Token name, ) { final extendz = name.next; if (extendz == null || extendz.kind != Keyword.EXTENDS.kind) return; final superclazz = extendz.next; if (superclazz == null) return; if (superclazz.lexeme.endsWith('MockBloc')) return; if (superclazz.lexeme.endsWith('MockCubit')) return; if (superclazz.lexeme.endsWith('Bloc') || superclazz.lexeme.endsWith('Cubit')) { final expectedFileName = '${name.lexeme.toSnakeCase()}.dart'; final actualFileName = path.basename(context.document.uri.toString()); if (actualFileName == expectedFileName) return; context.reportToken( token: name, message: 'Prefer following file naming conventions.', hint: 'Prefer moving ${name.lexeme} into $expectedFileName.dart', ); } } } ================================================ FILE: packages/bloc_lint/lib/src/rules/prefer_void_public_cubit_methods.dart ================================================ import 'package:bloc_lint/bloc_lint.dart'; /// {@template prefer_void_public_cubit_methods} /// The prefer_void_public_cubit_methods lint rule. /// {@endtemplate} class PreferVoidPublicCubitMethods extends LintRule { /// {@macro prefer_void_public_cubit_methods} PreferVoidPublicCubitMethods([Severity? severity]) : super(name: rule, severity: severity ?? Severity.warning); /// The name of the lint rule. static const rule = 'prefer_void_public_cubit_methods'; @override Listener? create(LintContext context) => _Listener(context); } class _Listener extends Listener { _Listener(this.context); final LintContext context; var _isOverride = false; static const allowedReturnTypes = ['void', 'Future', 'FutureOr']; @override void beginMetadata(Token token) { _isOverride = token.next?.lexeme == 'override'; } @override void beginMethod( DeclarationKind declarationKind, Token? augmentToken, Token? externalToken, Token? staticToken, Token? covariantToken, Token? varFinalOrConst, Token? getOrSet, Token name, String? enclosingDeclarationName, ) { if (declarationKind != DeclarationKind.Class) return; if (_isOverride || staticToken != null) return; if (!(enclosingDeclarationName?.endsWith('Cubit') ?? false)) { return; } if (name.previous?.type == Keyword.SWITCH) return; final methodName = name.lexeme; if (enclosingDeclarationName == methodName) return; if (methodName.startsWith('_')) return; if (getOrSet?.keyword == Keyword.SET) return; if (allowedReturnTypes.contains(_getReturnType(getOrSet ?? name))) return; context.reportToken( token: name, message: ''' Prefer void public methods on cubit instances. Try adjusting the return type to `void`, `Future`, or `FutureOr`.''', hint: 'Prefer `void` return types.', ); } } String _getReturnType(Token name) { const dynamic = 'dynamic'; final previous = name.previous; if (previous == null) return dynamic; if (previous.type != TokenType.GT) return previous.lexeme; var bracketCount = 0; final chars = []; Token? current = previous; bool isDone() { if (current == null) return true; if (bracketCount <= 0) return true; return false; } do { chars.insert(0, current!.lexeme); if (current.type == TokenType.GT) bracketCount++; if (current.type == TokenType.LT) bracketCount--; current = current.previous; } while (!isDone()); if (current != null) chars.insert(0, current.lexeme); return chars.join(); } ================================================ FILE: packages/bloc_lint/lib/src/rules/rules.dart ================================================ export 'avoid_build_context_extensions.dart'; export 'avoid_flutter_imports.dart'; export 'avoid_public_bloc_methods.dart'; export 'avoid_public_fields.dart'; export 'prefer_bloc.dart'; export 'prefer_build_context_extensions.dart'; export 'prefer_cubit.dart'; export 'prefer_file_naming_conventions.dart'; export 'prefer_void_public_cubit_methods.dart'; ================================================ FILE: packages/bloc_lint/lib/src/string_case.dart ================================================ import 'package:collection/collection.dart'; /// Extension on [String] that add support for converting /// to snake_case. extension SnakeCaseX on String { /// Returns the snake_case equivalent of the current string. String toSnakeCase() { return split('').mapIndexed((index, character) { if (index == 0) return character.toLowerCase(); if (character.toUpperCase() == character) { return '_${character.toLowerCase()}'; } return character; }).join(); } } ================================================ FILE: packages/bloc_lint/lib/src/text_document.dart ================================================ import 'dart:math'; import 'package:path/path.dart' as p; /// The newline symbol (\n). const newline = 10; /// The carriage return symbol (\r). const carriageReturn = 13; /// {@template text_document} /// A simplified, Dart representation of VSCode's Language Server /// [TextDocument](https://github.com/microsoft/vscode-languageserver-node/blob/main/textDocument/src/main.ts). /// {@endtemplate} class TextDocument { /// {@macro text_document} TextDocument({required Uri uri, required String content}) : _uri = uri, _content = content; static final _ignoreForFileRegExp = RegExp( r'^\s*//\s*ignore_for_file:(.*?)$', dotAll: true, multiLine: true, ); static final _ignoreForLineRegExp = RegExp(r'^\s*//\s*ignore:(.*)$'); static final _ignoreAfterLineRegExp = RegExp(r'\s*//\s*ignore:(.*)$'); final Uri _uri; final String _content; List? _lineOffsets; /// The associated URI for this document. Most documents have the file scheme, /// indicating that they represent files on disk. However, some documents may /// have other schemes indicating that they are not available on disk. Uri get uri => _uri; /// Whether the line for the current range contains an // ignore: for the given rule. Set ignoreForLine({required Range range}) { return { ..._ignoresAboveLine(range: range), ..._ignoresAfterLine(range: range), }; } Set _ignoresAboveLine({required Range range}) { final previousLine = range.start.line - 1; if (previousLine < 0) return const {}; final line = getText( range: Range( start: Position(character: 0, line: previousLine), end: Position(character: _content.length, line: previousLine), ), ); return _lineIgnores(line); } Set _ignoresAfterLine({required Range range}) { final afterText = getText( range: Range( start: Position(character: range.end.character, line: range.end.line), end: Position(character: _content.length, line: range.end.line), ), ); final index = afterText.indexOf(_ignoreAfterLineRegExp); if (index == -1) return const {}; final line = afterText.substring(index); return _lineIgnores(line); } Set _lineIgnores(String line) { final result = {}; final matches = _ignoreForLineRegExp.allMatches(line); if (matches.isEmpty) return result; for (final match in matches) { final contents = match.group(1); if (contents == null) continue; result.addAll(contents.split(',').map((segment) => segment.trim())); } return result; } /// Returns a list of rules ignored for the current file. /// e.g. // ignore_for_file: avoid_flutter_imports, prefer_bloc Set get ignoreForFile { final result = {}; final matches = _ignoreForFileRegExp.allMatches(_content); if (matches.isEmpty) return result; for (final match in matches) { final contents = match.group(1); if (contents == null) continue; result.addAll(contents.split(',').map((segment) => segment.trim())); } return result; } /// Get the text of this document. Provide a [Range] to get a substring. String getText({Range? range}) { if (range != null) { final start = offsetAt(range.start); final end = offsetAt(range.end); return _content.substring(start, end); } return _content; } /// Convert a [Position] to a zero-based offset. int offsetAt(Position position) { final lineOffsets = _getLineOffsets(); if (position.line >= lineOffsets.length) { return _content.length; } else if (position.line < 0) { return 0; } final lineOffset = lineOffsets[position.line]; if (position.character <= 0) { return lineOffset; } final nextLineOffset = (position.line + 1 < lineOffsets.length) ? lineOffsets[position.line + 1] : _content.length; final offset = min(lineOffset + position.character, nextLineOffset); return _ensureBeforeEndOfLine(offset: offset, lineOffset: lineOffset); } /// Converts a zero-based offset to a [Position]. Position positionAt(int offset) { // ignore: parameter_assignments offset = max(min(offset, _content.length), 0); final lineOffsets = _getLineOffsets(); var low = 0; var high = lineOffsets.length; if (high == 0) return Position(character: offset, line: 0); while (low < high) { final mid = ((low + high) / 2).floor(); if (lineOffsets[mid] > offset) { high = mid; } else { low = mid + 1; } } final line = low - 1; // ignore: parameter_assignments offset = _ensureBeforeEndOfLine( offset: offset, lineOffset: lineOffsets[line], ); return Position(character: offset - lineOffsets[line], line: line); } List _getLineOffsets() { _lineOffsets ??= _computeLineOffsets(_content, isAtLineStart: true); return _lineOffsets!; } List _computeLineOffsets( String content, { required bool isAtLineStart, int textOffset = 0, }) { final result = isAtLineStart ? [textOffset] : []; for (var i = 0; i < content.length; i++) { final char = content.codeUnitAt(i); if (_isEndOfLine(char)) { if (char == carriageReturn) { final nextCharIsnewline = i + 1 < content.length && content.codeUnitAt(i + 1) == newline; if (nextCharIsnewline) { i++; } } result.add(textOffset + i + 1); } } return result; } bool _isEndOfLine(int char) => char == newline || char == carriageReturn; int _ensureBeforeEndOfLine({required int offset, required int lineOffset}) { while (offset > lineOffset && _isEndOfLine(_content.codeUnitAt(offset - 1))) { offset--; } return offset; } } /// {@template position} /// A specific position within a [TextDocument]. /// {@endtemplate} class Position { /// {@macro position} const Position({required this.line, required this.character}); /// The line number. final int line; /// The character offset within the line. final int character; /// Converts a [Position] into a [Map]. Map toJson() => {'line': line, 'character': character}; } /// {@template range} /// A range of content within a [TextDocument]. /// {@endtemplate} class Range { /// {@macro range} const Range({required this.start, required this.end}); /// The starting position. final Position start; /// The ending position. final Position end; /// Converts a [Range] to a [Map]. Map toJson() { return {'start': start.toJson(), 'end': end.toJson()}; } } /// Relevant types of text documents. enum TextDocumentType { /// A bloc file. bloc, /// A cubit file. cubit, /// Any other file. other, } /// Extensions on [TextDocument] that provide access to /// document type information. extension TextDocumentX on TextDocument { /// Returns the [TextDocumentType] for the given document. TextDocumentType get type { final basename = p.basename(uri.path); return basename.endsWith('_bloc.dart') ? TextDocumentType.bloc : basename.endsWith('_cubit.dart') ? TextDocumentType.cubit : TextDocumentType.other; } } /// Extensions on [TextDocumentType] that provide access to /// convenience methods for interpreting the type. extension TextDocumentTypeX on TextDocumentType { /// Whether the document type is a bloc. bool get isBloc => this == TextDocumentType.bloc; /// Whether the document type is a cubit. bool get isCubit => this == TextDocumentType.cubit; /// Whether the document type is other. bool get isOther => this == TextDocumentType.other; } ================================================ FILE: packages/bloc_lint/pubspec.yaml ================================================ name: bloc_lint description: Official lint rules for development when using the bloc state management library. version: 0.4.0 repository: https://github.com/felangel/bloc/tree/master/packages/bloc_lint issue_tracker: https://github.com/felangel/bloc/issues homepage: https://github.com/felangel/bloc documentation: https://bloclibrary.dev topics: [bloc, state-management, lint] funding: [https://github.com/sponsors/felangel] environment: sdk: ">=3.7.0 <4.0.0" dependencies: _fe_analyzer_shared: ">=93.0.0 <95.0.0" checked_yaml: ^2.0.0 collection: ^1.0.0 glob: ^2.0.0 json_annotation: ^4.9.0 path: ^1.0.0 pubspec_lock_parse: ^2.0.0 dev_dependencies: build_runner: ^2.0.0 json_serializable: ^6.0.0 meta: ^1.0.0 mocktail: ^1.0.0 test: ^1.0.0 screenshots: - description: The bloc_lint package logo. path: screenshots/logo.png ================================================ FILE: packages/bloc_lint/test/src/analysis_options_test.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:bloc_lint/src/analysis_options.dart'; import 'package:bloc_lint/src/diagnostic.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; void main() { group(AnalysisOptionsYaml, () { test('(de)serializes correctly', () { final options = AnalysisOptionsYaml( include: ['include.yaml'], analyzer: const AnalyzerOptions(exclude: ['exclude']), bloc: const BlocLintOptions( rules: {'prefer_bloc': LinterRuleState.enabled}, ), ); expect( json.encode(AnalysisOptionsYaml.fromJson(options.toJson()).toJson()), equals(json.encode(options.toJson())), ); }); }); group(AnalysisOptions, () { group('tryParse', () { test('returns null for invalid file', () { expect(AnalysisOptions.tryParse(File('invalid')), isNull); }); }); group('parse', () { test('completes for empty file', () { final tempDir = Directory.systemTemp.createTempSync(); final file = File(p.join(tempDir.path, 'analysis_options.yaml')) ..writeAsStringSync('{}'); final parsed = AnalysisOptions.parse(file); expect(parsed.file.path, equals(file.path)); expect(parsed.yaml.analyzer, isNull); expect(parsed.yaml.include, isNull); expect(parsed.yaml.bloc, isNull); }); test('completes for valid file', () { final tempDir = Directory.systemTemp.createTempSync(); final file = File(p.join(tempDir.path, 'analysis_options.yaml')) ..writeAsStringSync(''' include: package:bloc_lint/recommended.yaml analyzer: exclude: - "**.g.dart" linter: rules: public_member_api_docs: false bloc: rules: prefer_bloc: true '''); final parsed = AnalysisOptions.parse(file); expect(parsed.file.path, equals(file.path)); expect(parsed.yaml.analyzer?.exclude, equals(['**.g.dart'])); expect( parsed.yaml.include, equals(['package:bloc_lint/recommended.yaml']), ); expect( parsed.yaml.bloc?.rules, equals({'prefer_bloc': LinterRuleState.enabled}), ); }); }); group('tryResolve', () { test('returns null for invalid file', () { expect(AnalysisOptions.tryResolve(File('invalid')), isNull); }); }); group('resolve', () { test('resolves with no includes', () { final tempDir = Directory.systemTemp.createTempSync(); final file = File(p.join(tempDir.path, 'analysis_options.yaml')) ..writeAsStringSync(''' analyzer: exclude: - "**.g.dart" linter: rules: public_member_api_docs: false bloc: rules: prefer_bloc: true '''); final parsed = AnalysisOptions.resolve(file); expect(parsed.file.path, equals(file.path)); expect(parsed.yaml.analyzer?.exclude, equals(['**.g.dart'])); expect(parsed.yaml.include, isNull); expect( parsed.yaml.bloc?.rules, equals({'prefer_bloc': LinterRuleState.enabled}), ); }); test('resolves with package include', () { final tempDir = Directory.systemTemp.createTempSync(); File(p.join(tempDir.path, 'bloc_lint', 'lib', 'recommended.yaml')) ..createSync(recursive: true) ..writeAsStringSync(''' bloc: rules: - avoid_flutter_imports - avoid_public_bloc_methods - avoid_public_fields '''); File(p.join(tempDir.path, '.dart_tool', 'package_config.json')) ..createSync(recursive: true) ..writeAsStringSync(''' { "packages": [ { "name": "bloc_lint", "rootUri": "../bloc_lint", "packageUri": "lib/", "languageVersion": "3.7" } ] } '''); File(p.join(tempDir.path, 'pubspec.lock')).writeAsStringSync(''' packages: bloc: dependency: "direct main" description: name: bloc sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" url: "https://pub.dev" source: hosted version: "9.0.0" sdks: dart: ">=3.6.0 <4.0.0" '''); final file = File(p.join(tempDir.path, 'analysis_options.yaml')) ..writeAsStringSync(''' include: package:bloc_lint/recommended.yaml analyzer: exclude: - "**.g.dart" bloc: rules: prefer_bloc: true '''); final parsed = AnalysisOptions.resolve(file); expect(parsed.file.path, equals(file.path)); expect(parsed.yaml.analyzer?.exclude, equals(['**.g.dart'])); expect(parsed.yaml.include, ['package:bloc_lint/recommended.yaml']); expect( parsed.yaml.bloc?.rules, equals({ 'avoid_flutter_imports': LinterRuleState.enabled, 'avoid_public_bloc_methods': LinterRuleState.enabled, 'avoid_public_fields': LinterRuleState.enabled, 'prefer_bloc': LinterRuleState.enabled, }), ); }); test('resolves with nested includes', () { final tempDir = Directory.systemTemp.createTempSync(); File(p.join(tempDir.path, 'one.yaml')).writeAsStringSync(''' analyzer: exclude: - one.dart bloc: rules: avoid_flutter_imports: error '''); File(p.join(tempDir.path, 'two.yaml')).writeAsStringSync(''' analyzer: exclude: - two.dart bloc: rules: prefer_bloc: error prefer_cubit: warning '''); File(p.join(tempDir.path, 'three.yaml')).writeAsStringSync(''' include: two.yaml analyzer: exclude: - three.dart bloc: rules: prefer_bloc: false '''); final options = File(p.join(tempDir.path, 'analysis_options.yaml')) ..writeAsStringSync(''' include: - one.yaml - three.yaml analyzer: exclude: - "**.g.dart" bloc: rules: avoid_public_fields: true '''); final parsed = AnalysisOptions.resolve(options); expect(parsed.file.path, equals(options.path)); expect( parsed.yaml.analyzer?.exclude, equals(['one.dart', 'two.dart', 'three.dart', '**.g.dart']), ); expect( parsed.yaml.include, equals(['two.yaml', 'one.yaml', 'three.yaml']), ); expect( parsed.yaml.bloc?.rules, equals({ 'avoid_flutter_imports': LinterRuleState.error, 'prefer_cubit': LinterRuleState.warning, 'prefer_bloc': LinterRuleState.disabled, 'avoid_public_fields': LinterRuleState.enabled, }), ); }); }); }); group(LinterRuleState, () { test('toSeverity is correct', () { expect( LinterRuleState.enabled.toSeverity(fallback: Severity.info), equals(Severity.info), ); expect(LinterRuleState.disabled.toSeverity(), equals(null)); expect(LinterRuleState.info.toSeverity(), equals(Severity.info)); expect(LinterRuleState.error.toSeverity(), equals(Severity.error)); expect(LinterRuleState.warning.toSeverity(), equals(Severity.warning)); expect(LinterRuleState.hint.toSeverity(), equals(Severity.hint)); }); }); } ================================================ FILE: packages/bloc_lint/test/src/diagnostic_test.dart ================================================ import 'dart:convert'; import 'package:bloc_lint/bloc_lint.dart'; import 'package:test/test.dart'; void main() { group(Diagnostic, () { group('toJson', () { test('returns correct value', () { expect( json.encode( const Diagnostic( range: Range( start: Position(line: 1, character: 2), end: Position(line: 3, character: 4), ), source: 'source', message: 'message', description: 'description', hint: 'hint', code: 'code', severity: Severity.error, ).toJson(), ), equals( json.encode({ 'range': { 'start': {'line': 1, 'character': 2}, 'end': {'line': 3, 'character': 4}, }, 'source': 'source', 'message': 'message', 'description': 'description', 'hint': 'hint', 'code': 'code', 'severity': 'error', }), ), ); }); }); }); } ================================================ FILE: packages/bloc_lint/test/src/lint_test_helper.dart ================================================ import 'dart:convert'; import 'dart:io'; import 'package:bloc_lint/bloc_lint.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; const lintMarker = '^'; @isTest void lintTest( String description, { required LintRule Function() rule, required String path, required String content, }) { test(description, () { final lines = const LineSplitter().convert(content); final sanitizedLines = StringBuffer(); final lintedLines = []; for (var i = 0; i < lines.length; i++) { final line = lines[i]; final lineNumber = i + 1; final isLint = line.contains('^') && line.replaceAll('^', '').trim().isEmpty; if (!isLint) { sanitizedLines.writeln(line); continue; } sanitizedLines.writeln(); final lintedLine = lineNumber - 2; lintedLines.add( Range( start: Position(line: lintedLine, character: line.indexOf('^')), end: Position(line: lintedLine, character: line.lastIndexOf('^') + 1), ), ); } const linter = Linter(); final lintRule = rule(); final tempDir = Directory.systemTemp.createTempSync(); final tempFile = File(p.join(tempDir.path, path)) ..createSync(recursive: true) ..writeAsStringSync(content); File(p.join(tempDir.path, 'analysis_options.yaml')).writeAsStringSync(''' bloc: rules: - ${lintRule.name} '''); File(p.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync(''' name: _ environment: sdk: ">=3.6.0 <4.0.0" dependencies: bloc: any '''); File(p.join(tempDir.path, 'pubspec.lock')).writeAsStringSync(''' packages: bloc: dependency: "direct main" description: name: bloc sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" url: "https://pub.dev" source: hosted version: "9.0.0" sdks: dart: ">=3.6.0 <4.0.0" '''); final diagnostics = linter.analyze( uri: tempFile.uri, content: sanitizedLines.toString(), ); for (final entry in diagnostics.entries) { final diagnostics = entry.value; if (diagnostics.isEmpty) expect(lintedLines, isEmpty); for (final diagnostic in diagnostics) { expect(diagnostic.code, equals(lintRule.name)); expect(diagnostic.source, equals('bloc')); expect(diagnostic.severity, equals(lintRule.severity)); final reportedRange = json.encode(diagnostic.range.toJson()); final expectedRanges = lintedLines.map((l) => json.encode(l.toJson())); expect(expectedRanges, contains(reportedRange)); } } }); } ================================================ FILE: packages/bloc_lint/test/src/linter_test.dart ================================================ import 'dart:io'; import 'package:bloc_lint/bloc_lint.dart'; import 'package:mocktail/mocktail.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; import 'lint_test_helper.dart'; class _MockLintRule extends Mock implements LintRule {} class _MockListener extends Mock implements Listener {} class _FakeLintContext extends Fake implements LintContext {} void main() { group(Linter, () { const name = 'prefer_bloc'; late Listener listener; late LintRule rule; late Linter linter; setUpAll(() { registerFallbackValue(_FakeLintContext()); }); setUp(() { listener = _MockListener(); rule = _MockLintRule(); linter = const Linter(); when(() => rule.create(any())).thenReturn(listener); when(() => rule.name).thenReturn(name); }); group('analyze', () { late Directory tempDir; setUp(() { tempDir = Directory.systemTemp.createTempSync(); File(p.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync(''' name: _ environment: sdk: ">=3.6.0 <4.0.0" dependencies: bloc: any '''); File(p.join(tempDir.path, 'pubspec.lock')).writeAsStringSync(''' packages: bloc: dependency: "direct main" description: name: bloc sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" url: "https://pub.dev" source: hosted version: "9.0.0" sdks: dart: ">=3.6.0 <4.0.0" '''); File( p.join(tempDir.path, 'analysis_options.yaml'), ).writeAsStringSync('{}'); }); tearDown(() { tempDir.deleteSync(recursive: true); }); test('does nothing if file/directory does not exist', () { final invalid = File('invalid'); expect(linter.analyze(uri: invalid.uri), isEmpty); }); test('does nothing if pubspec.lock is malformed', () { File(p.join(tempDir.path, 'pubspec.lock')).writeAsStringSync('invalid'); final file = File(p.join(tempDir.path, 'main.dart')) ..writeAsStringSync(''' void main() { print('hello world'); } '''); expect( linter.analyze(uri: file.uri), equals({file.path: []}), ); }); test('analyzes an individual file', () { final file = File(p.join(tempDir.path, 'main.dart')) ..writeAsStringSync(''' void main() { print('hello world'); } '''); expect( linter.analyze(uri: file.uri), equals({file.path: []}), ); File(p.join(tempDir.path, 'analysis_options.yaml')).writeAsStringSync( ''' bloc: rules: ''', ); expect( linter.analyze(uri: file.uri), equals({file.path: []}), ); }); test('analyzes a nested directory file', () { final nested = Directory(p.join(tempDir.path, 'nested')) ..createSync(recursive: true); final main = File(p.join(nested.path, 'main.dart')) ..writeAsStringSync('void main() {}'); final other = File(p.join(nested.path, 'other.dart')) ..writeAsStringSync('void other() {}'); expect( linter.analyze(uri: nested.uri), equals({main.path: [], other.path: []}), ); }); lintTest( 'does not report when rule is ignored for file', rule: AvoidFlutterImports.new, path: 'counter_bloc.dart', content: ''' // ignore_for_file: ${AvoidFlutterImports.rule} import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not report when rule is ignored for file (w/leading space)', rule: AvoidFlutterImports.new, path: 'counter_bloc.dart', content: ''' // ignore_for_file: ${AvoidFlutterImports.rule} import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not report when rule is ignored for file (w/in-between space)', rule: AvoidFlutterImports.new, path: 'counter_bloc.dart', content: ''' // ignore_for_file: ${AvoidFlutterImports.rule} import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not report when rule is ignored for file (w/trailing space)', rule: AvoidFlutterImports.new, path: 'counter_bloc.dart', content: ''' // ignore_for_file: ${AvoidFlutterImports.rule} import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); }); lintTest( 'does not report when ignore_for_file contains type=lint', rule: AvoidFlutterImports.new, path: 'counter_bloc.dart', content: ''' // ignore_for_file: type=lint import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not report when //ignore: type=lint above', rule: AvoidFlutterImports.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; // ignore: type=lint import 'package:flutter/material.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not report when rule is ignored for line (above)', rule: AvoidFlutterImports.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; // ignore: ${AvoidFlutterImports.rule} import 'package:flutter/material.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not report when //ignore: type=lint after', rule: AvoidFlutterImports.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; // ignore: type=lint enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not report when rule is ignored for line (after)', rule: AvoidFlutterImports.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; // ignore: ${AvoidFlutterImports.rule} enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not report when rule is ignored for line (after w/leading space)', rule: AvoidFlutterImports.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; // ignore: ${AvoidFlutterImports.rule} enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not report when rule is ignored for line (after w/in-between space)', rule: AvoidFlutterImports.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; // ignore: ${AvoidFlutterImports.rule} enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not report when rule is ignored for line (after w/trailing space)', rule: AvoidFlutterImports.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; // ignore: ${AvoidFlutterImports.rule} enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not report when file is generated', rule: AvoidFlutterImports.new, path: 'counter_bloc.g.dart', content: ''' import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not report when file is in a dot directory', rule: AvoidFlutterImports.new, path: '.fvm/counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); }); } ================================================ FILE: packages/bloc_lint/test/src/rules/avoid_build_context_extensions_test.dart ================================================ import 'package:bloc_lint/src/rules/rules.dart'; import 'package:test/test.dart'; import '../lint_test_helper.dart'; void main() { group(AvoidBuildContextExtensions, () { group('lints when', () { group('context.read', () { lintTest( 'is used in a field assignment', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatefulWidget { const MyWidget({super.key}); @override State createState() => _MyWidgetState(); } class _MyWidgetState extends State { late final a = context.read(); ^^^^^^^^^^^^ @override Widget build(BuildContext context) { return const SizedBox(); } } ''', ); lintTest( 'is used in a variable assignment', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { final cubit = context.read(); ^^^^^^^^^^^^ return const SizedBox(); } } ''', ); lintTest( 'is used in a method call', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { context.read().increment(); ^^^^^^^^^^^^ return const SizedBox(); } } ''', ); lintTest( 'is declared with another variable on the same line', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { final String someVar, counterBloc = context.read(); ^^^^^^^^^^^^ return const SizedBox(); } } ''', ); lintTest( 'is used with an inferred bloc type', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { final CounterBloc counterBloc = context.read(); ^^^^^^^^^^^^ return const SizedBox(); } } ''', ); }); group('context.watch', () { lintTest( 'is used in a getter', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatefulWidget { const MyWidget({super.key}); @override State createState() => _MyWidgetState(); } class _MyWidgetState extends State { CounterCubit get cubit => context.watch(); ^^^^^^^^^^^^^ @override Widget build(BuildContext context) { return const SizedBox(); } } ''', ); lintTest( 'is used in a variable assignment', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { final count = context.watch().state; ^^^^^^^^^^^^^ return Text(count.toString()); } } ''', ); lintTest( 'is used in a widget', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: r''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { return Text('${context.watch().state}'); ^^^^^^^^^^^^^ } } ''', ); lintTest( 'is used in a ternary operator', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { final state = true ? context.watch().state : null; ^^^^^^^^^^^^^ return Text(state.toString()); } } ''', ); lintTest( 'is used with Cubit type', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { final userCubit = context.watch(); ^^^^^^^^^^^^^ return const SizedBox(); } } ''', ); }); group('context.select', () { lintTest( 'is used in a variable assignment', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { final isEven = context.select((cubit) => cubit.state.isEven); ^^^^^^^^^^^^^^ return Text(isEven ? 'true' : 'false'); } } ''', ); lintTest( 'is used in a widget', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: r''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { return Text('${context.select((cubit) => cubit.state)}'); ^^^^^^^^^^^^^^ } } ''', ); }); }); group('does not lint when', () { lintTest( 'using BlocProvider.of', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { final cubit = BlocProvider.of(context); return const SizedBox(); } } ''', ); lintTest( 'using BlocProvider(create: ...)', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => CounterCubit(), child: const SizedBox(), ); } } ''', ); lintTest( 'using other method calls on context', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { final size = context.size; final theme = context.theme; return const SizedBox(); } } ''', ); lintTest( 'using context in a string', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { final message = 'context.read is allowed'; return const SizedBox(); } } ''', ); lintTest( 'using context.read with non-bloc types', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { final provider = context.read(); return const SizedBox(); } } ''', ); lintTest( 'using context.watch with non-bloc types', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { final provider = context.watch(); return const SizedBox(); } } ''', ); lintTest( 'using context.select with non-bloc types', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { final value = context.select((provider) => provider.value); return const SizedBox(); } } ''', ); lintTest( 'using context extensions with implicit dynamic types', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { // These would be invalid Dart but should not trigger our rule context.read(); context.watch(); return const SizedBox(); } } ''', ); lintTest( 'using context.read with type inference for non-bloc types', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { final MyProvider provider = context.read(); return const SizedBox(); } } ''', ); lintTest( 'the call target is not context (e.g. cascade)', rule: AvoidBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyThing { MyThing read() => this; } class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { final thing = MyThing()..read(); return const SizedBox(); } } ''', ); }); }); } ================================================ FILE: packages/bloc_lint/test/src/rules/avoid_flutter_imports_test.dart ================================================ import 'package:bloc_lint/bloc_lint.dart'; import 'package:test/test.dart'; import '../lint_test_helper.dart'; void main() { group(AvoidFlutterImports, () { lintTest( 'lints when bloc contains flutter import', rule: AvoidFlutterImports.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'lints when cubit contains flutter import', rule: AvoidFlutterImports.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ class CounterCubit extends Cubit { CounterBloc() : super(0); } ''', ); lintTest( 'does not lint when no flutter import exists', rule: AvoidFlutterImports.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not lint when flutter import exists outside of bloc', rule: AvoidFlutterImports.new, path: 'main.dart', content: ''' import 'package:flutter/material.dart'; void main() => runApp(MyApp()); ''', ); }); } ================================================ FILE: packages/bloc_lint/test/src/rules/avoid_public_bloc_methods_test.dart ================================================ import 'package:bloc_lint/src/rules/rules.dart'; import 'package:test/test.dart'; import '../lint_test_helper.dart'; void main() { group(AvoidPublicBlocMethods, () { lintTest( 'lints when bloc contains public methods', rule: AvoidPublicBlocMethods.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); void foo() { print ('foo'); } ^^^ } ''', ); lintTest( 'lints when file name does not follow naming conventions', rule: AvoidPublicBlocMethods.new, path: 'main.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); void foo() { print ('foo'); } ^^^ } ''', ); lintTest( 'does not lint when no public methods exist', rule: AvoidPublicBlocMethods.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not lint allowed methods', rule: AvoidPublicBlocMethods.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); @override Future close() => super.close(); } ''', ); lintTest( 'does not lint public method overrides', rule: AvoidPublicBlocMethods.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); @override bool operator==(bool value) => false; } ''', ); lintTest( 'does not lint when public methods exist on Cubit', rule: AvoidPublicBlocMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); void increment() => emit(state + 1); } ''', ); lintTest( 'does not lint on internal switch expression', rule: AvoidPublicBlocMethods.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); bool _isEven(int x) => switch (x) { int() => false, }; } ''', ); }); } ================================================ FILE: packages/bloc_lint/test/src/rules/avoid_public_fields_test.dart ================================================ import 'package:bloc_lint/bloc_lint.dart'; import 'package:test/test.dart'; import '../lint_test_helper.dart'; void main() { group(AvoidPublicFields, () { lintTest( 'lints when bloc contains a public, mutable field', rule: AvoidPublicFields.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); var count = 0; ^^^^^^^^^^^^^^ } ''', ); lintTest( 'lints when file name does not follow naming conventions', rule: AvoidPublicFields.new, path: 'main.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); var count = 0; ^^^^^^^^^^^^^^ } ''', ); lintTest( 'lints when cubit contains a public, mutable field', rule: AvoidPublicFields.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); var count = 0; ^^^^^^^^^^^^^^ } ''', ); lintTest( 'lints when bloc contains a public, mutable field with type', rule: AvoidPublicFields.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); int count = 0; ^^^^^^^^^^^^^^ } ''', ); lintTest( 'lints when cubit contains a public, mutable field with type', rule: AvoidPublicFields.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); int count = 0; ^^^^^^^^^^^^^^ } ''', ); lintTest( 'lints when bloc has a public, final field with type', rule: AvoidPublicFields.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc(this.count) : super(0); final int count; ^^^^^^^^^^^^^^^^ } ''', ); lintTest( 'lints when cubit has a public, final field with type', rule: AvoidPublicFields.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit(this.count) : super(0); final int count; ^^^^^^^^^^^^^^^^ } ''', ); lintTest( 'lints when bloc has a public, final field with private type', rule: AvoidPublicFields.new, path: 'counter_bloc.dart', content: ''' typedef _Count = int; import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc(this.count) : super(0); final _Count count; ^^^^^^^^^^^^^^^^^^^ } ''', ); lintTest( 'lints when cubit has a public, final field with private type', rule: AvoidPublicFields.new, path: 'counter_cubit.dart', content: ''' typedef _Count = int; import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit(this.count) : super(0); final _Count count; ^^^^^^^^^^^^^^^^^^^ } ''', ); lintTest( 'lints when bloc has a public, late field with private type', rule: AvoidPublicFields.new, path: 'counter_bloc.dart', content: ''' typedef _Count = int; import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc(this.count) : super(0); late _Count count; ^^^^^^^^^^^^^^^^^^ } ''', ); lintTest( 'lints when bloc has a public, late, final field with private type', rule: AvoidPublicFields.new, path: 'counter_bloc.dart', content: ''' typedef _Count = int; import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc(this.count) : super(0); late final _Count count; ^^^^^^^^^^^^^^^^^^^^^^^^ } ''', ); lintTest( 'lints when cubit has a public, late, final field with private type', rule: AvoidPublicFields.new, path: 'counter_cubit.dart', content: ''' typedef _Count = int; import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit(this.count) : super(0); late final _Count count; ^^^^^^^^^^^^^^^^^^^^^^^^ } ''', ); lintTest( 'lints when bloc has a public, late field with type', rule: AvoidPublicFields.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc(this.count) : super(0); late int count; ^^^^^^^^^^^^^^^ } ''', ); lintTest( 'lints when cubit has a public, late field with type', rule: AvoidPublicFields.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit(this.count) : super(0); late int count; ^^^^^^^^^^^^^^^ } ''', ); lintTest( 'lints when bloc has a public, late, final field with type', rule: AvoidPublicFields.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc(this.count) : super(0); late final int count; ^^^^^^^^^^^^^^^^^^^^^ } ''', ); lintTest( 'lints when cubit has a public, late, final field with type', rule: AvoidPublicFields.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit(this.count) : super(0); late final int count; ^^^^^^^^^^^^^^^^^^^^^ } ''', ); lintTest( 'does not lint when bloc contains a private, mutable field', rule: AvoidPublicFields.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); var _count = 0; } ''', ); lintTest( 'does not lint when cubit contains a private, mutable field', rule: AvoidPublicFields.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); var _count = 0; } ''', ); lintTest( 'does not lint when bloc contains a private, mutable field with type', rule: AvoidPublicFields.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); StreamSubscription? _subscription; } ''', ); lintTest( 'does not lint when cubit contains a private, mutable field with type', rule: AvoidPublicFields.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); StreamSubscription? _subscription; } ''', ); lintTest( 'does not lint when bloc has a private, late, final field', rule: AvoidPublicFields.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); late final int _count; } ''', ); lintTest( 'does not lint when cubit has a private, late, final field', rule: AvoidPublicFields.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); late final int _count; } ''', ); lintTest( 'does not lint when bloc has a public, static field', rule: AvoidPublicFields.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); static const count = 0; } ''', ); lintTest( 'does not lint when cubit has a public, static field', rule: AvoidPublicFields.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); static const count = 0; } ''', ); lintTest( 'does not lint when bloc has no fields', rule: AvoidPublicFields.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not lint when cubit has no fields', rule: AvoidPublicFields.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); } ''', ); lintTest( 'does not lint when external class contains public fields', rule: AvoidPublicFields.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class Other { var state = 0; } class CounterCubit extends Cubit { CounterCubit() : super(0); } ''', ); }); } ================================================ FILE: packages/bloc_lint/test/src/rules/prefer_bloc_test.dart ================================================ import 'package:bloc_lint/src/rules/rules.dart'; import 'package:test/test.dart'; import '../lint_test_helper.dart'; void main() { group(PreferBloc, () { lintTest( 'lints when class extends Cubit', rule: PreferBloc.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { ^^^^^^^^^^^^ CounterCubit() : super(0); } ''', ); lintTest( 'lints when class extends HydratedCubit', rule: PreferBloc.new, path: 'counter_cubit.dart', content: ''' import 'package:hydrated_bloc/hydrated_bloc.dart'; class CounterCubit extends HydratedCubit { ^^^^^^^^^^^^ CounterCubit() : super(0); } ''', ); lintTest( 'lints when class extends MockCubit', rule: PreferBloc.new, path: 'app_test.dart', content: ''' import 'package:bloc/bloc.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:mocktail/mocktail.dart'; class _MockCounterCubit extends MockCubit implements CounterCubit {} ^^^^^^^^^^^^^^^^^ ''', ); lintTest( 'does not lint when class extends Bloc', rule: PreferBloc.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not lint when class extends MockBloc', rule: PreferBloc.new, path: 'app_test.dart', content: ''' import 'package:bloc/bloc.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:mocktail/mocktail.dart'; class _MockCounterBloc extends MockBloc implements CounterBloc {} ''', ); }); } ================================================ FILE: packages/bloc_lint/test/src/rules/prefer_build_context_extensions_test.dart ================================================ import 'package:bloc_lint/src/rules/rules.dart'; import 'package:test/test.dart'; import '../lint_test_helper.dart'; void main() { group(PreferBuildContextExtensions, () { group('BlocBuilder', () { lintTest( 'lints when using BlocBuilder', rule: PreferBuildContextExtensions.new, path: 'my_widget.dart', content: r''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( ^^^^^^^^^^^ builder: (context, state) => Text('$state'), ); } } ''', ); lintTest( 'lints when using BlocBuilder (buildWhen)', rule: PreferBuildContextExtensions.new, path: 'my_widget.dart', content: r''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( ^^^^^^^^^^^ buildWhen: (previous, current) => previous != current, builder: (context, state) => Text('$state'), ); } } ''', ); lintTest( 'lints when using BlocBuilder (explicit bloc)', rule: PreferBuildContextExtensions.new, path: 'my_widget.dart', content: r''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({required this.bloc, super.key}); final CounterBloc bloc; @override Widget build(BuildContext context) { return BlocBuilder( ^^^^^^^^^^^ bloc: bloc, builder: (context, state) => Text('$state'), ); } } ''', ); }); group('BlocSelector', () { lintTest( 'lints when using BlocSelector', rule: PreferBuildContextExtensions.new, path: 'my_widget.dart', content: r''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { return BlocSelector( ^^^^^^^^^^^^ selector: (state) => state.isEven, builder: (context, isEven) => Text('$isEven'), ); } } ''', ); }); group('BlocProvider', () { lintTest( 'lints when using BlocProvider.of (assignment)', rule: PreferBuildContextExtensions.new, path: 'my_widget.dart', content: r''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { final count = BlocProvider.of(context).state; ^^^^^^^^^^^^^^^ return const Text('\$count'); } } ''', ); lintTest( 'lints when using BlocProvider.of (invocation)', rule: PreferBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { return FloatingActionButton( child: const Icon(Icons.add), onPressed: () => BlocProvider.of().add(CounterEvent.increment); ^^^^^^^^^^^^^^^ ); } } ''', ); lintTest( 'does not lint when using BlocProvider(create: ...)', rule: PreferBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => CounterCubit(), child: const SizedBox(), ); } } ''', ); lintTest( 'does not lint when using BlocProvider.value', rule: PreferBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatefulWidget { const MyWidget({super.key}); @override State createState() => _MyWidgetState(); } class _MyWidgetState extends State { final bloc = CounterBloc(); @override Widget build(BuildContext context) { return BlocProvider.value( value: bloc, child: const SizedBox(), ); } @override void dispose() { bloc.close(); } } ''', ); }); group('RepositoryProvider', () { lintTest( 'lints when using RepositoryProvider.of (assignment)', rule: PreferBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => WeatherBloc( weatherRepository: RepositoryProvider.of(context), ^^^^^^^^^^^^^^^^^^^^^ ), child: WeatherView(), ); } } ''', ); lintTest( 'lints when using RepositoryProvider.of (invocation)', rule: PreferBuildContextExtensions.new, path: 'my_button.dart', content: ''' import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyButton extends StatelessWidget { const MyButton({super.key}); @override Widget build(BuildContext context) { return FloatingActionButton( child: const Icon(Icons.add), onPressed: () => RepositoryProvider.of().add(), ^^^^^^^^^^^^^^^^^^^^^ ); } } ''', ); lintTest( 'does not lint when using RepositoryProvider(create: ...)', rule: PreferBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatelessWidget { const MyWidget({super.key}); @override Widget build(BuildContext context) { return RepositoryProvider( create: (_) => MyRepository(), child: const SizedBox(), ); } } ''', ); lintTest( 'does not lint when using RepositoryProvider.value', rule: PreferBuildContextExtensions.new, path: 'my_widget.dart', content: ''' import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MyWidget extends StatefulWidget { const MyWidget({super.key}); @override State createState() => _MyWidgetState(); } class _MyWidgetState extends State { final repository = MyRepository(); @override Widget build(BuildContext context) { return RepositoryProvider.value( value: repository, child: const SizedBox(), ); } } ''', ); }); }); } ================================================ FILE: packages/bloc_lint/test/src/rules/prefer_cubit_test.dart ================================================ import 'package:bloc_lint/src/rules/rules.dart'; import 'package:test/test.dart'; import '../lint_test_helper.dart'; void main() { group(PreferCubit, () { lintTest( 'lints when class extends Bloc', rule: PreferCubit.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { ^^^^^^^^^^^ CounterBloc() : super(0); } ''', ); lintTest( 'lints when class extends HydratedBloc', rule: PreferCubit.new, path: 'counter_bloc.dart', content: ''' import 'package:hydrated_bloc/hydrated_bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends HydratedBloc { ^^^^^^^^^^^ CounterBloc() : super(0); } ''', ); lintTest( 'lints when class extends MockBloc', rule: PreferCubit.new, path: 'app_test.dart', content: ''' import 'package:bloc/bloc.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:mocktail/mocktail.dart'; class _MockCounterBloc extends MockBloc implements CounterBloc {} ^^^^^^^^^^^^^^^^ ''', ); lintTest( 'does not lint when class extends Cubit', rule: PreferCubit.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); } ''', ); lintTest( 'does not lint when class extends MockCubit', rule: PreferCubit.new, path: 'app_test.dart', content: ''' import 'package:bloc/bloc.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:mocktail/mocktail.dart'; class _MockCounterCubit extends MockCubit implements CounterCubit {} ''', ); }); } ================================================ FILE: packages/bloc_lint/test/src/rules/prefer_file_naming_conventions_test.dart ================================================ import 'package:bloc_lint/src/rules/rules.dart'; import 'package:test/test.dart'; import '../lint_test_helper.dart'; void main() { group(PreferFileNamingConventions, () { lintTest( 'lints when CounterBloc is declared in main.dart', rule: PreferFileNamingConventions.new, path: 'main.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { ^^^^^^^^^^^ CounterBloc() : super(0); } ''', ); lintTest( 'lints when CounterBloc is declared in counter_cubit.dart', rule: PreferFileNamingConventions.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { ^^^^^^^^^^^ CounterBloc() : super(0); } ''', ); lintTest( 'lints when CounterCubit is declared in main.dart', rule: PreferFileNamingConventions.new, path: 'main.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { ^^^^^^^^^^^^ CounterCubit() : super(0); } ''', ); lintTest( 'lints when CounterCubit is declared in counter_bloc.dart', rule: PreferFileNamingConventions.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { ^^^^^^^^^^^^ CounterCubit() : super(0); } ''', ); lintTest( 'does not lint when CounterBloc is declared in counter_bloc.dart', rule: PreferFileNamingConventions.new, path: 'counter_bloc.dart', content: ''' import 'package:bloc/bloc.dart'; enum CounterEvent { increment, decrement } class CounterBloc extends Bloc { CounterBloc() : super(0); } ''', ); lintTest( 'does not lint when CounterCubit is declared in counter_cubit.dart', rule: PreferFileNamingConventions.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); } ''', ); lintTest( 'does not lint when extending MockBloc in tests', rule: PreferFileNamingConventions.new, path: 'counter_test.dart', content: ''' import 'package:bloc/bloc.dart'; import 'package:bloc_test/bloc_test.dart'; class MockCounterBloc extends MockBloc implements CounterBloc {} ''', ); lintTest( 'does not lint when extending MockCubit in tests', rule: PreferFileNamingConventions.new, path: 'counter_test.dart', content: ''' import 'package:bloc/bloc.dart'; import 'package:bloc_test/bloc_test.dart'; class MockCounterCubit extends MockCubit implements CounterCubit {} ''', ); }); } ================================================ FILE: packages/bloc_lint/test/src/rules/prefer_void_public_cubit_methods_test.dart ================================================ import 'package:bloc_lint/src/rules/rules.dart'; import 'package:test/test.dart'; import '../lint_test_helper.dart'; void main() { group(PreferVoidPublicCubitMethods, () { lintTest( 'does not lint when no public methods exist on Cubit', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); } ''', ); lintTest( 'does not lint when void public methods exist on Cubit', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); void increment() => emit(state + 1); } ''', ); lintTest( 'does not lint when Future public methods exist on Cubit', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); Future increment() async => emit(state + 1); } ''', ); lintTest( 'does not lint when FutureOr public methods exist on Cubit', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); FutureOr increment() async => emit(state + 1); } ''', ); lintTest( 'does not lint when void public getter exists on Cubit', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); void get foo => null; } ''', ); lintTest( 'does not lint when explicit void public setter exists on Cubit', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); String _tag = ''; void set tag(String value) { tag = value; } } ''', ); lintTest( 'does not lint when implicit void public setter exists on Cubit', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); String _tag = ''; set tag(String value) { tag = value; } } ''', ); lintTest( 'does not lint for overridden methods', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); @override String toString() => 'CounterCubit'; } ''', ); lintTest( 'does not lint when non-void public methods exist on other classes', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class Dash { bool get isCool => true; } class CounterCubit extends Cubit { CounterCubit() : super(0); } ''', ); lintTest( 'lints when file name does not follow naming conventions', rule: PreferVoidPublicCubitMethods.new, path: 'main.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); void increment() => emit(state + 1); bool get isEven => state.isEven; ^^^^^^ } ''', ); lintTest( 'lints when public getter exists on Cubit (bool)', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); void increment() => emit(state + 1); bool get isEven => state.isEven; ^^^^^^ } ''', ); lintTest( 'lints when public methods exist on Cubit (int)', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); int increment() { ^^^^^^^^^ emit(state + 1); return state; } } ''', ); lintTest( 'lints when public methods exist on Cubit (Future)', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); Future increment() async { ^^^^^^^^^ emit(state + 1); return state; } } ''', ); lintTest( 'lints when public methods exist on Cubit (Future>)', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); Future> increment() async { ^^^^^^^^^ emit(state + 1); return {'count': state}; } } ''', ); lintTest( 'lints when public methods exist on Cubit (explicit dynamic)', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); dynamic increment() { ^^^^^^^^^ emit(state + 1); return state; } } ''', ); lintTest( 'lints when public methods exist on Cubit (implicit dynamic)', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); increment() { ^^^^^^^^^ emit(state + 1); return state; } } ''', ); lintTest( 'does not lint on internal switch expression', rule: PreferVoidPublicCubitMethods.new, path: 'counter_cubit.dart', content: ''' import 'package:bloc/bloc.dart'; class ToggleCubit extends Cubit { ToggleCubit() : super(false); void toggle() => switch (state) { true => emit(false), false => emit(true), }; } ''', ); }); } ================================================ FILE: packages/bloc_lint/test/src/text_document_test.dart ================================================ import 'package:bloc_lint/bloc_lint.dart'; import 'package:test/test.dart'; Position at({required int line, required int char}) { return Position(character: char, line: line); } Position position(int line, int char) => at(line: line, char: char); Range range(int startLine, int startChar, int endLine, int endChar) { return Range( start: at(line: startLine, char: startChar), end: at(line: endLine, char: endChar), ); } TextDocument createDocument(String content) { return TextDocument(uri: Uri.parse('test://hello/world'), content: content); } void main() { group(TextDocument, () { group('lines, offsets and positions', () { test('empty content', () { final document = createDocument(''); expect(document.offsetAt(position(0, 0)), equals(0)); final pos = document.positionAt(0); expect(pos.line, equals(0)); expect(pos.character, equals(0)); }); test('single line', () { const content = 'Hello World'; final document = createDocument(content); for (var i = 0; i < content.length; i++) { expect(document.offsetAt(position(0, i)), equals(i)); final pos = document.positionAt(i); expect(pos.line, equals(0)); expect(pos.character, equals(i)); } }); test('multiple lines', () { const content = 'abcde\nfghij\nklmno\n'; final document = createDocument(content); for (var i = 0; i < content.length; i++) { final line = (i / 6).floor(); final char = i % 6; expect(document.offsetAt(position(line, char)), equals(i)); final pos = document.positionAt(i); expect(pos.line, equals(line)); expect(pos.character, equals(char)); } // Out of bounds. expect(document.offsetAt(position(3, 0)), content.length); expect(document.offsetAt(position(3, 1)), content.length); var pos = document.positionAt(18); expect(pos.line, equals(3)); expect(pos.character, equals(0)); pos = document.positionAt(19); expect(pos.line, equals(3)); expect(pos.character, equals(0)); }); test('getText', () { const content = 'abcde\nfghij\nklmno'; final document = createDocument(content); expect(document.getText(), equals(content)); expect(document.getText(range: range(0, 0, 0, 5)), equals('abcde')); expect(document.getText(range: range(0, 4, 1, 1)), equals('e\nf')); }); test('invalid input at beginning of file', () { final document = createDocument('asdf'); expect(document.offsetAt(position(-1, 0)), 0); expect(document.offsetAt(position(0, -1)), 0); final pos = document.positionAt(-1); expect(pos.line, equals(0)); expect(pos.character, equals(0)); }); test('invalid input at end of file', () { final document = createDocument('asdf'); expect(document.offsetAt(position(1, 1)), 4); final pos = document.positionAt(8); expect(pos.line, equals(0)); expect(pos.character, equals(4)); }); test('invalid input at beginning of line', () { final document = createDocument('a\ns\nd\r\nf'); expect(document.offsetAt(position(0, -1)), 0); expect(document.offsetAt(position(1, -1)), 2); expect(document.offsetAt(position(2, -1)), 4); expect(document.offsetAt(position(3, -1)), 7); }); test('invalid input at end of line', () { final document = createDocument('a\ns\nd\r\nf'); expect(document.offsetAt(position(0, 10)), 1); expect(document.offsetAt(position(1, 10)), 3); expect(document.offsetAt(position(2, 2)), 5); expect(document.offsetAt(position(2, 3)), 5); expect(document.offsetAt(position(2, 10)), 5); expect(document.offsetAt(position(3, 10)), 8); final pos = document.positionAt(6); expect(pos.line, equals(2)); expect(pos.character, equals(1)); }); }); }); group(TextDocumentType, () { test('detects file types', () { final blocFile = TextDocument( uri: Uri.parse('counter_bloc.dart'), content: '', ); expect(blocFile.type, equals(TextDocumentType.bloc)); expect(blocFile.type.isBloc, isTrue); expect(blocFile.type.isCubit, isFalse); expect(blocFile.type.isOther, isFalse); final cubitFile = TextDocument( uri: Uri.parse('counter_cubit.dart'), content: '', ); expect(cubitFile.type, equals(TextDocumentType.cubit)); expect(cubitFile.type.isBloc, isFalse); expect(cubitFile.type.isCubit, isTrue); expect(cubitFile.type.isOther, isFalse); final otherFile = TextDocument(uri: Uri.parse('main.dart'), content: ''); expect(otherFile.type, equals(TextDocumentType.other)); expect(otherFile.type.isBloc, isFalse); expect(otherFile.type.isCubit, isFalse); expect(otherFile.type.isOther, isTrue); }); }); group('ignoreForFile', () { test('returns empty when no file ignores exist', () { final document = TextDocument( uri: Uri.parse('counter_bloc.dart'), content: ''' void main() { print("hello world"); } ''', ); expect(document.ignoreForFile, isEmpty); }); test('detects ignore_for_file at start of file', () { final document = TextDocument( uri: Uri.parse('counter_bloc.dart'), content: ''' // ignore_for_file: ${PreferBloc.rule}, ${PreferCubit.rule} void main() { print("hello world"); } ''', ); expect( document.ignoreForFile, equals({PreferBloc.rule, PreferCubit.rule}), ); }); test('detects multiple ignore_for_file', () { final document = TextDocument( uri: Uri.parse('counter_bloc.dart'), content: ''' // ignore_for_file: ${PreferBloc.rule} // ignore_for_file: ${PreferCubit.rule} void main() { print("hello world"); } ''', ); expect( document.ignoreForFile, equals({PreferBloc.rule, PreferCubit.rule}), ); }); test('ignore_for_file throughout file', () { final document = TextDocument( uri: Uri.parse('counter_bloc.dart'), content: ''' void main() { // ignore_for_file: ${PreferBloc.rule} print("hello world"); } // ignore_for_file: ${PreferCubit.rule} ''', ); expect( document.ignoreForFile, equals({PreferBloc.rule, PreferCubit.rule}), ); }); }); group('ignoreForLine', () { test('returns empty when no ignore exists', () { final document = TextDocument( uri: Uri.parse('counter_cubit.dart'), content: ''' import 'package:flutter/material.dart'; ''', ); const range = Range( start: Position(character: 0, line: 0), end: Position(character: 39, line: 0), ); expect(document.ignoreForLine(range: range), isEmpty); }); test('returns ignores above line', () { final document = TextDocument( uri: Uri.parse('counter_cubit.dart'), content: ''' // ignore: ${PreferBloc.rule}, ${PreferCubit.rule} import 'package:flutter/material.dart';''', ); const range = Range( start: Position(character: 0, line: 1), end: Position(character: 39, line: 1), ); expect( document.ignoreForLine(range: range), equals({PreferBloc.rule, PreferCubit.rule}), ); }); test('returns ignores after line', () { final document = TextDocument( uri: Uri.parse('counter_cubit.dart'), content: ''' import 'package:flutter/material.dart'; // ignore: ${PreferBloc.rule}, ${PreferCubit.rule}''', ); const range = Range( start: Position(character: 0, line: 0), end: Position(character: 39, line: 0), ); expect( document.ignoreForLine(range: range), equals({PreferBloc.rule, PreferCubit.rule}), ); }); }); } ================================================ FILE: packages/bloc_test/CHANGELOG.md ================================================ # 10.0.0 - refactor: `blocTest` depends on core interfaces instead of `BlocBase` ([#4311](https://github.com/felangel/bloc/pull/4311)) - chore: update to `bloc: ^9.0.0` - chore: bump minimum Dart SDK to 2.14 - chore: add `funding` to `pubspec.yaml` - chore: update sponsors # 9.1.7 - chore: update copyright year - chore: update sponsors # 9.1.6 - chore: update sponsors ([#4054](https://github.com/felangel/bloc/pull/4054)) # 9.1.5 - fix: `blocTest` supports `async` `expect` ([#3976](https://github.com/felangel/bloc/pull/3976)) # 9.1.4 - deps: support `mocktail: ^1.0.0` ([#3917](https://github.com/felangel/bloc/pull/3917)) - chore: add topics to `pubspec.yaml` ([#3914](https://github.com/felangel/bloc/pull/3914)) # 9.1.3 - fix: test timeouts due to uncaught exceptions which occur with `package:test ^1.22.2` ([#3854](https://github.com/felangel/bloc/pull/3854)) # 9.1.2 - docs: upgrade to Dart 3 - refactor: standardize analysis_options # 9.1.1 - chore: upgrade to `bloc ^8.1.1` ([#3723](https://github.com/felangel/bloc/pull/3723)) - refactor: `BlocObserver` instances to use `const` constructors ([#3713](https://github.com/felangel/bloc/pull/3713)) - refactor: upgrade to Dart 2.19 ([#3699](https://github.com/felangel/bloc/pull/3699)) - remove deprecated `invariant_booleans` lint rule - docs: fix snippet in `README` ([#3552](https://github.com/felangel/bloc/pull/3552)) # 9.1.0 - feat: upgrade to `bloc: ^8.1.0` ([#3502](https://github.com/felangel/bloc/pull/3502)) # 9.0.3 - chore: support for mocktail v0.3.0 ([#3252](https://github.com/felangel/bloc/pull/3252)) - docs: update GetStream utm tags ([#3136](https://github.com/felangel/bloc/pull/3136)) - docs: update VGV sponsors logo ([#3125](https://github.com/felangel/bloc/pull/3125)) # 9.0.2 - fix: throw uncaught exceptions ([#3070](https://github.com/felangel/bloc/pull/3070)) - chore: upgrade to `bloc v8.0.2` - docs: update example to follow naming conventions ([#3032](https://github.com/felangel/bloc/pull/3032)) # 9.0.1 - chore: upgrade to `bloc v8.0.1` # 9.0.0 - **BREAKING**: feat: upgrade to `bloc v8.0.0` - feat: `MockBloc` no longer implicitly requires `registerFallbackValue` for events and states # 9.0.0-dev.5 - **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.5` # 9.0.0-dev.4 - **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.4` # 9.0.0-dev.3 - **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.3` # 9.0.0-dev.2 - **BREAKING**: feat: upgrade to `mocktail v0.2.0` # 9.0.0-dev.1 - **BREAKING**: feat: upgrade to `bloc v8.0.0-dev.2` - feat: `MockBloc` no longer implicitly requires `registerFallbackValue` for events and states # 8.5.0 - feat: prettier diffing when using `blocTest` and `expect` does not match emitted states ([#1783](https://github.com/felangel/bloc/issues/1783)) # 8.4.0 - feat: upgrade to `mocktail ^0.2.0` # 8.3.0 - feat: accept optional tags in `blocTest` - `tags` is optional and if it is passed, it declares user-defined tags that are applied to the test. These tags can be used to select or skip the test on the command line, or to do bulk test configuration. # 8.2.0 - feat: upgrade to `bloc ^7.2.0` # 8.1.0 - feat: add `setUp` and `tearDown` to `blocTest` # 8.0.2 - fix: revert `package:mocktail` export to reduce conflicts with `package:mockito` # 8.0.1 - fix: allow `blocTest` to capture non-exceptions - feat: export `package:mocktail` # 8.0.0 - **BREAKING**: feat: opt into null safety - upgrade Dart SDK constraints to `>=2.12.0-0 <3.0.0` - **BREAKING**: feat: `seed` returns a `Function` to support dynamic seed values - **BREAKING**: refactor: remove `emitsExactly` - **BREAKING**: feat: introduce `MockCubit` - **BREAKING**: refactor: `MockBloc` uses [package:mocktail](https://pub.dev/packages/mocktail) - **BREAKING**: refactor: `expect` returns a `Function` with `Matcher` support - **BREAKING**: refactor: `errors` returns a `Function` with `Matcher` support - **BREAKING**: refactor: `whenListen` does not stub `skip` - feat: `MockBloc` and `MockCubit` automatically stub core API - feat: add optional `initialState` to `whenListen` - feat: upgrade to `bloc ^7.0.0` - feat: upgrade to `mocktail: ^0.1.0` # 8.0.0-nullsafety.6 - chore: upgrade to `bloc ^7.0.0-nullsafety.4` # 8.0.0-nullsafety.5 - feat: upgrade to `mocktail: ^0.1.0` - feat: use `package:test` instead of `package:test_api` # 8.0.0-nullsafety.4 - **BREAKING**: feat: `seed` returns a `Function` to support dynamic seed values # 8.0.0-nullsafety.3 - feat: upgrade to `mocktail: ">=0.0.2-dev.5 <0.0.2"` # 8.0.0-nullsafety.2 - fix: restrict to `mocktail: ">=0.0.1-dev.12 <0.0.1"` - feat: use `package:test_api` instead of `package:test` for sound null safety # 8.0.0-nullsafety.1 - chore: upgrade to `bloc ^7.0.0-nullsafety.3` - chore: upgrade to `mocktail ^0.0.1-dev.12` # 8.0.0-nullsafety.0 - **BREAKING**: feat: opt into null safety - **BREAKING**: feat: upgrade Dart SDK constraints to `>=2.12.0-0 <3.0.0` - **BREAKING**: refactor: remove `emitsExactly` - **BREAKING**: refactor: `MockBloc` uses [package:mocktail](https://pub.dev/packages/mocktail) - **BREAKING**: feat: introduce `MockCubit` which uses [package:mocktail](https://pub.dev/packages/mocktail) - **BREAKING**: refactor: `expect` returns a `Function` with `Matcher` support - **BREAKING**: refactor: `errors` returns a `Function` with `Matcher` support - **BREAKING**: refactor: `whenListen` does not stub `skip` - feat: introduce `MockCubit` - feat: `MockBloc` and `MockCubit` automatically stub core API - feat: add optional `initialState` to `whenListen` # 7.1.0 - feat: add `seed` property to `blocTest` # 7.0.6 - chore: revert support `dart >=2.7.0` # 7.0.5 - fix: update to `mockito ^4.1.2` - chore: update to `dart >=2.10.0` # 7.0.4 - feat: `blocTest` provides warning to implement deep equality when shallow equality is true # 7.0.3 - restrict `mockito` to `<4.1.2` to prevent breaking changes due to NNBD # 7.0.2 - fix: `blocTest` timeouts when verify fails ([#1639](https://github.com/felangel/bloc/issues/1639)) - fix: `blocTest` timeouts when expect fails ([#1645](https://github.com/felangel/bloc/issues/1645)) # 7.0.1 - chore: deprecate `emitsExactly` in favor of `blocTest` - fix: capture uncaught exceptions in `Cubit` # 7.0.0 - **BREAKING**: upgrade to `bloc ^6.0.0` - **BREAKING**: `MockBloc` only requires `State` type - **BREAKING**: `whenListen` only requires `State` type - **BREAKING**: `blocTest` only requires `State` type - **BREAKING**: `blocTest` `skip` defaults to `0` - **BREAKING**: `blocTest` make `build` synchronous - fix: `blocTest` improve `wait` behavior when debouncing, etc... - feat: `blocTest` do not require `async` on `act` and `verify` - feat: remove external dependency on [package:cubit_test](https://pub.dev/packages/cubit_test) - feat: `MockBloc` is compatible with `cubit` - feat: `whenListen` is compatible with `cubit` - feat: `blocTest` is compatible with `cubit` # 7.0.0-dev.2 - **BREAKING**: `blocTest` make `build` synchronous - fix: `blocTest` improve `wait` behavior when debouncing, etc... - feat: `blocTest` do not require `async` on `act` and `verify` # 7.0.0-dev.1 - **BREAKING**: upgrade to `bloc ^6.0.0-dev.1` - **BREAKING**: `MockBloc` only requires `State` type - **BREAKING**: `whenListen` only requires `State` type - **BREAKING**: `blocTest` only requires `State` type - **BREAKING**: `blocTest` `skip` defaults to `0` - feat: remove external dependency on [package:cubit_test](https://pub.dev/packages/cubit_test) - feat: `MockBloc` is compatible with `cubit` - feat: `whenListen` is compatible with `cubit` - feat: `blocTest` is compatible with `cubit` # 6.0.1 - fix: upgrade to `bloc ^5.0.1` - fix: upgrade to `cubit_test ^0.1.1` - docs: minor documentation updates # 6.0.0 - feat: upgrade to `bloc ^5.0.0` - refactor: internal implementation updates to use [cubit_test](https://pub.dev/packages/cubit_test) # 6.0.0-dev.4 - Update to `bloc ^5.0.0-dev.11`. # 6.0.0-dev.3 - Update to `bloc ^5.0.0-dev.10`. # 6.0.0-dev.2 - Update to `bloc ^5.0.0-dev.7`. # 6.0.0-dev.1 - Update to `bloc ^5.0.0-dev.6`. - Internal implementation updates to use [cubit_test](https://pub.dev/packages/cubit_test) # 5.1.0 - Add `errors` to `blocTest` to enable expecting unhandled exceptions within blocs. - Update `whenListen` to also handle stubbing the state property of the bloc. # 5.0.0 - Update to `bloc: ^4.0.0` # 5.0.0-dev.4 - Update to `bloc: ^4.0.0-dev.4` # 5.0.0-dev.3 - Update to `bloc: ^4.0.0-dev.3` # 5.0.0-dev.2 - Update to `bloc: ^4.0.0-dev.2` # 5.0.0-dev.1 - Update to `bloc: ^4.0.0-dev.1` # 4.0.0 - `blocTest` and `emitsExactly` skip `initialState` by default and expose optional `skip` ([#910](https://github.com/felangel/bloc/issues/910)) - `blocTest` async `build` ([#910](https://github.com/felangel/bloc/issues/910)) - `blocTest` `expect` is optional ([#910](https://github.com/felangel/bloc/issues/910)) - `blocTest` `verify` includes the built bloc ([#910](https://github.com/felangel/bloc/issues/910)) # 3.1.0 - Add `verify` to `blocTest` ([#781](https://github.com/felangel/bloc/issues/781)) # 3.0.1 - Enable `blocTest` to add more than one asynchronous event at a time ([#724](https://github.com/felangel/bloc/issues/724)) # 3.0.0 - Update to `bloc: ^3.0.0` - `emitsExactly` supports optional `duration` for async operators like `debounceTime` ([#726](https://github.com/felangel/bloc/issues/726)) - `blocTest` supports optional `wait` for async operators like `debounceTime` ([#726](https://github.com/felangel/bloc/issues/726)) # 3.0.0-dev.1 - Update to `bloc: ^3.0.0-dev.1` # 2.2.2 - Minor internal improvements (fixed analysis warning in `emitsExactly`) # 2.2.1 - Minor documentation improvements (syntax highlighting in README) # 2.2.0 - `emitsExactly` and `blocTest` support `Iterable Bloc Test Package

    Pub build codecov Star on Github style: bloc lint Flutter Website Awesome Flutter Flutter Samples License: MIT Discord Bloc Library

    --- A Dart package that makes testing blocs and cubits easy. Built to work with [bloc](https://pub.dev/packages/bloc) and [mocktail](https://pub.dev/packages/mocktail). **Learn more at [bloclibrary.dev](https://bloclibrary.dev)!** --- ## Sponsors Our top sponsors are shown below! [[Become a Sponsor](https://github.com/sponsors/felangel)]
    --- ## Create a Mock ```dart import 'package:bloc_test/bloc_test.dart'; class MockCounterBloc extends MockBloc implements CounterBloc {} class MockCounterCubit extends MockCubit implements CounterCubit {} ``` ## Stub the State Stream **whenListen** creates a stub response for the `listen` method on a bloc or cubit. Use `whenListen` if you want to return a canned `Stream` of states. `whenListen` also handles stubbing the `state` to stay in sync with the emitted state. ```dart // Create a mock instance final counterBloc = MockCounterBloc(); // Stub the state stream whenListen( counterBloc, Stream.fromIterable([0, 1, 2, 3]), initialState: 0, ); // Assert that the initial state is correct. expect(counterBloc.state, equals(0)); // Assert that the stubbed stream is emitted. await expectLater(counterBloc.stream, emitsInOrder([0, 1, 2, 3])); // Assert that the current state is in sync with the stubbed stream. expect(counterBloc.state, equals(3)); ``` ## Unit Test with blocTest **blocTest** creates a new `bloc`-specific test case with the given `description`. `blocTest` will handle asserting that the `bloc` emits the `expect`ed states (in order) after `act` is executed. `blocTest` also handles ensuring that no additional states are emitted by closing the `bloc` stream before evaluating the `expect`ation. `setUp` is optional and should be used to set up any dependencies prior to initializing the `bloc` under test. `setUp` should be used to set up state necessary for a particular test case. For common set up code, prefer to use `setUp` from `package:test/test.dart`. `build` should construct and return the `bloc` under test. `seed` is an optional `Function` that returns a state which will be used to seed the `bloc` before `act` is called. `act` is an optional callback which will be invoked with the `bloc` under test and should be used to interact with the `bloc`. `skip` is an optional `int` which can be used to skip any number of states. `skip` defaults to 0. `wait` is an optional `Duration` which can be used to wait for async operations within the `bloc` under test such as `debounceTime`. `expect` is an optional `Function` that returns a `Matcher` which the `bloc` under test is expected to emit after `act` is executed. `verify` is an optional callback which is invoked after `expect` and can be used for additional verification/assertions. `verify` is called with the `bloc` returned by `build`. `errors` is an optional `Function` that returns a `Matcher` which the `bloc` under test is expected to throw after `act` is executed. `tearDown` is optional and can be used to execute any code after the test has run. `tearDown` should be used to clean up after a particular test case. For common tear down code, prefer to use `tearDown` from `package:test/test.dart`. `tags` is optional and if it is passed, it declares user-defined tags that are applied to the test. These tags can be used to select or skip the test on the command line, or to do bulk test configuration. ```dart group('CounterBloc', () { blocTest( 'emits [] when nothing is added', build: () => CounterBloc(), expect: () => [], ); blocTest( 'emits [1] when CounterIncrementPressed is added', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterIncrementPressed()), expect: () => [1], ); }); ``` `blocTest` can optionally be used with a seeded state. ```dart blocTest( 'emits [10] when seeded with 9', build: () => CounterBloc(), seed: () => 9, act: (bloc) => bloc.add(CounterIncrementPressed()), expect: () => [10], ); ``` `blocTest` can also be used to `skip` any number of emitted states before asserting against the expected states. The default value is 0. ```dart blocTest( 'emits [2] when CounterIncrementPressed is added twice', build: () => CounterBloc(), act: (bloc) => bloc..add(CounterIncrementPressed())..add(CounterIncrementPressed()), skip: 1, expect: () => [2], ); ``` `blocTest` can also be used to wait for async operations like `debounceTime` by providing a `Duration` to `wait`. ```dart blocTest( 'emits [MyState] when MyEvent is added', build: () => MyBloc(), act: (bloc) => bloc.add(MyEvent()), wait: const Duration(milliseconds: 300), expect: () => [isA()], ); ``` `blocTest` can also be used to `verify` internal bloc functionality. ```dart blocTest( 'emits [MyState] when MyEvent is added', build: () => MyBloc(), act: (bloc) => bloc.add(MyEvent()), expect: () => [isA()], verify: (_) { verify(() => repository.someMethod(any())).called(1); } ); ``` `blocTest` can also be used to expect that exceptions have been thrown. ```dart blocTest( 'throws Exception when null is added', build: () => MyBloc(), act: (bloc) => bloc.add(null), errors: () => [isA()] ); ``` **Note:** when using `blocTest` with state classes which don't override `==` and `hashCode` you can provide an `Iterable` of matchers instead of explicit state instances. ```dart blocTest( 'emits [MyState] when MyEvent is added', build: () => MyBloc(), act: (bloc) => bloc.add(MyEvent()), expect: () => [isA()], ); ``` ## Dart Versions - Dart 2: >= 2.14 ## Maintainers - [Felix Angelov](https://github.com/felangel) ================================================ FILE: packages/bloc_test/analysis_options.yaml ================================================ include: - package:bloc_lint/recommended.yaml - ../../analysis_options.yaml ================================================ FILE: packages/bloc_test/example/main.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:test/test.dart'; // Mock Cubit class MockCounterCubit extends MockCubit implements CounterCubit {} // Mock Bloc class MockCounterBloc extends MockBloc implements CounterBloc {} void main() { mainCubit(); mainBloc(); } void mainCubit() { group('whenListen', () { test("Let's mock the CounterCubit's stream!", () { // Create Mock CounterCubit Instance final cubit = MockCounterCubit(); // Stub the listen with a fake Stream whenListen(cubit, Stream.fromIterable([0, 1, 2, 3])); // Expect that the CounterCubit instance emitted the stubbed Stream of // states expectLater(cubit.stream, emitsInOrder([0, 1, 2, 3])); }); }); group('CounterCubit', () { blocTest( 'emits [] when nothing is called', build: () => CounterCubit(), expect: () => const [], ); blocTest( 'emits [1] when increment is called', build: () => CounterCubit(), act: (cubit) => cubit.increment(), expect: () => const [1], ); }); } void mainBloc() { group('whenListen', () { test("Let's mock the CounterBloc's stream!", () { // Create Mock CounterBloc Instance final bloc = MockCounterBloc(); // Stub the listen with a fake Stream whenListen(bloc, Stream.fromIterable([0, 1, 2, 3])); // Expect that the CounterBloc instance emitted the stubbed Stream of // states expectLater(bloc.stream, emitsInOrder([0, 1, 2, 3])); }); }); group('CounterBloc', () { blocTest( 'emits [] when nothing is added', build: () => CounterBloc(), expect: () => const [], ); blocTest( 'emits [1] when CounterIncrementPressed is added', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterIncrementPressed()), expect: () => const [1], ); }); } // ignore: prefer_file_naming_conventions class CounterCubit extends Cubit { CounterCubit() : super(0); void increment() => emit(state + 1); } abstract class CounterEvent {} class CounterIncrementPressed extends CounterEvent {} // ignore: prefer_file_naming_conventions class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) => emit(state + 1)); } } ================================================ FILE: packages/bloc_test/lib/bloc_test.dart ================================================ /// A testing library which makes it easy to test blocs. /// /// Get started at [bloclibrary.dev](https://bloclibrary.dev) 🚀 library bloc_test; export 'src/bloc_test.dart'; export 'src/mock_bloc.dart'; export 'src/when_listen.dart'; ================================================ FILE: packages/bloc_test/lib/src/bloc_test.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:diff_match_patch/diff_match_patch.dart'; import 'package:meta/meta.dart'; import 'package:test/test.dart' as test; /// Creates a new `bloc`-specific test case with the given [description]. /// [blocTest] will handle asserting that the `bloc` emits the [expect]ed /// states (in order) after [act] is executed. /// [blocTest] also handles ensuring that no additional states are emitted /// by closing the `bloc` stream before evaluating the [expect]ation. /// /// [setUp] is optional and should be used to set up /// any dependencies prior to initializing the `bloc` under test. /// [setUp] should be used to set up state necessary for a particular test case. /// For common set up code, prefer to use `setUp` from `package:test/test.dart`. /// /// [build] should construct and return the `bloc` under test. /// /// [seed] is an optional `Function` that returns a state /// which will be used to seed the `bloc` before [act] is called. /// /// [act] is an optional callback which will be invoked with the `bloc` under /// test and should be used to interact with the `bloc`. /// /// [skip] is an optional `int` which can be used to skip any number of states. /// [skip] defaults to 0. /// /// [wait] is an optional `Duration` which can be used to wait for /// async operations within the `bloc` under test such as `debounceTime`. /// /// [expect] is an optional `Function` that returns a `Matcher` which the `bloc` /// under test is expected to emit after [act] is executed. /// /// [verify] is an optional callback which is invoked after [expect] /// and can be used for additional verification/assertions. /// [verify] is called with the `bloc` returned by [build]. /// /// [errors] is an optional `Function` that returns a `Matcher` which the `bloc` /// under test is expected to throw after [act] is executed. /// /// [tearDown] is optional and can be used to /// execute any code after the test has run. /// [tearDown] should be used to clean up after a particular test case. /// For common tear down code, prefer to use `tearDown` from `package:test/test.dart`. /// /// [tags] is optional and if it is passed, it declares user-defined tags /// that are applied to the test. These tags can be used to select or /// skip the test on the command line, or to do bulk test configuration. /// /// ```dart /// blocTest( /// 'CounterBloc emits [1] when increment is added', /// build: () => CounterBloc(), /// act: (bloc) => bloc.add(CounterEvent.increment), /// expect: () => [1], /// ); /// ``` /// /// [blocTest] can optionally be used with a seeded state. /// /// ```dart /// blocTest( /// 'CounterBloc emits [10] when seeded with 9', /// build: () => CounterBloc(), /// seed: () => 9, /// act: (bloc) => bloc.add(CounterEvent.increment), /// expect: () => [10], /// ); /// ``` /// /// [blocTest] can also be used to [skip] any number of emitted states /// before asserting against the expected states. /// [skip] defaults to 0. /// /// ```dart /// blocTest( /// 'CounterBloc emits [2] when increment is added twice', /// build: () => CounterBloc(), /// act: (bloc) { /// bloc /// ..add(CounterEvent.increment) /// ..add(CounterEvent.increment); /// }, /// skip: 1, /// expect: () => [2], /// ); /// ``` /// /// [blocTest] can also be used to wait for async operations /// by optionally providing a `Duration` to [wait]. /// /// ```dart /// blocTest( /// 'CounterBloc emits [1] when increment is added', /// build: () => CounterBloc(), /// act: (bloc) => bloc.add(CounterEvent.increment), /// wait: const Duration(milliseconds: 300), /// expect: () => [1], /// ); /// ``` /// /// [blocTest] can also be used to [verify] internal bloc functionality. /// /// ```dart /// blocTest( /// 'CounterBloc emits [1] when increment is added', /// build: () => CounterBloc(), /// act: (bloc) => bloc.add(CounterEvent.increment), /// expect: () => [1], /// verify: (_) { /// verify(() => repository.someMethod(any())).called(1); /// } /// ); /// ``` /// /// **Note:** when using [blocTest] with state classes which don't override /// `==` and `hashCode` you can provide an `Iterable` of matchers instead of /// explicit state instances. /// /// ```dart /// blocTest( /// 'emits [StateB] when EventB is added', /// build: () => MyBloc(), /// act: (bloc) => bloc.add(EventB()), /// expect: () => [isA()], /// ); /// ``` /// /// If [tags] is passed, it declares user-defined tags that are applied to the /// test. These tags can be used to select or skip the test on the command line, /// or to do bulk test configuration. All tags should be declared in the /// [package configuration file][configuring tags]. The parameter can be an /// [Iterable] of tag names, or a [String] representing a single tag. /// /// [configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags @isTest void blocTest, State>( String description, { required B Function() build, FutureOr Function()? setUp, State Function()? seed, dynamic Function(B bloc)? act, Duration? wait, int skip = 0, dynamic Function()? expect, dynamic Function(B bloc)? verify, dynamic Function()? errors, FutureOr Function()? tearDown, dynamic tags, }) { test.test( description, () async { await testBloc( setUp: setUp, build: build, seed: seed, act: act, wait: wait, skip: skip, expect: expect, verify: verify, errors: errors, tearDown: tearDown, ); }, tags: tags, ); } /// Internal [blocTest] runner which is only visible for testing. /// This should never be used directly -- please use [blocTest] instead. @visibleForTesting Future testBloc, State>({ required B Function() build, FutureOr Function()? setUp, State Function()? seed, dynamic Function(B bloc)? act, Duration? wait, int skip = 0, dynamic Function()? expect, dynamic Function(B bloc)? verify, dynamic Function()? errors, FutureOr Function()? tearDown, }) async { var shallowEquality = false; final unhandledErrors = []; final localBlocObserver = Bloc.observer; final testObserver = _TestBlocObserver( localBlocObserver, unhandledErrors.add, ); Bloc.observer = testObserver; try { await _runZonedGuarded(() async { await setUp?.call(); final states = []; final bloc = build(); if (seed != null) bloc.emit(seed()); final subscription = bloc.stream.skip(skip).listen(states.add); try { await act?.call(bloc); } catch (error) { if (errors == null) rethrow; unhandledErrors.add(error); } if (wait != null) await Future.delayed(wait); await Future.delayed(Duration.zero); await bloc.close(); if (expect != null) { final dynamic expected = await expect(); shallowEquality = '$states' == '$expected'; try { test.expect(states, test.wrapMatcher(expected)); } on test.TestFailure catch (e) { if (shallowEquality || expected is! List) rethrow; final diff = _diff(expected: expected, actual: states); final message = '${e.message}\n$diff'; throw test.TestFailure(message); } } await subscription.cancel(); await verify?.call(bloc); await tearDown?.call(); }); } catch (error) { if (shallowEquality && error is test.TestFailure) { throw test.TestFailure( ''' ${error.message} WARNING: Please ensure state instances extend Equatable, override == and hashCode, or implement Comparable. Alternatively, consider using Matchers in the expect of the blocTest rather than concrete state instances.\n''', ); } if (errors == null || !unhandledErrors.contains(error)) { rethrow; } } if (errors != null) test.expect(unhandledErrors, test.wrapMatcher(errors())); } Future _runZonedGuarded(Future Function() body) { final completer = Completer(); runZonedGuarded(() async { await body(); if (!completer.isCompleted) completer.complete(); }, (error, stackTrace) { if (!completer.isCompleted) completer.completeError(error, stackTrace); }); return completer.future; } class _TestBlocObserver extends BlocObserver { const _TestBlocObserver(this._localObserver, this._onError); final BlocObserver _localObserver; final void Function(Object error) _onError; @override void onCreate(BlocBase bloc) { _localObserver.onCreate(bloc); super.onCreate(bloc); } @override void onEvent(Bloc bloc, Object? event) { _localObserver.onEvent(bloc, event); super.onEvent(bloc, event); } @override void onChange(BlocBase bloc, Change change) { _localObserver.onChange(bloc, change); super.onChange(bloc, change); } @override void onTransition( Bloc bloc, Transition transition, ) { _localObserver.onTransition(bloc, transition); super.onTransition(bloc, transition); } @override void onError(BlocBase bloc, Object error, StackTrace stackTrace) { _localObserver.onError(bloc, error, stackTrace); _onError(error); super.onError(bloc, error, stackTrace); } @override void onDone( Bloc bloc, Object? event, [ Object? error, StackTrace? stackTrace, ]) { _localObserver.onDone(bloc, event, error, stackTrace); super.onDone(bloc, event, error, stackTrace); } @override void onClose(BlocBase bloc) { _localObserver.onClose(bloc); super.onClose(bloc); } } String _diff({required dynamic expected, required dynamic actual}) { final buffer = StringBuffer(); final differences = diff(expected.toString(), actual.toString()); buffer ..writeln('${"=" * 4} diff ${"=" * 40}') ..writeln() ..writeln(differences.toPrettyString()) ..writeln() ..writeln('${"=" * 4} end diff ${"=" * 36}'); return buffer.toString(); } extension on List { String toPrettyString() { String identical(String str) => '\u001b[90m$str\u001B[0m'; String deletion(String str) => '\u001b[31m[-$str-]\u001B[0m'; String insertion(String str) => '\u001b[32m{+$str+}\u001B[0m'; final buffer = StringBuffer(); for (final difference in this) { switch (difference.operation) { case DIFF_EQUAL: buffer.write(identical(difference.text)); break; case DIFF_DELETE: buffer.write(deletion(difference.text)); break; case DIFF_INSERT: buffer.write(insertion(difference.text)); break; } } return buffer.toString(); } } ================================================ FILE: packages/bloc_test/lib/src/mock_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:mocktail/mocktail.dart'; /// {@template mock_bloc} /// Extend or mixin this class to mark the implementation as a [MockBloc]. /// /// A mocked bloc implements all fields and methods with a default /// implementation that does not throw a [NoSuchMethodError], /// and may be further customized at runtime to define how it may behave using /// [when] and `whenListen`. /// /// _**Note**: It is critical to explicitly provide the event and state /// types when extending [MockBloc]_. /// /// **GOOD** /// ```dart /// class MockCounterBloc extends MockBloc /// implements CounterBloc {} /// ``` /// /// **BAD** /// ```dart /// class MockCounterBloc extends MockBloc implements CounterBloc {} /// ``` /// {@endtemplate} class MockBloc extends _MockBlocBase implements Bloc {} /// {@template mock_cubit} /// Extend or mixin this class to mark the implementation as a [MockCubit]. /// /// A mocked cubit implements all fields and methods with a default /// implementation that does not throw a [NoSuchMethodError], /// and may be further customized at runtime to define how it may behave using /// [when] and `whenListen`. /// /// _**Note**: It is critical to explicitly provide the state /// types when extending [MockCubit]_. /// /// **GOOD** /// ```dart /// class MockCounterCubit extends MockCubit /// implements CounterCubit {} /// ``` /// /// **BAD** /// ```dart /// class MockCounterCubit extends MockBloc implements CounterCubit {} /// ``` /// {@endtemplate} class MockCubit extends _MockBlocBase implements Cubit {} class _MockBlocBase extends Mock implements BlocBase { _MockBlocBase() { when(() => stream).thenAnswer((_) => Stream.empty()); when(close).thenAnswer((_) => Future.value()); } } ================================================ FILE: packages/bloc_test/lib/src/when_listen.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:mocktail/mocktail.dart'; /// Creates a stub response for the `listen` method on a [bloc]. /// Use [whenListen] if you want to return a canned `Stream` of states /// for a [bloc] instance. /// /// [whenListen] also handles stubbing the `state` of the [bloc] to stay /// in sync with the emitted state. /// /// Return a canned state stream of `[0, 1, 2, 3]` /// when `counterBloc.stream.listen` is called. /// /// ```dart /// whenListen(counterBloc, Stream.fromIterable([0, 1, 2, 3])); /// ``` /// /// Assert that the `counterBloc` state `Stream` is the canned `Stream`. /// /// ```dart /// await expectLater( /// counterBloc.stream, /// emitsInOrder( /// [equals(0), equals(1), equals(2), equals(3), emitsDone], /// ) /// ); /// expect(counterBloc.state, equals(3)); /// ``` /// /// Optionally provide an [initialState] to stub the state of the [bloc] /// before any subscriptions. /// /// ```dart /// whenListen( /// counterBloc, /// Stream.fromIterable([0, 1, 2, 3]), /// initialState: 0, /// ); /// /// expect(counterBloc.state, equals(0)); /// ``` void whenListen( BlocBase bloc, Stream stream, { State? initialState, }) { final broadcastStream = stream.asBroadcastStream(); if (initialState != null) { when(() => bloc.state).thenReturn(initialState); } when(() => bloc.stream).thenAnswer( (_) => broadcastStream.map((state) { when(() => bloc.state).thenReturn(state); return state; }), ); } ================================================ FILE: packages/bloc_test/pubspec.yaml ================================================ name: bloc_test description: A testing library which makes it easy to test blocs. Built to be used with the bloc state management package. version: 10.0.0 repository: https://github.com/felangel/bloc/tree/master/packages/bloc_test issue_tracker: https://github.com/felangel/bloc/issues homepage: https://bloclibrary.dev documentation: https://bloclibrary.dev/getting-started topics: [bloc, state-management, test] funding: [https://github.com/sponsors/felangel] environment: sdk: ">=2.14.0 <4.0.0" dependencies: bloc: ^9.1.0 diff_match_patch: ^0.4.1 meta: ^1.3.0 mocktail: ^1.0.0 test: ^1.16.0 dev_dependencies: bloc_lint: ^0.3.2 rxdart: ^0.28.0 screenshots: - description: The bloc test package logo. path: screenshots/logo.png ================================================ FILE: packages/bloc_test/pubspec_overrides.yaml ================================================ dependency_overrides: bloc: path: ../bloc ================================================ FILE: packages/bloc_test/test/bloc_bloc_test_test.dart ================================================ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; import 'blocs/blocs.dart'; class MockRepository extends Mock implements Repository {} void unawaited(Future? _) {} void main() { group('blocTest', () { group('CounterBloc', () { blocTest( 'supports matchers (contains)', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: () => contains(1), ); blocTest( 'supports matchers (containsAll)', build: () => CounterBloc(), act: (bloc) => bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment), expect: () => containsAll([2, 1]), ); blocTest( 'supports matchers (containsAllInOrder)', build: () => CounterBloc(), act: (bloc) => bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment), expect: () => containsAllInOrder([1, 2]), ); blocTest( 'emits [] when nothing is added', build: () => CounterBloc(), expect: () => const [], ); blocTest( 'emits [1] when CounterEvent.increment is added', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [1], ); blocTest( 'emits [1] when CounterEvent.increment is added with async act', build: () => CounterBloc(), act: (bloc) async { await Future.delayed(const Duration(seconds: 1)); bloc.add(CounterEvent.increment); }, expect: () => const [1], ); blocTest( 'emits [1, 2] when CounterEvent.increment is called multiple times ' 'with async act', build: () => CounterBloc(), act: (bloc) async { bloc.add(CounterEvent.increment); await Future.delayed(const Duration(milliseconds: 10)); bloc.add(CounterEvent.increment); }, expect: () => const [1, 2], ); blocTest( 'emits [2] when CounterEvent.increment is added twice and skip: 1', build: () => CounterBloc(), act: (bloc) { bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment); }, skip: 1, expect: () => const [2], ); blocTest( 'emits [11] when CounterEvent.increment is added and emitted 10', build: () => CounterBloc(), seed: () => 10, act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [11], ); blocTest( 'emits [11] when CounterEvent.increment is added and seed 10', build: () => CounterBloc(), seed: () => 10, act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [11], ); blocTest( 'emits [1] when CounterEvent.increment is added and expect is async', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: () async => [1], ); test('fails immediately when expectation is incorrect', () async { const expectedError = 'Expected: [2]\n' ' Actual: [1]\n' ' Which: at location [0] is <1> instead of <2>\n' '\n' '==== diff ========================================\n' '\n' '''\x1B[90m[\x1B[0m\x1B[31m[-2-]\x1B[0m\x1B[32m{+1+}\x1B[0m\x1B[90m]\x1B[0m\n''' '\n' '==== end diff ====================================\n'; late Object actualError; final completer = Completer(); await runZonedGuarded(() async { unawaited( testBloc( build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [2], ).then((_) => completer.complete()), ); await completer.future; }, (Object error, _) { actualError = error; if (!completer.isCompleted) completer.complete(); }); expect((actualError as TestFailure).message, expectedError); }); test( 'fails immediately when ' 'uncaught exception occurs within bloc', () async { late Object actualError; final completer = Completer(); await runZonedGuarded(() async { unawaited( testBloc( build: () => ErrorCounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [1], ).then((_) => completer.complete()), ); await completer.future; }, (Object error, _) { actualError = error; if (!completer.isCompleted) completer.complete(); }); expect(actualError, isA()); }); test('fails immediately when exception occurs in act', () async { final exception = Exception('oops'); late Object actualError; final completer = Completer(); await runZonedGuarded(() async { unawaited( testBloc( build: () => ErrorCounterBloc(), act: (_) => throw exception, expect: () => const [1], ).then((_) => completer.complete()), ); await completer.future; }, (Object error, _) { actualError = error; if (!completer.isCompleted) completer.complete(); }); expect(actualError, exception); }); test('future still completes when uncaught exception occurs', () async { await expectLater( () => testBloc( build: () => ErrorCounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [1], ), throwsA(isA()), ); }); }); group('AsyncCounterBloc', () { blocTest( 'emits [] when nothing is added', build: () => AsyncCounterBloc(), expect: () => const [], ); blocTest( 'emits [1] when CounterEvent.increment is added', build: () => AsyncCounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [1], ); blocTest( 'emits [1, 2] when CounterEvent.increment is called multiple ' 'times with async act', build: () => AsyncCounterBloc(), act: (bloc) async { bloc.add(CounterEvent.increment); await Future.delayed(const Duration(milliseconds: 10)); bloc.add(CounterEvent.increment); }, expect: () => const [1, 2], ); blocTest( 'emits [2] when CounterEvent.increment is added twice and skip: 1', build: () => AsyncCounterBloc(), act: (bloc) => bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment), skip: 1, expect: () => const [2], ); blocTest( 'emits [11] when CounterEvent.increment is added and emitted 10', build: () => AsyncCounterBloc(), seed: () => 10, act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [11], ); }); group('DebounceCounterBloc', () { blocTest( 'emits [] when nothing is added', build: () => DebounceCounterBloc(), expect: () => const [], ); blocTest( 'emits [1] when CounterEvent.increment is added', build: () => DebounceCounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), wait: const Duration(milliseconds: 300), expect: () => const [1], ); blocTest( 'emits [2] when CounterEvent.increment ' 'is added twice and skip: 1', build: () => DebounceCounterBloc(), act: (bloc) async { bloc.add(CounterEvent.increment); await Future.delayed(const Duration(milliseconds: 305)); bloc.add(CounterEvent.increment); }, skip: 1, wait: const Duration(milliseconds: 300), expect: () => const [2], ); blocTest( 'emits [11] when CounterEvent.increment is added and emitted 10', build: () => DebounceCounterBloc(), seed: () => 10, act: (bloc) => bloc.add(CounterEvent.increment), wait: const Duration(milliseconds: 300), expect: () => const [11], ); }); group('InstantEmitBloc', () { blocTest( 'emits [1] when nothing is added', build: () => InstantEmitBloc(), expect: () => const [1], ); blocTest( 'emits [1, 2] when CounterEvent.increment is added', build: () => InstantEmitBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [1, 2], ); blocTest( 'emits [1, 2, 3] when CounterEvent.increment is called ' 'multiple times with async act', build: () => InstantEmitBloc(), act: (bloc) async { bloc.add(CounterEvent.increment); await Future.delayed(const Duration(milliseconds: 10)); bloc.add(CounterEvent.increment); }, expect: () => const [1, 2, 3], ); blocTest( 'emits [3] when CounterEvent.increment is added twice and skip: 2', build: () => InstantEmitBloc(), act: (bloc) => bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment), skip: 2, expect: () => const [3], ); blocTest( 'emits [11, 12] when CounterEvent.increment is added and seeded 10', build: () => InstantEmitBloc(), seed: () => 10, act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [11, 12], ); }); group('MultiCounterBloc', () { blocTest( 'emits [] when nothing is added', build: () => MultiCounterBloc(), expect: () => const [], ); blocTest( 'emits [1, 2] when CounterEvent.increment is added', build: () => MultiCounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [1, 2], ); blocTest( 'emits [1, 2, 3, 4] when CounterEvent.increment is called ' 'multiple times with async act', build: () => MultiCounterBloc(), act: (bloc) async { bloc.add(CounterEvent.increment); await Future.delayed(const Duration(milliseconds: 10)); bloc.add(CounterEvent.increment); }, expect: () => const [1, 2, 3, 4], ); blocTest( 'emits [4] when CounterEvent.increment is added twice and skip: 3', build: () => MultiCounterBloc(), act: (bloc) => bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment), skip: 3, expect: () => const [4], ); blocTest( 'emits [11, 12] when CounterEvent.increment is added and emitted 10', build: () => MultiCounterBloc(), seed: () => 10, act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [11, 12], ); }); group('ComplexBloc', () { blocTest( 'emits [] when nothing is added', build: () => ComplexBloc(), expect: () => const [], ); blocTest( 'emits [ComplexStateB] when ComplexEventB is added', build: () => ComplexBloc(), act: (bloc) => bloc.add(ComplexEventB()), expect: () => [isA()], ); blocTest( 'emits [ComplexStateA] when [ComplexEventB, ComplexEventA] ' 'is added and skip: 1', build: () => ComplexBloc(), act: (bloc) => bloc ..add(ComplexEventB()) ..add(ComplexEventA()), skip: 1, expect: () => [isA()], ); }); group('ErrorCounterBloc', () { blocTest( 'emits [] when nothing is added', build: () => ErrorCounterBloc(), expect: () => const [], ); blocTest( 'emits [2] when increment is added twice and skip: 1', build: () => ErrorCounterBloc(), act: (bloc) => bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment), skip: 1, expect: () => const [2], errors: () => isNotEmpty, ); blocTest( 'emits [1] when increment is added', build: () => ErrorCounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [1], errors: () => isNotEmpty, ); blocTest( 'throws ErrorCounterBlocException when increment is added', build: () => ErrorCounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), errors: () => [isA()], ); blocTest( 'emits [1] and throws ErrorCounterBlocError ' 'when increment is added', build: () => ErrorCounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [1], errors: () => [isA()], ); blocTest( 'emits [1, 2] when increment is added twice', build: () => ErrorCounterBloc(), act: (bloc) => bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment), expect: () => const [1, 2], errors: () => isNotEmpty, ); blocTest( 'throws two ErrorCounterBlocErrors ' 'when increment is added twice', build: () => ErrorCounterBloc(), act: (bloc) => bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment), errors: () => [ isA(), isA(), ], ); blocTest( 'emits [1, 2] and throws two ErrorCounterBlocErrors ' 'when increment is added twice', build: () => ErrorCounterBloc(), act: (bloc) => bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment), expect: () => const [1, 2], errors: () => [ isA(), isA(), ], ); }); group('ExceptionCounterBloc', () { blocTest( 'emits [] when nothing is added', build: () => ExceptionCounterBloc(), expect: () => const [], ); blocTest( 'emits [2] when increment is added twice and skip: 1', build: () => ExceptionCounterBloc(), act: (bloc) => bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment), skip: 1, expect: () => const [2], errors: () => isNotEmpty, ); blocTest( 'emits [1] when increment is added', build: () => ExceptionCounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [1], errors: () => isNotEmpty, ); blocTest( 'throws ExceptionCounterBlocException when increment is added', build: () => ExceptionCounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), errors: () => [isA()], ); blocTest( 'emits [1] and throws ExceptionCounterBlocException ' 'when increment is added', build: () => ExceptionCounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [1], errors: () => [isA()], ); blocTest( 'emits [1, 2] when increment is added twice', build: () => ExceptionCounterBloc(), act: (bloc) => bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment), expect: () => const [1, 2], errors: () => isNotEmpty, ); blocTest( 'throws two ExceptionCounterBlocExceptions ' 'when increment is added twice', build: () => ExceptionCounterBloc(), act: (bloc) => bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment), errors: () => [ isA(), isA(), ], ); blocTest( 'emits [1, 2] and throws two ExceptionCounterBlocException ' 'when increment is added twice', build: () => ExceptionCounterBloc(), act: (bloc) => bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment), expect: () => const [1, 2], errors: () => [ isA(), isA(), ], ); }); group('SideEffectCounterBloc', () { late Repository repository; setUp(() { repository = MockRepository(); when(() => repository.sideEffect()).thenReturn(null); }); blocTest( 'emits [] when nothing is added', build: () => SideEffectCounterBloc(repository), expect: () => const [], ); blocTest( 'emits [1] when CounterEvent.increment is added', build: () => SideEffectCounterBloc(repository), act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [1], verify: (_) { verify(() => repository.sideEffect()).called(1); }, ); blocTest( 'emits [2] when CounterEvent.increment ' 'is added twice and skip: 1', build: () => SideEffectCounterBloc(repository), act: (bloc) => bloc ..add(CounterEvent.increment) ..add(CounterEvent.increment), skip: 1, expect: () => const [2], ); blocTest( 'does not require an expect', build: () => SideEffectCounterBloc(repository), act: (bloc) => bloc.add(CounterEvent.increment), verify: (_) { verify(() => repository.sideEffect()).called(1); }, ); blocTest( 'async verify', build: () => SideEffectCounterBloc(repository), act: (bloc) => bloc.add(CounterEvent.increment), verify: (_) async { await Future.delayed(Duration.zero); verify(() => repository.sideEffect()).called(1); }, ); blocTest( 'setUp is executed before build/act', setUp: () { when(() => repository.sideEffect()).thenThrow(Exception()); }, build: () => SideEffectCounterBloc(repository), act: (bloc) => bloc.add(CounterEvent.increment), expect: () => const [], errors: () => [isException], ); test('fails immediately when verify is incorrect', () async { const expectedError = '''Expected: <2>\n Actual: <1>\nUnexpected number of calls\n'''; late Object actualError; final completer = Completer(); await runZonedGuarded(() async { unawaited( testBloc( build: () => SideEffectCounterBloc(repository), act: (bloc) => bloc.add(CounterEvent.increment), verify: (_) { verify(() => repository.sideEffect()).called(2); }, ).then((_) => completer.complete()), ); await completer.future; }, (Object error, _) { actualError = error; if (!completer.isCompleted) completer.complete(); }); expect((actualError as TestFailure).message, expectedError); }); test('shows equality warning when strings are identical', () async { const expectedError = ''' Expected: [Instance of 'ComplexStateA'] Actual: [Instance of 'ComplexStateA'] Which: at location [0] is instead of \n WARNING: Please ensure state instances extend Equatable, override == and hashCode, or implement Comparable. Alternatively, consider using Matchers in the expect of the blocTest rather than concrete state instances.\n'''; late Object actualError; final completer = Completer(); await runZonedGuarded(() async { unawaited( testBloc( build: () => ComplexBloc(), act: (bloc) => bloc.add(ComplexEventA()), expect: () => [ComplexStateA()], ).then((_) => completer.complete()), ); await completer.future; }, (Object error, _) { actualError = error; if (!completer.isCompleted) completer.complete(); }); expect((actualError as TestFailure).message, expectedError); }); }); }); group('tearDown', () { late int tearDownCallCount; int? state; setUp(() { tearDownCallCount = 0; }); tearDown(() { expect(tearDownCallCount, equals(1)); }); blocTest( 'is called after the test is run', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), verify: (bloc) { state = bloc.state; }, tearDown: () { tearDownCallCount++; expect(state, equals(1)); }, ); }); } ================================================ FILE: packages/bloc_test/test/bloc_observer_test.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; import 'blocs/counter_bloc.dart'; import 'blocs/exception_counter_bloc.dart'; class _MockBlocObserver extends Mock implements BlocObserver {} void main() { group('BlocObserver', () { late BlocObserver blocObserver; setUp(() { blocObserver = _MockBlocObserver(); final previousObserver = Bloc.observer; addTearDown(() => Bloc.observer = previousObserver); Bloc.observer = blocObserver; }); blocTest( 'calls onCreate', build: () => CounterBloc(), verify: (bloc) { // ignore: invalid_use_of_protected_member verify(() => blocObserver.onCreate(bloc)).called(1); }, ); blocTest( 'calls onEvent', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), verify: (bloc) { verify( // ignore: invalid_use_of_protected_member () => blocObserver.onEvent(bloc, CounterEvent.increment), ).called(1); }, ); blocTest( 'calls onChange', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), verify: (bloc) { const change = Change(currentState: 0, nextState: 1); // ignore: invalid_use_of_protected_member verify(() => blocObserver.onChange(bloc, change)).called(1); }, ); blocTest( 'calls onTransition', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), verify: (bloc) { const transition = Transition( event: CounterEvent.increment, currentState: 0, nextState: 1, ); // ignore: invalid_use_of_protected_member verify(() => blocObserver.onTransition(bloc, transition)).called(1); }, ); blocTest( 'calls onError', build: () => ExceptionCounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), setUp: () { registerFallbackValue(StackTrace.empty); }, verify: (bloc) { verify( // ignore: invalid_use_of_protected_member () => blocObserver.onError( bloc, ExceptionCounterBlocException(), any(), ), ).called(1); }, errors: () => containsOnce(isA()), ); blocTest( 'calls onDone', build: () => CounterBloc(), act: (bloc) => bloc.add(CounterEvent.increment), verify: (bloc) { verify( // ignore: invalid_use_of_protected_member () => blocObserver.onDone(bloc, CounterEvent.increment), ).called(1); }, ); blocTest( 'calls onClose', build: () => CounterBloc(), verify: (bloc) { // ignore: invalid_use_of_protected_member verify(() => blocObserver.onClose(bloc)).called(1); }, ); }); } ================================================ FILE: packages/bloc_test/test/blocs/async_counter_bloc.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'blocs.dart'; class AsyncCounterBloc extends Bloc { AsyncCounterBloc() : super(0) { on( (event, emit) async { switch (event) { case CounterEvent.increment: await Future.delayed(const Duration(microseconds: 1)); return emit(state + 1); } }, transformer: (events, mapper) => events.asyncExpand(mapper), ); } } ================================================ FILE: packages/bloc_test/test/blocs/blocs.dart ================================================ export 'async_counter_bloc.dart'; export 'complex_bloc.dart'; export 'counter_bloc.dart'; export 'debounce_counter_bloc.dart'; export 'error_counter_bloc.dart'; export 'exception_counter_bloc.dart'; export 'instant_emit_bloc.dart'; export 'multi_counter_bloc.dart'; export 'side_effect_counter_bloc.dart'; export 'sum_bloc.dart'; ================================================ FILE: packages/bloc_test/test/blocs/complex_bloc.dart ================================================ import 'package:bloc/bloc.dart'; abstract class ComplexEvent {} class ComplexEventA extends ComplexEvent {} class ComplexEventB extends ComplexEvent {} abstract class ComplexState {} class ComplexStateA extends ComplexState {} class ComplexStateB extends ComplexState {} class ComplexBloc extends Bloc { ComplexBloc() : super(ComplexStateA()) { on((event, emit) => emit(ComplexStateA())); on((event, emit) => emit(ComplexStateB())); } } ================================================ FILE: packages/bloc_test/test/blocs/counter_bloc.dart ================================================ import 'package:bloc/bloc.dart'; enum CounterEvent { increment } class CounterBloc extends Bloc { CounterBloc() : super(0) { on((event, emit) { switch (event) { case CounterEvent.increment: return emit(state + 1); } }); } } ================================================ FILE: packages/bloc_test/test/blocs/debounce_counter_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'package:rxdart/rxdart.dart'; import 'blocs.dart'; EventTransformer debounce() { return (events, mapper) { return events .debounceTime(const Duration(milliseconds: 300)) .switchMap(mapper); }; } class DebounceCounterBloc extends Bloc { DebounceCounterBloc() : super(0) { on( (event, emit) { switch (event) { case CounterEvent.increment: return emit(state + 1); } }, transformer: debounce(), ); } } ================================================ FILE: packages/bloc_test/test/blocs/error_counter_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'blocs.dart'; class ErrorCounterBlocError extends Error {} class ErrorCounterBloc extends Bloc { ErrorCounterBloc() : super(0) { on((event, emit) { switch (event) { case CounterEvent.increment: emit(state + 1); throw ErrorCounterBlocError(); } }); } } ================================================ FILE: packages/bloc_test/test/blocs/exception_counter_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'blocs.dart'; class ExceptionCounterBlocException implements Exception {} class ExceptionCounterBloc extends Bloc { ExceptionCounterBloc() : super(0) { on((event, emit) { switch (event) { case CounterEvent.increment: emit(state + 1); throw ExceptionCounterBlocException(); } }); } } ================================================ FILE: packages/bloc_test/test/blocs/instant_emit_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'blocs.dart'; class InstantEmitBloc extends Bloc { InstantEmitBloc() : super(0) { on((event, emit) { switch (event) { case CounterEvent.increment: return emit(state + 1); } }); add(CounterEvent.increment); } } ================================================ FILE: packages/bloc_test/test/blocs/multi_counter_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'blocs.dart'; class MultiCounterBloc extends Bloc { MultiCounterBloc() : super(0) { on((event, emit) { switch (event) { case CounterEvent.increment: emit(state + 1); emit(state + 1); break; } }); } } ================================================ FILE: packages/bloc_test/test/blocs/side_effect_counter_bloc.dart ================================================ import 'package:bloc/bloc.dart'; import 'blocs.dart'; class Repository { void sideEffect() {} } class SideEffectCounterBloc extends Bloc { SideEffectCounterBloc(this._repository) : super(0) { on((event, emit) { switch (event) { case CounterEvent.increment: _repository.sideEffect(); return emit(state + 1); } }); } final Repository _repository; } ================================================ FILE: packages/bloc_test/test/blocs/sum_bloc.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'counter_bloc.dart'; class SumEvent { const SumEvent(this.value); final int value; } class SumBloc extends Bloc { SumBloc(CounterBloc counterBloc) : super(0) { on((event, emit) => emit(state + event.value)); _countSubscription = counterBloc.stream.listen( (count) => add(SumEvent(count)), ); } late StreamSubscription _countSubscription; @override Future close() { _countSubscription.cancel(); return super.close(); } } ================================================ FILE: packages/bloc_test/test/cubit_bloc_test_test.dart ================================================ import 'package:bloc_test/bloc_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; import 'cubits/cubits.dart'; class MockRepository extends Mock implements Repository {} void main() { group('blocTest', () { group('CounterCubit', () { blocTest( 'emits [] when nothing is called', build: () => CounterCubit(), expect: () => [], ); blocTest( 'emits [1] when increment is called', build: () => CounterCubit(), act: (cubit) => cubit.increment(), expect: () => [1], ); blocTest( 'emits [1] when increment is called with async act', build: () => CounterCubit(), act: (cubit) => cubit.increment(), expect: () => [1], ); blocTest( 'emits [1, 2] when increment is called multiple times ' 'with async act', build: () => CounterCubit(), act: (cubit) => cubit ..increment() ..increment(), expect: () => [1, 2], ); blocTest( 'emits [3] when increment is called and seed is 2 ' 'with async act', build: () => CounterCubit(), seed: () => 2, act: (cubit) => cubit.increment(), expect: () => [3], ); blocTest( 'emits [1] when increment is called and expect is async', build: () => CounterCubit(), act: (cubit) => cubit.increment(), expect: () async => [1], ); }); group('AsyncCounterCubit', () { blocTest( 'emits [] when nothing is called', build: () => AsyncCounterCubit(), expect: () => [], ); blocTest( 'emits [1] when increment is called', build: () => AsyncCounterCubit(), act: (cubit) => cubit.increment(), expect: () => [1], ); blocTest( 'emits [1, 2] when increment is called multiple ' 'times with async act', build: () => AsyncCounterCubit(), act: (cubit) async { await cubit.increment(); await cubit.increment(); }, expect: () => [1, 2], ); }); group('DelayedCounterCubit', () { blocTest( 'emits [] when nothing is called', build: () => DelayedCounterCubit(), expect: () => [], ); blocTest( 'emits [] when increment is called without wait', build: () => DelayedCounterCubit(), act: (cubit) => cubit.increment(), expect: () => [], ); blocTest( 'emits [1] when increment is called with wait', build: () => DelayedCounterCubit(), act: (cubit) => cubit.increment(), wait: const Duration(milliseconds: 300), expect: () => [1], ); }); group('InstantEmitCubit', () { blocTest( 'emits [] when nothing is called', build: () => InstantEmitCubit(), expect: () => [], ); blocTest( 'emits [2] when increment is called', build: () => InstantEmitCubit(), act: (cubit) => cubit.increment(), expect: () => [2], ); blocTest( 'emits [2, 3] when increment is called ' 'multiple times with async act', build: () => InstantEmitCubit(), act: (cubit) => cubit ..increment() ..increment(), expect: () => [2, 3], ); }); group('MultiCounterCubit', () { blocTest( 'emits [] when nothing is called', build: () => MultiCounterCubit(), expect: () => [], ); blocTest( 'emits [1, 2] when increment is called', build: () => MultiCounterCubit(), act: (cubit) => cubit.increment(), expect: () => [1, 2], ); blocTest( 'emits [1, 2, 3, 4] when increment is called ' 'multiple times with async act', build: () => MultiCounterCubit(), act: (cubit) => cubit ..increment() ..increment(), expect: () => [1, 2, 3, 4], ); }); group('ComplexCubit', () { blocTest( 'emits [] when nothing is called', build: () => ComplexCubit(), expect: () => [], ); blocTest( 'emits [ComplexStateB] when emitB is called', build: () => ComplexCubit(), act: (cubit) => cubit.emitB(), expect: () => [isA()], ); }); group('SideEffectCounterCubit', () { late Repository repository; setUp(() { repository = MockRepository(); when(() => repository.sideEffect()).thenReturn(null); }); blocTest( 'emits [] when nothing is called', build: () => SideEffectCounterCubit(repository), expect: () => [], ); blocTest( 'emits [1] when increment is called', build: () => SideEffectCounterCubit(repository), act: (cubit) => cubit.increment(), expect: () => [1], verify: (_) async { verify(() => repository.sideEffect()).called(1); }, ); blocTest( 'does not require an expect', build: () => SideEffectCounterCubit(repository), act: (cubit) => cubit.increment(), verify: (_) async { verify(() => repository.sideEffect()).called(1); }, ); }); group('ExceptionCubit', () { final exception = Exception('oops'); blocTest( 'errors supports matchers', build: () => ExceptionCubit(), act: (cubit) => cubit.throwException(exception), errors: () => contains(exception), ); blocTest( 'captures uncaught exceptions', build: () => ExceptionCubit(), act: (cubit) => cubit.throwException(exception), errors: () => [equals(exception)], ); blocTest( 'captures calls to addError', build: () => ExceptionCubit(), // ignore: invalid_use_of_protected_member act: (cubit) => cubit.addError(exception), errors: () => [equals(exception)], ); }); group('ErrorCubit', () { final error = Error(); blocTest( 'errors supports matchers', build: () => ErrorCubit(), act: (cubit) => cubit.throwError(error), errors: () => contains(error), ); blocTest( 'captures uncaught errors', build: () => ErrorCubit(), act: (cubit) => cubit.throwError(error), errors: () => [equals(error)], ); blocTest( 'captures calls to addError', build: () => ErrorCubit(), // ignore: invalid_use_of_protected_member act: (cubit) => cubit.addError(error), errors: () => [equals(error)], ); }); }); } ================================================ FILE: packages/bloc_test/test/cubits/async_counter_cubit.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; class AsyncCounterCubit extends Cubit { AsyncCounterCubit() : super(0); Future increment() async { await Future.delayed(const Duration(microseconds: 1)); emit(state + 1); } } ================================================ FILE: packages/bloc_test/test/cubits/complex_cubit.dart ================================================ import 'package:bloc/bloc.dart'; abstract class ComplexState {} class ComplexStateA extends ComplexState {} class ComplexStateB extends ComplexState {} class ComplexCubit extends Cubit { ComplexCubit() : super(ComplexStateA()); void emitA() => emit(ComplexStateA()); void emitB() => emit(ComplexStateB()); } ================================================ FILE: packages/bloc_test/test/cubits/counter_cubit.dart ================================================ import 'package:bloc/bloc.dart'; class CounterCubit extends Cubit { CounterCubit() : super(0); void increment() => emit(state + 1); } ================================================ FILE: packages/bloc_test/test/cubits/cubits.dart ================================================ export 'async_counter_cubit.dart'; export 'complex_cubit.dart'; export 'counter_cubit.dart'; export 'delayed_counter_cubit.dart'; export 'error_cubit.dart'; export 'exception_cubit.dart'; export 'instant_emit_cubit.dart'; export 'multi_counter_cubit.dart'; export 'side_effect_counter_cubit.dart'; export 'sum_cubit.dart'; ================================================ FILE: packages/bloc_test/test/cubits/delayed_counter_cubit.dart ================================================ import 'package:bloc/bloc.dart'; class DelayedCounterCubit extends Cubit { DelayedCounterCubit() : super(0); void increment() { Future.delayed( const Duration(milliseconds: 300), () { if (!isClosed) emit(state + 1); }, ); } } ================================================ FILE: packages/bloc_test/test/cubits/error_cubit.dart ================================================ import 'package:bloc/bloc.dart'; class ErrorCubit extends Cubit { ErrorCubit() : super(0); void throwError(Error e) => throw e; } ================================================ FILE: packages/bloc_test/test/cubits/exception_cubit.dart ================================================ import 'package:bloc/bloc.dart'; class ExceptionCubit extends Cubit { ExceptionCubit() : super(0); void throwException(Exception e) => throw e; } ================================================ FILE: packages/bloc_test/test/cubits/instant_emit_cubit.dart ================================================ import 'package:bloc/bloc.dart'; class InstantEmitCubit extends Cubit { InstantEmitCubit() : super(0) { emit(1); } void increment() => emit(state + 1); } ================================================ FILE: packages/bloc_test/test/cubits/multi_counter_cubit.dart ================================================ import 'package:bloc/bloc.dart'; class MultiCounterCubit extends Cubit { MultiCounterCubit() : super(0); void increment() { emit(state + 1); emit(state + 1); } } ================================================ FILE: packages/bloc_test/test/cubits/side_effect_counter_cubit.dart ================================================ import 'package:bloc/bloc.dart'; class Repository { void sideEffect() {} } class SideEffectCounterCubit extends Cubit { SideEffectCounterCubit(this._repository) : super(0); final Repository _repository; void increment() { _repository.sideEffect(); emit(state + 1); } } ================================================ FILE: packages/bloc_test/test/cubits/sum_cubit.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'counter_cubit.dart'; class SumCubit extends Cubit { SumCubit(CounterCubit counterCubit) : super(0) { _countSubscription = counterCubit.stream.listen( (count) => emit(state + count), ); } late StreamSubscription _countSubscription; @override Future close() { _countSubscription.cancel(); return super.close(); } } ================================================ FILE: packages/bloc_test/test/mock_bloc_test.dart ================================================ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; import 'blocs/blocs.dart'; import 'cubits/cubits.dart'; class MockCounterBloc extends MockBloc implements CounterBloc {} class MockCounterCubit extends MockCubit implements CounterCubit {} void main() { group('MockBloc', () { late CounterBloc counterBloc; setUp(() { counterBloc = MockCounterBloc(); }); test('is compatible with when', () { when(() => counterBloc.state).thenReturn(10); expect(counterBloc.state, 10); }); test('is compatible with listen', () { expect( counterBloc.stream.listen((_) {}), isA>(), ); }); test('is compatible with add', () { counterBloc.add(CounterEvent.increment); }); test('is compatible with addError without StackTrace', () { // ignore: invalid_use_of_protected_member counterBloc.addError(Exception('oops')); }); test('is compatible with addError with StackTrace', () { // ignore: invalid_use_of_protected_member counterBloc.addError(Exception('oops'), StackTrace.empty); }); test('is compatible with onEvent', () { // ignore: invalid_use_of_protected_member counterBloc.onEvent(CounterEvent.increment); }); test('is compatible with onError', () { // ignore: invalid_use_of_protected_member counterBloc.onError(Exception('oops'), StackTrace.empty); }); test('is compatible with onTransition', () { // ignore: invalid_use_of_protected_member counterBloc.onTransition( const Transition( currentState: 0, event: CounterEvent.increment, nextState: 1, ), ); }); test('is compatible with close', () { expect(counterBloc.close(), completes); }); test('is automatically compatible with whenListen', () { whenListen( counterBloc, Stream.fromIterable([0, 1, 2, 3]), ); expectLater( counterBloc.stream, emitsInOrder( [equals(0), equals(1), equals(2), equals(3), emitsDone], ), ); }); }); group('MockCubit', () { late CounterCubit counterCubit; setUp(() { counterCubit = MockCounterCubit(); }); test('is compatible with when', () { when(() => counterCubit.state).thenReturn(10); expect(counterCubit.state, 10); }); test('is automatically compatible with whenListen', () { whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); expectLater( counterCubit.stream, emitsInOrder( [equals(0), equals(1), equals(2), equals(3), emitsDone], ), ); }); }); } ================================================ FILE: packages/bloc_test/test/when_listen_test.dart ================================================ import 'dart:async'; import 'package:bloc_test/bloc_test.dart'; import 'package:test/test.dart'; import 'cubits/cubits.dart'; class MockCounterCubit extends MockCubit implements CounterCubit {} void unawaited(Future? _) {} void main() { group('whenListen', () { test('can mock the stream of a single cubit with an empty Stream', () { final counterCubit = MockCounterCubit(); whenListen(counterCubit, const Stream.empty()); expectLater(counterCubit.stream, emitsInOrder([])); }); test('can mock the stream of a single cubit', () async { final counterCubit = MockCounterCubit(); whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); await expectLater( counterCubit.stream, emitsInOrder( [equals(0), equals(1), equals(2), equals(3), emitsDone], ), ); }); test('can mock the stream of a single cubit with delays', () async { final counterCubit = MockCounterCubit(); final controller = StreamController(); whenListen(counterCubit, controller.stream); unawaited( expectLater( counterCubit.stream, emitsInOrder( [equals(0), equals(1), equals(2), equals(3), emitsDone], ), ), ); controller.add(0); await Future.delayed(Duration.zero); controller.add(1); await Future.delayed(Duration.zero); controller.add(2); await Future.delayed(Duration.zero); controller.add(3); await controller.close(); }); test('can mock the state of a single cubit with delays', () async { final counterCubit = MockCounterCubit(); final controller = StreamController(); whenListen(counterCubit, controller.stream); unawaited( expectLater( counterCubit.stream, emitsInOrder( [equals(0), equals(1), equals(2), equals(3), emitsDone], ), ).then((dynamic _) { expect(counterCubit.state, equals(3)); }), ); controller.add(0); await Future.delayed(Duration.zero); controller.add(1); await Future.delayed(Duration.zero); controller.add(2); await Future.delayed(Duration.zero); controller.add(3); await controller.close(); }); test('can mock the state of a single cubit', () async { final counterCubit = MockCounterCubit(); whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); await expectLater( counterCubit.stream, emitsInOrder( [equals(0), equals(1), equals(2), equals(3), emitsDone], ), ); expect(counterCubit.state, equals(3)); }); test('can mock the initial state of a single cubit', () async { final counterCubit = MockCounterCubit(); whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), initialState: 0, ); expect(counterCubit.state, equals(0)); await expectLater( counterCubit.stream, emitsInOrder( [equals(0), equals(1), equals(2), equals(3), emitsDone], ), ); expect(counterCubit.state, equals(3)); }); test('can mock the stream of a single cubit as broadcast stream', () { final counterCubit = MockCounterCubit(); whenListen( counterCubit, Stream.fromIterable([0, 1, 2, 3]), ); expectLater( counterCubit.stream, emitsInOrder( [equals(0), equals(1), equals(2), equals(3), emitsDone], ), ); expectLater( counterCubit.stream, emitsInOrder( [equals(0), equals(1), equals(2), equals(3), emitsDone], ), ); }); test( 'can mock the stream of a cubit dependency ' '(with initial state)', () async { final controller = StreamController(); final counterCubit = MockCounterCubit(); whenListen(counterCubit, controller.stream); final sumCubit = SumCubit(counterCubit); unawaited(expectLater(sumCubit.stream, emitsInOrder([0, 1, 3, 6]))); controller ..add(0) ..add(1) ..add(2) ..add(3); await controller.close(); expect(sumCubit.state, equals(6)); }); test('can mock the stream of a cubit dependency', () async { final controller = StreamController(); final counterCubit = MockCounterCubit(); whenListen(counterCubit, controller.stream); final sumCubit = SumCubit(counterCubit); unawaited(expectLater(sumCubit.stream, emitsInOrder([1, 3, 6]))); controller ..add(1) ..add(2) ..add(3); await controller.close(); expect(sumCubit.state, equals(6)); }); }); } ================================================ FILE: packages/bloc_tools/.gitignore ================================================ # Files and directories created by pub .dart_tool/ .packages pubspec.lock # Conventional directory for build outputs build/ # Directory created by dartdoc doc/api/ # Temporary Files .tmp/ # Files generated during tests .test_coverage.dart coverage/ ================================================ FILE: packages/bloc_tools/CHANGELOG.md ================================================ # 0.1.0-dev.23 - deps: bump `pkg:bloc_lint` to `^0.4.0` # 0.1.0-dev.22 - fix: version config mismatch # 0.1.0-dev.21 - deps: bump `pkg:bloc_lint` to `^0.3.7` # 0.1.0-dev.20 - deps: bump `pkg:bloc_lint` to `^0.3.3` - docs: add `bloc_lint` badge to `README` - refactor: analysis options updates # 0.1.0-dev.19 - deps: bump `pkg:bloc_lint` to `^0.3.2` # 0.1.0-dev.18 - fix: migrate to `pkg:lsp_server_ce` - deps: bump `pkg:bloc_lint` to `^0.3.0` # 0.1.0-dev.17 - deps: bump `pkg:bloc_lint` to `^0.2.1` - docs: various `README.md` improvements # 0.1.0-dev.16 - deps: bump `pkg:bloc_lint` to `^0.2.0` # 0.1.0-dev.15 - deps: bump `pkg:bloc_lint` to `^0.2.0-dev.6` # 0.1.0-dev.14 - deps: bump `pkg:bloc_lint` to `^0.2.0-dev.5` # 0.1.0-dev.13 - fix: various bug fixes for Windows - deps: bump `pkg:bloc_lint` to `^0.2.0-dev.4` # 0.1.0-dev.12 - deps: bump `pkg:bloc_lint` to `^0.2.0-dev.3` # 0.1.0-dev.11 - deps: bump `pkg:bloc_lint` to `^0.2.0-dev.2` # 0.1.0-dev.10 - fix: language server diagnostic uri resolution on windows - deps: bump `pkg:bloc_lint` to `^0.2.0-dev.1` # 0.1.0-dev.9 - feat: add `bloc lint` command ```sh $ bloc lint --help Lint Dart source code. Usage: bloc lint [arguments] -h, --help Print this usage information. Run "bloc help" to see global options. ``` # 0.1.0-dev.8 - feat: add `bloc new