简体   繁体   English

Flutter:检测在屏幕上不可见但在小部件树中的任何小部件的重建

[英]Flutter: Detect rebuild of any widget which is not visible on screen but is in the widget tree

Summary:概括:

As showing a page/route using the Navigator, a new branch is created from the nearest MaterialApp parent.当使用导航器显示页面/路由时,从最近的MaterialApp父级创建了一个新分支。 Meaning both pages ( Main & New ) will be in memory and will rebuild if they are listening to the same ChangeNotifier .这意味着两个页面( MainNew )都将在内存中,并且如果它们正在侦听相同的ChangeNotifier就会重建。

I am having trouble finding out which widget is on-screen currently visible to the user.我无法找出用户当前在屏幕上可见的小部件。 I need this to handle a scenario to skip performing asynchronous or long processes with some side effects, from a widget that might be in the widget tree but currently not visible.我需要这个来处理一个场景,从一个可能在小部件树中但当前不可见的小部件中跳过执行异步或长过程有一些副作用。

Note : The sample code given here represents the basic architecture of the app I am currently working on, but reproduces the exact problem.注意:此处给出的示例代码代表了我目前正在开发的应用程序的基本架构,但重现了确切的问题。

I am having this problem with a very different and complex widget tree that I have in my app, executing the doLongProcess() from a widget that is not visible on the screen.我在我的应用程序中有一个非常不同且复杂的小部件树,从屏幕上不可见的小部件执行doLongProcess()时遇到了这个问题。 Also doLongProcess() changes some common property in my app which causes an issue, as any background widget can modify the details which are visible on the other widget. doLongProcess()更改了我的应用程序中的一些常见属性,这会导致问题,因为任何后台小部件都可以修改其他小部件上可见的详细信息。

I am looking for a solution to this issue, if there's any other way to achieve the goal except finding which widget is on the screen then please let me know that as well.我正在寻找此问题的解决方案,如果除了查找屏幕上的小部件之外还有其他任何方法可以实现目标,那么也请告诉我。

My final goal is to allow the long process to be executed from only the visible widget(s).我的最终目标是允许仅从可见小部件执行漫长的过程。

Please run the app once, to understand the following details properly.请运行该应用程序一次,以正确理解以下详细信息。

Note 2 : I have tried to use mounted property of the state to determine if it can be used or not but it shows true for both widgets ( MainPage TextDisplay and NewPage TextDisplay )注 2 :我尝试使用状态的mounted属性来确定它是否可以使用,但它对两个小部件( MainPage TextDisplayNewPage TextDisplay )都显示为真

Let me know in the comments if more details or I missed something which is required.如果有更多详细信息或我错过了所需的内容,请在评论中告诉我。


Use the following sample code with provider dependency included for reproducing the problem:使用包含provider依赖项的以下示例代码来重现问题:

// add in pubspec.yaml:  provider: ^4.3.2+1

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    print('MainPage: build');
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextDisplay(
              name: 'MainPage TextDisplay',
            ),
            SizedBox(
              height: 20,
            ),
            RaisedButton(
              child: Text('Open New Page'),
              onPressed: () => Navigator.of(context).push(MaterialPageRoute(
                builder: (context) => NewPage(),
              )),
            ),
          ],
        ),
      ),
    );
  }
}

class TextDisplay extends StatefulWidget {
  final String name;

  const TextDisplay({Key key, @required this.name}) : super(key: key);

  @override
  _TextDisplayState createState() => _TextDisplayState();
}

class _TextDisplayState extends State<TextDisplay> {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: ChangeNotifierProvider.value(
        value: dataHolder,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Center(child: Text(widget.name)),
            SizedBox(
              height: 20,
            ),
            Consumer<DataHolder>(
              builder: (context, holder, child) {
                // need to detect if this widget is on the screen,
                // only then we should go ahead with this long process
                // otherwise we should skip this long process
                doLongProcess(widget.name);

                return Text(holder.data);
              },
            ),
            RaisedButton(
              child: Text('Randomize'),
              onPressed: () => randomizeData(),
            ),
          ],
        ),
      ),
    );
  }

  void doLongProcess(String name) {
    print('$name: '
        'Doing a long process using the new data, isMounted: $mounted');
  }
}

class NewPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('NewPage: build');
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: true,
        title: Text('New Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextDisplay(
              name: 'NewPage TextDisplay',
            ),
          ],
        ),
      ),
    );
  }
}

/////////////////// Data Holder Class and methods ///////////////////

class DataHolder extends ChangeNotifier {
  String _data;

  String get data => _data ?? 'Nothing to show, Yet!';

  setData(String newData) {
    print('\n new data found: $newData');
    _data = newData;
    notifyListeners();
  }
}

final dataHolder = DataHolder();

randomizeData() {
  int mills = DateTime.now().millisecondsSinceEpoch;
  dataHolder.setData(mills.toString());
}

Posting solution for others to refer.发布解决方案供其他人参考。

Refer to this flutter plugin/package: https://pub.dev/packages/visibility_detector参考这个flutter插件/包: https : //pub.dev/packages/visibility_detector

The solution code:解决方案代码:

// add in pubspec.yaml:  provider: ^4.3.2+1

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:visibility_detector/visibility_detector.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    print('MainPage: build');
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextDisplay(
              name: 'MainPage TextDisplay',
            ),
            SizedBox(
              height: 20,
            ),
            RaisedButton(
              child: Text('Open New Page'),
              onPressed: () => Navigator.of(context).push(MaterialPageRoute(
                builder: (context) => NewPage(),
              )),
            ),
          ],
        ),
      ),
    );
  }
}

class TextDisplay extends StatefulWidget {
  final String name;

  const TextDisplay({Key key, @required this.name}) : super(key: key);

  @override
  _TextDisplayState createState() => _TextDisplayState();
}

class _TextDisplayState extends State<TextDisplay> {
  /// this holds the latest known status of the widget's visibility
  /// if [true] then the widget is fully visible, otherwise it is false.
  ///
  /// Note: it is also [false] if the widget is partially visible since we are
  /// only checking if the widget is fully visible or not
  bool _isVisible = true;

  @override
  Widget build(BuildContext context) {
    return Container(
      child: ChangeNotifierProvider.value(
        value: dataHolder,

        /// This is the widget which identifies if the widget is visible or not
        /// To my suprise this is an external plugin which is developed by Google devs 
        /// for the exact same purpose
        child: VisibilityDetector(
          key: ValueKey<String>(widget.name),
          onVisibilityChanged: (info) {
            // print('\n ------> Visibility info:'
            //     '\n name: ${widget.name}'
            //     '\n visibleBounds: ${info.visibleBounds}'
            //     '\n visibleFraction: ${info.visibleFraction}'
            //     '\n size: ${info.size}');

            /// We use this fraction value to determine if the TextDisplay widget is 
            /// fully visible or not
            /// range for fractional value is:  0 <= visibleFraction <= 1
            ///
            /// Meaning we can also use fractional values like, 0.25, 0.3 or 0.5 to 
            /// find if the widget is 25%, 30% or 50% visible on screen
            _isVisible = info.visibleFraction == 1;
          },
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Center(child: Text(widget.name)),
              SizedBox(
                height: 20,
              ),
              Consumer<DataHolder>(
                builder: (context, holder, child) {
                  /// now that we have the status of the widget's visiblity
                  /// we can skip the long process when the widget is not visible.
                  if (_isVisible) {
                    doLongProcess(widget.name);
                  }

                  return Text(holder.data);
                },
              ),
              RaisedButton(
                child: Text('Randomize'),
                onPressed: () => randomizeData(),
              ),
            ],
          ),
        ),
      ),
    );
  }

  void doLongProcess(String name) {
    print('\n  ============================ \n');
    print('$name: '
        'Doing a long process using the new data, isMounted: $mounted');
    final element = widget.createElement();
    print('\n name: ${widget.name}'
        '\n element: $element'
        '\n owner: ${element.state.context.owner}');
    print('\n  ============================ \n');
  }
}

class NewPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    print('NewPage: build');
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: true,
        title: Text('New Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextDisplay(
              name: 'NewPage TextDisplay',
            ),
          ],
        ),
      ),
    );
  }
}

/////////////////// Data Holder Class and methods ///////////////////

class DataHolder extends ChangeNotifier {
  String _data;

  String get data => _data ?? 'Nothing to show, Yet!';

  setData(String newData) {
    print('\n new data found: $newData');
    _data = newData;
    notifyListeners();
  }
}

final dataHolder = DataHolder();

randomizeData() {
  int mills = DateTime.now().millisecondsSinceEpoch;
  dataHolder.setData(mills.toString());
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM