1

I'm fairly new to Flutter providers. I use Riverpod.

I have a Future provider that provide some data from a JSON file - in the future it will be from a API response.

import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/pokemon.dart';

final pokemonProvider = FutureProvider<List<Pokemon>>((ref) async {
  var response =
      await rootBundle.loadString('assets/mock_data/pokemons.json');
  List<dynamic> data = jsonDecode(response);
  return List<Pokemon>.from(data.map((i) => Pokemon.fromMap(i)));
});

I subscribe to with ref.watch in ConsumerState widgets, e.g.:

class PokemonsPage extends ConsumerStatefulWidget {
  const PokemonsPage({Key? key}) : super(key: key);
  @override
  ConsumerState<PokemonsPage> createState() => _PokemonsPageState();
}

class _PokemonsPageState extends ConsumerState<PokemonsPage> {
  @override
  Widget build(BuildContext context) {
    final AsyncValue<List<Pokemon>> pokemons =
        ref.watch(pokemonProvider);

    return pokemons.when(
      loading: () => const CircularProgressIndicator(),
      error: (err, stack) => Text('Error: $err'),
      data: (pokemons) {
        return Material(
            child: ListView.builder(
              itemCount: pokemons.length,
              itemBuilder: (context, index) {
                Pokemon pokemon = pokemons[index];
                return ListTile(
                  title: Text(pokemon.name),
                );
              },
        ));
      },
    );
  }
}

But in that case, what is the best practice to write/update data to the JSON file/API?

It seems providers are used for reading/providing data, not updating it, so I'm confused.

Should the same provider pokemonProvider be used for that? If yes, what is the FutureProvider method that should be used and how to call it? If not, what is the best practice?

bolino
  • 867
  • 1
  • 10
  • 27

2 Answers2

2

I am new to riverpod too but I'll try to explain the approach we took.

The examples with FutureProviders calling to apis are a little bit misleading for me, because the provider only offers the content for a single api call, not access to the entire api.

To solve that, we found the Repository Pattern to be very useful. We use the provider to export a class containing the complete api (or a mock one for test purposes), and we control the state (a different object containing the different situations) to manage the responses and updates.

Your example would be something like this:

First we define our state object:

enum PokemonListStatus { none, error, loaded }

class PokemonListState {
  final String? error;
  final List<Pokemon> pokemons;
  final PokemonListStatus status;

  const PokemonListState.loaded(this.pokemons)
      : error = null,
        status = PokemonListStatus.loaded,
        super();

  const PokemonListState.error(this.error)
      : pokemons = const [],
        status = PokemonListStatus.error,
        super();

  const PokemonListState.initial()
      : pokemons = const [],
        error = null,
        status = PokemonListStatus.none,
        super();
}

Now our provider and repository class (abstract is optional, but let's take that approach so you can keep the example for testing):

final pokemonRepositoryProvider =
    StateNotifierProvider<PokemonRepository, PokemonListState>((ref) {
  final pokemonRepository = JsonPokemonRepository(); // Or ApiRepository
  pokemonRepository.getAllPokemon();
  return pokemonRepository;
});

///
/// Define abstract class. Useful for testing
///
abstract class PokemonRepository extends StateNotifier<PokemonListState> {
  PokemonRepository()
      : super(const PokemonListState.initial()); 

  Future<void> getAllPokemon();
  Future<void> addPokemon(Pokemon pk);
}

And the implementation for each repository:

///
/// Class to manage pokemon api
///
class ApiPokemonRepository extends PokemonRepository {
  ApiPokemonRepository() : super();

  Future<void> getAllPokemon() async {
    try {
      // ... calls to API for retrieving pokemon
      // updates cached list with recently obtained data and call watchers.
      state = PokemonListState.loaded( ... );
    } catch (e) {
      state = PokemonListState.error(e.toString());
    }
  }

  Future<void> addPokemon(Pokemon pk) async {
    try {
      // ... calls to API for adding pokemon
      // updates cached list and calls providers watching.
      state = PokemonListState.loaded([...state.pokemons, pk]);
    } catch (e) {
      state = PokemonListState.error(e.toString());
    }
  }
}

and

///
/// Class to manage pokemon local json
///
class JsonPokemonRepository extends PokemonRepository {
  JsonPokemonRepository() : super();

  Future<void> getAllPokemon() async {
    var response =
        await rootBundle.loadString('assets/mock_data/pokemons.json');
    List<dynamic> data = jsonDecode(response);
    // updates cached list with recently obtained data and call watchers.
    final pokemons = List<Pokemon>.from(data.map((i) => Pokemon.fromMap(i)));
    state = PokemonListState.loaded(pokemons);
  }

  Future<void> addPokemon(Pokemon pk) async {
    // ... and write json to disk for example
    // updates cached list and calls providers watching.
    state = PokemonListState.loaded([...state.pokemons, pk]);
  }
}

Then in build, your widget with a few changes:

class PokemonsPage extends ConsumerStatefulWidget {
  const PokemonsPage({Key? key}) : super(key: key);
  @override
  ConsumerState<PokemonsPage> createState() => _PokemonsPageState();
}

class _PokemonsPageState extends ConsumerState<PokemonsPage> {
  @override
  Widget build(BuildContext context) {
    final statePokemons =
        ref.watch(pokemonRepositoryProvider);

    if (statePokemons.status == PokemonListStatus.error) {
      return Text('Error: ${statePokemons.error}');
    } else if (statePokemons.status == PokemonListStatus.none) {
      return const CircularProgressIndicator();
    } else {
      final pokemons = statePokemons.pokemons;
      return Material(
            child: ListView.builder(
              itemCount: pokemons.length,
              itemBuilder: (context, index) {
                Pokemon pokemon = pokemons[index];
                return ListTile(
                  title: Text(pokemon.name),
                );
              },
        ));
    }
  }
}

Not sure if this is the best approach but it is working for us so far.

Koesh
  • 431
  • 5
  • 12
  • Great answer, very interesting approach, thanks. I'll dig into it in the coming days. Would be interested in other approaches as well. – bolino Aug 19 '22 at 16:17
  • I'm a bit confused on how to call the method . addPokemon, from any widget in the app. If it's only for adding, we don't need to subscribe to the notifier right? On which object to call the .addPokemon method then, how to get the repository from the provider, and not the state? – bolino Aug 24 '22 at 13:34
  • Gotcha, I just instanciated the repository elsewhere and called the method on it. – bolino Aug 24 '22 at 15:43
1

you can try it like this:


class Pokemon {
  Pokemon(this.name);

  final String name;
}

final pokemonProvider =
    StateNotifierProvider<PokemonRepository, AsyncValue<List<Pokemon>>>(
        (ref) => PokemonRepository(ref.read));

class PokemonRepository extends StateNotifier<AsyncValue<List<Pokemon>>> {
  PokemonRepository(this._reader) : super(const AsyncValue.loading()) {
    _init();
  }

  final Reader _reader;

  Future<void> _init() async {
    final List<Pokemon> pokemons;
    try {
      pokemons = await getApiPokemons();
    } catch (e, s) {
      state = AsyncValue.error(e, stackTrace: s);
      return;
    }

    state = AsyncValue.data(pokemons);
  }

  Future<void> getAllPokemon() async {
    state = const AsyncValue.loading();
    /// do something...
    state = AsyncValue.data(pokemons);
  }

  Future<void> addPokemon(Pokemon pk) async {}
  Future<void> updatePokemon(Pokemon pk) async {}
  Future<void> deletePokemon(Pokemon pk) async {}
}

Ruble
  • 2,589
  • 3
  • 6
  • 29
  • Looks good and simple. I tried it, works well. When the child widget update the repository, the parent widget (which is the one subscribed to the notifier) doesn't get its state refreshed, though. – bolino Aug 24 '22 at 15:42