From 08679029aac119b8107f6a64a21a72c1339537b4 Mon Sep 17 00:00:00 2001 From: Stepan Date: Fri, 29 Nov 2024 16:08:55 +0400 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20Lab6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/components/utils/debounce.dart | 20 ++++ lib/data/repositories/api_interface.dart | 2 + .../repositories/characters_repository.dart | 6 +- lib/main.dart | 13 ++- lib/presentation/home_page/bloc/bloc.dart | 30 +++++ lib/presentation/home_page/bloc/events.dart | 9 ++ lib/presentation/home_page/bloc/state.dart | 34 ++++++ lib/presentation/home_page/home_page.dart | 105 ++++++++++-------- pubspec.lock | 48 ++++++++ pubspec.yaml | 3 + 10 files changed, 217 insertions(+), 53 deletions(-) create mode 100644 lib/components/utils/debounce.dart create mode 100644 lib/presentation/home_page/bloc/bloc.dart create mode 100644 lib/presentation/home_page/bloc/events.dart create mode 100644 lib/presentation/home_page/bloc/state.dart diff --git a/lib/components/utils/debounce.dart b/lib/components/utils/debounce.dart new file mode 100644 index 0000000..60a18e1 --- /dev/null +++ b/lib/components/utils/debounce.dart @@ -0,0 +1,20 @@ +import 'dart:async'; +import 'dart:ui'; + +class Debounce { //помощник + factory Debounce() => _instance; //обращение через статический конструктор + + Debounce._(); + + static final Debounce _instance = Debounce._(); + + static Timer? _timer; + + static void run(//создаём задержку для ввода пользователя в поисковую строку + VoidCallback action, { + Duration delay = const Duration(milliseconds: 500), + }) { + _timer?.cancel(); + _timer = Timer(delay, action); + } +} \ No newline at end of file diff --git a/lib/data/repositories/api_interface.dart b/lib/data/repositories/api_interface.dart index 7558e67..3ff1042 100644 --- a/lib/data/repositories/api_interface.dart +++ b/lib/data/repositories/api_interface.dart @@ -1,6 +1,8 @@ import '../../domain/models/card.dart'; +typedef OnErrorCallback = void Function(String? error); + abstract class ApiInterface{ Future?> loadData(); } \ No newline at end of file diff --git a/lib/data/repositories/characters_repository.dart b/lib/data/repositories/characters_repository.dart index 162962c..8c87429 100644 --- a/lib/data/repositories/characters_repository.dart +++ b/lib/data/repositories/characters_repository.dart @@ -13,10 +13,10 @@ class AgentsRepository extends ApiInterface { requestBody: true, )); - static const String _baseUrl = 'https://valorant-api.com/v1/agents'; + static const String _baseUrl = 'https://valorant-api.com/v1/ agents'; @override - Future?> loadData({String? q}) async { + Future?> loadData({OnErrorCallback? onError,String? q,}) async { try { // Формирование URL для запроса final Response response = await _dio.get(_baseUrl); @@ -37,7 +37,7 @@ class AgentsRepository extends ApiInterface { return data; } on DioException catch (e) { // Обработка ошибок запроса - print('Ошибка при загрузке данных: $e'); + onError?.call(e.error?.toString()); return null; } } diff --git a/lib/main.dart b/lib/main.dart index fde759a..1710bb4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test_app/presentation/home_page/bloc/bloc.dart'; +import 'data/repositories/characters_repository.dart'; import 'presentation/home_page/home_page.dart'; void main() { @@ -17,7 +20,15 @@ class MyApp extends StatelessWidget { colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange), useMaterial3: true, ), - home: const MyHomePage(title: 'Афанасьев Степан Сергеевич'), + home: RepositoryProvider( //даём нашему репозиторию доступ + lazy: true, //ленивое создание объекта + create: (_) => AgentsRepository(), + child: BlocProvider( //даём доступ Bloc для нашей страницы + lazy: false, + create: (context) => HomeBloc(context.read()), //в конструктор нашего Блока передаём репозиторий с нашей апи, то есть у нас появляется доступ по дереву к апи + child: const MyHomePage(title: 'Агенты Valorant'), + ), + ), ); } } diff --git a/lib/presentation/home_page/bloc/bloc.dart b/lib/presentation/home_page/bloc/bloc.dart new file mode 100644 index 0000000..4b50176 --- /dev/null +++ b/lib/presentation/home_page/bloc/bloc.dart @@ -0,0 +1,30 @@ +import 'package:flutter_test_app/presentation/home_page/bloc/state.dart'; +import 'package:flutter_test_app/presentation/home_page/bloc/events.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../data/repositories/characters_repository.dart'; + +class HomeBloc extends Bloc{//наследуем от Bloc и передаём состояния + final AgentsRepository rep; + + HomeBloc(this.rep) : super(const HomeState()) { + on(_onLoadData); + } + + Future _onLoadData( + HomeLoadDataEvent event, Emitter emit) async { + emit(state.copyWith(isLoading: true)); //метода emit изменяет наши состояния + + String? error; + + final data = await rep.loadData( //загрузка данных + q: event.search, + onError: (e) => error = e, + ); + + emit(state.copyWith( //метода emit изменяет наши состояния + isLoading: false, //флаг загрузки меняем на false + data: data, + error: error, + )); + } +} \ No newline at end of file diff --git a/lib/presentation/home_page/bloc/events.dart b/lib/presentation/home_page/bloc/events.dart new file mode 100644 index 0000000..70d3132 --- /dev/null +++ b/lib/presentation/home_page/bloc/events.dart @@ -0,0 +1,9 @@ +abstract class HomeEvent{ //наследуем все события + const HomeEvent(); +} + +class HomeLoadDataEvent extends HomeEvent{ + final String? search; //событие поиска + + const HomeLoadDataEvent({this.search}); //событие на загрузку данных +} \ No newline at end of file diff --git a/lib/presentation/home_page/bloc/state.dart b/lib/presentation/home_page/bloc/state.dart new file mode 100644 index 0000000..fe51cd7 --- /dev/null +++ b/lib/presentation/home_page/bloc/state.dart @@ -0,0 +1,34 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:equatable/equatable.dart'; +import '../../../domain/models/card.dart'; + +@CopyWith() +class HomeState extends Equatable{ //сравнение двух состояний через Equatable + final List? data; + final bool isLoading; //отображение загрузки + final String? error; //отображение ошибок + + const HomeState({ + this.data, + this.isLoading = false, + this.error, + }); + + HomeState copyWith({ //copyWith создаёт копию объекта с изменением некоторых данных + List? data, + bool? isLoading, + String? error, + }) => + HomeState( + data: data ?? this.data, + isLoading: isLoading ?? this.isLoading, + error: error ?? this.error, + ); + + @override + List get props => [ + data, + isLoading, + error, + ]; +} \ No newline at end of file diff --git a/lib/presentation/home_page/home_page.dart b/lib/presentation/home_page/home_page.dart index b4835de..fe1b880 100644 --- a/lib/presentation/home_page/home_page.dart +++ b/lib/presentation/home_page/home_page.dart @@ -1,13 +1,15 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test_app/data/repositories/mock_repository.dart'; import 'package:flutter_test_app/main.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; - import '../../data/repositories/characters_repository.dart'; import '../../domain/models/card.dart'; import '../details_page/details_page.dart'; - - +import 'bloc/bloc.dart'; +import 'bloc/events.dart'; +import 'bloc/state.dart'; +import '../../components/utils/debounce.dart'; part 'card.dart'; class MyHomePage extends StatefulWidget { @@ -44,21 +46,27 @@ class Body extends StatefulWidget { class _BodyState extends State { final searchController = TextEditingController(); final ScrollController _scrollController = ScrollController(); - late Future?> data; - final repo = AgentsRepository(); @override void initState() { - data = repo.loadData(); + WidgetsBinding.instance.addPostFrameCallback((_) { // привязываем логику к кадру + context.read().add(const HomeLoadDataEvent()); // добавляем данные после считывания + }); super.initState(); } @override void dispose() { - searchController.dispose(); + searchController.dispose(); // очистка контроллера _scrollController.dispose(); super.dispose(); } + Future _onRefresh() { + context + .read() + .add(HomeLoadDataEvent(search: searchController.text)); + return Future.value(null); //прекращение отображения загрузки + } // Метод для прокрутки к определённой карточке void _scrollToCard(String searchQuery, List? dataList) { @@ -78,53 +86,52 @@ class _BodyState extends State { @override Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(12), - child: CupertinoSearchTextField( - controller: searchController, - onSubmitted: (search) { - data.then((dataList) { - _scrollToCard(search, dataList); - }); - }, - ), + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: CupertinoSearchTextField( + controller: searchController, + onChanged: (search ) { //вызов задержки пока пользователь не перестанет печатать в поисковой строке + Debounce.run(() => context + .read() + .add(HomeLoadDataEvent(search: search))); + }, ), - Expanded( - child: Center( - child: FutureBuilder?>( - future: data, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const CircularProgressIndicator(); - } else if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } else if (snapshot.hasData && snapshot.data != null) { - return ListView.builder( - controller: _scrollController, - itemCount: snapshot.data!.length, - itemBuilder: (context, index) { - final cardData = snapshot.data![index]; - return _Card.fromData( - cardData, - onLike: (String title, bool isLiked) => - _showSnackBar(context, title, isLiked), - onTap: () => _navToDetails(context, cardData), - ); - }, - ); - } else { - return const Text('No data available'); - } + ), + BlocBuilder( //ждёт изменения состояния + builder: (context, state) => state.error != null + ? Text( + state.error ?? '', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(color: Colors.red), + ) + : state.isLoading + ? const CircularProgressIndicator() + : Expanded( + child: RefreshIndicator( + onRefresh: _onRefresh, + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: state.data?.length ?? 0, + itemBuilder: (context, index) { + final data = state.data?[index]; + return data != null + ? _Card.fromData( + data, + onLike: (String title, bool isLiked) => + _showSnackBar(context, title, isLiked), + onTap: () => _navToDetails(context, data), + ) + : const SizedBox.shrink(); }, ), ), ), - ], - ), + ), + ], ); } diff --git a/pubspec.lock b/pubspec.lock index c6591b5..3212b9d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -166,6 +166,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + copy_with_extension: + dependency: transitive + description: + name: copy_with_extension + sha256: fbcf890b0c34aedf0894f91a11a579994b61b4e04080204656b582708b5b1125 + url: "https://pub.dev" + source: hosted + version: "5.0.4" + copy_with_extension_gen: + dependency: "direct main" + description: + name: copy_with_extension_gen + sha256: "51cd11094096d40824c8da629ca7f16f3b7cea5fc44132b679617483d43346b0" + url: "https://pub.dev" + source: hosted + version: "5.0.4" crypto: dependency: transitive description: @@ -214,6 +230,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -243,6 +267,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" flutter_lints: dependency: "direct dev" description: @@ -416,6 +448,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -448,6 +488,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" pub_semver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a84075f..10b3558 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,9 @@ dependencies: pretty_dio_logger: ^1.3.1 json_annotation: ^4.8.1 html: ^0.15.0 + equatable: ^2.0.5 + flutter_bloc: ^8.1.5 + copy_with_extension_gen: ^5.0.4 dev_dependencies: flutter_test: