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

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

总结:

如使用导航器显示 page/route,从最近的 MaterialApp 父级创建了一个新分支。这意味着两个页面 (Main & New) 都将在内存中,并且如果它们正在收听相同的 ChangeNotifier.

将重建

我无法确定用户当前在屏幕上可以看到哪个小部件。 我需要它来处理一个场景,以跳过执行具有一些副作用的异步或长进程,从一个可能在小部件树中但当前不可见的小部件。

注意:此处给出的示例代码代表了我当前正在开发的应用程序的基本架构,但重现了确切的问题。

我的应用程序中有一个非常不同且复杂的小部件树,我遇到了这个问题,从屏幕上不可见的小部件执行 doLongProcess()。此外 doLongProcess() 更改了我的应用程序中一些常见的 属性,这会导致问题,因为任何背景小部件都可以修改在其他小部件上可见的详细信息。

我正在寻找这个问题的解决方案,如果除了查找屏幕上的哪个小部件之外还有其他方法可以实现目标,那么也请告诉我。

我的最终目标是只允许从可见的小部件执行长过程。

请运行应用一次,正确理解以下内容。

注2: 我尝试使用状态的 mounted 属性 来确定它是否可以使用,但它对两个小部件(MainPage TextDisplayNewPage TextDisplay

如果有更多详细信息或我遗漏了一些必需的内容,请在评论中告诉我。


使用包含 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());
}

发布解决方案供他人参考。

参考这个flutter plugin/package: https://pub.dev/packages/visibility_detector

解决方法代码:

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