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

Posted

技术标签:

【中文标题】Flutter:检测任何在屏幕上不可见但在小部件树中的小部件的重建【英文标题】:Flutter: Detect rebuild of any widget which is not visible on screen but is in the widget tree 【发布时间】:2020-12-12 20:30:36 【问题描述】:

总结:

当使用导航器显示页面/路由时,会从最近的MaterialApp 父级创建一个新分支。这意味着两个页面(MainNew)都将在内存中,如果它们正在收听相同的ChangeNotifier,它们将重建。

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

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

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

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

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

请运行该应用程序一次,以正确理解以下详细信息。

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

如果有更多详细信息或我遗漏了需要的内容,请在 cmets 中告诉我。


使用以下示例代码包含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());


【问题讨论】:

【参考方案1】:

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

参考这个flutter插件/包: 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());


【讨论】:

以上是关于Flutter:检测任何在屏幕上不可见但在小部件树中的小部件的重建的主要内容,如果未能解决你的问题,请参考以下文章

在多个屏幕中使用表单颤振“在小部件树中检测到重复的 GlobalKey”错误

通过 addPostFrameCallback 访问 Flutter Provider 时说小部件在小部件树之外,但颤振检查器显示其他情况

在小部件树中检测到重复的 GlobalKey

如何在小部件测试中找到屏幕外的 ListView 子项?

Flutter - 在充满其他小部件的屏幕上检测点击

Flutter - 动态小部件定位