From 22de980f50f27b3ae14eccc5e3d8e2ee24673ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=AF=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=B2?= Date: Mon, 11 Nov 2024 18:13:49 +0400 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devtools_options.yaml | 3 + lib/components/utils/debounce.dart | 20 ++++ lib/data/repositories/api_interface.dart | 2 + lib/data/repositories/bosses_repository.dart | 3 +- lib/presentation/home_page/bloc/bloc.dart | 24 +++- lib/presentation/home_page/bloc/events.dart | 4 +- lib/presentation/home_page/bloc/state.dart | 31 ++++- lib/presentation/home_page/home_page.dart | 112 ++++++++++++------- 8 files changed, 144 insertions(+), 55 deletions(-) create mode 100644 devtools_options.yaml create mode 100644 lib/components/utils/debounce.dart diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/components/utils/debounce.dart b/lib/components/utils/debounce.dart new file mode 100644 index 0000000..6e1c470 --- /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); + } +} diff --git a/lib/data/repositories/api_interface.dart b/lib/data/repositories/api_interface.dart index a3f8b97..038c22c 100644 --- a/lib/data/repositories/api_interface.dart +++ b/lib/data/repositories/api_interface.dart @@ -1,5 +1,7 @@ import 'package:pmu_labs/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/bosses_repository.dart b/lib/data/repositories/bosses_repository.dart index 51a4e90..9afef6a 100644 --- a/lib/data/repositories/bosses_repository.dart +++ b/lib/data/repositories/bosses_repository.dart @@ -49,7 +49,7 @@ class BossesRepository extends ApiInterface { static const String _baseUrl = 'https://sekiro.fandom.com/ru/api.php'; @override - Future?> loadData({String? q}) async { + Future?> loadData({OnErrorCallback? onError,String? q,}) async { try { String url = ''; if(q != null && q != ""){ @@ -94,6 +94,7 @@ class BossesRepository extends ApiInterface { final List? data = dto.data?.map((e) => e.toDomain()).toList(); return data; } on DioException catch (e) { + onError?.call(e.error?.toString()); return null; } } diff --git a/lib/presentation/home_page/bloc/bloc.dart b/lib/presentation/home_page/bloc/bloc.dart index 7a794fc..6de58b4 100644 --- a/lib/presentation/home_page/bloc/bloc.dart +++ b/lib/presentation/home_page/bloc/bloc.dart @@ -3,14 +3,28 @@ import 'package:pmu_labs/presentation/home_page/bloc/events.dart'; import 'package:pmu_labs/presentation/home_page/bloc/state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class HomeBloc extends Bloc{ +class HomeBloc extends Bloc { final BossesRepository rep; - HomeBloc(this.rep) : super(const HomeState()){ + HomeBloc(this.rep) : super(const HomeState()) { on(_onLoadData); } - void _onLoadData(HomeLoadDataEvent event, Emitter emit){ - emit(state.copyWith(data: rep.loadData())); + Future _onLoadData( + HomeLoadDataEvent event, Emitter emit) async { + emit(state.copyWith(isLoading: true)); + + String? error; + + final data = await rep.loadData( + q: event.search, + onError: (e) => error = e, + ); + + emit(state.copyWith( + isLoading: 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 index 7936249..7868db8 100644 --- a/lib/presentation/home_page/bloc/events.dart +++ b/lib/presentation/home_page/bloc/events.dart @@ -3,5 +3,7 @@ abstract class HomeEvent{ } class HomeLoadDataEvent extends HomeEvent{ - const HomeLoadDataEvent(); + 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 index e245dba..bacb3bb 100644 --- a/lib/presentation/home_page/bloc/state.dart +++ b/lib/presentation/home_page/bloc/state.dart @@ -1,13 +1,32 @@ import 'package:equatable/equatable.dart'; import 'package:pmu_labs/domain/models/card.dart'; -class HomeState extends Equatable{ - final Future?>? data; +class HomeState extends Equatable { + final List? data; + final bool isLoading; + final String? error; - const HomeState({this.data}); + const HomeState({ + this.data, + this.isLoading = false, + this.error, + }); - HomeState copyWith({Future?>? data}) => HomeState(data: data ?? this.data); + HomeState 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]; -} \ No newline at end of file + List get props => [ + data, + isLoading, + error, + ]; +} diff --git a/lib/presentation/home_page/home_page.dart b/lib/presentation/home_page/home_page.dart index 074ee26..4d7fa71 100644 --- a/lib/presentation/home_page/home_page.dart +++ b/lib/presentation/home_page/home_page.dart @@ -1,8 +1,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:pmu_labs/data/repositories/bosses_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart%20%20'; import 'package:pmu_labs/presentation/details_page/details_page.dart'; +import 'package:pmu_labs/presentation/home_page/bloc/bloc.dart'; +import 'package:pmu_labs/presentation/home_page/bloc/events.dart'; +import 'package:pmu_labs/presentation/home_page/bloc/state.dart'; +import '../../components/utils/debounce.dart'; import '../../domain/models/card.dart'; part 'card.dart'; @@ -40,54 +44,78 @@ class Body extends StatefulWidget { class _BodyState extends State { final searchController = TextEditingController(); - late Future?> data; - final repo = BossesRepository(); + @override void initState() { - data = repo.loadData(); + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().add(const HomeLoadDataEvent()); + }); super.initState(); } + + @override + void dispose() { + searchController.dispose(); + super.dispose(); + } + + 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) { - setState(() { - data = repo.loadData(q: search); - }); - }, - ), + 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) => SingleChildScrollView( - child: snapshot.hasData - ? Column( - mainAxisAlignment: MainAxisAlignment.center, - children: snapshot.data?.map((data) { - return _Card.fromData( - data, - onLike: (String title, bool isLiked) => - _showSnackBar(context, title, isLiked), - onTap: () => _navToDetails(context, data), - ); - }).toList() ?? - [], - ) - : const CircularProgressIndicator(), - ), - ), - ), - ), - ])); -} + ), + 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(); + }, + ), + ), + ), + ), + ], + ); + } void _navToDetails(BuildContext context, CardData data) { Navigator.push(