feat(app): Добавить пример со Scope (#5)

* feat(app): Добавить пример со Scope

* fix scope

* feat: добавить скоуп с внутренней зависимостью от репозитория (#6)

Co-authored-by: Artem Barkalov <artembark@gmail.com>

* feat: исправить обалсть видимости ProfileScope

* feat: добавить фикс namespace плагинов

---------

Co-authored-by: PetrovY <y.petrov@friflex.com>
Co-authored-by: Artem Barkalov <artembark@gmail.com>
This commit is contained in:
Yuri Petrov
2025-02-26 13:40:43 +03:00
committed by GitHub
parent af3b941711
commit ca4cb20d58
22 changed files with 684 additions and 135 deletions

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class MainScreen extends StatelessWidget {
const MainScreen({super.key});
@@ -8,7 +9,27 @@ class MainScreen extends StatelessWidget {
return Scaffold(
appBar: AppBar(
title: const Text('Main Screen'),
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
context.push('/profile');
},
child: const Text('Открыть профиль'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.push('/profile_scope');
},
child: const Text('Открыть профиль с областью видимости'),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,14 @@
import '../../domain/repository/i_profile_repository.dart';
/// {@template ProfileMockRepository}
///
/// {@endtemplate}
final class ProfileMockRepository implements IProfileRepository {
@override
String get name => 'ProfileMockRepository';
@override
Future<String> fetchUserProfile(String id) {
return Future.value('MOCK Yura Petrov');
}
}

View File

@@ -0,0 +1,24 @@
import 'package:friflex_starter/app/http/i_http_client.dart';
import '../../domain/repository/i_profile_repository.dart';
/// {@template ProfileRepository}
///
/// {@endtemplate}
final class ProfileRepository implements IProfileRepository {
final IHttpClient httpClient;
ProfileRepository({required this.httpClient});
@override
String get name => 'ProfileRepository';
@override
Future<String> fetchUserProfile(String id) async {
// Какой-то запрос к серверу
await Future.delayed(const Duration(seconds: 1));
// httpClient.get('https://example.com/profile/$id');
return 'Yura Petrov';
}
}

View File

@@ -0,0 +1,39 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:friflex_starter/features/profile/domain/repository/i_profile_repository.dart';
part 'profile_event.dart';
part 'profile_state.dart';
class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
ProfileBloc(this._profileRepository) : super(ProfileInitialState()) {
// Вам необходимо добавлять только
// один обработчик событий в конструкторе
on<ProfileEvent>((event, emit) async {
if (event is ProfileFetchProfileEvent) {
await _fetchProfile(event, emit);
}
});
}
final IProfileRepository _profileRepository;
Future<void> _fetchProfile(
ProfileFetchProfileEvent event,
Emitter<ProfileState> emit,
) async {
try {
emit(ProfileWaitingState());
final data = await _profileRepository.fetchUserProfile(event.id);
emit(ProfileSuccessState(data: data));
} on Object catch (error, stackTrace) {
emit(
ProfileErrorState(
message: 'Ошибка при загрузке профиля',
error: error,
stackTrace: stackTrace,
),
);
}
}
}

View File

@@ -0,0 +1,17 @@
part of 'profile_bloc.dart';
sealed class ProfileEvent extends Equatable {
const ProfileEvent();
@override
List<Object> get props => [];
}
final class ProfileFetchProfileEvent extends ProfileEvent {
final String id;
const ProfileFetchProfileEvent({required this.id});
@override
List<Object> get props => [id];
}

View File

@@ -0,0 +1,36 @@
part of 'profile_bloc.dart';
sealed class ProfileState extends Equatable {
const ProfileState();
@override
List<Object> get props => [];
}
final class ProfileInitialState extends ProfileState {}
final class ProfileWaitingState extends ProfileState {}
final class ProfileErrorState extends ProfileState {
final String message;
final Object error;
final StackTrace? stackTrace;
const ProfileErrorState({
required this.message,
required this.error,
this.stackTrace,
});
@override
List<Object> get props => [message, error];
}
final class ProfileSuccessState extends ProfileState {
final Object data;
const ProfileSuccessState({required this.data});
@override
List<Object> get props => [data];
}

View File

@@ -0,0 +1,8 @@
import 'package:friflex_starter/di/di_base_repo.dart';
/// {@template IProfileRepository}
///
/// {@endtemplate}
abstract interface class IProfileRepository with DiBaseRepo {
Future<String> fetchUserProfile(String id);
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/features/profile/domain/bloc/profile_bloc.dart';
// Класс экрана, где мы инициализируем ProfileBloc
// и вызываем событие ProfileFetchProfileEvent
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
final profileRepository = context.di.repositories.profileRepository;
// Здесь мы инициализируем ProfileBloc
// и вызываем событие ProfileFetchProfileEvent
// Или любые другие события, которые вам нужны
return BlocProvider(
create: (context) => ProfileBloc(profileRepository)
..add(const ProfileFetchProfileEvent(id: '1')),
child: const _ProfileScreenView(),
);
}
}
/// Виджет, который отображает UI экрана
class _ProfileScreenView extends StatelessWidget {
const _ProfileScreenView();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Profile'),
),
body: Center(
child: BlocBuilder<ProfileBloc, ProfileState>(
builder: (context, state) {
return switch (state) {
ProfileSuccessState() => Text('Data: ${state.props.first}'),
ProfileErrorState() => Text('Error: ${state.message}'),
_ => const CircularProgressIndicator(),
};
},
),
),
);
}
}

View File

@@ -0,0 +1,14 @@
import '../../domain/repository/i_profile_scope_repository.dart';
/// {@template ProfileScopeMockRepository}
///
/// {@endtemplate}
final class ProfileScopeMockRepository implements IProfileScopeRepository {
@override
String get name => 'ProfileScopeMockRepository';
@override
Future<String> fetchUserProfile(String id) async {
return 'MOCK Yura Petrov';
}
}

View File

@@ -0,0 +1,24 @@
import 'package:friflex_starter/app/http/i_http_client.dart';
import '../../domain/repository/i_profile_scope_repository.dart';
/// {@template ProfileScopeRepository}
///
/// {@endtemplate}
final class ProfileScopeRepository implements IProfileScopeRepository {
final IHttpClient httpClient;
ProfileScopeRepository({required this.httpClient});
@override
String get name => 'ProfileScopeRepository';
@override
Future<String> fetchUserProfile(String id) async {
// Какой-то запрос к серверу
await Future.delayed(const Duration(seconds: 1));
// httpClient.get('https://example.com/profile/$id');
return 'Yura Petrov';
}
}

View File

@@ -0,0 +1,41 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:friflex_starter/features/profile_scope/domain/repository/i_profile_scope_repository.dart';
part 'profile_scope_event.dart';
part 'profile_scope_state.dart';
class ProfileScopeBloc extends Bloc<ProfileScopeEvent, ProfileScopeState> {
ProfileScopeBloc({required IProfileScopeRepository profileRepository})
: _profileRepository = profileRepository,
super(ProfileScopeInitialState()) {
// Вам необходимо добавлять только
// один обработчик событий в конструкторе
on<ProfileScopeEvent>((event, emit) async {
return switch (event) {
ProfileScopeFetchProfileEvent() => await _fetchProfile(event, emit),
};
});
}
final IProfileScopeRepository _profileRepository;
Future<void> _fetchProfile(
ProfileScopeFetchProfileEvent event,
Emitter<ProfileScopeState> emit,
) async {
try {
emit(ProfileScopeWaitingState());
final data = await _profileRepository.fetchUserProfile(event.id);
emit(ProfileScopeSuccessState(data: data));
} on Object catch (error, stackTrace) {
emit(
ProfileScopeErrorState(
message: 'Ошибка при загрузке профиля',
error: error,
stackTrace: stackTrace,
),
);
}
}
}

View File

@@ -0,0 +1,17 @@
part of 'profile_scope_bloc.dart';
sealed class ProfileScopeEvent extends Equatable {
const ProfileScopeEvent();
@override
List<Object> get props => [];
}
final class ProfileScopeFetchProfileEvent extends ProfileScopeEvent {
final String id;
const ProfileScopeFetchProfileEvent({required this.id});
@override
List<Object> get props => [id];
}

View File

@@ -0,0 +1,36 @@
part of 'profile_scope_bloc.dart';
sealed class ProfileScopeState extends Equatable {
const ProfileScopeState();
@override
List<Object> get props => [];
}
final class ProfileScopeInitialState extends ProfileScopeState {}
final class ProfileScopeWaitingState extends ProfileScopeState {}
final class ProfileScopeErrorState extends ProfileScopeState {
final String message;
final Object error;
final StackTrace? stackTrace;
const ProfileScopeErrorState({
required this.message,
required this.error,
this.stackTrace,
});
@override
List<Object> get props => [message, error];
}
final class ProfileScopeSuccessState extends ProfileScopeState {
final Object data;
const ProfileScopeSuccessState({required this.data});
@override
List<Object> get props => [data];
}

View File

@@ -0,0 +1,8 @@
import 'package:friflex_starter/di/di_base_repo.dart';
/// {@template IProfileScopeRepository}
///
/// {@endtemplate}
abstract interface class IProfileScopeRepository with DiBaseRepo {
Future<String> fetchUserProfile(String id);
}

View File

@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:friflex_starter/app/app_context_ext.dart';
import 'package:friflex_starter/features/profile_scope/domain/bloc/profile_scope_bloc.dart';
class ProfileInheritedScope extends InheritedWidget {
const ProfileInheritedScope({
required this.profileScopeBloc,
required super.child,
super.key,
});
final ProfileScopeBloc profileScopeBloc;
@override
bool updateShouldNotify(ProfileInheritedScope oldWidget) =>
profileScopeBloc != oldWidget.profileScopeBloc;
}
class ProfileScope extends StatefulWidget {
const ProfileScope({
required this.child,
super.key,
});
final Widget child;
static ProfileInheritedScope? maybeOf(
BuildContext context, {
bool listen = false,
}) {
return listen
? context.dependOnInheritedWidgetOfExactType<ProfileInheritedScope>()
: context.getInheritedWidgetOfExactType<ProfileInheritedScope>();
}
static ProfileInheritedScope of(
BuildContext context, {
bool listen = false,
}) {
final result = maybeOf(context, listen: listen);
if (result == null) {
throw StateError(
'ProfileScope is not found above widget ${context.widget}',
);
}
return result;
}
@override
State<StatefulWidget> createState() => _ProfileScopeState();
}
class _ProfileScopeState extends State<ProfileScope> {
late final ProfileScopeBloc _profileScopeBloc;
@override
void initState() {
super.initState();
_profileScopeBloc =
ProfileScopeBloc(profileRepository: context.di.repositories.profileScopeRepository);
_profileScopeBloc.add(const ProfileScopeFetchProfileEvent(id: '1'));
}
@override
void dispose() {
_profileScopeBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ProfileInheritedScope(profileScopeBloc: _profileScopeBloc, child: widget.child);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:friflex_starter/features/profile_scope/domain/bloc/profile_scope_bloc.dart';
import 'package:friflex_starter/features/profile_scope/presentation/profile_scope.dart';
// Класс экрана, где мы инициализируем ProfileScopeBloc
class ProfileScopeScreen extends StatelessWidget {
const ProfileScopeScreen({super.key});
@override
Widget build(BuildContext context) {
return const ProfileScope(
child: _ProfileScopeView(),
);
}
}
class _ProfileScopeView extends StatelessWidget {
const _ProfileScopeView();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile Scope')),
body: Center(
child: BlocBuilder<ProfileScopeBloc, ProfileScopeState>(
bloc: ProfileScope.of(context).profileScopeBloc,
builder: (context, state) {
return switch (state) {
ProfileScopeSuccessState() => Text('Data: ${state.props.first}'),
ProfileScopeErrorState() => Text('Error: ${state.message}'),
_ => const CircularProgressIndicator(),
};
},
),
),
);
}
}