diff --git a/lib/main.dart b/lib/main.dart index 975c592..2130a8d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_basics/sample_app/sample_app_screen.dart'; +import 'package:flutter_basics/sample_app/sample_app.dart'; void main() { runApp(const MainApp()); diff --git a/lib/sample_app/bloc/sample_bloc.dart b/lib/sample_app/bloc/sample_bloc.dart index 72998f2..881a5d2 100644 --- a/lib/sample_app/bloc/sample_bloc.dart +++ b/lib/sample_app/bloc/sample_bloc.dart @@ -1,8 +1,10 @@ import 'dart:async'; +import 'dart:convert'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter_basics/sample_app/sample_app_screen.dart'; +import 'package:flutter_basics/sample_app/sample_app.dart'; +import 'package:http/http.dart'; import 'package:meta/meta.dart'; part 'sample_event.dart'; @@ -14,17 +16,24 @@ class SampleBloc extends Bloc { } FutureOr _fetchData(FetchData event, Emitter emit) async { - await Future.delayed(const Duration(seconds: 1)); - final list = []; - for (var i = 0; i < 5; i++) { - list.add( - MyItem( - name: 'Name $i', - description: 'Description $i', - url: 'https://picsum.photos/id/$i/200', - ), - ); + final httpClient = Client(); + const baseUrl = 'rickandmortyapi.com'; + const endpoint = 'api/character'; + final url = Uri.https(baseUrl, endpoint); + final response = await httpClient.get(url); + + if (response.statusCode != 200) {} + emit(SampleError()); + try { + final charactersJson = jsonDecode(response.body) as Map; + final characters = (charactersJson['results'] as List) + .map( + (dynamic e) => Character.fromJson(e as Map), + ) + .toList(); + emit(SampleSucces(items: characters)); + } catch (e) { + emit(SampleError()); } - emit(SampleSucces(items: list)); } } diff --git a/lib/sample_app/bloc/sample_state.dart b/lib/sample_app/bloc/sample_state.dart index 15d276f..a02b057 100644 --- a/lib/sample_app/bloc/sample_state.dart +++ b/lib/sample_app/bloc/sample_state.dart @@ -7,8 +7,10 @@ final class SampleInitial extends SampleState {} final class SampleLoading extends SampleState {} +final class SampleError extends SampleState {} + final class SampleSucces extends SampleState { SampleSucces({required this.items}); - final List items; + final List items; } diff --git a/lib/sample_app/models/character.dart b/lib/sample_app/models/character.dart new file mode 100644 index 0000000..cfd77d9 --- /dev/null +++ b/lib/sample_app/models/character.dart @@ -0,0 +1,52 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'character.g.dart'; + +@JsonSerializable() + +/// [Character] is a character entity fetched from rick and morty api +class Character { + /// Default constructor + const Character({ + required this.id, + required this.name, + required this.image, + required this.species, + required this.status, + }); + + /// Json deserialize + factory Character.fromJson(Map json) => + _$CharacterFromJson(json); + + /// Id of the character + final int id; + + /// Name of the character + final String name; + + /// Image of the character + final String image; + + /// Species of the character + final String species; + + /// Status of the character + final Status status; +} + +/// Status of the character +enum Status { + @JsonValue('Alive') + + /// alive + alive, + @JsonValue('Dead') + + /// dead + dead, + @JsonValue('unknown') + + /// unknown + unknown, +} diff --git a/lib/sample_app/models/character.g.dart b/lib/sample_app/models/character.g.dart new file mode 100644 index 0000000..22e511c --- /dev/null +++ b/lib/sample_app/models/character.g.dart @@ -0,0 +1,58 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: implicit_dynamic_parameter + +part of 'character.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Character _$CharacterFromJson(Map json) { + return $checkedNew('Character', json, () { + final val = Character( + id: $checkedConvert(json, 'id', (v) => v as int), + name: $checkedConvert(json, 'name', (v) => v as String), + image: $checkedConvert(json, 'image', (v) => v as String), + species: $checkedConvert(json, 'species', (v) => v as String), + status: $checkedConvert( + json, + 'status', + (v) => _$enumDecode(_$StatusEnumMap, v), + ), + ); + return val; + }); +} + +K _$enumDecode( + Map enumValues, + Object? source, { + K? unknownValue, +}) { + if (source == null) { + throw ArgumentError( + 'A value must be provided. Supported values: ' + '${enumValues.values.join(', ')}', + ); + } + + return enumValues.entries.singleWhere( + (e) => e.value == source, + orElse: () { + if (unknownValue == null) { + throw ArgumentError( + '`$source` is not one of the supported values: ' + '${enumValues.values.join(', ')}', + ); + } + return MapEntry(unknownValue, enumValues.values.first); + }, + ).key; +} + +const _$StatusEnumMap = { + Status.alive: 'Alive', + Status.dead: 'Dead', + Status.unknown: 'unknown', +}; diff --git a/lib/sample_app/models/models.dart b/lib/sample_app/models/models.dart new file mode 100644 index 0000000..6a238df --- /dev/null +++ b/lib/sample_app/models/models.dart @@ -0,0 +1 @@ +export 'character.dart'; diff --git a/lib/sample_app/sample_app.dart b/lib/sample_app/sample_app.dart new file mode 100644 index 0000000..e87b5ce --- /dev/null +++ b/lib/sample_app/sample_app.dart @@ -0,0 +1,3 @@ +export 'bloc/sample_bloc.dart'; +export 'models/models.dart'; +export 'view/view.dart'; diff --git a/lib/sample_app/sample_app_screen.dart b/lib/sample_app/sample_app_screen.dart deleted file mode 100644 index d3e226c..0000000 --- a/lib/sample_app/sample_app_screen.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_basics/sample_app/bloc/sample_bloc.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SampleAppScreen extends StatelessWidget { - const SampleAppScreen({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => SampleBloc()..add(const FetchData()), - child: const SampleAppScreenView(), - ); - } -} - -class SampleAppScreenView extends StatelessWidget { - const SampleAppScreenView({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('My first app'), - ), - body: BlocBuilder( - builder: (context, state) { - if (state is SampleSucces) { - return ListView.builder( - itemBuilder: (context, index) { - final item = state.items[index]; - return ListTile( - title: Text(item.name, style: const TextStyle(fontSize: 30)), - subtitle: Text( - item.description, - style: const TextStyle(fontSize: 24), - ), - leading: Image.network(item.url), - trailing: const Icon(Icons.arrow_forward_ios), - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) { - return DetailsScreen(item: item); - }, - ), - ); - }, - ); - }, - itemCount: state.items.length, - ); - } - return const Center(child: CircularProgressIndicator()); - }, - ), - ); - } -} - -class DetailsScreen extends StatelessWidget { - const DetailsScreen({required this.item, super.key}); - - final MyItem item; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(item.name), - ), - body: Text(item.description, style: const TextStyle(fontSize: 30)), - ); - } -} - -class MyItem { - MyItem({ - required this.name, - required this.description, - required this.url, - }); - - final String name; - final String description; - final String url; -} diff --git a/lib/sample_app/view/character_list.dart b/lib/sample_app/view/character_list.dart new file mode 100644 index 0000000..f36242f --- /dev/null +++ b/lib/sample_app/view/character_list.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_basics/sample_app/sample_app.dart'; + +class CharacterList extends StatelessWidget { + const CharacterList({required this.characters, super.key}); + + final List characters; + + @override + Widget build(BuildContext context) { + return GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + ), + itemBuilder: (context, index) => CharacterItemView( + character: characters[index], + ), + itemCount: characters.length, + ); + } +} + +class CharacterItemView extends StatelessWidget { + @visibleForTesting + const CharacterItemView({ + required this.character, + super.key, + }); + + final Character character; + + @override + Widget build(BuildContext context) { + return InkWell( + child: Stack( + fit: StackFit.expand, + children: [ + Positioned.fill( + child: Hero( + tag: 'image_hero_${character.name}', + child: FittedBox( + fit: BoxFit.fill, + child: Image.network(character.image), + ), + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + color: Colors.white70, + padding: const EdgeInsets.all(5), + child: Center( + child: Text( + character.name, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 25, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/sample_app/view/sample_app_screen.dart b/lib/sample_app/view/sample_app_screen.dart new file mode 100644 index 0000000..dd64ddf --- /dev/null +++ b/lib/sample_app/view/sample_app_screen.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_basics/sample_app/bloc/sample_bloc.dart'; +import 'package:flutter_basics/sample_app/view/character_list.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SampleAppScreen extends StatelessWidget { + const SampleAppScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SampleBloc()..add(const FetchData()), + child: const SampleAppScreenView(), + ); + } +} + +class SampleAppScreenView extends StatelessWidget { + const SampleAppScreenView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('My first app'), + ), + body: BlocBuilder( + builder: (context, state) { + if (state is SampleSucces) { + return CharacterList(characters: state.items); + } + return const Center(child: CircularProgressIndicator()); + }, + ), + ); + } +} diff --git a/lib/sample_app/view/view.dart b/lib/sample_app/view/view.dart new file mode 100644 index 0000000..73de7da --- /dev/null +++ b/lib/sample_app/view/view.dart @@ -0,0 +1 @@ +export 'sample_app_screen.dart';