I have a parent widget and a child widget which does not hold any state.
From my understanding, initState function will be called every time before the page is rendered (Correct me if i am wrong).
So in my case, when i render my parent widget, the initState function in my child widget is not working.
Can someone please explain why this is the case, and what is the right way to do it?
My code:
class HomePage extends StatefulWidget {
HomePage({Key? key}) : super(key: key);
#override
_HomePageState createState() =>
_HomePageState();
}
class _HomePageState extends State<HomePage>
with AutomaticKeepAliveClientMixin {
int _balanceIndex = 0;
List<HomeBalanceCardModel> _balanceCardInfos =
HomeBalanceCardInfos.getMockData();
_onBalancePageChange(index, _) {
setState(() {
_balanceIndex = index;
});
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
backgroundColor: Colors.white,
centerTitle: false,
pinned: true,
floating: false,
snap: false,
primary: true,
expandedHeight: 250.0,
title: HomeAppBar(title: "Banking"),
flexibleSpace: FlexibleSpaceBar(
background: HomeBalanceCarousel(
balanceCardInfos: _balanceCardInfos,
onPageChanged: _onBalancePageChange,
)),
),
SliverToBoxAdapter(
child:
Padding(padding: EdgeInsets.only(top: 20), child: HomeAdBanner()),
),
SliverToBoxAdapter(
child:LoanList(),
)
],
));
}
My child Widget:
class LoanList extends StatefulWidget {
const LoanList({Key? key,}) : super(key: key);
#override
_LoanListState createState() => _LoanListState();
}
class _LoanListState extends State<LoanList> {
#override
void initState() {
super.initState();
print('running init state');
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
_buildTab(),
_buildList(),
],
),
);
}
}
Related
When trying to use the NestedScrollView with a ListView inside a different NestedScrollView Flutter throws a stack overflow error:
════════ Exception caught by widgets library ═══════════════════════════════════
The following StackOverflowError was thrown building PrimaryScrollController(no controller):
Stack Overflow
Here's a minimal-ish code where it happens:
import 'package:flutter/material.dart';
void main() async {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const NestedScrollView1();
}
}
class NestedScrollView1 extends StatelessWidget {
const NestedScrollView1({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: NestedScrollView(
physics: const ClampingScrollPhysics(),
headerSliverBuilder: (_, __) => [
SliverToBoxAdapter(
child: Container(
color: Colors.blue,
height: 100,
),
)
],
body: NestedScrollView2(),
),
);
}
}
class NestedScrollView2 extends StatelessWidget {
final ScrollController scrollController = ScrollController();
NestedScrollView2({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return NestedScrollView(
controller: PrimaryScrollController.of(context),
physics: const ClampingScrollPhysics(),
headerSliverBuilder: (ctx, __) => [
SliverToBoxAdapter(
child: Container(
color: Colors.red,
height: 100,
),
),
],
body: const ListOfItems(),
);
}
}
class ListOfItems extends StatelessWidget {
const ListOfItems({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ListView(
physics: const ClampingScrollPhysics(),
// controller: PrimaryScrollController.of(context),
children: [
Container(color: Colors.green, height: 200),
Container(color: Colors.yellow, height: 200),
Container(color: Colors.green, height: 200),
Container(color: Colors.yellow, height: 200),
Container(color: Colors.green, height: 200),
Container(color: Colors.yellow, height: 200),
Container(color: Colors.green, height: 200),
Container(color: Colors.yellow, height: 200),
],
);
}
}
if you uncomment the controller line in ListView - it throws a stack overflow like this:
════════ Exception caught by widgets library ═══════════════════════════════════
The following StackOverflowError was thrown building ListView(scrollDirection: vertical, _NestedScrollController#81c19(inner, one client, offset 0.0), ClampingScrollPhysics, dependencies: [MediaQuery]):
Stack Overflow
The relevant error-causing widget was
ListView
Thing is on my project I have a page with a TabBarView and one of its sections has a TabBarView of it's own, and I wanted to use the NestedScrollView's to hold the tabs inside headerSliverBuilder's. Is there any way to go around this, without telling designer to reconsider the page UI or building complex custom scroll logic?
Edit: for clarity, adding a draw.io screenshot of the layout I'm trying to achieve (cannot put images right into the posts yet, ugh).
(Forgot to post an answer, better late than never I hope)
I have managed to achieve what I needed in a hack-ish solution from my colleague of just using CustomScrollView with the nested TabBar and ListView inside of SliverFillRemaining:
import 'package:flutter/material.dart';
void main() async {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: OuterTabView(),
);
}
}
class OuterTabView extends StatefulWidget {
const OuterTabView({Key? key}) : super(key: key);
#override
State<OuterTabView> createState() => _OuterTabViewState();
}
class _OuterTabViewState extends State<OuterTabView> with TickerProviderStateMixin {
late TabController _tabControllerOut;
#override
void initState() {
super.initState();
_tabControllerOut = TabController(length: 3, vsync: this);
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: NestedScrollView(
headerSliverBuilder: (_, __) {
return <Widget>[
SliverToBoxAdapter(
child: TabBar(
tabs: const [
SizedBox(height: 40),
SizedBox(height: 40),
SizedBox(height: 40),
],
controller: _tabControllerOut,
),
),
];
},
body: TabBarView(
controller: _tabControllerOut,
children: const [
Tab1WithNestedTabView(),
Tab2(),
Tab3(),
],
),
),
),
);
}
}
class Tab1WithNestedTabView extends StatefulWidget {
const Tab1WithNestedTabView({Key? key}) : super(key: key);
#override
State<Tab1WithNestedTabView> createState() => _Tab1WithNestedTabViewState();
}
class _Tab1WithNestedTabViewState extends State<Tab1WithNestedTabView> with TickerProviderStateMixin {
late TabController _tabControllerIn;
#override
void initState() {
_tabControllerIn = TabController(length: 2, vsync: this);
super.initState();
}
#override
Widget build(BuildContext context) {
return CustomScrollView(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
controller: PrimaryScrollController.of(context),
slivers: [
SliverToBoxAdapter(
child: TabBar(
controller: _tabControllerIn,
tabs: const [
SizedBox(height: 40),
SizedBox(height: 40),
],
),
),
SliverFillRemaining(
child: TabBarView(
controller: _tabControllerIn,
children: const [
ItemList(
color1: Colors.green,
color2: Colors.yellow,
),
ItemList(
color1: Colors.tealAccent,
color2: Colors.black54,
),
],
),
),
],
);
}
}
class Tab2 extends StatelessWidget {
const Tab2({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return CustomScrollView(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
controller: PrimaryScrollController.of(context),
slivers: const [
SliverFillRemaining(
child: ItemList(
color1: Colors.blue,
color2: Colors.yellow,
),
),
],
);
}
}
class Tab3 extends StatelessWidget {
const Tab3({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const ItemList(
color1: Colors.deepOrange,
color2: Colors.pinkAccent,
);
}
}
// Sample list
class ItemList extends StatelessWidget {
final Color color1;
final Color color2;
const ItemList({
required this.color1,
required this.color2,
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return ListView.builder(
physics: const ClampingScrollPhysics(),
itemBuilder: (context, index) {
return Container(
alignment: Alignment.center,
color: index.isOdd ? color1 : color2,
height: 200,
child: Text(index.toString()),
);
},
);
}
}
I admit it's not exactly the most graceful way, but worked fine enough for me. If anyone finds a better one - I'll be happy to mark that one as an accepted answer.
class _BottomNavigationBar extends StatelessWidget {
const _BottomNavigationBar({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return SafeArea(
top: false,
bottom: true,
left: true,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_BottomNavigationBar(),
_BottomNavigationBar(),
_BottomNavigationBar(),
_BottomNavigationBar(),
],
),
);
}
}
class _NavigationBarItem extends StatelessWidget {
const _NavigationBarItem({super.key});
#override
Widget build(BuildContext context) {
return SafeArea(child: Text('item'));
}
i am meeting this mistake in this window
i did a lot ways but they didnot work they didnt work
So i have a list of objects with different block_type such as title or text. This what i show in the app:
This is the code :
class AppView extends StatefulWidget {
final List tappedItems;
const AppView({Key? key, required this.tappedItems}) : super(key: key);
#override
_AppViewState createState() => _AppViewState();
}
class _AppViewState extends State<AppView> {
final _scaffoldKey = GlobalKey<ScaffoldState>();
final List listOfItems = [
{"block_type": "title", "block_data": "Books"},
{"block_type": "text", "block_data": "This is the textblock of books"},
{"block_type": "title", "block_data": "Publishers"},
{"block_type": "text", "block_data": "This is the textblock of publishers"},
];
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
key: _scaffoldKey,
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
leading: IconButton(
icon: const Icon(Icons.menu,
size: 40), // change this size and style
onPressed: () => _scaffoldKey.currentState?.openDrawer(),
),
actions: const [
Padding(
padding: EdgeInsets.fromLTRB(5, 20, 80, 5),
)
],
pinned: false,
expandedHeight: 100,
toolbarHeight: 100,
snap: true,
floating: true,
flexibleSpace: FlexibleSpaceBar(),
),
const SliverToBoxAdapter(),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text(
jsonEncode(listOfItems),
style: TextStyle(fontSize: 40, color: Colors.black),
),
);
},
childCount: 1,
),
),
],
),
drawer: const AppMenu(),
),
);
}
}
I also have different widgets for the blocks TitleBlock and TextBlock. This is an example for TitleBlock:
import 'package:flutter/cupertino.dart';
class TitleBlock extends StatefulWidget {
const TitleBlock({Key? key}) : super(key: key);
#override
_TitleBlockState createState() => _TitleBlockState();
}
class _TitleBlockState extends State<TitleBlock> {
#override
Widget build(BuildContext context) {
return Container();
}
}
I want show these seperate block widgets such as TitleBlock and TextBlock based on the block_type of the list of objects. I want to loop through the objects because this list of objects i showed is just an example. In my use case i can have different list of objects.
I want to show the blocks TitleBlock and TextBlock like this:
SliverChildBuilderDelegate gives you access to the builder, just like ListView.builder
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(
title: Text(listOfItems[index]['block_type']),
subtitle: Text(listOfItems[index]['block_data']),
);
},
childCount: listOfItems.length
),
),
Based on your object's block_type you can call in different widgets with a ternary operator like in place of you ListTile, you can use the following,
listOfItems[index]["block_type"] == "title" ? TitleBlock(listOfItems[index]) : TextBlock(listOfItems[index])
And you can use the object i.e., Map<String, String> in the StatefulWidget (TitleBlock or TextBlock) to add any further info like,
class TitleBlock extends StatefulWidget {
const TitleBlock({Key? key, required this.dataMap}) : super(key: key);
final Map<String, String> dataMap;
#override
_TitleBlockState createState() => _TitleBlockState();
}
class _TitleBlockState extends State<TitleBlock> {
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(widget.dataMap["block_type"]),
subtitle: Text(widget.dataMap["block_data"]),
);
}
}
class TextBlock extends StatefulWidget {
const TextBlock({Key? key, required this.dataMap}) : super(key: key);
final Map<String, String> dataMap;
#override
_TextBlockState createState() => _TextBlockState();
}
class _TextBlockState extends State<TextBlock> {
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(widget.dataMap["block_type"]),
subtitle: Text(widget.dataMap["block_data"]),
);
}
}
I want to implement something like an Instagram profile page with the NestedScrollView widget. So I combine this with a TabBar widget. I have two tabs. Detail and comments. Then I have other widgets on top of the TabBar. Everything works as expected so far. But the problem starts when I scroll. Even though there is nothing inside my tabs, NestedScrollView scrolls too much, and my TabBar widget comes to the top of the screen. In Instagram, if you have no photos, you can not scroll the page. But in my application, I can scroll. And this is the behavior I want to prevent. How can I do this? I also share my codes and screenshots of the unwanted behavior.
This is the page
This is the over scroll even though there is nothing to show inside the tab bar
These are the codes:
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Home Page"),
),
body: DefaultTabController(
length: 2,
child: NestedScrollView(
headerSliverBuilder: (context, isScrolled) {
return [
SliverToBoxAdapter(
child: Container(
margin: const EdgeInsets.all(16),
child: const Placeholder(
fallbackHeight: 300,
fallbackWidth: double.infinity,
color: Colors.amberAccent,
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: StickyTabBarDelegate(
child: const TabBar(
labelColor: Colors.black,
tabs: [
Tab(text: "Detail"),
Tab(text: "Comments"),
],
),
),
)
];
},
body: const TabBarView(
children: [
DetailTab(),
CommentsTab(),
],
),
),
),
);
}
}
class DetailTab extends StatelessWidget {
const DetailTab({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container();
}
}
class CommentsTab extends StatelessWidget {
const CommentsTab({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container();
}
}
class StickyTabBarDelegate extends SliverPersistentHeaderDelegate {
final TabBar child;
StickyTabBarDelegate({required this.child});
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Colors.white,
child: child,
);
}
#override
double get maxExtent => child.preferredSize.height;
#override
double get minExtent => child.preferredSize.height;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
In continuation with question
The solution provided above is good. But hard for me to implement in my project.
Expected results:
I've created two tabs.
In each tab I have SingleChildScrollView wrapped with Scrollbar.
I can not have the primary scrollcontroller in both the tabs, because that throws me exception: "ScrollController attached to multiple scroll views."
For Tab ONE I use primary scrollcontroller, for Tab TWO I created Scrollcontroller and attached it.
Widgets in both the tabs should be scrollabale using keyboard and mouse.
Actual results:
For Tab ONE with primary scrollcontroller I can scroll both by keyboard and dragging scrollbar.
But for Tab TWO with non primary scrollcontroller, I have to scroll only by dragging scrollbar. This tab doesn't respond to keyboard page up /down keys.
When keyboard keys are used in Tab TWO actually contents of tab ONE are getting scrolled.
Check code:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: TabExample(),
);
}
}
class TabExample extends StatefulWidget {
const TabExample({Key key}) : super(key: key);
#override
_TabExampleState createState() => _TabExampleState();
}
class _TabExampleState extends State<TabExample> {
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Text('Tab ONE')),
Tab(icon: Text('Tab TWO')),
],
),
title: Text('Tabs Demo'),
),
body: TabBarView(
children: [
WidgetC(),
WidgetD(),
],
),
),
);
}
}
class WidgetC extends StatefulWidget {
const WidgetC({Key key}) : super(key: key);
#override
_WidgetCState createState() => _WidgetCState();
}
class _WidgetCState extends State<WidgetC>
with AutomaticKeepAliveClientMixin<WidgetC> {
List<Widget> children;
#override
void initState() {
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.blue,
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetC'),
isAlwaysShown: true,
showTrackOnHover: true,
child: SingleChildScrollView(
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}
class WidgetD extends StatefulWidget {
const WidgetD({Key key}) : super(key: key);
#override
_WidgetDState createState() => _WidgetDState();
}
class _WidgetDState extends State<WidgetD>
with AutomaticKeepAliveClientMixin<WidgetD> {
List<Widget> children;
ScrollController _scrollController;
#override
void initState() {
_scrollController = ScrollController();
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.green,
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetD'),
isAlwaysShown: true,
showTrackOnHover: true,
controller: _scrollController,
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}
This has been accepted as a bug in flutter.
Pl follow for progress here: https://github.com/flutter/flutter/issues/83711
Note for other developers facing same issue.
To overcome the mentioned problem, I changed my design layout. Instead of tabbar view I used Navigationrail widget. This solved my problem.
NavigationRail widget allowed me to attach primary scroll controller to multiple widgets without giving me exception: "ScrollController attached to multiple scroll views."
Sample code.
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
/// This is the main application widget.
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
/// This is the stateful widget that the main application instantiates.
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _selectedIndex = 0;
WidgetC _widgetC = WidgetC();
WidgetD _widgetD = WidgetD();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('NavigationRail Demo'), centerTitle: true),
body: Row(
children: <Widget>[
NavigationRail(
elevation: 8.0,
selectedIndex: _selectedIndex,
onDestinationSelected: (int index) {
setState(() {
_selectedIndex = index;
});
},
labelType: NavigationRailLabelType.all,
groupAlignment: 0.0,
destinations: const <NavigationRailDestination>[
NavigationRailDestination(
icon: Icon(Icons.favorite_border),
selectedIcon: Icon(Icons.favorite),
label: Text('Tab ONE'),
),
NavigationRailDestination(
icon: Icon(Icons.bookmark_border),
selectedIcon: Icon(Icons.book),
label: Text('Tab TWO'),
),
],
),
const VerticalDivider(thickness: 1, width: 1),
// This is the main content.
Expanded(
child: _getPageAtIndex(_selectedIndex),
)
],
),
);
}
Widget _getPageAtIndex(int index) {
switch (index) {
case 0:
return _widgetC;
case 1:
return _widgetD;
}
return Container();
}
}
class WidgetC extends StatefulWidget {
const WidgetC({Key key}) : super(key: key);
#override
_WidgetCState createState() => _WidgetCState();
}
class _WidgetCState extends State<WidgetC>
with AutomaticKeepAliveClientMixin<WidgetC> {
List<Widget> children;
#override
void initState() {
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetC'),
isAlwaysShown: true,
showTrackOnHover: true,
child: SingleChildScrollView(
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}
class WidgetD extends StatefulWidget {
const WidgetD({Key key}) : super(key: key);
#override
_WidgetDState createState() => _WidgetDState();
}
class _WidgetDState extends State<WidgetD>
with AutomaticKeepAliveClientMixin<WidgetD> {
List<Widget> children;
// ScrollController _scrollController;
#override
void initState() {
// _scrollController = ScrollController();
children = [];
for (int i = 0; i < 20; i++) {
children.add(
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Container(
height: 100,
width: double.infinity,
color: Colors.primaries[Random().nextInt(Colors.primaries.length)],
child: Center(child: Text('$i')),
),
),
);
}
super.initState();
}
#override
void dispose() {
// _scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
return Scrollbar(
key: PageStorageKey('WidgetD'),
isAlwaysShown: true,
showTrackOnHover: true,
// controller: _scrollController,
child: SingleChildScrollView(
// controller: _scrollController,
child: Column(
children: children,
),
),
);
}
#override
bool get wantKeepAlive => true;
}