4-5 labs
This commit is contained in:
parent
42c2570fff
commit
36efeceab0
32
lib/data/dtos/characters_dto.dart
Normal file
32
lib/data/dtos/characters_dto.dart
Normal file
@ -0,0 +1,32 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'characters_dto.g.dart';
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class CharactersDto {
|
||||
final List<CharacterDataDto>? data;
|
||||
|
||||
const CharactersDto({this.data});
|
||||
factory CharactersDto.fromJson(Map<String, dynamic> json) => _$CharactersDtoFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class CharacterDataDto {
|
||||
final String? id;
|
||||
final String? type;
|
||||
final CharacterAttributesDataDto? attributes;
|
||||
|
||||
const CharacterDataDto({this.id, this.type, this.attributes});
|
||||
factory CharacterDataDto.fromJson(Map<String, dynamic> json) => _$CharacterDataDtoFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class CharacterAttributesDataDto{
|
||||
final String? name;
|
||||
final String? born;
|
||||
final String? died;
|
||||
final String? image;
|
||||
|
||||
const CharacterAttributesDataDto({this.name, this.born, this.died, this.image});
|
||||
factory CharacterAttributesDataDto.fromJson(Map<String, dynamic> json) => _$CharacterAttributesDataDtoFromJson(json);
|
||||
}
|
33
lib/data/dtos/characters_dto.g.dart
Normal file
33
lib/data/dtos/characters_dto.g.dart
Normal file
@ -0,0 +1,33 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'characters_dto.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
CharactersDto _$CharactersDtoFromJson(Map<String, dynamic> json) =>
|
||||
CharactersDto(
|
||||
data: (json['data'] as List<dynamic>?)
|
||||
?.map((e) => CharacterDataDto.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
CharacterDataDto _$CharacterDataDtoFromJson(Map<String, dynamic> json) =>
|
||||
CharacterDataDto(
|
||||
id: json['id'] as String?,
|
||||
type: json['type'] as String?,
|
||||
attributes: json['attributes'] == null
|
||||
? null
|
||||
: CharacterAttributesDataDto.fromJson(
|
||||
json['attributes'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
CharacterAttributesDataDto _$CharacterAttributesDataDtoFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
CharacterAttributesDataDto(
|
||||
name: json['name'] as String?,
|
||||
born: json['born'] as String?,
|
||||
died: json['died'] as String?,
|
||||
image: json['image'] as String?,
|
||||
);
|
26
lib/data/dtos/cocktailss_dto.dart
Normal file
26
lib/data/dtos/cocktailss_dto.dart
Normal file
@ -0,0 +1,26 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'cocktailss_dto.g.dart';
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class CocktailssDto {
|
||||
final List<CocktailDataDto>? drinks;
|
||||
|
||||
const CocktailssDto({this.drinks});
|
||||
factory CocktailssDto.fromJson(Map<String, dynamic> json) => _$CocktailssDtoFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class CocktailDataDto {
|
||||
final String? idDrink;
|
||||
final String? strDrink;
|
||||
final String? strDrinkThumb;
|
||||
|
||||
const CocktailDataDto({
|
||||
this.idDrink,
|
||||
this.strDrink,
|
||||
this.strDrinkThumb,
|
||||
});
|
||||
|
||||
factory CocktailDataDto.fromJson(Map<String, dynamic> json) => _$CocktailDataDtoFromJson(json);
|
||||
}
|
21
lib/data/dtos/cocktailss_dto.g.dart
Normal file
21
lib/data/dtos/cocktailss_dto.g.dart
Normal file
@ -0,0 +1,21 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'cocktailss_dto.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
CocktailssDto _$CocktailssDtoFromJson(Map<String, dynamic> json) =>
|
||||
CocktailssDto(
|
||||
drinks: (json['drinks'] as List<dynamic>?)
|
||||
?.map((e) => CocktailDataDto.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
CocktailDataDto _$CocktailDataDtoFromJson(Map<String, dynamic> json) =>
|
||||
CocktailDataDto(
|
||||
idDrink: json['idDrink'] as String?,
|
||||
strDrink: json['strDrink'] as String?,
|
||||
strDrinkThumb: json['strDrinkThumb'] as String?,
|
||||
);
|
11
lib/data/mappers/characters_mapper.dart
Normal file
11
lib/data/mappers/characters_mapper.dart
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
import 'package:flutter_app/data/dtos/characters_dto.dart';
|
||||
import 'package:flutter_app/domain/models/card.dart';
|
||||
|
||||
extension CharacterDataDtoToModel on CharacterDataDto {
|
||||
CardData toDomain() => CardData(
|
||||
attributes?.name ?? 'UNKNOWN',
|
||||
imageUrl: attributes?.image,
|
||||
article: '${attributes?.born} - ${attributes?.died}',
|
||||
);
|
||||
}
|
10
lib/data/mappers/сocktail_mapper.dart
Normal file
10
lib/data/mappers/сocktail_mapper.dart
Normal file
@ -0,0 +1,10 @@
|
||||
import 'package:flutter_app/data/dtos/cocktailss_dto.dart';
|
||||
import 'package:flutter_app/domain/models/card.dart';
|
||||
|
||||
extension CocktailDataDtoToModel on CocktailDataDto {
|
||||
CardData toDomain() => CardData(
|
||||
strDrink ?? 'UNKNOWN',
|
||||
article: idDrink ?? 'No ID',
|
||||
imageUrl: strDrinkThumb,
|
||||
);
|
||||
}
|
@ -3,18 +3,12 @@ import 'package:flutter/cupertino.dart';
|
||||
class CardData {
|
||||
final String text;
|
||||
final String article;
|
||||
final IconData icon;
|
||||
final String? imageUrl;
|
||||
final String color;
|
||||
final String price;
|
||||
|
||||
|
||||
CardData(
|
||||
this.text, {
|
||||
required this.article,
|
||||
required this.icon,
|
||||
this.imageUrl,
|
||||
required this.color,
|
||||
required this.price,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,14 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_app/domain/models/card.dart';
|
||||
|
||||
class DetailsPage extends StatelessWidget {
|
||||
final CardData data;
|
||||
final double randomPrice;
|
||||
|
||||
const DetailsPage(this.data, {super.key});
|
||||
DetailsPage(this.data, {super.key})
|
||||
: randomPrice = 10 + Random().nextDouble() * 30;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -16,7 +19,9 @@ class DetailsPage extends StatelessWidget {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Image.network(data.imageUrl ?? ' ',),
|
||||
child: Image.network(
|
||||
data.imageUrl ?? ' ',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 10.0, bottom: 4.0),
|
||||
@ -29,23 +34,12 @@ class DetailsPage extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Артикул: ' + data.article,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Цена: ' + data.price,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Цвет: ' + data.color,
|
||||
'Price: \$${randomPrice.toStringAsFixed(2)}',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -54,3 +48,5 @@ class DetailsPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -5,18 +5,14 @@ typedef OnLikeCallback = void Function(String title, bool isLiked)?;
|
||||
class _Card extends StatefulWidget {
|
||||
final String text;
|
||||
final String article;
|
||||
final IconData icon;
|
||||
final String? imageUrl;
|
||||
final String price;
|
||||
final OnLikeCallback onLike;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const _Card(
|
||||
this.text, {
|
||||
this.icon = Icons.ac_unit_outlined,
|
||||
required this.article,
|
||||
this.imageUrl,
|
||||
required this.price,
|
||||
this.onLike,
|
||||
this.onTap,
|
||||
});
|
||||
@ -30,9 +26,7 @@ class _Card extends StatefulWidget {
|
||||
_Card(
|
||||
data.text,
|
||||
article: data.article,
|
||||
icon: data.icon,
|
||||
imageUrl: data.imageUrl,
|
||||
price: data.price,
|
||||
onLike: onLike,
|
||||
onTap: onTap,
|
||||
);
|
||||
@ -83,10 +77,10 @@ class _CardState extends State<_Card> {
|
||||
style: Theme.of(context).textTheme.headlineLarge,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
/*Text(
|
||||
widget.price,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
),*/
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -1,9 +1,16 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_app/repositories/mock_repository.dart';
|
||||
|
||||
import '../../domain/models/card.dart';
|
||||
import '../../repositories/CocktailRepository.dart';
|
||||
import '../../repositories/potter_repository.dart';
|
||||
import '../details_page/details_page.dart';
|
||||
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
|
||||
part 'card.dart';
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
@ -35,78 +42,94 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
class Body extends StatelessWidget {
|
||||
class Body extends StatefulWidget {
|
||||
const Body({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final data = [
|
||||
CardData(
|
||||
'Свитер',
|
||||
article: '4359358830',
|
||||
icon: Icons.hail,
|
||||
imageUrl: 'https://imgcdn.loverepublic.ru/upload/images/43593/4359358830_55.jpg',
|
||||
color: ' черный',
|
||||
price: '6 999 ₽',
|
||||
),
|
||||
CardData(
|
||||
'Джинсы',
|
||||
article: '4450204304',
|
||||
icon: Icons.hail,
|
||||
imageUrl: 'https://imgcdn.loverepublic.ru/upload/images/44505/4450515115_63.jpg',
|
||||
color: 'синий / индиго',
|
||||
price: '16 999 ₽',
|
||||
),
|
||||
CardData(
|
||||
'Брюки',
|
||||
article: '4450515115',
|
||||
icon: Icons.hail,
|
||||
imageUrl: 'https://imgcdn.loverepublic.ru/upload/images/44502/4450204707_57.jpg',
|
||||
color: 'черный / антрацитовый',
|
||||
price: '5 999 ₽',
|
||||
),
|
||||
_BodyState createState() => _BodyState();
|
||||
}
|
||||
|
||||
];
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: data.map((data) {
|
||||
return _Card.fromData(
|
||||
data,
|
||||
onLike: (String title, bool isLiked) =>
|
||||
_showSnackBar(context, title, isLiked),
|
||||
onTap: () => _navToDetails(context, data),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
class _BodyState extends State<Body> {
|
||||
late TextEditingController searchController;
|
||||
late Future<List<CardData>?> data;
|
||||
/*final PotterRepository repo = PotterRepository();*/
|
||||
final CocktailRepository repo = CocktailRepository();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
searchController = TextEditingController();
|
||||
data = repo.loadData(null);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@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,
|
||||
onChanged: (search) {
|
||||
setState(() {
|
||||
data = repo.loadData(search.isNotEmpty ? search : null);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: FutureBuilder<List<CardData>?>(
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _navToDetails (BuildContext context, CardData data) {
|
||||
void _navToDetails(BuildContext context, CardData data) {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(builder: (context) => DetailsPage(data)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
'Racoon $title ${isLiked ? 'liked!' : 'disliked :('}',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
backgroundColor: Colors.orangeAccent,
|
||||
duration: const Duration(seconds: 1),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
39
lib/repositories/CocktailRepository.dart
Normal file
39
lib/repositories/CocktailRepository.dart
Normal file
@ -0,0 +1,39 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_app/domain/models/card.dart';
|
||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||
import 'api_interface.dart';
|
||||
|
||||
class CocktailRepository extends ApiInterface {
|
||||
static final Dio _dio = Dio()
|
||||
..interceptors.add(PrettyDioLogger(
|
||||
requestHeader: true,
|
||||
requestBody: true,
|
||||
));
|
||||
|
||||
static const String _baseUrl = 'https://www.thecocktaildb.com/api/json/v1/1';
|
||||
|
||||
@override
|
||||
Future<List<CardData>?> loadData(String? q) async {
|
||||
try {
|
||||
final String url = q != null
|
||||
? '$_baseUrl/search.php?s=$q' // Поиск по названию
|
||||
: '$_baseUrl/filter.php?c=Ordinary_Drink'; // Получение списка коктейлей по категории
|
||||
|
||||
final Response<dynamic> response = await _dio.get<Map<dynamic, dynamic>>(url);
|
||||
|
||||
if (response.data != null && response.data['drinks'] != null) {
|
||||
final List<CardData> data = (response.data['drinks'] as List).map((drink) {
|
||||
return CardData(
|
||||
drink['strDrink'], // Название коктейля
|
||||
article: drink['idDrink'], // ID коктейля
|
||||
imageUrl: drink['strDrinkThumb'], // Фото коктейля
|
||||
);
|
||||
}).toList();
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
} on DioException catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
5
lib/repositories/api_interface.dart
Normal file
5
lib/repositories/api_interface.dart
Normal file
@ -0,0 +1,5 @@
|
||||
import 'package:flutter_app/domain/models/card.dart';
|
||||
|
||||
abstract class ApiInterface {
|
||||
Future<List<CardData>?> loadData(String? q);
|
||||
}
|
34
lib/repositories/mock_repository.dart
Normal file
34
lib/repositories/mock_repository.dart
Normal file
@ -0,0 +1,34 @@
|
||||
import 'package:flutter_app/domain/models/card.dart';
|
||||
import 'package:flutter_app/repositories/api_interface.dart';
|
||||
|
||||
class MockRepository extends ApiInterface {
|
||||
@override
|
||||
Future<List<CardData>?> loadData(String? q) async {
|
||||
return [
|
||||
CardData(
|
||||
'Свитер',
|
||||
article: '4359358830',
|
||||
/*icon: Icons.hail,*/
|
||||
imageUrl: 'https://imgcdn.loverepublic.ru/upload/images/43593/4359358830_55.jpg',
|
||||
/*color: ' черный',
|
||||
price: '6 999 ₽',*/
|
||||
),
|
||||
CardData(
|
||||
'Джинсы',
|
||||
article: '4450204304',
|
||||
/*icon: Icons.hail,*/
|
||||
imageUrl: 'https://imgcdn.loverepublic.ru/upload/images/44505/4450515115_63.jpg',
|
||||
/*color: 'синий / индиго',
|
||||
price: '16 999 ₽',*/
|
||||
),
|
||||
CardData(
|
||||
'Брюки',
|
||||
article: '4450515115',
|
||||
/*icon: Icons.hail,*/
|
||||
imageUrl: 'https://imgcdn.loverepublic.ru/upload/images/44502/4450204707_57.jpg',
|
||||
/*color: 'черный / антрацитовый',
|
||||
price: '5 999 ₽',*/
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
36
lib/repositories/potter_repository.dart
Normal file
36
lib/repositories/potter_repository.dart
Normal file
@ -0,0 +1,36 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_app/data/mappers/characters_mapper.dart';
|
||||
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
|
||||
|
||||
import '../data/dtos/characters_dto.dart';
|
||||
import '../domain/models/card.dart';
|
||||
import 'api_interface.dart';
|
||||
|
||||
class PotterRepository extends ApiInterface {
|
||||
static final Dio _dio = Dio()
|
||||
..interceptors.add(PrettyDioLogger(
|
||||
requestHeader: true,
|
||||
requestBody: true,
|
||||
));
|
||||
|
||||
static const String _baseUrl = 'https://api.potterdb.com';
|
||||
|
||||
@override
|
||||
Future<List<CardData>?> loadData(String? q) async {
|
||||
try {
|
||||
const String url = '$_baseUrl/v1/characters';
|
||||
|
||||
final Response<dynamic> response = await _dio.get<Map<dynamic, dynamic>>(
|
||||
url,
|
||||
queryParameters: q != null ? {"filter[name_cont]": q} : null,
|
||||
);
|
||||
|
||||
final CharactersDto dto = CharactersDto.fromJson(
|
||||
response.data as Map<String, dynamic>);
|
||||
final List<CardData>? data = dto.data?.map((e) => e.toDomain()).toList();
|
||||
return data;
|
||||
} on DioException catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
24
pubspec.lock
24
pubspec.lock
@ -182,6 +182,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.7"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.7.0"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -400,6 +416,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
pretty_dio_logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pretty_dio_logger
|
||||
sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -35,8 +35,9 @@ dependencies:
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
|
||||
json_annotation: ^4.8.1
|
||||
json_annotation: ^4.9.0
|
||||
dio: ^5.4.2+1
|
||||
pretty_dio_logger: ^1.3.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Loading…
Reference in New Issue
Block a user