Статья для начинающих в Riverpod

До этого пользовался Provider совместно с BLoC и недавно решился попробовать Riverpod в одном из проектов. В ходе работы столкнулся с проблемой, которую многие могут не замечать.

При использовании Logger для Http запросов он отправлял несколько запросов, даже если был отправлен лишь 1. Для устранения проблемы нужно будет создать собственный провайдер для Dio и работать уже с ним.

Первым делом: создаем localeProvider

final localeProvider = StateNotifierProvider<LocaleNotifier, Locale>((ref) {
  return LocaleNotifier();
});

class LocaleNotifier extends StateNotifier<Locale> {
  LocaleNotifier() : super(sharedPreference.locale) {
    onAppStart();
  }

  void changeLanguage(Locale locale) {
    try {
      if (!L10n.all.contains(locale)) return;
      state = locale;
    } catch (error) {
      state = sharedPreference.locale;
    }
  }

  void onAppStart() {
    try {
      final locale = sharedPreference.locale;
      state = locale;
    } catch (error) {
      state = const Locale('ru');
    }
  }
}

2. Соединяем его с MaterialApp

Обертываем наш MyApp в ProviderScope() и через метод watch "слушаем" изменение языка. И добавляем ProviderContainer в нашем main.dart файле.

final ProviderContainer container = ProviderContainer();

  runApp(
    ProviderScope(
      child: WorkspaceApp(),
    ),
  );

  Widget build(BuildContext context, WidgetRef ref) {
    final locale = ref.watch(localeProvider);
    return MaterialApp.router(
      title: "Workspace",
      locale: locale,
      routerConfig: _router,
      supportedLocales: L10n.all,
      theme: LightTheme,
      darkTheme: null,
      debugShowCheckedModeBanner: false,
      themeMode: ThemeMode.light,
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      builder: EasyLoading.init(),
    );
  }

3. HttpQuery для get запроса

Создаем http_query.dart файл и вставляем код ниже:

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logger/logger.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';

class HttpQuery {

  final dioProvider = Provider((ref) {
    PrettyDioLogger logger = PrettyDioLogger(
      requestHeader: true,
      requestBody: true,
      responseBody: false,
      responseHeader: true,
      error: true,
      compact: true,
      maxWidth: 100,
    );
    Dio dio = Dio(BaseOptions(baseUrl: '${sharedPreference.chosenServer}/api/'));
    dio.interceptors.add(ErrorInterceptor());
    dio.interceptors.add(logger);
    return dio;
  });
  
  /* ---------------------------------- HttpQuery ---------------------------------- */

  Future<dynamic> get(
      {required String url, Map<String, dynamic>? queryParameters, Map<String, dynamic>? headerData}) async {
    try {
      container.read(dioProvider).interceptors.add(ErrorInterceptor());
      container.read(dioProvider).interceptors.add(logger);
      Map<String, dynamic> header = {
        "Content-Type": "application/json",
      };

      Map<String, dynamic> queryParameters1 = queryParameters ?? {};
      String? token = await UserSecureStorage.getToken();
      if (token != "") header.addAll({"Authorization": 'Bearer $token'});
      if (headerData != null) header.addAll(headerData);
      final Response result = await container.read(dioProvider).get(
            url,
            options: Options(
              sendTimeout: 30000,
              receiveTimeout: 60000,
              headers: header,
            ),
            queryParameters: queryParameters1,
          );
      return result.data;
    } on DioError catch (error) {
      if (error.type == DioErrorType.connectTimeout) {
        debugPrint(error.type.name);
      }
      if (error.type == DioErrorType.receiveTimeout) {
        debugPrint(error.type.name);
      }
      return error;
    }
  }
}

Наш dioProvider мы можем использовать за место getIt. В нем описываем базовые параметры для Dio, в моем случае я вставил туда базовое начало моих url запросов.

И создаем providerContainer для доступа к провайдера без WidgetRef. Далее для получения результата Response мы отправляем запрос через наш dioProvider получая доступ к нему через наш providerContainer.

4. Изменение языка

Используем вместо StatelessWidget - ConsumerWidget

class LanguagePickerWidget extends ConsumerWidget {
  const LanguagePickerWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final locale = ref.read(localeProvider);

    return DropdownButtonHideUnderline(
      child: DropdownButton(
        value: locale,
        icon: Container(width: 12),
        items: L10n.all.map(
          (locale) {
            final flag = L10n.getFlag(locale.languageCode);

            return DropdownMenuItem(
              value: locale,
              onTap: () {
                ref.read(localeProvider.notifier).changeLanguage(locale);
              },
              child: Center(
                child: Text(
                  flag,
                  style: const TextStyle(fontSize: 32),
                ),
              ),
            );
          },
        ).toList(),
        onChanged: (_) {},
      ),
    );
  }
}

Получаем доступ к нашему localeProvider и через метод notifier, который получает уведомления о изменении состоянии мы меняем наш язык.

Комментарии (4)


  1. nikita_dol
    14.04.2023 08:59
    +1

    .interceptors.add

    Зачем добавлять новые при каждом вызове get? Лучше поместить это в dioProvider

    final container = ProviderContainer();

    Зачем создавать контейнер для 1 зависимости?

    LocaleNotifier() : super(sharedPreference.locale) {

    sharedPreference лучше брать из контейнера


    1. Garyshker Автор
      14.04.2023 08:59

      Спасибо, исправлю)

      Но как по-другому, без контейнера получить доступ к провайдеру риверпода?


      1. nikita_dol
        14.04.2023 08:59
        +1

        В целом в корне дерева создаётся контейнер с помощью ProviderScope или если нужно, то создаётся ранее и потом вставляется в корень дерева с помощью UncontrolledProviderScope

        Так же, обычно провайдеры создают как top-level variables, что бы они не пересоздавались.

        Так же достаточно удобная возможность - использование riverpod как DI:

        final httpQueryProvider = Provider(
          (ref) => HttpQuery(
            ref.watch(dioProvider),
          ),
          dependencies: [
            dioProvider,
          ],
          name: 'httpQuery',
        );
        
        class HttpQuery {
          HttpQuery(this._dio);
        
          final Dio _dio;
        
          Future<Object?> get({
            required String url,
            Map<String, Object?>? queryParameters,
            Map<String, Object?>? headerData,
          })  {
            ...
          }
        }

        Кстати, я пересмотрел статью ещё раз и не понял как связан LocaleNotifier и HttpQuery


        1. Garyshker Автор
          14.04.2023 08:59

          Спасибо, понятнее стало)

          Возможно я неправильно объяснял в начале, там изменение языка это по сути лишь пример. Для рядового, кто только щупает риверпод, то для них при его использовании могут быть проблемы с мульти-запросами, во-о-от я и попытался своим решением поделиться, так как в интернете ничего особо не нашел. Поэтому рад к корректировкам и благодарен тебе.