6 лаба сделана (поиск вроде тоже работает)

This commit is contained in:
mayday 2024-12-10 13:04:11 +04:00
parent 69f28b9297
commit 4014a00a68
9 changed files with 280 additions and 98 deletions

49
lib/bloc.dart Normal file
View File

@ -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<String> {
DebouncedSearchCubit() : super('');
Timer? _debounce;
// Метод для обновления поиска с задержкой
void search(String query) {
if (_debounce?.isActive ?? false) _debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
emit(query);
});
}
@override
Future<void> close() {
_debounce?.cancel();
return super.close();
}
}
class HomeBloc extends Bloc<HomeEvent, HomeState> {
final CharacterService characterService;
HomeBloc(this.characterService) : super(const HomeState()) {
on<HomeLoadDataEvent>(_onLoadData);
}
Future<void> _onLoadData(HomeLoadDataEvent event, Emitter<HomeState> 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()));
}
}
}

View File

@ -59,7 +59,6 @@ class Character {
} }
} }
class Survivor extends Character { class Survivor extends Character {
Survivor({ Survivor({
required String name, required String name,

View File

@ -7,16 +7,31 @@ const String baseUrl = 'http://192.168.1.83:5000'; // IP-адрес вмест
class CharacterService { class CharacterService {
Future<List<Character>> getCharacters({String search = ''}) async { Future<List<Character>> getCharacters({String search = ''}) async {
try { 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) { if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body); final List<dynamic> data = json.decode(response.body);
print('Characters received: $data'); // Печать данных
// Возвращаем список объектов Character
return data.map((item) => Character.fromJson(item)).toList(); return data.map((item) => Character.fromJson(item)).toList();
} else { } else {
throw Exception('Ошибка загрузки данных с сервера. Статус: ${response.statusCode}'); // Ошибка, если сервер вернул не 200 статус
print('Error: Server responded with status ${response.statusCode}');
throw Exception('Ошибка загрузки данных с сервера');
} }
} catch (e) { } catch (e) {
print('Ошибка при получении данных: $e'); // Обработка ошибок при выполнении запроса
print('Error fetching characters: $e');
throw Exception('Не удалось загрузить персонажей'); throw Exception('Не удалось загрузить персонажей');
} }
} }

17
lib/events.dart Normal file
View File

@ -0,0 +1,17 @@
import 'package:equatable/equatable.dart';
abstract class HomeEvent extends Equatable {
const HomeEvent();
@override
List<Object?> get props => [];
}
class HomeLoadDataEvent extends HomeEvent {
final String searchQuery;
const HomeLoadDataEvent({this.searchQuery = ''});
@override
List<Object?> get props => [searchQuery];
}

View File

@ -1,74 +1,72 @@
import 'package:flutter/material.dart'; 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 'character_service.dart';
import 'pages/CharacterDetailPage.dart'; import 'character.dart';
import 'pages/character_detail_page.dart';
void main() {
runApp(
MultiBlocProvider(
providers: [
BlocProvider<HomeBloc>(
create: (_) => HomeBloc(CharacterService()),
),
BlocProvider<DebouncedSearchCubit>(
create: (_) => DebouncedSearchCubit(),
),
],
child: MyApp(),
),
);
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'Identity V Characters', title: 'My App',
home: MyHomePage(title: 'Персонажи Identity V'), home: MyHomePage(title: 'Identity'),
); );
} }
} }
class MyHomePage extends StatefulWidget { class MyHomePage extends StatelessWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title; final String title;
@override MyHomePage({required this.title});
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final CharacterService _characterService = CharacterService();
late Future<List<Character>> _futureCharacters;
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_futureCharacters = _characterService.getCharacters();
}
void _searchCharacters(String query) {
setState(() {
_futureCharacters = _characterService.getCharacters(search: query);
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Загружаем данные сразу после инициализации страницы
context.read<HomeBloc>().add(HomeLoadDataEvent());
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(widget.title), title: Text(title),
actions: [ actions: [
IconButton( IconButton(
icon: Icon(Icons.search), icon: Icon(Icons.search),
onPressed: () { onPressed: () {
showSearch( showSearch(
context: context, context: context,
delegate: CharacterSearchDelegate(_searchCharacters), delegate: CharacterSearchDelegate(),
); );
}, },
), ),
], ],
), ),
body: FutureBuilder<List<Character>>( body: BlocBuilder<HomeBloc, HomeState>(
future: _futureCharacters, builder: (context, state) {
builder: (context, snapshot) { if (state.status == HomeStatus.loading) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator()); return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) { } else if (state.status == HomeStatus.error) {
return Center(child: Text('Ошибка: ${snapshot.error}')); return Center(child: Text('Ошибка: ${state.errorMessage}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) { } else if (state.status == HomeStatus.loaded) {
return Center(child: Text('Нет персонажей')); final characters = state.characters;
} else {
final characters = snapshot.data!;
return ListView.builder( return ListView.builder(
itemCount: characters.length, itemCount: characters.length,
@ -81,27 +79,46 @@ class _MyHomePageState extends State<MyHomePage> {
), ),
title: Text(character.name), title: Text(character.name),
subtitle: Text(character.typeString), subtitle: Text(character.typeString),
trailing: IconButton( onTap: () {
icon: Icon( // Переход на страницу с деталями персонажа
character.isLiked ? Icons.favorite : Icons.favorite_border, Navigator.push(
color: character.isLiked ? Colors.red : null, context,
), MaterialPageRoute(
onPressed: () { builder: (context) => CharacterDetailPage(character: character),
setState(() {
character.isLiked = !character.isLiked;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
character.isLiked
? '${character.name} понравился вам!'
: '${character.name} убран из лайков.',
),
duration: Duration(seconds: 2),
), ),
); );
}, },
);
},
);
} else {
return Center(child: Text('Нет данных.'));
}
},
), ),
);
}
}
class CharacterSearchDelegate extends SearchDelegate {
@override
Widget buildSuggestions(BuildContext context) {
// Показываем предложения на основе текущего запроса
return BlocProvider<HomeBloc>(
create: (context) => HomeBloc(CharacterService()),
child: BlocBuilder<HomeBloc, HomeState>(
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: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@ -113,37 +130,35 @@ class _MyHomePageState extends State<MyHomePage> {
); );
}, },
); );
}
}, },
), ),
); );
} }
}
class CharacterSearchDelegate extends SearchDelegate {
final Function(String) onSearch;
CharacterSearchDelegate(this.onSearch);
@override
Widget buildSuggestions(BuildContext context) {
return ListView();
}
@override @override
Widget buildResults(BuildContext context) { Widget buildResults(BuildContext context) {
onSearch(query); // Выполняем поиск // Отправляем запрос с задержкой
return FutureBuilder<List<Character>>( final debouncedSearchCubit = context.read<DebouncedSearchCubit>();
future: CharacterService().getCharacters(search: query), // Запрашиваем персонажей с фильтром debouncedSearchCubit.search(query);
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { return BlocBuilder<DebouncedSearchCubit, String>(
builder: (context, searchQuery) {
// Отправляем запрос на сервер через HomeBloc
if (searchQuery.isNotEmpty) {
context.read<HomeBloc>().add(HomeLoadDataEvent(searchQuery: searchQuery));
}
return BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
if (state.status == HomeStatus.loading) {
return Center(child: CircularProgressIndicator()); return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) { } else if (state.status == HomeStatus.error) {
return Center(child: Text('Ошибка: ${snapshot.error}')); return Center(child: Text('Ошибка: ${state.errorMessage}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) { } else if (state.status == HomeStatus.loaded) {
return Center(child: Text('Нет результатов для "$query"')); final characters = state.characters
} else { .where((character) => character.name.toLowerCase().contains(searchQuery.toLowerCase()))
final characters = snapshot.data!; .toList();
return ListView.builder( return ListView.builder(
itemCount: characters.length, itemCount: characters.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
@ -151,12 +166,24 @@ class CharacterSearchDelegate extends SearchDelegate {
return ListTile( return ListTile(
title: Text(character.name), title: Text(character.name),
subtitle: Text(character.typeString), subtitle: Text(character.typeString),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CharacterDetailPage(character: character),
),
); );
}, },
); );
},
);
} else {
return Center(child: Text('Нет результатов'));
} }
}, },
); );
},
);
} }
@override @override
@ -165,7 +192,8 @@ class CharacterSearchDelegate extends SearchDelegate {
IconButton( IconButton(
icon: Icon(Icons.clear), icon: Icon(Icons.clear),
onPressed: () { onPressed: () {
query = ''; // Очищаем запрос query = ''; // Очистить запрос
showSuggestions(context); // Показать предложения
}, },
), ),
]; ];
@ -176,7 +204,7 @@ class CharacterSearchDelegate extends SearchDelegate {
return IconButton( return IconButton(
icon: Icon(Icons.arrow_back), icon: Icon(Icons.arrow_back),
onPressed: () { onPressed: () {
close(context, null); // Закрываем поиск close(context, null); // Закрыть поиск
}, },
); );
} }

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../character.dart'; import '../character.dart'; // Убедитесь, что путь правильный
class CharacterDetailPage extends StatelessWidget { class CharacterDetailPage extends StatelessWidget {
final Character character; final Character character;
CharacterDetailPage({required this.character}); // Конструктор с обязательным параметром
const CharacterDetailPage({Key? key, required this.character}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -28,7 +29,6 @@ class CharacterDetailPage extends StatelessWidget {
), ),
SizedBox(height: 20), SizedBox(height: 20),
Text( Text(
character.name, // Имя персонажа character.name, // Имя персонажа
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),

33
lib/state.dart Normal file
View File

@ -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<Character> characters; // Список персонажей
final String errorMessage;
const HomeState({
this.status = HomeStatus.initial,
this.characters = const [],
this.errorMessage = '',
});
// Метод для обновления состояния
HomeState copyWith({
HomeStatus? status,
List<Character>? characters,
String? errorMessage,
}) {
return HomeState(
status: status ?? this.status,
characters: characters ?? this.characters,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [status, characters, errorMessage];
}

View File

@ -9,6 +9,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" version: "2.11.0"
bloc:
dependency: transitive
description:
name: bloc
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
url: "https://pub.dev"
source: hosted
version: "8.1.4"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -49,6 +57,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" 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: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -62,6 +78,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -147,6 +171,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.15.0" version: "1.15.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -155,6 +187,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" version: "1.9.0"
provider:
dependency: transitive
description:
name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
url: "https://pub.dev"
source: hosted
version: "6.1.2"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter

View File

@ -33,7 +33,8 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
http: ^1.2.2 http: ^1.2.2
equatable: ^2.0.5
flutter_bloc: ^8.1.5
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8