英文:
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() => _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();
// The item list where the data is stored.
List<String> 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<String> newData = items.length >= 60 ? [] : List.generate(20, (index) => "List Item ${index + items.length}");
// 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 >= _scrollController.position.maxScrollExtent && !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 < 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("Nothing more to load"),
),
);
}
},
// 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 && _scrollController.position.userScrollDirection == ScrollDirection.reverse ? 0: kBottomNavigationBarHeight,
child: child,
);
},
child: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Profile',
),
],
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
final List
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<Home> createState() => _HomeState();
}
When you instantiate Home, hand over the callback:
final List<Widget> pages = [Home(callback: callbackNavigationBarVisible), Profile()];
In your _HomeState change this:
_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();
}
});
答案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<Menu> {
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<Widget> 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 >= _scrollController.position.maxScrollExtent && !loading) {
mockFetch();
}
widget.callback(_scrollController); // <= Passes the scroll controller to the Menu widget.
});
}
...
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论