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

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

Retrieve a widget's scrollController into another widget?

问题

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

menu.dart

class Menu extends StatefulWidget {
  @override
  _MenuState createState() => _MenuState();
}

class _MenuState extends State<Menu> {
  final List<Widget> pages = [Home(), Profile()];
  int _selectedIndex = 0;

  void callbackFunction(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Save State'),
      ),
      body: IndexedStack(
        index: _selectedIndex,
        children: pages,
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: 'Profile',
          ),
        ],
        currentIndex: _selectedIndex,
        onTap: (index) {
          setState(() {
            _selectedIndex = index;
          });
        },
      ),
    );
  }
}

home.dart

class Home extends StatefulWidget {
  Home({Key? key}) : super(key: key);

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  final ScrollController _scrollController = ScrollController();
  // 数据存储在item列表中。
  List<String> items = [];
  // 用于检查加载状态的标志。
  bool loading = false, allLoaded = false;

  // 加载数据到item列表的函数。
  mockFetch() async {
    // 没有更多数据可加载。
    if (allLoaded) {
      return;
    }

    // 将loading标志设置为true,以防止多次调用此函数。
    setState(() {
      loading = true;
    });

    // 模拟API调用期间可能发生的延迟。
    await Future.delayed(Duration(milliseconds: 500));

    // 如果item列表大于或等于60,将返回一个空数组以模拟数据流的结束。否则,将生成20个额外的项。
    List<String> newData = items.length >= 60
        ? []
        : List.generate(20, (index) => "List Item ${index + items.length}");

    // 将新数据添加到item列表中。
    if (newData.isNotEmpty) {
      items.addAll(newData);
    }

    // 重置标志。
    setState(() {
      loading = false;
      // 如果数组为空,则返回false。
      allLoaded = newData.isEmpty;
    });
  }

  @override
  void initState() {
    super.initState();
    // 在MyHomePage的状态首次初始化时调用mockFetch()函数。
    mockFetch();
    // 设置监听器。
    _scrollController.addListener(() {
      if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent &&
          !loading) {
        mockFetch();
      }
    });
  }

  @override
  void dispose() {
    super.dispose();
    _scrollController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // 此处返回Home页面的UI。
  }
}

现在,我理解你希望在首页中根据滚动方向显示或隐藏底部导航栏。你修改了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

class Menu extends StatefulWidget {
@override
_MenuState createState() =&gt; _MenuState();
}
class _MenuState extends State&lt;Menu&gt; {
final List&lt;Widget&gt; pages = [Home(), Profile()];
int _selectedIndex = 0;
void callbackFunction(int index) {
setState(() {
_selectedIndex = index;
}); 
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(&#39;Save State&#39;),
),  
body: IndexedStack(
index: _selectedIndex,
children: pages,
),  
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: &#39;Home&#39;,
),  
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: &#39;Profile&#39;,
),  
],  
currentIndex: _selectedIndex,
onTap: (index){
setState(() {
_selectedIndex = index;
});
},
),
);
}
}

home.dart

class Home extends StatefulWidget {
Home({Key? key}) : super(key: key);
@override
State&lt;Home&gt; createState() =&gt; _HomeState();
}
class _HomeState extends State&lt;Home&gt; {
final ScrollController _scrollController = ScrollController();
// The item list where the data is stored.
List&lt;String&gt; items = [];
// Flags used to check the loading status.
bool loading = false, allLoaded = false;
// Function that loads data in the item list.
mockFetch() async {
// No more data to load.
if (allLoaded) {
return;
}
// Set the loading flag to true to prevent this function to be called several times.
setState(() {
loading = true;
});
// Simulates the delay that might occurs during an API call.
await Future.delayed(Duration(milliseconds: 500));
// If the item list is higher or equal to 60, an empty array is returned to simulate
// the end of the data stream. If not, 20 more items are generated. 
List&lt;String&gt; newData = items.length &gt;= 60 ? [] : List.generate(20, (index) =&gt; &quot;List Item ${index + items.length}&quot;);
// Add the new data to the item list.
if (newData.isNotEmpty) {
items.addAll(newData);
}
// Reset the flags.
setState(() {
loading = false;
// Returns false if the array is empty.
allLoaded = newData.isEmpty;
});
}
@override
void initState() {
super.initState();
// Call the mockFetch() function when the state of MyHomePage is initialized for the first time.
mockFetch();
// Set a listener.
_scrollController.addListener(() {
if (_scrollController.position.pixels &gt;= _scrollController.position.maxScrollExtent &amp;&amp; !loading) {
mockFetch();
}
});
}
@override
void dispose() {
super.dispose();
_scrollController.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: LayoutBuilder(builder: (context, constraints) {
if (items.isNotEmpty) {
return Stack(
// Dispay the data.
children: [
ListView.separated(
// Assign the scroll controller to the ListView.
controller: _scrollController,
itemBuilder: (context, index) {
// Display the newly loaded items.
if (index &lt; items.length) {
return ListTile(
title: Text(items[index]),
);
}
// An extra item has been added to the index of the list meaning that
// all the items have been loaded.
else {
// Inform the user that there is no more data to load.
return Container(
width: constraints.maxWidth,
height: 50,
child: Center(
child: Text(&quot;Nothing more to load&quot;),
),
);
}
},
// Add a separator between each item.
separatorBuilder: (context, index) {
return Divider(height: 1);
},
// Add an extra item to the length of the list when all of the items are loaded.
itemCount: items.length + (allLoaded ? 1 : 0)),
if (loading)...[
// Display a progress indicator at the bottom of the list whenever some data is loading.
Positioned(
left: 0,
bottom: 0,
child: Container(
width: constraints.maxWidth,
height: 80,
child: Center(
child: CircularProgressIndicator(),
),
)
)
]
],
);
}
else {
return Container(
child: Center(
child: CircularProgressIndicator(),
),
);
}
}
),
); // Scaffold
}
}

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:

bottomNavigationBar: AnimatedBuilder(
animation: _scrollController,
builder: (context, child) {
return AnimatedContainer(
duration: Duration(milliseconds: 300),
height: _scrollController.hasClients &amp;&amp; _scrollController.position.userScrollDirection == ScrollDirection.reverse ? 0: kBottomNavigationBarHeight,
child: child,
);
},
child: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: &#39;Home&#39;,
),  
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: &#39;Profile&#39;,
),  
],  
currentIndex: _selectedIndex,
onTap: (index){
setState(() {
_selectedIndex = index;
});
},
),
),

),

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;

  @override
State<Home> createState() => _HomeState();
}

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:

bool isNavigationBarVisible = true;

In your AnimatedContainer, make the height dependend on that:

height: isNavigationBarVisible ? kBottomNavigationBarHeight : 0,

Add a callback function to the _MenuState:

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

Change your Home:

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

When you instantiate Home, hand over the callback:

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

In your _HomeState change this:

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

答案2

得分: 0

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

在Menu类中:

class _MenuState extends State<Menu> {
   ScrollController _scrollController = ScrollController(); // 初始化新的滚动控制器

   void callbackSetScrollController(scrollController) { // 获取首页滚动控制器
     setState(()
       _scrollController = scrollController;
     });
   }

将页面列表变量替换为:

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

用以下方式替换:

body: IndexedStack(
     index: _selectedIndex,
     children: [Home(callback: callbackSetScrollController), Search(), Profile()],
),

否则会出现错误:

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

最后,在Home类中:

class Home extends StatefulWidget {
    final Function (ScrollController scrollController) callback;
    Home({ Key? key, required this.callback }) : super(key: key);

   ...

    void initState() {
      super.initState();
      mockFetch();

     _scrollController.addListener(() {
       if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent && !loading) {
         mockFetch();
       }

       widget.callback(_scrollController);  // <= 将滚动控制器传递给Menu小部件。
     });
   }

   ...
英文:

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

In the Menu class:

class _MenuState extends State&lt;Menu&gt; {
ScrollController _scrollController = ScrollController(); // Initializes a new scroll controller
void callbackSetScrollController(scrollController) { // Retrieves the Home scroll controller
setState(()
_scrollController = scrollController;
});
}

Replace the page list variable:

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

with

   body: IndexedStack(
index: _selectedIndex,
children: [Home(callback: callbackSetScrollController), Search(), Profile()],
),

or an error will occur:

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

And finally in the Home class:

class Home extends StatefulWidget {
final Function (ScrollController scrollController) callback;
Home({ Key? key, required this.callback }) : super(key: key);
...
void initState() {
super.initState();
mockFetch();
_scrollController.addListener(() {
if (_scrollController.position.pixels &gt;= _scrollController.position.maxScrollExtent &amp;&amp; !loading) {
mockFetch();
}
widget.callback(_scrollController);  // &lt;= Passes the scroll controller to the Menu widget.
});
}
...

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:

确定