Управление состоянием Flutter с помощью провайдера 2: Провайдер как фреймворк инъекции зависимостей и MultiProvider

Это вторая часть серии уроков по управлению состоянием Flutter с помощью пакета provider. В первой части мы узнали, как мы можем управлять состоянием наших виджетов из классов моделей, которые наследуются от ChangeNotifier, а также об использовании ChangeNotifierProvider для обеспечения нашего дерева виджетов этими классами, и, наконец, об использовании класса Consumer для чтения этих изменений состояния и реакции на них.

📽 Видеоверсия доступна на YouTube и Odysee

В этой второй части мы углубимся в следующие концепции:

  • Сначала мы посмотрим, как заставить несколько виджетов реагировать на изменения состояния на основе одного провайдера.
  • Мы узнаем, как использовать провайдер в качестве фреймворка для инъекции зависимостей
  • Наконец, мы будем использовать MultiProvider, чтобы сделать наш код более читабельным.

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

Управление состоянием нескольких виджетов с помощью одного провайдера состояния

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

У нас есть базовое приложение счетчика. Я хочу добиться того, чтобы иметь возможность хранить числовой счетчик и показывать его в различных виджетах, а также иметь возможность увеличивать его из всех этих виджетов.

Сначала скопируйте MyHomePage из main.dart в новый файл second_counter.dart и переименуйте его в SecondCounter, примерно так:

// lib/second_counter.dart

import 'package:flutter/material.dart';
import 'package:flutter_state_management_easy_provider/main_model.dart';
import 'package:provider/provider.dart';

class SecondCounter extends StatefulWidget {
  const SecondCounter({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<SecondCounter> createState() => _SecondCounterState();
}

class _SecondCounterState extends State<SecondCounter> {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => MainModel(),
      child: Consumer<MainModel>(
        builder: (context, model, child) => Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  '${model.counter}',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () => model.counter++,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        ),
      ),
    );
  }
}
Войдите в полноэкранный режим Выйти из полноэкранного режима

Теперь добавьте новый элемент в Column в MyHomePage в виде кнопки, чтобы открыть этот новый экран:

// main.dart

// [...]

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => MainModel(),
      child: Consumer<MainModel>(
        builder: (context, model, child) => Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  '${model.counter}',
                  style: Theme.of(context).textTheme.headline4,
                ),
                // Add this button to open SecondCounter
                Padding(
                  padding: const EdgeInsets.only(top: 20.0),
                  child: TextButton(
                    child: const Text('Open counter 2'),
                    onPressed: () => Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) => const SecondCounter(
                          title: 'Second counter',
                        ),
                      ),
                    ),
                  ),
                )
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: () => model.counter++,
            tooltip: 'Increment',
            child: const Icon(Icons.add),
          ),
        ),
      ),
    );
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Попробуйте запустить приложение. Попробуйте увеличить значение счетчика, а затем перейдите на второй экран и попробуйте увеличить значение счетчика оттуда. Каков результат? Ну, у нас есть два экрана, и каждый из них с независимым значением, а это не то, чего мы хотим. Почему это происходит? Причина в том, что мы фактически создаем два экземпляра MainModel, один в MyHomePage и другой в SecondCounter, каждый из которых предоставляется своим виджетам-потомкам через ChangeNotifierProvider.

Для того чтобы иметь уникальный счетчик во всем приложении, нам придется переместить этот ChangeNotifierProvider на один уровень вверх, пока оба экрана не повиснут на нем, например, мы можем разместить его прямо над MyHomePage, в классе, который обертывает MaterialApp (файл main.dart):

// main.dart

// [...]

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<MainModel>(
      create: (_) => MainModel(),
      child: MaterialApp(
        title: 'Flutter State Management Basic',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const MyHomePage(title: 'Flutter State Management Basic'),
      ),
    );
  }
}

// [...]
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь удалите ChangeNotifierProvider из MyHomePage и SecondCounter, чтобы методы build() напрямую возвращали Consumer<MainModel>.

Запустите приложение снова и попробуйте увеличить счетчики, вы увидите, что теперь есть только один уникальный счетчик. Это потому, что существует только один экземпляр MainModel, и любой виджет ниже ChangeNotifierProvider, который его предоставляет, может получить к нему доступ, чтобы использовать его состояние.

Провайдер как фреймворк для инъекции зависимостей

Инъекция зависимостей – это техника разработки программного обеспечения, при которой зависимости класса инжектируются извне, а не инстанцируются внутри него. Я мог бы написать несколько статей об этой технике и ее применении, но для того, чтобы понять друг друга и сосредоточиться на рассматриваемой теме, мы научимся использовать provider для создания экземпляра и включения его в дерево виджетов, чтобы его можно было использовать из любой точки приложения.

Для этого я собираюсь привести пример пакета logs, который я использую, – logger. Я хочу создать экземпляр и записывать в журнал трассировку, когда счетчик увеличивается на первом экране, а также на втором экране, и я хочу, чтобы этот экземпляр был общим для всего приложения. Первым делом установите пакет:

flutter pub add logger
Войти в полноэкранный режим Выход из полноэкранного режима

Сейчас мы будем использовать класс Provider для выполнения этой задачи. Это основной класс, который дал название этому пакету, на самом деле и ChangeNotifierProvider, и другие типы провайдеров, которые мы увидим в этой серии уроков, являются вариациями или формами этого оригинального класса:

// main.dart

// [...]

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<MainModel>(
      create: (_) => MainModel(),
      child: Provider<Logger>(
        create: (_) => Logger(),
        child: MaterialApp(
          title: 'Flutter State Management Basic',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const MyHomePage(title: 'Flutter State Management Basic'),
        ),
      ),
    );
  }
}

// [...]
Вход в полноэкранный режим Выход из полноэкранного режима

Как видите, чтобы сделать “дыру” для этого нового провайдера, мы просто вставляем один в другой. Теперь мы собираемся изменить FloatingActionButton каждого экрана, чтобы, помимо увеличения счета, печатать трассировку журнала. Чтобы получить этот Logger, который мы только что создали, мы используем Provider.of<Logger>(context, listen: false). Этим вызовом мы указываем тип класса, который хотим получить, передаем текущий context и указываем listen: false. Мы увидим, почему последний аргумент имеет такое значение позже в этой серии уроков, а пока оставьте его в значении false:

// main.dart

// Alter the existent FloatingActionButton:
floatingActionButton: FloatingActionButton(
  onPressed: () {
    model.counter++;
    Provider.of<Logger>(context, listen: false).d(
      'Increment counter from MyHomePage',
    );
  },
  tooltip: 'Increment',
  child: const Icon(Icons.add),
),
Войти в полноэкранный режим Выход из полноэкранного режима
// second_counter.dart

// Alter the existent FloatingActionButton:
floatingActionButton: FloatingActionButton(
  onPressed: () {
    model.counter++;
    Provider.of<Logger>(context, listen: false).d(
      'Increment counter from SecondCounter',
    );
  },
  tooltip: 'Increment',
  child: const Icon(Icons.add),
),
Войти в полноэкранный режим Выход из полноэкранного режима

Как вы видите, с помощью класса Provider вы можете предоставить экземпляры классов, которые вы хотите, любому виджету вашего приложения.

Сделайте код более читабельным с помощью MultiProvider

То, что мы видели сегодня, очень хорошо и очень полезно, но обратите внимание, что обычные приложения не имеют только 2 зависимости, как в нашем примере, они могут иметь 5, 10, 100 или более различных зависимостей. Представьте, как каждая из них будет включена в следующую, как сейчас в MyApp, это было бы очень сложно поддерживать и читать, и именно поэтому существует MultiProvider.

MultiProvider – это способ объявить все ваши провайдеры в массиве. Давайте посмотрим его в действии, изменим MyApp следующим образом:

// main.dart

// [...]

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<MainModel>(create: (_) => MainModel()),
        Provider<Logger>(
          create: (_) => Logger(),
        ),
      ],
      child: MaterialApp(
        title: 'Flutter State Management Basic',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const MyHomePage(title: 'Flutter State Management Basic'),
      ),
    );
  }
}

// [...]
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Заключение

В этом втором уроке мы рассмотрели основные концепции пакета провайдеров. Мы увидели, как управлять состоянием в различных точках с помощью единственного ChangeNotifier, расположенного в верхней части дерева виджетов. Мы также использовали Provider для предоставления отдельных экземпляров приложению и организовали все наши провайдеры с помощью MultiProvider.

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

Если у вас есть какие-либо вопросы или предложения, не стесняйтесь оставлять комментарии ниже.

Вы можете найти финальное приложение этого учебника здесь.

Спасибо за чтение и удачного кодинга 🙂

Оцените статью
Procodings.ru
Добавить комментарий