Ненужные перестройки виджета при использовании селектора (провайдера) внутри StreamBuilder

Я использую Selector, который перестраивается при изменении данных в Bloc. Что работает нормально, но когда данные меняются, он перезагружает все дерево, а не только конструктор внутри Selector.

В моем случае селектор находится внутри StreamBuilder. Мне это нужно, потому что поток подключен к API. Итак, внутри потока я создаю какой-то виджет, и один из них - Selector. Selector перестраивает виджеты, зависящие от данных из потока.

Вот мой код. Я не хочу, чтобы Stream вызывали снова и снова. Также вызывается Stream, потому что build вызывается каждый раз, когда виджет селектора перестраивается.

main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_test/data_bloc.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MultiProvider(providers: [
        ChangeNotifierProvider<DataBloc>(
          create: (_) => DataBloc(),
        )
      ], child: ProviderTest()),
    );
  }
}

class ProviderTest extends StatefulWidget {
  @override
  _ProviderTestState createState() => _ProviderTestState();
}

class _ProviderTestState extends State<ProviderTest> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          Text("Outside Stream Builder"),
          StreamBuilder(
            stream: Provider.of<DataBloc>(context).getString(),
            builder: (_, AsyncSnapshot<String> snapshot) {
              if (snapshot.hasData) {
                return Column(
                  children: <Widget>[
                    Text("Widget Generated by Stream Data"),
                    Text("Data From Strem : " + snapshot.data),
                    RaisedButton(
                        child: Text("Reload Select"),
                        onPressed: () {
                          Provider.of<DataBloc>(context, listen: false).changeValue(5);
                        }),
                    Selector<DataBloc, int>(
                        selector: (_, val) =>
                            Provider.of<DataBloc>(context, listen: false).val,
                        builder: (_, val, __) {
                          return Container(
                            child: Text(val.toString()),
                          );
                        }),
                  ],
                );
              }

              return Container();
            },
          )
        ],
      ),
    );
  }
}

bloc.dart

import 'package:flutter/foundation.dart';

class DataBloc with ChangeNotifier {

  int _willChange = 0;

  int get val => _willChange;

  void changeValue(int val){
    _willChange++;
    notifyListeners();
  }

  Stream<String> getString() {
    print("Stream Called");
    return Stream.fromIterable(["one", "two", "three"]);
  }

}

Также, если я удалю StreamBuilder, Selector будет действовать так же, как и предполагалось. Почему StreamBuilder Rebuilds в этом случае? Есть ли способ предотвратить это?


person sh0umik    schedule 02.01.2020    source источник
comment
Вы пытались абстрагироваться от вашей onPressed функции (например, ссылки на нее где-нибудь) от RaisedButton? Таким образом, функция обратного вызова (и, следовательно, изменение потока) не оценивается каждый раз, когда Stream вызывает перестройку.   -  person Sven    schedule 03.01.2020
comment
Вы можете создать слушателя для своего Stream в своем initState, который обновляет переменную, которая хранит самую последнюю версию ваших данных, а затем использовать эту переменную для заполнения ваших виджетов. Таким образом, поток будет подписан только на первую загрузку виджета, а не при перестроении.   -  person João Soares    schedule 03.01.2020
comment
Да, я пробовал абстракцию, она не работает. Но я нашел проблему. Я ответил ниже. Это было простое решение, и я многому научился после его решения. Все дело в том, как Provider и InheritedWidget работают вместе.   -  person sh0umik    schedule 03.01.2020


Ответы (2)


На основе кода, которым вы поделились, вы можете создать прослушиватель для вашего Stream на вашем initState, который обновляет переменную, которая хранит самую последнюю версию ваших данных, а затем использовать эту переменную для заполнения ваших виджетов. Таким образом, поток будет подписан только на первую загрузку виджета, а не при перестроении. Я не могу протестировать его напрямую, так как у меня нет вашего проекта. Но, пожалуйста, попробуйте.

Пример кода на основе вашего кода

class ProviderTest extends StatefulWidget {
  @override
  _ProviderTestState createState() => _ProviderTestState();
}

class _ProviderTestState extends State<ProviderTest> {
  String _snapshotData;

  @override
  void initState() {
    listenToGetString();
    super.initState();
  }

  void listenToGetString(){
    Provider.of<DataBloc>(context).getString().listen((snapshot){
      setState(() {
        _snapshotData = snapshot.data;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          Text("Outside Stream Builder"),
          Column(
            children: <Widget>[
              Text("Widget Generated by Stream Data"),
              Text("Data From Strem : " + _snapshotData),
              RaisedButton(
                child: Text("Reload Select"),
                onPressed: () {
                  Provider.of<DataBloc>(context, listen: false).changeValue(5);
                }
              ),
              Selector<DataBloc, int>(
                selector: (_, val) =>
                  Provider.of<DataBloc>(context, listen: false).val,
                builder: (_, val, __) {
                  return Container(
                    child: Text(val.toString()),
                  );
                }
              ),
            ],
          )
        ],
      ),
    );
  }
}
person João Soares    schedule 03.01.2020

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

Дело и цитата, которая решает эту проблему. (Цитата из сообщения в блоге выше)

Когда виджет регистрируется как зависимость InheritedWidget поставщика, этот виджет будет перестраиваться каждый раз, когда происходит изменение «предоставленных данных» (точнее, когда вызывается notifyListeners () или когда поток StreamProvider испускает новые данные или когда Будущее FutureProvider завершено).

Это означает, что переменная, которую я изменяю, и поток, который я указываю, существуют в одном и том же блоке! это была ошибка. Поэтому, когда я меняю val и вызываю notifyListener() в одном блоке, все перезагружается, что является поведением по умолчанию.

Все, что мне нужно было сделать, чтобы решить эту проблему, - это создать еще один блок и абстрагировать поток для этого конкретного блока (я думаю, что это тоже хорошая практика). Теперь notifyListener() не влияет на поток.

data_bloc.dart

class DataBloc with ChangeNotifier {

  int _willChange = 0;

  String data = "";

  int get val => _willChange;

  void changeValue(int val){
    _willChange++;
    notifyListeners();
  }

  Future<String> getData () async {
    return "Data";
  }

}

stream_bloc.dart

import 'package:flutter/foundation.dart';

class StreamBloc with ChangeNotifier {

  Stream<String> getString() {
    print("Stream Called");
    return Stream.fromIterable(["one", "two", "three"]);
  }

}

И проблема решена. Теперь Stream будет вызываться только при его вызове, но не при изменении переменной в data_bloc.

person sh0umik    schedule 03.01.2020