获取一个小部件的scrollController并传递给另一个小部件?

huangapple go评论88阅读模式
英文:

Retrieve a widget's scrollController into another widget?

问题

在我的应用中,一个名为Menu的类负责通过IndexedStack小部件来显示应用程序的不同页面,同时还有一个底部导航栏,以保存每个页面的状态:

menu.dart

  1. class Menu extends StatefulWidget {
  2. @override
  3. _MenuState createState() => _MenuState();
  4. }
  5. class _MenuState extends State<Menu> {
  6. final List<Widget> pages = [Home(), Profile()];
  7. int _selectedIndex = 0;
  8. void callbackFunction(int index) {
  9. setState(() {
  10. _selectedIndex = index;
  11. });
  12. }
  13. @override
  14. Widget build(BuildContext context) {
  15. return Scaffold(
  16. appBar: AppBar(
  17. title: Text('Save State'),
  18. ),
  19. body: IndexedStack(
  20. index: _selectedIndex,
  21. children: pages,
  22. ),
  23. bottomNavigationBar: BottomNavigationBar(
  24. items: [
  25. BottomNavigationBarItem(
  26. icon: Icon(Icons.home),
  27. label: 'Home',
  28. ),
  29. BottomNavigationBarItem(
  30. icon: Icon(Icons.person),
  31. label: 'Profile',
  32. ),
  33. ],
  34. currentIndex: _selectedIndex,
  35. onTap: (index) {
  36. setState(() {
  37. _selectedIndex = index;
  38. });
  39. },
  40. ),
  41. );
  42. }
  43. }

home.dart

  1. class Home extends StatefulWidget {
  2. Home({Key? key}) : super(key: key);
  3. @override
  4. State<Home> createState() => _HomeState();
  5. }
  6. class _HomeState extends State<Home> {
  7. final ScrollController _scrollController = ScrollController();
  8. // 数据存储在item列表中。
  9. List<String> items = [];
  10. // 用于检查加载状态的标志。
  11. bool loading = false, allLoaded = false;
  12. // 加载数据到item列表的函数。
  13. mockFetch() async {
  14. // 没有更多数据可加载。
  15. if (allLoaded) {
  16. return;
  17. }
  18. // 将loading标志设置为true,以防止多次调用此函数。
  19. setState(() {
  20. loading = true;
  21. });
  22. // 模拟API调用期间可能发生的延迟。
  23. await Future.delayed(Duration(milliseconds: 500));
  24. // 如果item列表大于或等于60,将返回一个空数组以模拟数据流的结束。否则,将生成20个额外的项。
  25. List<String> newData = items.length >= 60
  26. ? []
  27. : List.generate(20, (index) => "List Item ${index + items.length}");
  28. // 将新数据添加到item列表中。
  29. if (newData.isNotEmpty) {
  30. items.addAll(newData);
  31. }
  32. // 重置标志。
  33. setState(() {
  34. loading = false;
  35. // 如果数组为空,则返回false。
  36. allLoaded = newData.isEmpty;
  37. });
  38. }
  39. @override
  40. void initState() {
  41. super.initState();
  42. // 在MyHomePage的状态首次初始化时调用mockFetch()函数。
  43. mockFetch();
  44. // 设置监听器。
  45. _scrollController.addListener(() {
  46. if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent &&
  47. !loading) {
  48. mockFetch();
  49. }
  50. });
  51. }
  52. @override
  53. void dispose() {
  54. super.dispose();
  55. _scrollController.dispose();
  56. }
  57. @override
  58. Widget build(BuildContext context) {
  59. // 此处返回Home页面的UI。
  60. }
  61. }

现在,我理解你希望在首页中根据滚动方向显示或隐藏底部导航栏。你修改了menu.dart中的导航栏部分,并尝试从首页获取_scrollController变量以使AnimatedBuilder工作。你已经尝试了不同的策略,但没有成功。

要从home.dart中获取_scrollController变量并在menu.dart中使用它,可以考虑将_scrollController作为参数传递给Menu类,或者使用全局状态管理来共享该变量。一个常见的全局状态管理解决方案是使用Provider或GetX包。

这样,你可以在Menu类中访问_scrollController,并将其传递给AnimatedBuilder。不过,由于这需要更多的代码更改,我建议你查阅Provider或GetX的文档,以了解如何在你的应用程序中实现全局状态管理,以便共享_scrollController和其他状态。

英文:

In my app a Menu class with a bottom navigation bar is charged to display the different pages of the app through the IndexedStack widget in order to save the state of each page:

menu.dart

  1. class Menu extends StatefulWidget {
  2. @override
  3. _MenuState createState() =&gt; _MenuState();
  4. }
  5. class _MenuState extends State&lt;Menu&gt; {
  6. final List&lt;Widget&gt; pages = [Home(), Profile()];
  7. int _selectedIndex = 0;
  8. void callbackFunction(int index) {
  9. setState(() {
  10. _selectedIndex = index;
  11. });
  12. }
  13. @override
  14. Widget build(BuildContext context) {
  15. return Scaffold(
  16. appBar: AppBar(
  17. title: Text(&#39;Save State&#39;),
  18. ),
  19. body: IndexedStack(
  20. index: _selectedIndex,
  21. children: pages,
  22. ),
  23. bottomNavigationBar: BottomNavigationBar(
  24. items: [
  25. BottomNavigationBarItem(
  26. icon: Icon(Icons.home),
  27. label: &#39;Home&#39;,
  28. ),
  29. BottomNavigationBarItem(
  30. icon: Icon(Icons.person),
  31. label: &#39;Profile&#39;,
  32. ),
  33. ],
  34. currentIndex: _selectedIndex,
  35. onTap: (index){
  36. setState(() {
  37. _selectedIndex = index;
  38. });
  39. },
  40. ),
  41. );
  42. }
  43. }

home.dart

  1. class Home extends StatefulWidget {
  2. Home({Key? key}) : super(key: key);
  3. @override
  4. State&lt;Home&gt; createState() =&gt; _HomeState();
  5. }
  6. class _HomeState extends State&lt;Home&gt; {
  7. final ScrollController _scrollController = ScrollController();
  8. // The item list where the data is stored.
  9. List&lt;String&gt; items = [];
  10. // Flags used to check the loading status.
  11. bool loading = false, allLoaded = false;
  12. // Function that loads data in the item list.
  13. mockFetch() async {
  14. // No more data to load.
  15. if (allLoaded) {
  16. return;
  17. }
  18. // Set the loading flag to true to prevent this function to be called several times.
  19. setState(() {
  20. loading = true;
  21. });
  22. // Simulates the delay that might occurs during an API call.
  23. await Future.delayed(Duration(milliseconds: 500));
  24. // If the item list is higher or equal to 60, an empty array is returned to simulate
  25. // the end of the data stream. If not, 20 more items are generated.
  26. List&lt;String&gt; newData = items.length &gt;= 60 ? [] : List.generate(20, (index) =&gt; &quot;List Item ${index + items.length}&quot;);
  27. // Add the new data to the item list.
  28. if (newData.isNotEmpty) {
  29. items.addAll(newData);
  30. }
  31. // Reset the flags.
  32. setState(() {
  33. loading = false;
  34. // Returns false if the array is empty.
  35. allLoaded = newData.isEmpty;
  36. });
  37. }
  38. @override
  39. void initState() {
  40. super.initState();
  41. // Call the mockFetch() function when the state of MyHomePage is initialized for the first time.
  42. mockFetch();
  43. // Set a listener.
  44. _scrollController.addListener(() {
  45. if (_scrollController.position.pixels &gt;= _scrollController.position.maxScrollExtent &amp;&amp; !loading) {
  46. mockFetch();
  47. }
  48. });
  49. }
  50. @override
  51. void dispose() {
  52. super.dispose();
  53. _scrollController.dispose();
  54. }
  55. @override
  56. Widget build(BuildContext context) {
  57. return Scaffold(
  58. body: LayoutBuilder(builder: (context, constraints) {
  59. if (items.isNotEmpty) {
  60. return Stack(
  61. // Dispay the data.
  62. children: [
  63. ListView.separated(
  64. // Assign the scroll controller to the ListView.
  65. controller: _scrollController,
  66. itemBuilder: (context, index) {
  67. // Display the newly loaded items.
  68. if (index &lt; items.length) {
  69. return ListTile(
  70. title: Text(items[index]),
  71. );
  72. }
  73. // An extra item has been added to the index of the list meaning that
  74. // all the items have been loaded.
  75. else {
  76. // Inform the user that there is no more data to load.
  77. return Container(
  78. width: constraints.maxWidth,
  79. height: 50,
  80. child: Center(
  81. child: Text(&quot;Nothing more to load&quot;),
  82. ),
  83. );
  84. }
  85. },
  86. // Add a separator between each item.
  87. separatorBuilder: (context, index) {
  88. return Divider(height: 1);
  89. },
  90. // Add an extra item to the length of the list when all of the items are loaded.
  91. itemCount: items.length + (allLoaded ? 1 : 0)),
  92. if (loading)...[
  93. // Display a progress indicator at the bottom of the list whenever some data is loading.
  94. Positioned(
  95. left: 0,
  96. bottom: 0,
  97. child: Container(
  98. width: constraints.maxWidth,
  99. height: 80,
  100. child: Center(
  101. child: CircularProgressIndicator(),
  102. ),
  103. )
  104. )
  105. ]
  106. ],
  107. );
  108. }
  109. else {
  110. return Container(
  111. child: Center(
  112. child: CircularProgressIndicator(),
  113. ),
  114. );
  115. }
  116. }
  117. ),
  118. ); // Scaffold
  119. }
  120. }

Now the thing is I want my bottom navigation bar to show or hide according to the scrolling direction into the home page.
So I modified the navigation bar in menu.dart:

  1. bottomNavigationBar: AnimatedBuilder(
  2. animation: _scrollController,
  3. builder: (context, child) {
  4. return AnimatedContainer(
  5. duration: Duration(milliseconds: 300),
  6. height: _scrollController.hasClients &amp;&amp; _scrollController.position.userScrollDirection == ScrollDirection.reverse ? 0: kBottomNavigationBarHeight,
  7. child: child,
  8. );
  9. },
  10. child: SingleChildScrollView(
  11. physics: const NeverScrollableScrollPhysics(),
  12. child: BottomNavigationBar(
  13. items: [
  14. BottomNavigationBarItem(
  15. icon: Icon(Icons.home),
  16. label: &#39;Home&#39;,
  17. ),
  18. BottomNavigationBarItem(
  19. icon: Icon(Icons.person),
  20. label: &#39;Profile&#39;,
  21. ),
  22. ],
  23. currentIndex: _selectedIndex,
  24. onTap: (index){
  25. setState(() {
  26. _selectedIndex = index;
  27. });
  28. },
  29. ),
  30. ),

),

But of course I need to retrieve the _scrollController variable from the home page to get the AnimatedBuilder to work.
I've tried different strategies (callback functions, getters...) but none of them have worked so far.

Is there a way to get the _scrollController variable from the menu widget or should I consider a different approach ?

答案1

得分: 1

You need a connection from Home back to _MenuState.

你需要在Home与_MenuState之间建立连接。

What your _MenuState needs to know, could be contained in a variable:

_MenuState需要知道的内容可以包含在一个变量中:

bool isNavigationBarVisible = true;

bool isNavigationBarVisible = true;

In your AnimatedContainer, make the height dependend on that:

在你的AnimatedContainer中,使高度依赖于此:

height: isNavigationBarVisible ? kBottomNavigationBarHeight : 0,

height: isNavigationBarVisible ? kBottomNavigationBarHeight : 0,

Add a callback function to the _MenuState:

在_MenuState中添加一个回调函数:

void callbackNavigationBarVisible(bool visible) {
setState(() {
isNavigationBarVisible = visible;
});

void callbackNavigationBarVisible(bool visible) {
setState(() {
isNavigationBarVisible = visible;
});

Change your Home:

修改你的Home:

class Home extends StatefulWidget {

class Home extends StatefulWidget {

Home({Key? key, required this.callback}) : super(key: key);
final void Function(bool visible) callback;

  1. @override
  2. State<Home> createState() => _HomeState();
  3. }

When you instantiate Home, hand over the callback:

当你实例化Home时,传递回调函数:

final List pages = [Home(callback: callbackNavigationBarVisible), Profile()];

final List pages = [Home(callback: callbackNavigationBarVisible), Profile()];

In your _HomeState change this:

在你的_HomeState中更改这个部分:

_scrollController.addListener(() {
if (_scrollController.hasClients && _scrollController.position.userScrollDirection == ScrollDirection.reverse) {
widget.callback(false);
} else {
widget.callback(true);
}
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent && !loading) {
mockFetch();
}
});

英文:

You need a connection from Home back to _MenuState.

What your _MenuState needs to know, could be contained in a variable:

  1. bool isNavigationBarVisible = true;

In your AnimatedContainer, make the height dependend on that:

  1. height: isNavigationBarVisible ? kBottomNavigationBarHeight : 0,

Add a callback function to the _MenuState:

  1. void callbackNavigationBarVisible(bool visible) {
  2. setState(() {
  3. isNavigationBarVisible = visible;
  4. });

Change your Home:

  1. class Home extends StatefulWidget {
  2. Home({Key? key, reuired, required this.callback}) : super(key: key);
  3. final void Function(bool visible) callback;
  4. @override
  5. State&lt;Home&gt; createState() =&gt; _HomeState();
  6. }

When you instantiate Home, hand over the callback:

  1. final List&lt;Widget&gt; pages = [Home(callback: callbackNavigationBarVisible), Profile()];

In your _HomeState change this:

  1. _scrollController.addListener(() {
  2. if(_scrollController.hasClients &amp;&amp; _scrollController.position.userScrollDirection == ScrollDirection.reverse){
  3. widget.callback(false);
  4. }else{
  5. widget.callback(true);
  6. }
  7. if (_scrollController.position.pixels &gt;= _scrollController.position.maxScrollExtent &amp;&amp; !loading) {
  8. mockFetch();
  9. }
  10. });

答案2

得分: 0

Thanks to @Stephan的解释,我成功设置了传递首页滚动控制器的方式。

在Menu类中:

  1. class _MenuState extends State<Menu> {
  2. ScrollController _scrollController = ScrollController(); // 初始化新的滚动控制器
  3. void callbackSetScrollController(scrollController) { // 获取首页滚动控制器
  4. setState(()
  5. _scrollController = scrollController;
  6. });
  7. }

将页面列表变量替换为:

  1. final List<Widget> pages = [Home(callback: callbackSetScrollControlle), Search(), Profile()];

用以下方式替换:

  1. body: IndexedStack(
  2. index: _selectedIndex,
  3. children: [Home(callback: callbackSetScrollController), Search(), Profile()],
  4. ),

否则会出现错误:

错误:无法在字段初始化程序中访问'this'以读取'callback'。

最后,在Home类中:

  1. class Home extends StatefulWidget {
  2. final Function (ScrollController scrollController) callback;
  3. Home({ Key? key, required this.callback }) : super(key: key);
  4. ...
  5. void initState() {
  6. super.initState();
  7. mockFetch();
  8. _scrollController.addListener(() {
  9. if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent && !loading) {
  10. mockFetch();
  11. }
  12. widget.callback(_scrollController); // <= 将滚动控制器传递给Menu小部件。
  13. });
  14. }
  15. ...
英文:

Thanks to @Stephan's explanation I managed to set up a way to pass the home page scroll controller.

In the Menu class:

  1. class _MenuState extends State&lt;Menu&gt; {
  2. ScrollController _scrollController = ScrollController(); // Initializes a new scroll controller
  3. void callbackSetScrollController(scrollController) { // Retrieves the Home scroll controller
  4. setState(()
  5. _scrollController = scrollController;
  6. });
  7. }

Replace the page list variable:

  1. final List&lt;Widget&gt; pages = [Home(callback: callbackSetScrollControlle), Search(), Profile()];

with

  1. body: IndexedStack(
  2. index: _selectedIndex,
  3. children: [Home(callback: callbackSetScrollController), Search(), Profile()],
  4. ),

or an error will occur:

> Error: Can't access 'this' in a field initializer to read 'callback'

And finally in the Home class:

  1. class Home extends StatefulWidget {
  2. final Function (ScrollController scrollController) callback;
  3. Home({ Key? key, required this.callback }) : super(key: key);
  4. ...
  5. void initState() {
  6. super.initState();
  7. mockFetch();
  8. _scrollController.addListener(() {
  9. if (_scrollController.position.pixels &gt;= _scrollController.position.maxScrollExtent &amp;&amp; !loading) {
  10. mockFetch();
  11. }
  12. widget.callback(_scrollController); // &lt;= Passes the scroll controller to the Menu widget.
  13. });
  14. }
  15. ...

huangapple
  • 本文由 发表于 2023年5月6日 17:11:35
  • 转载请务必保留本文链接:https://go.coder-hub.com/76188082.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定