From 5b6082e6ebc6ac51017f95862d56292ba274b37a Mon Sep 17 00:00:00 2001 From: goldfest228 Date: Thu, 12 Dec 2024 13:08:00 +0400 Subject: [PATCH] lab6 --- lib/components/utils/debounce.dart | 20 +++ lib/data/repositories/api_interface.dart | 2 + lib/data/repositories/weapons_repository.dart | 4 +- 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 | 115 ++++++++---------- pubspec.lock | 49 ++++++++ pubspec.yaml | 3 + 10 files changed, 215 insertions(+), 64 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/weapons_repository.dart b/lib/data/repositories/weapons_repository.dart index 04ef6c0..6d3f2ff 100644 --- a/lib/data/repositories/weapons_repository.dart +++ b/lib/data/repositories/weapons_repository.dart @@ -16,7 +16,7 @@ class WeaponsRepository extends ApiInterface { static const String _baseUrl = 'https://valorant-api.com/v1/weapons'; @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 WeaponsRepository 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 17e734f..855f396 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/weapons_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: (_) => WeaponsRepository(), + 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..3d39a03 --- /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/weapons_repository.dart'; + +class HomeBloc extends Bloc{//наследуем от Bloc и передаём состояния + final WeaponsRepository 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 88f668d..2de5b00 100644 --- a/lib/presentation/home_page/home_page.dart +++ b/lib/presentation/home_page/home_page.dart @@ -1,12 +1,16 @@ -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 '../../components/utils/debounce.dart'; import '../../data/repositories/weapons_repository.dart'; import '../../domain/models/card.dart'; import '../details_page/details_page.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'bloc/bloc.dart'; +import 'bloc/events.dart'; +import 'bloc/state.dart'; + part 'card.dart'; class MyHomePage extends StatefulWidget { @@ -43,85 +47,74 @@ class Body extends StatefulWidget { class _BodyState extends State { final searchController = TextEditingController(); final ScrollController _scrollController = ScrollController(); - late Future?> data; - final repo = WeaponsRepository(); - List? filteredData; @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(); } - - // Метод для фильтрации карточек по запросу - void _filterCards(String searchQuery, List? dataList) { - if (dataList == null || dataList.isEmpty) return; - - final filteredList = dataList.where((data) => - data.text?.toLowerCase().contains(searchQuery.toLowerCase()) ?? false).toList(); - - setState(() { - filteredData = filteredList; - }); + Future _onRefresh() { + context + .read() + .add(HomeLoadDataEvent(search: searchController.text)); + return Future.value(null); //прекращение отображения загрузки } @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) { - _filterCards(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) { - final displayData = filteredData ?? snapshot.data!; - return ListView.builder( - controller: _scrollController, - itemCount: displayData.length, - itemBuilder: (context, index) { - final cardData = displayData[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 4ddc5cf..6105b60 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -161,6 +161,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + 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: @@ -209,6 +225,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -238,6 +262,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: @@ -379,6 +411,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -411,6 +451,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: @@ -578,3 +626,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.1.3 <4.0.0" + flutter: ">=1.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index 2300645..b396825 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: