How to have Stateful Widgets in tabBarView? - flutter

I had two Stateless Widgets in my Tab Bar View and my app was working fine. But then there was a requirement to make a button in one of the views and change the state of the button hence I made it as Stateful widgets but now the Tab Bar View doesn't accept it and gives the exception on running that ' type 'task' is not a subtype of type 'StatelessWidget''. Can we have only Stateless Widgets in TabBar views? How can i Fix this?
My main.dart file having the TabBarview
import 'package:flutter/material.dart';
import 'package:flutter_convertor/task.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: DefaultTabController(length: 2,child: MyHomePage(title: '')),
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
Widget build(BuildContext context){
final list = ListView.builder(
itemBuilder: (context, position) {
//some other implementation
},
);
return Scaffold(
appBar: AppBar(
title: Text('Home'), bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_transit)),
]),
),
body: TabBarView(children: [list, task()]));
}
}
My Stateful widget task
class task extends StatefulWidget{
#override
taskState createState() => new taskState();
}
class taskState extends State<task> {
int current_step = 0;
bool isButtonDisabled;
#override void initState() {
super.initState();
isButtonDisabled = false;
}
formReady(){
setState(() {
isButtonDisabled = !isButtonDisabled ;
});
}
#override
Widget build(BuildContext context) {
Column taskScreen = Column(
children: <Widget>[Expanded(
child: ListView(
children: <Widget>[
//other implementation
FlatButton(
color: Colors.red,
textColor: Colors.black,
shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(20.0)),
disabledColor: Color(0XFFf9c3c1),
disabledTextColor: Colors.white,
padding: EdgeInsets.all(8.0),
splashColor: Colors.red[400],
onPressed: isButtonDisabled ? null : _completePage
,
child: Text(
"Completed",
style: TextStyle(color: Colors.white, fontSize: 15.0, fontWeight: FontWeight.bold),
),
)
,
],
)
]);
return taskScreen;
}
}

You need to use the AutomaticKeepAliveClientMixin for this to work. It's a fairly simple mixin to use. You only need to include the
#override
bool get wantKeepAlive => true;
and add super.build(context) to your build method.
Your updated class would look something like this:
class task extends StatefulWidget{
#override
taskState createState() => new taskState();
}
class taskState extends State<task> with AutomaticKeepAliveClientMixin<task>
#override
Widget build(BuildContext context) {
super.build(context);
...
}
}

Related

My screen doesn't reflect the changes in my app though the setState method works well

I'm trying to call a StatefulWidget(i.e FirstPage()) within a MaterialApp. I'm pretty much new to flutter and I don't know where I went wrong. According to my knownledge I've used StatefulWidget to tell flutter my screen on that page is going to encounter some changes in UI. But I got no idea to fix this error.
main.dart file:
import 'package:flutter/material.dart';
import 'package:flutter_project/main.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: FirstPage());
}
}
class FirstPage extends StatefulWidget {
const FirstPage({Key? key}) : super(key: key);
#override
State<FirstPage> createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
#override
Widget build(BuildContext context) {
String buttonName = "Click";
int currentIndex = 0;
return Scaffold(
appBar: AppBar(
title: const Text("App title "),
),
body: Center(
child: currentIndex == 0
? Container(
width: double.infinity,
height: double.infinity,
color: Colors.blueAccent,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 280,
height: 80,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
side: BorderSide.none,
borderRadius: BorderRadius.circular(18),
),
backgroundColor: const Color.fromRGBO(9, 8, 99, 90),
foregroundColor: Colors.red,
),
onPressed: () {
setState(() {
buttonName = "Clicked";
//print(buttonName0);
});
},
child: Text(buttonName),
),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
//'BuildContext' - datatype and 'context' - variable name
return const SecondPage();
},
),
);
},
child: const Text("Move to new page"),
),
],
),
)
: Image.asset("images/img.png"),
),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(label: "Home", icon: Icon(Icons.home)),
BottomNavigationBarItem(label: "Settings", icon: Icon(Icons.settings))
],
currentIndex: currentIndex,
onTap: (int index) {
//index value here is returned by flutter by the function 'onTap'
setState(() {
currentIndex = index;
//print(currentIndex);
});
},
),
); //Scaffold represents the skeleton of the app(displays white page)
}
}
class SecondPage extends StatelessWidget {
const SecondPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
);
}
}
Images:
Before pressing Click and Settings button
After pressing Click and Settings looks the same
I want the screen to change the ElevatedButton Click to Clicked when onPressed() is triggered and also, the app should be able to switch settings page when the onTap() method is triggered in the bottom navigation bar.
The code worked initially when I refrained from calling an entire page of Scaffold from Material app, but as soon as I changed the part
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: FirstPage()); //<-- this part
}
}
I'm getting this error.
Put your variables outside the build method.Else it will reset to default on every build.
It will be like
class _FirstPageState extends State<FirstPage> {
//here
String buttonName = "Click";
int currentIndex = 0;
#override
Widget build(BuildContext context) {
// Not here
return Scaffold(
appBar: AppBar(
More about StatefulWidget

TabBar text not getting updated inside NestedScrollView on setState

In my real project, I'm using TabBar inside SliverPersistentHeader which is wrapped with NestedScrollView. The problem is when I call setState(() {}) the text doesn't update. Tough the actual value is updated. The text gets updated only when I perform scroll.
I know Tab is a const constructor and it is not supposed to be rebuilt on setState(() {}), however, when I run the same example without NestedScrollView and SliverPersistentHeader and the TabBar in AppBar it works as expected.
I've written a demo project showcasing the problem.
main.dart
import 'package:flutter/material.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>
with SingleTickerProviderStateMixin {
int _counter = 0;
TabController _tabController;
ScrollController _scrollController;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
void initState() {
_tabController = TabController(length: 1, vsync: this);
_scrollController = ScrollController();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Tabs Demo'),
),
body: NestedScrollView(
controller: _scrollController,
headerSliverBuilder: (context, innerBoxIsScrolled) {
print('$_counter');
return [
SliverPersistentHeader(
pinned: true,
delegate: SliverPersistentHeaderDelegateImpl(
tabBar: TabBar(
controller: _tabController,
labelColor: Colors.black,
tabs: [
Tab(
text: 'Tab $_counter',
)
],
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5.0),
boxShadow: [
BoxShadow(
color: Colors.grey[350],
offset: Offset(0.0, 1.0),
blurRadius: 0),
],
),
),
)
];
},
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
class SliverPersistentHeaderDelegateImpl
extends SliverPersistentHeaderDelegate {
final TabBar tabBar;
final Decoration decoration;
const SliverPersistentHeaderDelegateImpl({
this.decoration,
#required this.tabBar,
});
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
child: tabBar,
decoration: decoration,
);
}
#override
double get maxExtent => tabBar.preferredSize.height;
#override
double get minExtent => tabBar.preferredSize.height;
#override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}

Use different FABs with different tabs in Flutter, and change buttons while swiping between them?

I'm trying to set up tabs with FABs, like what's pictured in the Material Design guidelines.
I've pretty much got it working, by adding a listener on the TabController and changing my FAB there:
#override
void initState() {
...
_tabController = TabController(
length: 5,
vsync: this,
)..addListener(() {
setState(() {
_fabData = _fabDatas[_tabController.index];
});
});
...
}
#override
Widget build(BuildContext context) {
final fab = _fabData == null
? null
: FloatingActionButton(
isExtended: _fabData.expanded,
tooltip: _fabData.tooltip,
child: Icon(_fabData.icon),
onPressed: () {
_fabData.onPressed(context);
},
);
return Scaffold(
...
floatingActionButton: fab,
...
);
}
The problem is that tab controller listeners seem to be called only when the tab switch has finished completely, and not halfway through. If a user swipes from one tab to another, the tab will slide completely over, then come to a stop, and then the button will change.
Is there a way to trigger this in the middle of the swipe instead?
You do not need with listener, try this:
import 'package:flutter/material.dart';
class TabControllerApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Tabs work',
theme: ThemeData(
primarySwatch: Colors.blue,
),
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> {
final _fabData = [
'Tab1',
'Tab2',
'Tab3',
'Tab4',
'Tab5'
]; // Replace with your OBJECT!!!
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: _fabData.length,
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
bottom: TabBar(
tabs: _fabData
.map((String t) => Tab(
text: t,
))
.toList(),
),
),
body: TabBarView(
children: _fabData.map((String text) {
return Container(
child: Stack(
children: [
Positioned(
bottom: 16,
right: 16,
child: FloatingActionButton(
// TODO USE YOUR Object
isExtended: true, //_fabData.expanded,
//tooltip: _fabData.tooltip,
child: Icon(Icons.bookmark), //Icon(_fabData.icon),
onPressed: () {
//_fabData.onPressed(context);
},
),
)
],
),
);
}).toList()),
));
}
}

flutter drawer to remember the clicked item

I want to remember the item that was clicked in drawer .
I am using the same widget for drawer ( sameDrawerOnly ) in all three widgets ( MyHomePage , FirstPage and SecondPage) and using variable itemClicked to trackthe item that was tapped inside setState . But the conditional formatting is not working.
Here is the code
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
DrawerOnly sameDrawerOnly = DrawerOnly();
class MyApp extends StatelessWidget {
final appTitle = 'Drawer Demo';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: appTitle,
home: MyHomePage(title: appTitle),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
MyHomePage({Key key, this.title}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(child: Text('My Page!')),
drawer: sameDrawerOnly,
);
}
}
class DrawerOnly extends StatefulWidget {
const DrawerOnly ({
Key key,
}) : super(key: key);
#override
_DrawerOnlyState createState() => _DrawerOnlyState();
}
class _DrawerOnlyState extends State<DrawerOnly > {
int itemClicked = 0;
#override
Widget build(BuildContext ctxt) {
return Drawer(
child: new ListView(
children: <Widget>[
new DrawerHeader(
child: new Text("DRAWER HEADER.."),
decoration: new BoxDecoration(
color: Colors.orange
),
),
new ListTile(
title: new Text("Item => A", style: itemClicked==1 ? TextStyle( fontWeight: FontWeight.bold, color: Colors.red.withOpacity(0.6) ) : null),
onTap: () {
Navigator.pop(ctxt);
setState(() {
itemClicked=1;
});
Navigator.push(ctxt,
new MaterialPageRoute(builder: (ctxt) => new FirstPage()));
},
),
new ListTile(
title: new Text("Item => 2", style: itemClicked==2 ? TextStyle( fontWeight: FontWeight.bold , color: Colors.green.withOpacity(0.6) ) : TextStyle()),
onTap: () {
Navigator.pop(ctxt);
setState(() {
itemClicked=2;
});
Navigator.push(ctxt,
new MaterialPageRoute(builder: (ctxt) => new SecondPage()));
},
),
],
)
);
}
}
class FirstPage extends StatelessWidget {
#override
Widget build(BuildContext ctxt) {
return new Scaffold(
drawer: sameDrawerOnly,
appBar: new AppBar(title: new Text("First Page"),),
body: new Text("I belongs to First Page"),
);
}
}
class SecondPage extends StatelessWidget {
#override
Widget build(BuildContext ctxt) {
return new Scaffold(
drawer: sameDrawerOnly,
appBar: new AppBar(title: new Text("Second Page"),),
body: new Text("I belongs to Second Page"),
);
}
}
What went wrong
Although sameDrawerOnly was declared at the top most part of your file. Everytime the widget re-draws your app's screens, eg. opening FirstPage via MaterialPageRoute, the variable in the DrawerOnly widget will always stay to zero. Because it is always re-drawn based on your configuration.
What you can do
Hotfix: Make itemClicked a static variable. (Not Recommended)
// Before
int itemClicked
// After
static int itemClicked
Alternatively, you can refactor your code and use PageView instead of opening a new Scaffold widget every time you switch between drawer items. Then, you can now use currentPageValue to determine what item was selected by the user.
MyHomePage.dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final appTitle = 'Drawer Demo';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: appTitle,
home: MyHomePage(title: appTitle),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
MyHomePage({Key key, this.title}) : super(key: key);
#override
createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
PageController _pageController;
double currentPageValue = 0.0;
#override
void initState() {
super.initState();
_pageController = PageController();
_pageController.addListener(() {
setState(() {
currentPageValue = _pageController.page;
// Do whatever you like with the page value
});
});
}
#override
void dispose() {
_pageController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: PageView(
controller: _pageController,
children: <Widget>[
FirstPage(),
SecondPage(),
],
),
),
drawer: Drawer(
// Add a ListView to the drawer. This ensures the user can scroll
// through the options in the drawer if there isn't enough vertical
// space to fit everything.
child: ListView(
// Important: Remove any padding from the ListView.
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
child: Text('Drawer Header'),
decoration: BoxDecoration(
color: Colors.blue,
),
),
ListTile(
title: Text('Item 1'),
onTap: () {
_pageController.jumpToPage(0);
Navigator.pop(context);
},
),
ListTile(
title: Text('Item 2'),
onTap: () {
_pageController.jumpToPage(1);
Navigator.pop(context);
},
),
],
),
),
);
}
}
class FirstPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(color: Colors.red);
}
}
class SecondPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(color: Colors.yellow);
}
}
View on dartpad.dev.
More on:
https://flutter.dev/docs/cookbook/design/drawer

Hero on Tab behavior after flutter version upgrade

After upgrading Flutter from 1.7.8 (I have tested 1.9.1 and 1.10.2), there is some strange behavior of hero for me... I have hero image on the main page. On the second/detail page I have two tabs, first with hero, second empty.
(GoBack)From the first tab hero animation is working as expected, but (GoBack) from the second empty tab, image on the first page disappears. Is this expected behavior in newer versions?
[After some testing this hero tab behavior is from ˆ1.9.0 version, last ok 1.8.4]
Test code:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Hero Test'),
);
}
}
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) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Hero(
tag: "hero",
child: Image.network("https://dummyimage.com/200x200/ff0329/fff"),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailPage()),
);
},
child: Icon(Icons.add),
),
);
}
}
class DetailPage extends StatefulWidget {
#override
_DetailPageState createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage>
with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
_tabController = TabController(vsync: this, length: 2);
super.initState();
}
#override
void dispose() {
// TODO: implement dispose
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Route"),
bottom: TabBar(controller: _tabController, tabs: [
Tab(text: "1"),
Tab(text: "2"),
])),
body: TabBarView(controller: _tabController, children: <Widget>[
Column(
children: <Widget>[
Hero(
tag: "hero",
child: Image.network("https://dummyimage.com/200x200/ff0329/fff"),
),
],
),
Text(""),
]),
);
}
}
Bug... fixed in version v1.9.1+hotfix.6
more info https://github.com/flutter/flutter/issues/40239