Лучшая практика записи/обновления данных от поставщика Flutter

Я новичок в провайдерах Flutter. Я использую Риверпод.

У меня есть поставщик Future, который предоставляет некоторые данные из файла JSON - в будущем это будет ответ API.

      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)));
});

Я подписываюсь на сref.watchвConsumerStateвиджеты, например:

      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),
                );
              },
        ));
      },
    );
  }
}

Но в таком случае, как лучше всего записывать/обновлять данные в файл JSON/API?

Кажется, провайдеры используются для чтения/предоставления данных, а не для их обновления, поэтому я запутался.

Должен ли тот же провайдерpokemonProviderиспользоваться для этого? Если да, то какойFutureProviderметод, который следует использовать и как его вызвать? Если нет, то какова наилучшая практика?

2 ответа

Я тоже новичок в RiverPod, но я попытаюсь объяснить наш подход.

Примеры с FutureProviders, вызывающими API, немного вводят меня в заблуждение, потому что провайдер предлагает контент только для одного вызова API, а не доступ ко всему API.

Чтобы решить эту проблему, мы обнаружили, что шаблон репозитория очень полезен. Мы используем поставщика для экспорта класса, содержащего полный API (или фиктивный для целей тестирования), и мы контролируем состояние (другой объект, содержащий разные ситуации), чтобы управлять ответами и обновлениями.

Ваш пример будет примерно таким:

Сначала мы определяем наш объект состояния:

      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();
}

Теперь наш класс провайдера и репозитория (абстрактный не обязателен, но давайте воспользуемся этим подходом, чтобы вы могли оставить пример для тестирования):

      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);
}

И реализация для каждого репозитория:

      ///
/// 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());
    }
  }
}

и

      ///
/// 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]);
  }
}

Затем в сборке ваш виджет с несколькими изменениями:

      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),
                );
              },
        ));
    }
  }
}

Не уверен, что это лучший подход, но пока он работает для нас.

вы можете попробовать это так:

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

Другие вопросы по тегам