From a3b65e8c4e7c3714f70ce7b868bf3e10c38941fb Mon Sep 17 00:00:00 2001 From: K Date: Tue, 26 Nov 2024 01:44:56 +0400 Subject: [PATCH] laba 6............ --- lib/card_data.dart | 16 +- lib/components/utils/debounce.dart | 22 +++ lib/components/utils/error_callback.dart | 1 + lib/data/dto/album_dto.dart | 83 ++++++++- lib/data/mapper/album_mapper.dart | 66 ++++--- lib/data/repositories/album_repository.dart | 12 +- lib/home_page/bloc/events.dart | 18 ++ lib/home_page/bloc/home_bloc.dart | 42 +++++ lib/home_page/bloc/state.dart | 27 +++ lib/home_page/home_page.dart | 180 +++++++++++++------- lib/main.dart | 17 +- pubspec.lock | 40 +++++ pubspec.yaml | 2 + 13 files changed, 429 insertions(+), 97 deletions(-) create mode 100644 lib/components/utils/debounce.dart create mode 100644 lib/components/utils/error_callback.dart create mode 100644 lib/home_page/bloc/events.dart create mode 100644 lib/home_page/bloc/home_bloc.dart create mode 100644 lib/home_page/bloc/state.dart diff --git a/lib/card_data.dart b/lib/card_data.dart index 82ef497..801dd5c 100644 --- a/lib/card_data.dart +++ b/lib/card_data.dart @@ -1,4 +1,7 @@ +import 'data/dto/album_dto.dart'; + class CardData { + final String id; final String title; final String artist; final String year; @@ -8,5 +11,16 @@ class CardData { final String summary; final String? imageUrl; - CardData({required this.title, required this.artist, required this.year, required this.url, required this.genres, required this.tracks, required this.summary, required this.imageUrl}); + CardData({ + required this.id, + required this.title, + required this.artist, + required this.year, + required this.url, + required this.genres, + required this.tracks, + required this.summary, + this.imageUrl, + }); + } \ No newline at end of file diff --git a/lib/components/utils/debounce.dart b/lib/components/utils/debounce.dart new file mode 100644 index 0000000..806846d --- /dev/null +++ b/lib/components/utils/debounce.dart @@ -0,0 +1,22 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/cupertino.dart'; + +class Debounce { + factory Debounce() => _instance; + + Debounce._(); + + static final Debounce _instance = Debounce._(); + + static Timer? _timer; + + static void run( + VoidCallback action, { + Duration delay = const Duration(milliseconds: 5000), + }) { + _timer?.cancel(); + _timer = Timer(delay, action); + } +} \ No newline at end of file diff --git a/lib/components/utils/error_callback.dart b/lib/components/utils/error_callback.dart new file mode 100644 index 0000000..5e3110f --- /dev/null +++ b/lib/components/utils/error_callback.dart @@ -0,0 +1 @@ +typedef OnErrorCallback = void Function(String? error)?; \ No newline at end of file diff --git a/lib/data/dto/album_dto.dart b/lib/data/dto/album_dto.dart index 84794f1..9ed0ecd 100644 --- a/lib/data/dto/album_dto.dart +++ b/lib/data/dto/album_dto.dart @@ -4,10 +4,49 @@ import 'package:uuid/uuid.dart'; @JsonSerializable(createToJson: false) class AlbumDto { final List? data; + final AlbumPaginationDto? pagination; // Добавлено для пагинации - const AlbumDto({this.data,}); + const AlbumDto({this.data, this.pagination}); + + // Здесь убираем factory и оставляем ваш метод fetchAlbums + // factory AlbumDto.fromJson(Map json) => _$AlbumDtoFromJson(json); + + // Ваш метод fetchAlbums будет принимать JSON и вызывать конструктор + static AlbumDto fetchAlbums(Map json) { + return AlbumDto( + data: (json['data'] as List?) + ?.map((e) => AlbumDataDto.fromJson(e as Map)) + .toList(), + pagination: json['pagination'] != null + ? AlbumPaginationDto.fromJson(json['pagination']) + : null, + ); + } } +@JsonSerializable(createToJson: false) +class AlbumPaginationDto { + @JsonKey(name: 'current_page') + final int? currentPage; + @JsonKey(name: 'has_next_page') + final bool? hasNextPage; + @JsonKey(name: 'last_visible_page') + final int? lastVisiblePage; + + const AlbumPaginationDto({this.currentPage, this.hasNextPage, this.lastVisiblePage}); + + // Здесь убираем factory + // factory AlbumPaginationDto.fromJson(Map json) => _$AlbumPaginationDtoFromJson(json); + + // Метод для конструирования экземпляра + static AlbumPaginationDto fromJson(Map json) { + return AlbumPaginationDto( + currentPage: json['current_page'], + hasNextPage: json['has_next_page'], + lastVisiblePage: json['last_visible_page'], + ); + } +} @JsonSerializable(createToJson: false) class AlbumDataDto { @@ -32,6 +71,26 @@ class AlbumDataDto { this.url, this.images, }) : id = id ?? const Uuid().v4(); // Генерация id + + // Здесь убираем factory + // factory AlbumDataDto.fromJson(Map json) => _$AlbumDataDtoFromJson(json); + + // Метод для конструирования экземпляра + static AlbumDataDto fromJson(Map json) { + return AlbumDataDto( + id: json['id'], + title: json['title'], + artist: json['artist'], + year: json['year'], + genres: (json['genres'] as List?)?.map((e) => e as String).toList(), + tracks: (json['tracks'] as List?)?.map((e) => e as String).toList(), + summary: json['summary'], + url: json['url'], + images: json['images'] != null + ? AlbumDataImagesDto.fromJson(json['images']) + : null, + ); + } } @JsonSerializable(createToJson: false) @@ -39,6 +98,18 @@ class AlbumDataImagesDto { final AlbumDataImagesJPGDto? jpg; const AlbumDataImagesDto({this.jpg}); + + // Здесь убираем factory + // factory AlbumDataImagesDto.fromJson(Map json) => _$AlbumDataImagesDtoFromJson(json); + + // Метод для конструирования экземпляра + static AlbumDataImagesDto fromJson(Map json) { + return AlbumDataImagesDto( + jpg: json['jpg'] != null + ? AlbumDataImagesJPGDto.fromJson(json['jpg']) + : null, + ); + } } @JsonSerializable(createToJson: false) @@ -46,4 +117,14 @@ class AlbumDataImagesJPGDto { final String? image_url; const AlbumDataImagesJPGDto({this.image_url}); + + // Здесь убираем factory + // factory AlbumDataImagesJPGDto.fromJson(Map json) => _$AlbumDataImagesJPGDtoFromJson(json); + + // Метод для конструирования экземпляра + static AlbumDataImagesJPGDto fromJson(Map json) { + return AlbumDataImagesJPGDto( + image_url: json['image_url'], + ); + } } \ No newline at end of file diff --git a/lib/data/mapper/album_mapper.dart b/lib/data/mapper/album_mapper.dart index fa6d2b7..8f9bc82 100644 --- a/lib/data/mapper/album_mapper.dart +++ b/lib/data/mapper/album_mapper.dart @@ -3,10 +3,15 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:pmd_labs/data/dto/album_dto.dart'; import 'package:pmd_labs/card_data.dart'; +import 'package:uuid/uuid.dart'; + +import '../../home_page/home_page.dart'; extension AlbumDataDtoMapper on AlbumDataDto { List fetchAlbums(List albumsData) { + const String defaultImageUrl = "https://i.ibb.co/VwCkRD4/image.jpg"; List albums = []; + final Uuid uuid = Uuid(); for (var album in albumsData) { // Ищем изображение с самым большим размером @@ -15,34 +20,30 @@ extension AlbumDataDtoMapper on AlbumDataDto { // Ищем наиболее подходящее изображение List images = album['image'] as List; var largestImage = images.firstWhere( - (image) => image['size'] == 'mega', - orElse: () => - images.firstWhere( - (image) => image['size'] == 'extralarge', - orElse: () => - images.firstWhere( - (image) => image['size'] == 'large', - orElse: () => images.first // запасной вариант - ) - ) + (image) => image['size'] == 'mega', + orElse: () => images.firstWhere( + (image) => image['size'] == 'extralarge', + orElse: () => images.firstWhere( + (image) => image['size'] == 'large', + orElse: () => null, // запасной вариант + ), + ), ); - - albumImage = largestImage['#text'] as String?; + if(largestImage['#text'] != "") + albumImage = largestImage['#text'] as String?; + else + albumImage = defaultImageUrl; if (album['name'] != "(null)" && album['artist'] != "(null)") { + final id = uuid.v4(); albums.add(AlbumDataDto( - id: null, - // Вы можете установить значение id, если оно доступно + id: id, title: album['name'] as String?, artist: album['artist'] as String?, year: "", - // Год будет заполнен позже genres: [], - // Позже будет заполнен из второго респонса tracks: [], - // Позже будет заполнен из второго респонса summary: "", - // Описание url: album['url'] as String?, images: AlbumDataImagesDto( jpg: AlbumDataImagesJPGDto(image_url: albumImage), @@ -53,8 +54,7 @@ extension AlbumDataDtoMapper on AlbumDataDto { return albums; } - Future fetchAlbumDetails(Map data, - AlbumDataDto album) async { + Future fetchAlbumDetails(Map data, AlbumDataDto album) async { if (data['album'] != null) { // Получаем жанры и год String year = ""; @@ -86,7 +86,7 @@ extension AlbumDataDtoMapper on AlbumDataDto { // Обновляем дополнительную информацию, если необходимо album.summary = - data['album']?['wiki']?['summary'] ?? "missing";// или аналогичное поле + data['album']?['wiki']?['summary'] ?? "missing"; // или аналогичное поле } return album; // возвращаем заполненное DTO @@ -94,6 +94,7 @@ extension AlbumDataDtoMapper on AlbumDataDto { CardData toDomain() { return CardData( + id: id, title: title ?? 'UNKNOWN', artist: artist ?? 'UNKNOWN', year: year ?? 'UNKNOWN', @@ -101,8 +102,27 @@ extension AlbumDataDtoMapper on AlbumDataDto { summary: summary ?? 'UNKNOWN', genres: genres ?? ['UNKNOWN'], tracks: tracks ?? ['UNKNOWN'], - imageUrl: images?.jpg?.image_url ?? - 'UNKNOWN', // Привязываем imageUrl к DTO + imageUrl: images?.jpg?.image_url ?? 'UNKNOWN', // Привязываем imageUrl к DTO + ); + } +} + +extension AlbumDtoMapper on AlbumDto { + List fetchAlbumsWithPagination(List albumsData) { + // Используем существующий метод для извлечения данных альбомов + List albums = AlbumDataDto().fetchAlbums(albumsData); + + // Другие манипуляции с пагинацией можно добавить здесь, + // если получены дополнительные данные пагинации и необходим их анализ + return albums; + } + + HomeData toDomain() { + return HomeData( + data: data?.map((e) => e.toDomain()).toList(), + nextPage: (pagination?.hasNextPage ?? false) + ? ((pagination?.currentPage ?? 0) + 1) + : null, ); } } \ No newline at end of file diff --git a/lib/data/repositories/album_repository.dart b/lib/data/repositories/album_repository.dart index e5326cc..01a7cbf 100644 --- a/lib/data/repositories/album_repository.dart +++ b/lib/data/repositories/album_repository.dart @@ -18,12 +18,13 @@ class AlbumRepository extends ApiInterface { static const String apiKey = '535d9d508a785fae99bfb492d8f15a58'; static const String _baseUrl = 'https://api.your_album_api.com'; // Замените на ваш базовый URL - // Метод для загрузки списка альбомов по названию + // Метод для загрузки списка альбомов по названию с учетом пагинации @override - Future?> loadData({String? albumName}) async { - albumName ??= 'a'; + Future?> loadData({String? albumName, int page = 1, int pageSize = 5}) async { + if (albumName == null || albumName == "") + albumName = 'a'; final String url = - 'https://ws.audioscrobbler.com/2.0/?method=album.search&album=$albumName&api_key=$apiKey&format=json'; + 'https://ws.audioscrobbler.com/2.0/?method=album.search&album=$albumName&api_key=$apiKey&format=json&limit=$pageSize&page=$page'; try { final response = await _dio.get(url); @@ -45,8 +46,7 @@ class AlbumRepository extends ApiInterface { // Получение информации об альбоме Future getAlbumInfo(AlbumDataDto album) async { final String url = - 'https://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=$apiKey&artist=${album - .artist}&album=${album.title}&format=json'; + 'https://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=$apiKey&artist=${album.artist}&album=${album.title}&format=json'; try { final response = await _dio.get(url); diff --git a/lib/home_page/bloc/events.dart b/lib/home_page/bloc/events.dart new file mode 100644 index 0000000..5bedbf8 --- /dev/null +++ b/lib/home_page/bloc/events.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; + +abstract class HomeEvent extends Equatable { + const HomeEvent(); + + @override + List get props => []; +} + +class HomeLoadDataEvent extends HomeEvent { + final String? search; + final int? nextPage; + + const HomeLoadDataEvent({this.search, this.nextPage}); + + @override + List get props => [search, nextPage]; +} \ No newline at end of file diff --git a/lib/home_page/bloc/home_bloc.dart b/lib/home_page/bloc/home_bloc.dart new file mode 100644 index 0000000..d550140 --- /dev/null +++ b/lib/home_page/bloc/home_bloc.dart @@ -0,0 +1,42 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pmd_labs/data/repositories/album_repository.dart'; +import 'package:pmd_labs/home_page/bloc/state.dart'; +import '../home_page.dart'; +import 'events.dart'; + +class HomeBloc extends Bloc { + final AlbumRepository repo; + + HomeBloc(this.repo) : super(const HomeState()) { + on(_onLoadData); + } + + Future _onLoadData(HomeLoadDataEvent event, Emitter emit) async { + if (event.nextPage == null) { + emit(state.copyWith(isLoading: true)); + } else { + emit(state.copyWith(isPaginationLoading: true)); + } + + try { + final data = await repo.loadData(albumName: event.search, page: event.nextPage ?? 1); + + HomeData? updatedData; + Set existingIds; + if (event.nextPage != null && state.data != null) { + final existingData = state.data!.data ?? []; + final newDataList = data; + existingIds = existingData.map((item) => item.id).toSet(); + final newData = newDataList?.where((newItem) => !existingIds.contains(newItem.id)).toList(); + updatedData = HomeData(data: [...existingData, ...?newData], nextPage: (event.nextPage ?? 1) + 1); + } else { + updatedData = HomeData(data: data, nextPage: (event.nextPage ?? 1) + 1); + } + + emit(state.copyWith(data: updatedData, isLoading: false, isPaginationLoading: false)); + } catch (e) { + emit(state.copyWith(isLoading: false, isPaginationLoading: false)); + print("Error loading data: $e"); + } + } +} \ No newline at end of file diff --git a/lib/home_page/bloc/state.dart b/lib/home_page/bloc/state.dart new file mode 100644 index 0000000..b83aa53 --- /dev/null +++ b/lib/home_page/bloc/state.dart @@ -0,0 +1,27 @@ +import 'package:equatable/equatable.dart'; +import 'package:pmd_labs/card_data.dart'; + +import '../home_page.dart'; // Обязательно импортируем CardData + +class HomeState extends Equatable { + final HomeData? data; + final bool isLoading; + final bool isPaginationLoading; + + const HomeState({this.data, this.isLoading = false, this.isPaginationLoading = false}); + + HomeState copyWith({ + HomeData? data, + bool? isLoading, + bool? isPaginationLoading, + }) { + return HomeState( + data: data ?? this.data, + isLoading: isLoading ?? this.isLoading, + isPaginationLoading: isPaginationLoading ?? this.isPaginationLoading, + ); + } + + @override + List get props => [data, isLoading, isPaginationLoading]; +} \ No newline at end of file diff --git a/lib/home_page/home_page.dart b/lib/home_page/home_page.dart index 0195e09..60a2722 100644 --- a/lib/home_page/home_page.dart +++ b/lib/home_page/home_page.dart @@ -1,27 +1,21 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pmd_labs/components/utils/debounce.dart'; import 'package:pmd_labs/data/repositories/album_repository.dart'; import 'package:pmd_labs/details_page/detail_page.dart'; - +import 'package:pmd_labs/home_page/bloc/events.dart'; +import 'package:pmd_labs/home_page/bloc/state.dart'; import 'package:pmd_labs/card_data.dart'; import '../data/dto/album_dto.dart'; +import 'bloc/home_bloc.dart'; part 'card.dart'; - class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - final String title; @override @@ -32,20 +26,20 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - backgroundColor: const Color(0xFF231a24), // Цвет фона AppBar - title: Text( - widget.title, - style: Theme.of(context) - .textTheme - .headlineLarge - ?.copyWith(color: Colors.orange), // Цвет текста заголовка - ), - ), - body: Container( - color: const Color(0xFF403042), - child: const Body(), // Ваш виджет Body + appBar: AppBar( + backgroundColor: const Color(0xFF231a24), + title: Text( + widget.title, + style: Theme.of(context) + .textTheme + .headlineLarge + ?.copyWith(color: Colors.orange), ), + ), + body: Container( + color: const Color(0xFF403042), + child: const Body(), // Ваш виджет Body + ), ); } } @@ -59,17 +53,37 @@ class Body extends StatefulWidget { class _BodyState extends State { final AlbumRepository repo = AlbumRepository(); - var data = AlbumRepository().loadData(); + final ScrollController scrollController = ScrollController(); + final TextEditingController searchController = TextEditingController(); + late final Debounce debounce; + + @override + void initState() { + super.initState(); // Настройка времени дебаунса + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().add(const HomeLoadDataEvent()); + }); + + scrollController.addListener(_onNextPageListener); + } + + void _onNextPageListener() { + if (scrollController.position.pixels >= scrollController.position.maxScrollExtent && + !context.read().state.isPaginationLoading) { + final bloc = context.read(); + bloc.add(HomeLoadDataEvent( + search: searchController.text, + nextPage: bloc.state.data?.nextPage, + )); + } + } void _showSnackbar(BuildContext context, String title, bool isLiked) { WidgetsBinding.instance.addPostFrameCallback((_) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( 'Лайк на $title ${isLiked ? 'поставлен' : 'убран'}', - style: Theme - .of(context) - .textTheme - .bodyLarge, + style: Theme.of(context).textTheme.bodyLarge, ), backgroundColor: Colors.orangeAccent, duration: const Duration(seconds: 1), @@ -79,48 +93,86 @@ class _BodyState extends State { void _navToDetails(BuildContext context, CardData data) { Navigator.push( - context, CupertinoPageRoute(builder: (context) => DetailsPage(data))); + context, + CupertinoPageRoute(builder: (context) => DetailsPage(data)) + ); } - @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( - onChanged: (search) { - setState(() { - data = repo.loadData(albumName:search); - }); - }, - style: TextStyle(color: Colors.orange, fontFamily: 'Correction_Tape'), - ), + padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: CupertinoSearchTextField( + controller: searchController, + onChanged: (search) { + Debounce.run(() { + context.read().add(HomeLoadDataEvent(search: search)); + }); + }, + style: TextStyle(color: Colors.orange, fontFamily: 'Correction_Tape'), ), - 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() ?? [], + ), + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + return RefreshIndicator( + onRefresh: () async { + // Добавим состояние загрузки + context.read().add(HomeLoadDataEvent(search: searchController.text)); + }, + child: ListView.builder( + controller: scrollController, + padding: EdgeInsets.zero, + itemCount: state.data?.data?.length ?? 0, + itemBuilder: (context, index) { + final cardData = state.data?.data?[index]; + return cardData != null + ? _Card.fromData( + cardData, + onLike: (title, isLiked) => _showSnackbar(context, title, isLiked), + onTap: () => _navToDetails(context, cardData), ) - : const CircularProgressIndicator(), - ), - ), + : const SizedBox.shrink(); + }, + ), + ); + }, ), - ) - ], + ), + BlocBuilder( + builder: (context, state) { + if (state.isPaginationLoading) { + return const Padding( + padding: EdgeInsets.all(8.0), + child: CircularProgressIndicator(), + ); + } + return const SizedBox.shrink(); + }, + ), + ], + ), + ); + } - )); - }} + @override + void dispose() { + searchController.dispose(); + scrollController.dispose(); + super.dispose(); + } +} +class HomeData { + final List? data; + final int? nextPage; + + HomeData({this.data, this.nextPage}); +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index b31ba09..e2389c3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pmd_labs/data/repositories/album_repository.dart'; import 'package:pmd_labs/home_page/home_page.dart'; +import 'home_page/bloc/home_bloc.dart'; +import 'home_page/home_page.dart'; + void main() { runApp(const MyApp()); } @@ -12,7 +17,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'Лабы ПМУ', theme: ThemeData( textTheme: const TextTheme( headlineLarge: TextStyle(fontFamily: 'Correction_Tape'), @@ -23,7 +28,15 @@ class MyApp extends StatelessWidget { colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), - home: const MyHomePage(title: 'Catalog of Music Albums'), + home: RepositoryProvider( + lazy: true, + create: (_) => AlbumRepository(), + child: BlocProvider( + lazy: false, + create: (context) => HomeBloc(context.read()), + child: const MyHomePage(title: 'Catalog of Music Albums'), + ), + ) ); } } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 330b067..042ca33 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -38,6 +38,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: @@ -198,6 +206,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: @@ -227,6 +243,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: @@ -400,6 +424,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: @@ -432,6 +464,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 5ccf91b..04e25c1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,8 @@ dependencies: pretty_dio_logger: ^1.3.1 http: ^0.13.3 uuid: ^3.0.5 + equatable: ^2.0.7 + flutter_bloc: ^8.1.6 dev_dependencies: flutter_test: