вроде 5 готова

This commit is contained in:
Галина Федоренко 2024-11-04 16:34:24 +04:00
parent f50f089d2a
commit d64abe5ff6
11 changed files with 154 additions and 192 deletions

View File

@ -1,41 +0,0 @@
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) {
print('CharacterDataDto.fromJson: $json');
return _$CharacterDataDtoFromJson(json);
}
}
@JsonSerializable(createToJson: false)
class CharacterAttributesDataDto {
final String? fullName;
final String? title;
final String? family;
final String? imageUrl;
const CharacterAttributesDataDto({this.fullName, this.title, this.family, this.imageUrl});
factory CharacterAttributesDataDto.fromJson(Map<String, dynamic> json) {
print('CharacterAttributesDataDto.fromJson: $json');
return _$CharacterAttributesDataDtoFromJson(json);
}
}

View File

@ -1,33 +0,0 @@
// 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(
fullName: json['fullName'] as String?,
title: json['title'] as String?,
family: json['family'] as String?,
imageUrl: json['imageUrl'] as String?,
);

View File

@ -4,7 +4,7 @@ part 'thrones_character_dto.g.dart';
@JsonSerializable(createToJson: false) @JsonSerializable(createToJson: false)
class ThronesCharacterDto { class ThronesCharacterDto {
final String id; final int id;
final String firstName; final String firstName;
final String lastName; final String lastName;
final String fullName; final String fullName;

View File

@ -8,7 +8,7 @@ part of 'thrones_character_dto.dart';
ThronesCharacterDto _$ThronesCharacterDtoFromJson(Map<String, dynamic> json) => ThronesCharacterDto _$ThronesCharacterDtoFromJson(Map<String, dynamic> json) =>
ThronesCharacterDto( ThronesCharacterDto(
id: json['id'] as String, id: (json['id'] as num).toInt(),
firstName: json['firstName'] as String, firstName: json['firstName'] as String,
lastName: json['lastName'] as String, lastName: json['lastName'] as String,
fullName: json['fullName'] as String, fullName: json['fullName'] as String,

View File

@ -6,9 +6,14 @@ const _imagePlaceholder =
extension ThronesCharacterDtoToModel on ThronesCharacterDto { extension ThronesCharacterDtoToModel on ThronesCharacterDto {
CardData toDomain() => CardData( CardData toDomain() => CardData(
fullName,
imageUrl: imageUrl,
descriptionText: _makeDescriptionText(title, family), descriptionText: _makeDescriptionText(title, family),
imageUrl: imageUrl,
firstName: firstName,
lastName: lastName,
title: title,
family: family,
fullName: fullName,
text: '',
); );
String _makeDescriptionText(String? title, String? family) { String _makeDescriptionText(String? title, String? family) {

View File

@ -3,14 +3,9 @@ import 'package:flutter_app/data/dtos/thrones_character_dto.dart';
import 'package:flutter_app/data/mappers/characters_mapper.dart'; import 'package:flutter_app/data/mappers/characters_mapper.dart';
import 'package:flutter_app/data/repositories/api_interface.dart'; import 'package:flutter_app/data/repositories/api_interface.dart';
import 'package:flutter_app/domain/models/card.dart'; import 'package:flutter_app/domain/models/card.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
class ThronesRepository extends ApiInterface { class ThronesRepository extends ApiInterface {
static final Dio _dio = Dio() static final Dio _dio = Dio();
..interceptors.add(PrettyDioLogger(
requestHeader: true,
requestBody: true,
));
static const String _baseUrl = 'https://thronesapi.com'; static const String _baseUrl = 'https://thronesapi.com';
@ -29,8 +24,10 @@ class ThronesRepository extends ApiInterface {
.toList(); .toList();
final List<CardData> data = characters final List<CardData> data = characters
.where((character) => q == null || character.fullName.toLowerCase().contains(q.toLowerCase()))
.map((e) => e.toDomain()) .map((e) => e.toDomain())
.toList(); .toList();
return data; return data;
} on DioException catch (e) { } on DioException catch (e) {
onError?.call(e.response?.statusMessage); onError?.call(e.response?.statusMessage);

View File

@ -7,60 +7,68 @@ class MockRepository extends ApiInterface {
Future<List<CardData>?> loadData({OnErrorCallback? onError}) async { Future<List<CardData>?> loadData({OnErrorCallback? onError}) async {
return [ return [
CardData( CardData(
'Daenerys Targaryen', fullName: 'Daenerys Targaryen',
descriptionText: 'Mother of Dragons', descriptionText: 'Mother of Dragons',
icon: Icons.favorite, icon: Icons.favorite,
imageUrl: imageUrl:
'https://thronesapi.com/assets/images/daenerys.jpg', 'https://thronesapi.com/assets/images/daenerys.jpg',
text: '', firstName: '', lastName: '', title: '', family: '',
), ),
CardData( CardData(
'Samwell Tarly', fullName: 'Samwell Tarly',
descriptionText: 'Maester', descriptionText: 'Maester',
icon: Icons.favorite, icon: Icons.favorite,
imageUrl: imageUrl:
'https://thronesapi.com/assets/images/sam.jpg', 'https://thronesapi.com/assets/images/sam.jpg',
text: '', firstName: '', lastName: '', title: '', family: '',
), ),
CardData( CardData(
'Jon Snow', fullName: 'Jon Snow',
descriptionText: 'King of the North', descriptionText: 'King of the North',
icon: Icons.favorite, icon: Icons.favorite,
imageUrl: imageUrl:
'https://thronesapi.com/assets/images/jon-snow.jpg', 'https://thronesapi.com/assets/images/jon-snow.jpg',
text: '', firstName: '', lastName: '', title: '', family: '',
), ),
CardData( CardData(
'Arya Stark', fullName: 'Arya Stark',
descriptionText: 'No One', descriptionText: 'No One',
icon: Icons.favorite, icon: Icons.favorite,
imageUrl: imageUrl:
'https://thronesapi.com/assets/images/arya-stark.jpg', 'https://thronesapi.com/assets/images/arya-stark.jpg',
text: '', firstName: '', lastName: '', title: '', family: '',
), ),
CardData( CardData(
'Sansa Stark', fullName: 'Sansa Stark',
descriptionText: 'Lady of Winterfell', descriptionText: 'Lady of Winterfell',
icon: Icons.favorite, icon: Icons.favorite,
imageUrl: imageUrl:
'https://thronesapi.com/assets/images/sansa-stark.jpeg', 'https://thronesapi.com/assets/images/sansa-stark.jpeg',
text: '', firstName: '', lastName: '', title: '', family: '',
), ),
CardData( CardData(
'Brandon Stark', fullName: 'Brandon Stark',
descriptionText: 'Lord of Winterfell', descriptionText: 'Lord of Winterfell',
icon: Icons.favorite, icon: Icons.favorite,
imageUrl: imageUrl:
'https://thronesapi.com/assets/images/bran-stark.jpg', 'https://thronesapi.com/assets/images/bran-stark.jpg',
text: '', firstName: '', lastName: '', title: '', family: '',
), ),
CardData( CardData(
'Ned Stark', fullName: 'Ned Stark',
descriptionText: 'Lord of Winterfell', descriptionText: 'Lord of Winterfell',
icon: Icons.favorite, icon: Icons.favorite,
imageUrl: imageUrl:
'https://thronesapi.com/assets/images/ned-stark.jpg', 'https://thronesapi.com/assets/images/ned-stark.jpg',
text: '', firstName: '', lastName: '', title: '', family: '',
), ),
CardData( CardData(
'Robert Baratheon', fullName: 'Robert Baratheon',
descriptionText: 'Lord of the Seven Kingdoms', descriptionText: 'Lord of the Seven Kingdoms',
icon: Icons.favorite, icon: Icons.favorite,
imageUrl: imageUrl:
'https://thronesapi.com/assets/images/robert-baratheon.jpeg', 'https://thronesapi.com/assets/images/robert-baratheon.jpeg',
text: '', firstName: '', lastName: '', title: '', family: '',
), ),
]; ];
} }

View File

@ -5,11 +5,21 @@ class CardData {
final String descriptionText; final String descriptionText;
final IconData icon; final IconData icon;
final String? imageUrl; final String? imageUrl;
final String firstName;
final String lastName;
final String fullName;
final String title;
final String family;
CardData( CardData({
this.text, { required this.text,
required this.descriptionText, required this.descriptionText,
this.icon = Icons.catching_pokemon, this.icon = Icons.people,
this.imageUrl, this.imageUrl,
}); required this.firstName,
required this.lastName,
required this.fullName,
required this.title,
required this.family,
});
} }

View File

@ -11,9 +11,10 @@ class DetailsPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(), appBar: AppBar(),
body: Padding( body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -32,11 +33,11 @@ class DetailsPage extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.only(bottom: 4.0), padding: const EdgeInsets.only(bottom: 4.0),
child: Text( child: Text(
data.text, data.fullName,
style: Theme.of(context).textTheme.bodyLarge?.copyWith( style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontSize: 27.0, fontSize: 27.0,
fontFamily: 'Times New Roman', fontFamily: 'Times New Roman',
), ),
), ),
), ),
Text( Text(
@ -45,10 +46,32 @@ class DetailsPage extends StatelessWidget {
fontSize: 20.0, fontSize: 20.0,
fontFamily: 'Times New Roman', fontFamily: 'Times New Roman',
), ),
) ),
const SizedBox(height: 16.0),
Text(
'First Name: ${data.firstName}',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8.0),
Text(
'Last Name: ${data.lastName}',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8.0),
Text(
'Title: ${data.title}',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8.0),
Text(
'Family: ${data.family}',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 8.0),
], ],
), ),
) ),
),
); );
} }
} }

View File

@ -5,6 +5,7 @@ typedef OnLikeCallback = void Function(String title, bool isLiked)?;
class _Card extends StatefulWidget { class _Card extends StatefulWidget {
final String text; final String text;
final String descriptionText; final String descriptionText;
final String fullName;
final IconData icon; final IconData icon;
final String? imageUrl; final String? imageUrl;
final OnLikeCallback onLike; final OnLikeCallback onLike;
@ -14,6 +15,7 @@ class _Card extends StatefulWidget {
this.text, { this.text, {
this.icon = Icons.catching_pokemon, this.icon = Icons.catching_pokemon,
required this.descriptionText, required this.descriptionText,
required this.fullName,
this.imageUrl, this.imageUrl,
this.onLike, this.onLike,
this.onTap, this.onTap,
@ -27,6 +29,7 @@ class _Card extends StatefulWidget {
_Card( _Card(
data.text, data.text,
descriptionText: data.descriptionText, descriptionText: data.descriptionText,
fullName: data.fullName,
icon: data.icon, icon: data.icon,
imageUrl: data.imageUrl, imageUrl: data.imageUrl,
onLike: onLike, onLike: onLike,
@ -44,92 +47,74 @@ class _CardState extends State<_Card> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: widget.onTap, onTap: widget.onTap,
child: Container( child: Card(
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(8.0),
constraints: const BoxConstraints(minHeight: 140), child: Column(
decoration: BoxDecoration( crossAxisAlignment: CrossAxisAlignment.start,
color: Colors.white70, children: [
borderRadius: BorderRadius.circular(20), ClipRRect(
border: Border.all(color: Colors.grey.shade200), borderRadius: BorderRadius.circular(15),
boxShadow: [ child: SizedBox(
BoxShadow( height: 250,
color: Colors.grey.withOpacity(.5), width: double.infinity,
spreadRadius: 4, child: Image.network(
offset: const Offset(0, 5), widget.imageUrl ?? '',
blurRadius: 8, fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Placeholder(),
),
),
), ),
], Padding(
), padding: const EdgeInsets.all(8.0),
child: IntrinsicHeight( child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Text(
ClipRRect( widget.fullName,
borderRadius: const BorderRadius.only( style: Theme.of(context).textTheme.bodyLarge?.copyWith(
bottomLeft: Radius.circular(20), fontSize: 20.0,
topLeft: Radius.circular(20), fontFamily: 'Times New Roman',
),
child: SizedBox(
height: double.infinity,
width: 160,
child: Image.network(
widget.imageUrl ?? '',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Placeholder(),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.text,
style: Theme.of(context).textTheme.headlineLarge,
),
Text(
widget.descriptionText,
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
),
),
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 16.0,
bottom: 16.0,
),
child: GestureDetector(
onTap: () {
setState(() {
isLiked = !isLiked;
});
widget.onLike?.call(widget.text, isLiked);
},
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: isLiked
? const Icon(
Icons.favorite,
color: Colors.redAccent,
key: ValueKey<int>(0),
)
: const Icon(
Icons.favorite_border,
key: ValueKey<int>(1),
),
), ),
), ),
Text(
widget.descriptionText,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 16.0,
bottom: 16.0,
),
child: GestureDetector(
onTap: () {
setState(() {
isLiked = !isLiked;
});
widget.onLike?.call(widget.fullName, isLiked);
},
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: isLiked
? const Icon(
Icons.favorite,
color: Colors.redAccent,
key: ValueKey<int>(0),
)
: const Icon(
Icons.favorite_border,
key: ValueKey<int>(1),
),
),
), ),
), ),
], ),
), ],
), ),
), ),
); );

View File

@ -30,7 +30,7 @@ class Body extends StatefulWidget {
} }
class _BodyState extends State<Body> { class _BodyState extends State<Body> {
final searchController = TextEditingController(); late TextEditingController searchController;
late Future<List<CardData>?> data; late Future<List<CardData>?> data;
final repo = ThronesRepository(); final repo = ThronesRepository();
@ -38,7 +38,14 @@ class _BodyState extends State<Body> {
@override @override
void initState() { void initState() {
data = repo.loadData(); data = repo.loadData();
super.initState(); searchController = TextEditingController();
data = repo.loadData();
}
@override
void dispose() {
searchController.dispose();
super.dispose();
} }
@override @override
@ -75,6 +82,7 @@ class _BodyState extends State<Body> {
); );
} else { } else {
return ListView.builder( return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data!.length, itemCount: snapshot.data!.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final cardData = snapshot.data![index]; final cardData = snapshot.data![index];
@ -90,7 +98,7 @@ class _BodyState extends State<Body> {
}, },
), ),
), ),
), )
], ],
), ),
); );