Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update pokeapi example to riverpod v2 #99

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.freezed.dart linguist-generated=true
*.g.dart linguist-generated=true
13 changes: 3 additions & 10 deletions example/pokeapi_functional/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,6 @@ A new Flutter project.

## Getting Started

This project is a starting point for a Flutter application.

A few resources to get you started if this is your first Flutter project:

- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)

For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
- `flutter pub get`
- `flutter pub run build_runner build`
- `flutter run -d chrome`
5 changes: 5 additions & 0 deletions example/pokeapi_functional/lib/api/fetch_pokemon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,8 @@ TaskEither<String, Pokemon> fetchPokemon(int pokemonId) => TaskEither.tryCatch(
/// All the functions are simply chained together following the principle of composability.
TaskEither<String, Pokemon> fetchPokemonFromUserInput(String pokemonId) =>
_validateUserPokemonId(pokemonId).flatMapTask(fetchPokemon);

TaskEither<String, Pokemon> fetchRandomPokemon() => randomInt(
Constants.minimumPokemonId,
Constants.maximumPokemonId + 1,
).toIOEither<String>().flatMapTask(fetchPokemon);
40 changes: 13 additions & 27 deletions example/pokeapi_functional/lib/controllers/pokemon_provider.dart
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
import 'package:fpdart/fpdart.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:pokeapi_functional/api/fetch_pokemon.dart';
import 'package:pokeapi_functional/constants/constants.dart';
import 'package:pokeapi_functional/models/pokemon.dart';
import 'package:pokeapi_functional/unions/request_status.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

/// Manage the [Pokemon] state using [Either] ([TaskEither]) to handle possible errors.
///
/// Each [RequestStatus] changes the UI displayed to the user.
class PokemonState extends StateNotifier<RequestStatus> {
PokemonState() : super(const RequestStatus.initial());
import '../api/fetch_pokemon.dart';
import '../models/pokemon.dart';

/// Initial request, fetch random pokemon passing the pokemon id.
Future<Unit> fetchRandom() async => _pokemonRequest(
() => fetchPokemon(
randomInt(
Constants.minimumPokemonId,
Constants.maximumPokemonId + 1,
).run(),
),
);
part 'pokemon_provider.g.dart';

@riverpod
class PokemonState extends _$PokemonState {
@override
FutureOr<Pokemon> build() async =>
fetchRandomPokemon().getOrElse((l) => throw Exception(l)).run();

/// User request, try to convert user input to [int] and then
/// request the pokemon if successful.
Expand All @@ -31,17 +22,12 @@ class PokemonState extends StateNotifier<RequestStatus> {
Future<Unit> _pokemonRequest(
TaskEither<String, Pokemon> Function() request,
) async {
state = RequestStatus.loading();
state = AsyncLoading();
final pokemon = request();
state = (await pokemon.run()).match(
(error) => RequestStatus.error(error),
(pokemon) => RequestStatus.success(pokemon),
(error) => AsyncError(error, StackTrace.current),
(pokemon) => AsyncData(pokemon),
);
return unit;
}
}

/// Create and expose provider.
final pokemonProvider = StateNotifierProvider<PokemonState, RequestStatus>(
(_) => PokemonState(),
);
42 changes: 11 additions & 31 deletions example/pokeapi_functional/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_hooks/flutter_hooks.dart' show useTextEditingController;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:pokeapi_functional/controllers/pokemon_provider.dart';

Expand All @@ -13,14 +13,8 @@ class MyApp extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
/// [TextEditingController] using hooks
final controller = useTextEditingController();
final requestStatus = ref.watch(pokemonProvider);
useEffect(() {
/// Fetch the initial pokemon information (random pokemon).
Future.delayed(Duration.zero, () {
ref.read(pokemonProvider.notifier).fetchRandom();
SandroMaglione marked this conversation as resolved.
Show resolved Hide resolved
});
return;
}, []);
final requestStatus = ref.watch(pokemonStateProvider);
final pokemonNotifier = ref.watch(pokemonStateProvider.notifier);

return MaterialApp(
title: 'Fpdart PokeAPI',
Expand All @@ -29,45 +23,31 @@ class MyApp extends HookConsumerWidget {
children: [
/// [TextField] and [ElevatedButton] to input pokemon id to fetch
TextField(
textInputAction: TextInputAction.next,
textAlign: TextAlign.center,
controller: controller,
decoration: InputDecoration(
hintText: 'Insert pokemon id number',
),
),
ElevatedButton(
onPressed: () => ref
.read(
pokemonProvider.notifier,
)
.fetch(
controller.text,
),
onPressed: () => pokemonNotifier.fetch(controller.text),
child: Text('Get my pokemon!'),
),

/// Map each [RequestStatus] to a different UI
/// Map each [AsyncValue] to a different UI
requestStatus.when(
initial: () => Center(
child: Column(
children: [
Text('Loading intial pokemon'),
CircularProgressIndicator(),
],
),
),
loading: () => Center(
child: CircularProgressIndicator(),
),
loading: () => Center(child: CircularProgressIndicator()),

/// When either is [Left], display error message 💥
error: (error) => Text(error),
error: (error, stackTrace) => Text(error.toString()),

/// When either is [Right], display pokemon 🤩
success: (pokemon) => Card(
data: (pokemon) => Card(
child: Column(
children: [
Image.network(
pokemon.sprites.front_default,
pokemon.sprites.frontDefault,
width: 200,
height: 200,
),
Expand Down
38 changes: 15 additions & 23 deletions example/pokeapi_functional/lib/models/pokemon.dart
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
import 'package:pokeapi_functional/models/sprite.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

/// Pokemon information, with method to deserialize json
class Pokemon {
final int id;
final String name;
final int height;
final int weight;
final Sprite sprites;
part 'pokemon.freezed.dart';
part 'pokemon.g.dart';

const Pokemon({
required this.id,
required this.name,
required this.height,
required this.weight,
required this.sprites,
});
/// Pokemon information, with method to deserialize json
@freezed
class Pokemon with _$Pokemon {
const factory Pokemon({
required int id,
required String name,
required int height,
required int weight,
required Sprite sprites,
}) = _Pokemon;

static Pokemon fromJson(Map<String, dynamic> json) {
return Pokemon(
id: json['id'] as int,
name: json['name'] as String,
weight: json['weight'] as int,
height: json['height'] as int,
sprites: Sprite.fromJson(json['sprites'] as Map<String, dynamic>),
);
}
factory Pokemon.fromJson(Map<String, Object?> json) =>
_$PokemonFromJson(json);
}
Loading