Home > Back-end >  Best practice on how to write/update data from a Flutter provider
Best practice on how to write/update data from a Flutter provider

Time:08-20

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?

CodePudding user response:

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.

CodePudding user response:

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 {}
}

  • Related