diff --git a/lib/bloc.dart b/lib/bloc.dart new file mode 100644 index 0000000..c734589 --- /dev/null +++ b/lib/bloc.dart @@ -0,0 +1,49 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'events.dart'; +import 'state.dart'; +import 'character_service.dart'; +import 'character.dart'; +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class DebouncedSearchCubit extends Cubit { + DebouncedSearchCubit() : super(''); + + Timer? _debounce; + + // Метод для обновления поиска с задержкой + void search(String query) { + if (_debounce?.isActive ?? false) _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 500), () { + emit(query); + }); + } + + @override + Future close() { + _debounce?.cancel(); + return super.close(); + } +} + +class HomeBloc extends Bloc { + final CharacterService characterService; + + HomeBloc(this.characterService) : super(const HomeState()) { + on(_onLoadData); + } + + Future _onLoadData(HomeLoadDataEvent event, Emitter emit) async { + emit(state.copyWith(status: HomeStatus.loading)); + + try { + final characters = await characterService.getCharacters(search: event.searchQuery); + print('Characters loaded: $characters'); // Отладочный вывод + emit(state.copyWith(status: HomeStatus.loaded, characters: characters)); + } catch (e) { + print('Error: $e'); + emit(state.copyWith(status: HomeStatus.error, errorMessage: e.toString())); + } + } +} + diff --git a/lib/character.dart b/lib/character.dart index 4e00e79..e1d3ab7 100644 --- a/lib/character.dart +++ b/lib/character.dart @@ -59,7 +59,6 @@ class Character { } } - class Survivor extends Character { Survivor({ required String name, diff --git a/lib/character_service.dart b/lib/character_service.dart index 347ce72..7edb1c3 100644 --- a/lib/character_service.dart +++ b/lib/character_service.dart @@ -7,16 +7,31 @@ const String baseUrl = 'http://192.168.1.83:5000'; // IP-адрес вмест class CharacterService { Future> getCharacters({String search = ''}) async { try { - final response = await http.get(Uri.parse('$baseUrl/characters?search=$search')); + // Формируем URL с параметром поиска + final uri = Uri.parse('$baseUrl/characters?search=$search'); + // Выполняем HTTP-запрос + final response = await http.get(uri); + + // Логирование данных для отладки + print('Response status: ${response.statusCode}'); + print('Response body: ${response.body}'); // Выводим тело ответа для анализа + + // Проверяем успешный ответ от сервера if (response.statusCode == 200) { final List data = json.decode(response.body); + print('Characters received: $data'); // Печать данных + + // Возвращаем список объектов Character return data.map((item) => Character.fromJson(item)).toList(); } else { - throw Exception('Ошибка загрузки данных с сервера. Статус: ${response.statusCode}'); + // Ошибка, если сервер вернул не 200 статус + print('Error: Server responded with status ${response.statusCode}'); + throw Exception('Ошибка загрузки данных с сервера'); } } catch (e) { - print('Ошибка при получении данных: $e'); + // Обработка ошибок при выполнении запроса + print('Error fetching characters: $e'); throw Exception('Не удалось загрузить персонажей'); } } diff --git a/lib/events.dart b/lib/events.dart new file mode 100644 index 0000000..2abe254 --- /dev/null +++ b/lib/events.dart @@ -0,0 +1,17 @@ +import 'package:equatable/equatable.dart'; + +abstract class HomeEvent extends Equatable { + const HomeEvent(); + + @override + List get props => []; +} + +class HomeLoadDataEvent extends HomeEvent { + final String searchQuery; + + const HomeLoadDataEvent({this.searchQuery = ''}); + + @override + List get props => [searchQuery]; +} diff --git a/lib/main.dart b/lib/main.dart index 026a96e..bab2434 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,74 +1,72 @@ import 'package:flutter/material.dart'; -import 'character.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'bloc.dart'; +import 'events.dart'; +import 'state.dart'; import 'character_service.dart'; -import 'pages/CharacterDetailPage.dart'; +import 'character.dart'; +import 'pages/character_detail_page.dart'; + +void main() { + runApp( + MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => HomeBloc(CharacterService()), + ), + BlocProvider( + create: (_) => DebouncedSearchCubit(), + ), + ], + child: MyApp(), + ), + ); +} -void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Identity V Characters', - home: MyHomePage(title: 'Персонажи Identity V'), + title: 'My App', + home: MyHomePage(title: 'Identity'), ); } } -class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); - +class MyHomePage extends StatelessWidget { final String title; - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - final CharacterService _characterService = CharacterService(); - late Future> _futureCharacters; - final TextEditingController _searchController = TextEditingController(); - - @override - void initState() { - super.initState(); - _futureCharacters = _characterService.getCharacters(); - } - - void _searchCharacters(String query) { - setState(() { - _futureCharacters = _characterService.getCharacters(search: query); - }); - } + MyHomePage({required this.title}); @override Widget build(BuildContext context) { + // Загружаем данные сразу после инициализации страницы + context.read().add(HomeLoadDataEvent()); + return Scaffold( appBar: AppBar( - title: Text(widget.title), + title: Text(title), actions: [ IconButton( icon: Icon(Icons.search), onPressed: () { showSearch( context: context, - delegate: CharacterSearchDelegate(_searchCharacters), + delegate: CharacterSearchDelegate(), ); }, ), ], ), - body: FutureBuilder>( - future: _futureCharacters, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { + body: BlocBuilder( + builder: (context, state) { + if (state.status == HomeStatus.loading) { return Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center(child: Text('Ошибка: ${snapshot.error}')); - } else if (!snapshot.hasData || snapshot.data!.isEmpty) { - return Center(child: Text('Нет персонажей')); - } else { - final characters = snapshot.data!; + } else if (state.status == HomeStatus.error) { + return Center(child: Text('Ошибка: ${state.errorMessage}')); + } else if (state.status == HomeStatus.loaded) { + final characters = state.characters; return ListView.builder( itemCount: characters.length, @@ -81,28 +79,8 @@ class _MyHomePageState extends State { ), title: Text(character.name), subtitle: Text(character.typeString), - trailing: IconButton( - icon: Icon( - character.isLiked ? Icons.favorite : Icons.favorite_border, - color: character.isLiked ? Colors.red : null, - ), - onPressed: () { - setState(() { - character.isLiked = !character.isLiked; - }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - character.isLiked - ? '${character.name} понравился вам!' - : '${character.name} убран из лайков.', - ), - duration: Duration(seconds: 2), - ), - ); - }, - ), onTap: () { + // Переход на страницу с деталями персонажа Navigator.push( context, MaterialPageRoute( @@ -113,6 +91,8 @@ class _MyHomePageState extends State { ); }, ); + } else { + return Center(child: Text('Нет данных.')); } }, ), @@ -121,40 +101,87 @@ class _MyHomePageState extends State { } class CharacterSearchDelegate extends SearchDelegate { - final Function(String) onSearch; - - CharacterSearchDelegate(this.onSearch); - @override Widget buildSuggestions(BuildContext context) { - return ListView(); + // Показываем предложения на основе текущего запроса + return BlocProvider( + create: (context) => HomeBloc(CharacterService()), + child: BlocBuilder( + builder: (context, state) { + final suggestions = state.characters + .where((character) => character.name.toLowerCase().contains(query.toLowerCase())) + .toList(); + + return ListView.builder( + itemCount: suggestions.length, + itemBuilder: (context, index) { + final character = suggestions[index]; + return ListTile( + title: Text(character.name), + subtitle: Text(character.typeString), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CharacterDetailPage(character: character), + ), + ); + }, + ); + }, + ); + }, + ), + ); } @override Widget buildResults(BuildContext context) { - onSearch(query); // Выполняем поиск - return FutureBuilder>( - future: CharacterService().getCharacters(search: query), // Запрашиваем персонажей с фильтром - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center(child: Text('Ошибка: ${snapshot.error}')); - } else if (!snapshot.hasData || snapshot.data!.isEmpty) { - return Center(child: Text('Нет результатов для "$query"')); - } else { - final characters = snapshot.data!; - return ListView.builder( - itemCount: characters.length, - itemBuilder: (context, index) { - final character = characters[index]; - return ListTile( - title: Text(character.name), - subtitle: Text(character.typeString), - ); - }, - ); + // Отправляем запрос с задержкой + final debouncedSearchCubit = context.read(); + debouncedSearchCubit.search(query); + + return BlocBuilder( + builder: (context, searchQuery) { + // Отправляем запрос на сервер через HomeBloc + if (searchQuery.isNotEmpty) { + context.read().add(HomeLoadDataEvent(searchQuery: searchQuery)); } + + return BlocBuilder( + builder: (context, state) { + if (state.status == HomeStatus.loading) { + return Center(child: CircularProgressIndicator()); + } else if (state.status == HomeStatus.error) { + return Center(child: Text('Ошибка: ${state.errorMessage}')); + } else if (state.status == HomeStatus.loaded) { + final characters = state.characters + .where((character) => character.name.toLowerCase().contains(searchQuery.toLowerCase())) + .toList(); + + return ListView.builder( + itemCount: characters.length, + itemBuilder: (context, index) { + final character = characters[index]; + return ListTile( + title: Text(character.name), + subtitle: Text(character.typeString), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CharacterDetailPage(character: character), + ), + ); + }, + ); + }, + ); + } else { + return Center(child: Text('Нет результатов')); + } + }, + ); }, ); } @@ -165,7 +192,8 @@ class CharacterSearchDelegate extends SearchDelegate { IconButton( icon: Icon(Icons.clear), onPressed: () { - query = ''; // Очищаем запрос + query = ''; // Очистить запрос + showSuggestions(context); // Показать предложения }, ), ]; @@ -176,7 +204,7 @@ class CharacterSearchDelegate extends SearchDelegate { return IconButton( icon: Icon(Icons.arrow_back), onPressed: () { - close(context, null); // Закрываем поиск + close(context, null); // Закрыть поиск }, ); } diff --git a/lib/pages/CharacterDetailPage.dart b/lib/pages/character_detail_page.dart similarity index 87% rename from lib/pages/CharacterDetailPage.dart rename to lib/pages/character_detail_page.dart index d7a8757..6beffed 100644 --- a/lib/pages/CharacterDetailPage.dart +++ b/lib/pages/character_detail_page.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; -import '../character.dart'; +import '../character.dart'; // Убедитесь, что путь правильный class CharacterDetailPage extends StatelessWidget { final Character character; - CharacterDetailPage({required this.character}); + // Конструктор с обязательным параметром + const CharacterDetailPage({Key? key, required this.character}) : super(key: key); @override Widget build(BuildContext context) { @@ -28,7 +29,6 @@ class CharacterDetailPage extends StatelessWidget { ), SizedBox(height: 20), - Text( character.name, // Имя персонажа style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), diff --git a/lib/state.dart b/lib/state.dart new file mode 100644 index 0000000..0216afe --- /dev/null +++ b/lib/state.dart @@ -0,0 +1,33 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import '../character.dart'; // Добавим импорт для Character + +enum HomeStatus { initial, loading, loaded, error } + +class HomeState extends Equatable { + final HomeStatus status; + final List characters; // Список персонажей + final String errorMessage; + + const HomeState({ + this.status = HomeStatus.initial, + this.characters = const [], + this.errorMessage = '', + }); + + // Метод для обновления состояния + HomeState copyWith({ + HomeStatus? status, + List? characters, + String? errorMessage, + }) { + return HomeState( + status: status ?? this.status, + characters: characters ?? this.characters, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [status, characters, errorMessage]; +} diff --git a/pubspec.lock b/pubspec.lock index 90c8b14..61a4149 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" boolean_selector: dependency: transitive description: @@ -49,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -62,6 +78,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: @@ -147,6 +171,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -155,6 +187,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 6b7bf04..fa42868 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,7 +33,8 @@ dependencies: flutter: sdk: flutter http: ^1.2.2 - + equatable: ^2.0.5 + flutter_bloc: ^8.1.5 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8