Related
I am using a bottom navigation bar with 3 pages. One of them (HomePage) has a search icon. Every time I pressed on it, the keyboard appears for barely one second, then disappears and the app rebuilds itself. I tried other solutions online such as putting the future values in initState() but it did not work. Note that I have also implemented a sign up/sign in system. When the user has signed up/ signed in, they will be then redirected to the page which has a bottom navigation bar. (Also, I did some research, and apparently, only android phones are "suffering" from this problem when the flutter app becomes complex. I don't know how much it is true though)
Here is the code for the Bottom Navigation Bar
class BottomNavigationPage extends StatefulWidget{
final AfreecaInvestAccount _afreecaInvestAccount;
_BottomNavigationPageState createState() => _BottomNavigationPageState();
BottomNavigationPage(this._afreecaInvestAccount);
}
class _BottomNavigationPageState extends State<BottomNavigationPage> {
static List<Widget> widgetOptions;
int selectedIndex = 0;
#override
void initState() {
widgetOptions =
[
HomePage(widget._afreecaInvestAccount),
StockSearchPage(),
AccountPage(widget._afreecaInvestAccount)
];
super.initState();
}
void onTabTapped(int index) {
setState(() {
selectedIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: widgetOptions.elementAt(selectedIndex),
bottomNavigationBar: _bottomNavigationBar(),
);
}
BottomNavigationBar _bottomNavigationBar() {
return BottomNavigationBar(
items: const<BottomNavigationBarItem>
[
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),
BottomNavigationBarItem(icon: Icon(Icons.search), label: "Search Stocks"),
BottomNavigationBarItem(icon: Icon(Icons.account_box), label: "Account"),
],
currentIndex: selectedIndex,
onTap: onTabTapped,
);
}
}
Here is the code for the Home Page:
class HomePage extends StatefulWidget {
final AfreecaInvestAccount _afreecaInvestAccount;
#override
_HomePage createState() => _HomePage(_afreecaInvestAccount);
HomePage(this._afreecaInvestAccount);
}
class _HomePage extends State<HomePage> {
Future<List<int>> _futureBPandOSValue;
final AfreecaInvestAccount _afreecaInvestAccount;
FutureBuilder<List<int>> _futureBuilder;
String _accessToken;
List<String> list =
[
"TSLA",
"AMZN",
"GME",
"AAPL"
];
_HomePage(this._afreecaInvestAccount);
#override
void initState() {
// TODO: implement initState
// timer = Timer.periodic(Duration(seconds: 30), (timer) {build(context);});
_accessToken = _afreecaInvestAccount.accessToken;
_futureBPandOSValue = _futureBPAndOSValues(_accessToken);
_futureBuilder = FutureBuilder<List<int>> (
future: _futureBPandOSValue,
builder: _BPandOSValueBuilder,
);
super.initState();
}
void buildWidget() {
setState(() {
});
}
#override
Widget build(BuildContext context) {
// String _selectedItem;
return Scaffold(
appBar: AppBar(
title: Text('Home'),
automaticallyImplyLeading: false,
actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {
showSearch(context: context, delegate: StockSearch());
},
)
],
),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_futureBuilder,
SizedBox(height: 10.0,),
Card(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _returnStocksTable(context, _accessToken),
/* _returnStocksTable method is not included in this code block because it is not the cause of the problem */
)
),
],
),
)
);
}
}
For this code, showSearch is already provided by flutter itself :
onPressed: () {
showSearch(context: context, delegate: StockSearch());
},
Here is the StockSearch() file:
class StockSearch extends SearchDelegate<String> {
String accessToken = "djdjicjscnjdsncjs";
List<String> stockList =
[
"TSLA",
"AMZN",
"GME",
"AAPL"
];
final recentStocks = ['TSLA'];
#override
List<Widget> buildActions(BuildContext context) {
// actions for app bar
return [
IconButton(icon: Icon(Icons.clear), onPressed: () {
query = "";
})
];
}
#override
Widget buildLeading(BuildContext context) {
// leading icon on the left of the app bar
return IconButton(
icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow,
progress: transitionAnimation,
),
onPressed: () {
close(context, null);
}
);
}
#override
Widget buildResults(BuildContext context) {
return Container();
}
#override
Widget buildSuggestions(BuildContext context) {
// show when someone searches for stocks
final suggestionList = query.isEmpty? recentStocks
: stockList.where((stock) => stock.startsWith(query)).toList();
return ListView.builder(
itemBuilder: (context, index) => ListTile(
onTap: () {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Navigator.push(context, MaterialPageRoute(builder: (context) => TransactionPage(suggestionList.elementAt(index), accessToken)));
});
},
title: RichText(text: TextSpan(
text: suggestionList.elementAt(index).substring(0, query.length),
style: TextStyle( color: Colors.black, fontWeight: FontWeight.bold),
children: [TextSpan(
text: suggestionList.elementAt(index).substring(query.length),
style: TextStyle(color: Colors.grey)
)
]
)
),
),
itemCount: suggestionList.length,
);
}
}
Thank you in advance for your help!
I am trying to call a setState when a button is pressed so the ui can show the new list but even using functions i cant use setState or it will give me the error saying im calling setState inside a constructor.
This is my code for the statlessWidget:
class _MessageCard extends StatelessWidget {
final Mensagem message;
final int messageLenght;
final List<Mensagem> messageList;
var i;
_MessageCard(
{#required this.message,
#required this.messageLenght,
#required this.messageList});
#override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 600,
child: InkWell(
child: Container(
width: 900,
color: Colors.grey[200],
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
child: Center(
child: Container(
width: 600,
child: Column(
children: <Widget>[
ListTile(
leading: CircleAvatar(
child: Icon(
Icons.notifications,
color: Colors.red[400],
),
backgroundColor: Colors.grey[200],
),
title: Text(
(this.message.vDescricao ?? '').trim(),
style: TextStyle(
fontSize: 14,
color: Colors.black,
),
),
subtitle: Text(
(this.message.vData ?? '').trim() +
' ' +
(this.message.vHora ?? '').trim(),
style: TextStyle(
color: Colors.red[400],
fontSize: 13,
),
),
trailing: FlatButton(
child: Text(
Translations.of(context)
.trans('finishmessageshort'),
),
onPressed: () => _showDeleteAlertMessage(
this.message.vNumero, context)),
),
Divider(
color: Colors.black54,
),
],
),
),
),
),
),
),
));
}
Future _showDeleteAlertMessage(String id, BuildContext context) {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: new Text(
Translations.of(context).trans('finishmessage') + '?',
),
actions: <Widget>[
FlatButton(
child: new Text(
Translations.of(context).trans('closealert'),
),
onPressed: () {
Navigator.of(context).pop();
}),
FlatButton(
child: new Text(("Ok")),
onPressed: () =>
{_deleteMessage(id), Navigator.of(context).pop()},
)
],
);
});
}
_deleteMessage(String id) async {
for (i = 0; i < this.messageLenght; i++) {
if (this.messageList[0].vNumero == this.message.vNumero) {
this.messageList.removeAt(i);
_HomePageState().mensagemRepo.confirmMessage(this.message.vNumero);
await _HomePageState()._getMessages();
return this.messageList;
}
}
}
}
And this is my _getMessages()
_getMessages() async {
setState(() {
_loading = true;
_errorMsg = '';
});
try {
_messages = await mensagemRepo.getMessages();
print('loaded messages: ${_messages?.length}');
} catch (e) {
_errorMsg = e.toString();
}
setState(() {
_loading = false;
});
}
How can i make it so i can use this setState?
Thank you for your time and attention
Edit: Now updates List but not UI, because im not able to set HomePage state from MessageCard
You can only use setState in a StatefulWidget.
class MessageCard extends StatefulWidget {
#override
_MessageCardState createState() => _MessageCardState();
}
class _MessageCardState extends State<MessageCard> {
#override
Widget build(BuildContext context) {
// your build method here
}
}
Well, you can't set value for something that doesn't exist. Stateless by name itself makes it clear that it can't hold any state. Changing the widget to a stateful widget would work.
Stateless widget can not change the state once its rendered. To use setState and re-render the widget StatefulWidget is used.
Just change your MessageCard from Stateless Widget to StatefulWidget
class MessageCard extends StatefulWidget {
final Mensagem message;
final int messageLenght;
final List<Mensagem> messageList;
var i;
MessageCard(
{#required this.message,
#required this.messageLenght,
#required this.messageList});
#override
_MessageCardState createState() => _MessageCardState();
}
class _MessageCardState extends State<MessageCard> {
#override
Widget build(BuildContext context) {
// your build method here
}
}
Also, now "to use your MessageCard properties" like message, messageLenght, messageList, in _MessageCardState you have to use a property like widget.message, widget.messageList and widget.messageLenght respectively.
can this work to refresh the ui?
_getMessages() async {
_HomePageState()._messages = await mensagemRepo.getMessages();
print('loaded messages: ${_messages?.length}');
setState(() {
_HomePageState()._messagesList();
});
}
The code for _messagesList() is:
SliverChildBuilderDelegate _messagesList() {
int count() {
if (_errorMsg != '')
return 1;
else
return _messages == null ? 0 : _messages.length;
}
return SliverChildBuilderDelegate(
(BuildContext context, int index) {
print("i: $index");
if (_errorMsg != '') {
return Padding(
padding: EdgeInsets.all(20),
child: ErrorMessage(
error: _errorMsg,
),
);
} else {
return _MessageCard(
message: this._messages[index],
messageLength: this._messages.length,
messageList: this._messages);
}
},
childCount: count(),
);
}
I managed to make it work, by making both classes into one and calling a function to draw the messagecards, thank you all for your help and attention
I have just a stack that contains a guillotine menu and a list. But below page (list and fab) isn't clickable! First code is Main page, second code is books (below page) and third code is guillotine menu.
How can I make clickable below page?
main page
import 'package:firebase_example/Ui/Books.dart';
import 'package:firebase_example/Ui/GuillotineMenu.dart';
import 'package:flutter/material.dart';
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
final GlobalKey<ScaffoldState> _drawerKey = new GlobalKey();
#override
Widget build(BuildContext context) {
return new MaterialApp(
debugShowCheckedModeBanner: false,
home: SafeArea(
top: false,
bottom: false,
child: new Container(
child: new Stack(
alignment: Alignment.topLeft,
children: <Widget>[
Books(),
GuillotineMenu(),
],
),
),
),
);
What you're looking for is IgnorePointer. By using it you should be able to exclude your widget from hit testing (and so allow things under it to be touched instead).
You're going to have to be careful about how you implement the button that makes the guillotine menu open/close though. I'd advise making it a part of the item lower in the stack, and then setting IgnorePointer's ignoring = true when the guillotine is closed.
Note that there might be a 'better' way of implementing the guillotine menu using PageRoutes. That way you'd just be pushing/popping a new route on top of the existing navigator rather than having to maintain your own stack.
Here's the code:
class GuillotinePageRoute<T> extends PageRoute<T> {
GuillotinePageRoute({
#required this.builder,
RouteSettings settings: const RouteSettings(),
this.maintainState: true,
bool fullscreenDialog: false,
}) : assert(builder != null),
super(settings: settings, fullscreenDialog: fullscreenDialog);
final WidgetBuilder builder;
#override
final bool maintainState;
#override
Duration get transitionDuration => const Duration(milliseconds: 500);
#override
Color get barrierColor => null;
#override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
final Widget result = builder(context);
assert(() {
if (result == null) {
throw new FlutterError('The builder for route "${settings.name}" returned null.\n'
'Route builders must never be null.');
}
return true;
}());
return result;
}
#override
Widget buildTransitions(
BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
MediaQueryData queryData = MediaQuery.of(context);
var topInset = queryData.padding.top;
Offset origin = Offset((kToolbarHeight / 2.0), topInset + (kToolbarHeight / 2.0));
Curve curve = animation.status == AnimationStatus.forward ? Curves.bounceOut : Curves.bounceIn;
var rotateTween = new Tween(begin: -pi / 2.0, end: 0.0);
Cubic opacityCurve = Cubic(0.0, 1.0, 0.0, 1.0);
return new AnimatedBuilder(
animation: animation,
child: child,
builder: (context, child) {
return Opacity(
opacity: opacityCurve.transform(animation.value),
child: Transform(
transform: Matrix4.identity()..rotateZ(rotateTween.lerp(curve.transform(animation.value))),
origin: origin,
child: child,
),
);
},
);
}
#override
String get barrierLabel => null;
}
And using it in an example:
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MenuPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Column(children: [
AppBar(
leading: new IconButton(icon: Icon(Icons.menu), onPressed: () => Navigator.pop(context)),
elevation: 0.0,
),
Expanded(
child: Container(
child: Center(
child: Text(
"Menu page!",
style: TextStyle(color: Colors.white, decoration: TextDecoration.none),
),
),
color: Colors.blue,
),
),
]);
}
}
class MyHomePage extends StatelessWidget {
void pushGuillotine(BuildContext context, WidgetBuilder builder) {
Navigator.push(context, new GuillotinePageRoute(builder: builder));
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("This is a title"),
leading: new RotatedBox(
quarterTurns: -1,
child: IconButton(icon: Icon(Icons.menu), onPressed: () => pushGuillotine(context, (context) => MenuPage())),
),
),
body: Container(
color: Colors.blueGrey,
child: Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'This is the home page.',
),
new Text(
'Hello world!',
style: Theme.of(context).textTheme.display1,
),
],
),
),
),
);
}
}
class GuillotinePageRoute<T> extends PageRoute<T> {
GuillotinePageRoute({
#required this.builder,
RouteSettings settings: const RouteSettings(),
this.maintainState: true,
bool fullscreenDialog: false,
}) : assert(builder != null),
super(settings: settings, fullscreenDialog: fullscreenDialog);
final WidgetBuilder builder;
#override
final bool maintainState;
#override
Duration get transitionDuration => const Duration(milliseconds: 500);
#override
Color get barrierColor => null;
#override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
final Widget result = builder(context);
assert(() {
if (result == null) {
throw new FlutterError('The builder for route "${settings.name}" returned null.\n'
'Route builders must never be null.');
}
return true;
}());
return result;
}
#override
Widget buildTransitions(
BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
MediaQueryData queryData = MediaQuery.of(context);
var topInset = queryData.padding.top;
Offset origin = Offset((kToolbarHeight / 2.0), topInset + (kToolbarHeight / 2.0));
Curve curve = animation.status == AnimationStatus.forward ? Curves.bounceOut : Curves.bounceIn;
var rotateTween = new Tween(begin: -pi / 2.0, end: 0.0);
Cubic opacityCurve = Cubic(0.0, 1.0, 0.0, 1.0);
return new AnimatedBuilder(
animation: animation,
child: child,
builder: (context, child) {
return Opacity(
opacity: opacityCurve.transform(animation.value),
child: Transform(
transform: Matrix4.identity()..rotateZ(rotateTween.lerp(curve.transform(animation.value))),
origin: origin,
child: child,
),
);
},
);
}
#override
String get barrierLabel => null;
}
It's a stack use the gesture detector's onTapDown which is not available in a floating action button
If you have a widget with no interaction and want to make it clickable use the GestureDetector widget as parent, you can see the example in the link.
--- edit
This answer helps you when you need to make a widget without interaction have some, not exactly what this question is talk about, honest mistake but i could help others..
I am trying to create an application that manages Views with Tab.
The navigation bar and the tab bar are displayed in the application, and the content of the content to be displayed is managed by List.
This is the class that manages the content of Tab.
class TabItem {
TabItem({
Widget view,
Widget icon,
Widget activeIcon,
String title,
TickerProvider vsync,
}) : _view = view,
_title = title,
item = BottomNavigationBarItem(
icon: icon,
// activeIcon: activeIcon,
title: Text(title),
),
controller = AnimationController(
duration: kThemeAnimationDuration,
vsync: vsync,
) {
_animation = CurvedAnimation(
parent: controller,
curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn),
);
}
final Widget _view;
final String _title;
final BottomNavigationBarItem item;
final AnimationController controller;
CurvedAnimation _animation;
FadeTransition transition(
BottomNavigationBarType type, BuildContext context) {
~~~~~~~~~~~~
//transition animation setting method
~~~~~~~~~~~~
}
}
This is main.dart
Depending on the action of the user, the content in the List is displayed.
void main() {
SystemChrome
.setPreferredOrientations([DeviceOrientation.portraitUp]).then((_) {
runApp(MyApp());
});
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: RootTabView(),
);
}
}
class RootTabView extends StatefulWidget {
#override
_RootTabViewState createState() => _RootTabViewState();
}
class _RootTabViewState extends State<RootTabView>
with TickerProviderStateMixin {
int _currentIndex = 0;
BottomNavigationBarType _type = BottomNavigationBarType.fixed;
List<TabItem> _tabItems;
#override
void initState() {
super.initState();
// tabitems of view
_tabItems = <TabItem>[
TabItem(
view: hoge0View(),
icon: const Icon(Icons.accessibility),
title: 'hoge0',
vsync: this,
),
TabItem(
view: hoge1View(),
icon: const Icon(Icons.accessibility),
title: 'hoge1',
vsync: this,
),
TabItem(
view: hoge2View(),
icon: const Icon(Icons.accessibility),
title: 'hoge2',
vsync: this,
),
TabItem(
view: hoge3View(),
icon: const Icon(Icons.accessibility),
title: 'hoge3',
vsync: this,
)
];
for (TabItem tabItem in _tabItems) {
tabItem.controller.addListener(_rebuild);
}
_tabItems[_currentIndex].controller.value = 1.0;
}
#override
void dispose() {
for (TabItem tabItem in _tabItems) {
tabItem.controller.dispose();
}
super.dispose();
}
void _rebuild() {
if (this.mounted) {
setState(() {
// Rebuild in order to animate views.
});
}
}
Widget _buildTransitionsStack() {
final List<FadeTransition> transitions = <FadeTransition>[];
for (TabItem tabItem in _tabItems) {
transitions.add(tabItem.transition(_type, context));
}
// We want to have the newly animating (fading in) views on top.
transitions.sort((FadeTransition a, FadeTransition b) {
final Animation<double> aAnimation = a.opacity;
final Animation<double> bAnimation = b.opacity;
final double aValue = aAnimation.value;
final double bValue = bAnimation.value;
return aValue.compareTo(bValue);
});
return Stack(children: transitions);
}
#override
Widget build(BuildContext context) {
final BottomNavigationBar botNavBar = BottomNavigationBar(
items: _tabItems.map((TabItem tabItem) => tabItem.item).toList(),
currentIndex: _currentIndex,
type: _type,
onTap: (int index) {
setState(() {
var currentView = _tabItems[_currentIndex];
currentView.controller.reverse();
_currentIndex = index;
currentView = _tabItems[_currentIndex];
currentView.controller.forward();
});
},
);
return Scaffold(
appBar: AppBar(
title: Text(_tabItems[_currentIndex]._title),
actions: actions,
),
body: Center(child: _buildTransitionsStack()),
bottomNavigationBar: botNavBar,
);
}
}
Well, here I rewrite a part of the build method.
I placed a button on AppBar when index: 1 and wanted to tap and call the method of the view instance being displayed.
var actions = <Widget>[];
if (_currentIndex == 1) {
var editButton = FlatButton(
child: Text('Edit'),
onPressed: () {
// I want to call hoge1View method...
});
actions.add(editButton);
}
return Scaffold(
appBar: AppBar(
title: Text(_tabItems[_currentIndex]._title),
actions: actions,
),
body: Center(child: _buildTransitionsStack()),
bottomNavigationBar: botNavBar,
);
The implementation of Hoge1View is as follows.
I want to call showEditButtonDialog () when I tap the editButton.
class Hoge1View extends StatefulWidget {
#override
_Hoge1ViewState createState() => _Hoge1ViewState();
}
class _Hoge1ViewState extends State<DiaryView> {
Future showEditButtonDialog() async {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('test'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[Text('test message')],
),
),
actions: <Widget>[
FlatButton(
child: Text('action1'),
onPressed: () {
print('pressed');
},
)
],
);
});
}
#override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Container(),
Container(),
Container(),
]
);
}
}
I do not know how to get Hoge1View instances currently displayed on onPress.
I'm trying to build a Tabbed View that has lists as children.
Both the Category labels and the lists content will be fetched from a database.
I am passing the labels from the caller page and successfully passing them as a List.
Now I'm trying to load my lists, and I have built a Widget (myList) that returns successfully a Future ListView.
The problems are two:
Every time i swipe left or right, the list rebuilds itself, while I would like to have it built only once
How can I use the code I made to have the tabs' children actually reflect the labels and are loaded dinamically according to how many categories i have?
Right now my code is this:
import 'package:flutter/material.dart';
import 'package:flutter_app/ui/menu_category_list.dart';
// Each TabBarView contains a _Page and for each _Page there is a list
// of _CardData objects. Each _CardData object is displayed by a _CardItem.
List<Tab> Tabs(List<String> l){
List<Tab> list;
for (String c in l) {
list.add(new Tab(text: c));
}
return list;
}
class TabsDemo extends StatelessWidget {
const TabsDemo({ Key key , this.categorie}) : super(key: key);
final List<Tab> categorie;
#override
Widget build(BuildContext ctxt) {
return new MaterialApp(
title: "Nice app",
home: new DefaultTabController(
length: 5,
child: new Scaffold(
appBar: new AppBar(
title: new Text("Title"),
bottom: new TabBar(
tabs:
categories,
//new Tab(text: "First Tab"),
//new Tab(text: "Second Tab"),
),
),
body: new TabBarView(
children: [
new MenuCategoryList(),
new MenuCategoryList(),
new MenuCategoryList(),
new MenuCategoryList(),
new MenuCategoryList()
]
)
),
)
);
}
}
currently result
Thanks a lot in advance
You can use List<E>.generate to achieve this.
import 'package:flutter/material.dart';
Say you have a set of categories passed from your caller page. And let's say this is your list of categories.
List<String> categories = ["a", "b", "c", "d", "e", "f", "g", "h"];
Then you can do something like this to achieve what you desire.
class TabsDemo extends StatefulWidget {
#override
_TabsDemoState createState() => _TabsDemoState();
}
class _TabsDemoState extends State<TabsDemo> {
TabController _controller;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext ctxt) {
return new MaterialApp(
home: DefaultTabController(
length: categories.length,
child: new Scaffold(
appBar: new AppBar(
title: new Text("Title"),
bottom: new TabBar(
isScrollable: true,
tabs: List<Widget>.generate(categories.length, (int index){
print(categories[0]);
return new Tab(icon: Icon(Icons.directions_car), text: "some random text");
}),
),
),
body: new TabBarView(
children: List<Widget>.generate(categories.length, (int index){
print(categories[0]);
return new Text("again some random text");
}),
)
))
);
}
You can also set different set of widgets as the Tab's view. You can create a list of pages and follow the same method.
Absolutely true List<E>.generate best solution to solve.
Problems arise if you need to modify the arrays. They consist in the fact that when modifying an array you do not have the opportunity to use the same controller.
You can use the next custom widget for this case:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<String> data = ['Page 0', 'Page 1', 'Page 2'];
int initPosition = 1;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: CustomTabView(
initPosition: initPosition,
itemCount: data.length,
tabBuilder: (context, index) => Tab(text: data[index]),
pageBuilder: (context, index) => Center(child: Text(data[index])),
onPositionChange: (index){
print('current position: $index');
initPosition = index;
},
onScroll: (position) => print('$position'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
data.add('Page ${data.length}');
});
},
child: Icon(Icons.add),
),
);
}
}
/// Implementation
class CustomTabView extends StatefulWidget {
final int itemCount;
final IndexedWidgetBuilder tabBuilder;
final IndexedWidgetBuilder pageBuilder;
final Widget stub;
final ValueChanged<int> onPositionChange;
final ValueChanged<double> onScroll;
final int initPosition;
CustomTabView({
#required this.itemCount,
#required this.tabBuilder,
#required this.pageBuilder,
this.stub,
this.onPositionChange,
this.onScroll,
this.initPosition,
});
#override
_CustomTabsState createState() => _CustomTabsState();
}
class _CustomTabsState extends State<CustomTabView> with TickerProviderStateMixin {
TabController controller;
int _currentCount;
int _currentPosition;
#override
void initState() {
_currentPosition = widget.initPosition ?? 0;
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation.addListener(onScroll);
_currentCount = widget.itemCount;
super.initState();
}
#override
void didUpdateWidget(CustomTabView oldWidget) {
if (_currentCount != widget.itemCount) {
controller.animation.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
if (widget.initPosition != null) {
_currentPosition = widget.initPosition;
}
if (_currentPosition > widget.itemCount - 1) {
_currentPosition = widget.itemCount - 1;
_currentPosition = _currentPosition < 0 ? 0 :
_currentPosition;
if (widget.onPositionChange is ValueChanged<int>) {
WidgetsBinding.instance.addPostFrameCallback((_){
if(mounted) {
widget.onPositionChange(_currentPosition);
}
});
}
}
_currentCount = widget.itemCount;
setState(() {
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation.addListener(onScroll);
});
} else if (widget.initPosition != null) {
controller.animateTo(widget.initPosition);
}
super.didUpdateWidget(oldWidget);
}
#override
void dispose() {
controller.animation.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
if (widget.itemCount < 1) return widget.stub ?? Container();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
alignment: Alignment.center,
child: TabBar(
isScrollable: true,
controller: controller,
labelColor: Theme.of(context).primaryColor,
unselectedLabelColor: Theme.of(context).hintColor,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).primaryColor,
width: 2,
),
),
),
tabs: List.generate(
widget.itemCount,
(index) => widget.tabBuilder(context, index),
),
),
),
Expanded(
child: TabBarView(
controller: controller,
children: List.generate(
widget.itemCount,
(index) => widget.pageBuilder(context, index),
),
),
),
],
);
}
onPositionChange() {
if (!controller.indexIsChanging) {
_currentPosition = controller.index;
if (widget.onPositionChange is ValueChanged<int>) {
widget.onPositionChange(_currentPosition);
}
}
}
onScroll() {
if (widget.onScroll is ValueChanged<double>) {
widget.onScroll(controller.animation.value);
}
}
}
You can use dynamic children using for loop within your Tabbarview Widget
List<String> categories = ["category 1" , "category 2", "category 3",];
return TabBarView(
children:[
for(var category in categories)
Text(category), // this widget will show a text with specific category. You can use any other widget
],
);
Null safety version
import 'package:flutter/material.dart';
class CustomTabView extends StatefulWidget {
final int? itemCount;
final IndexedWidgetBuilder? tabBuilder;
final IndexedWidgetBuilder? pageBuilder;
final Widget? stub;
final ValueChanged<int>? onPositionChange;
final ValueChanged<double>? onScroll;
final int? initPosition;
CustomTabView({this.itemCount, this.tabBuilder, this.pageBuilder, this.stub,
this.onPositionChange, this.onScroll, this.initPosition});
#override
_CustomTabsState createState() => _CustomTabsState();
}
class _CustomTabsState extends State<CustomTabView> with TickerProviderStateMixin {
late TabController controller;
late int _currentCount;
late int _currentPosition;
#override
void initState() {
_currentPosition = widget.initPosition!;
controller = TabController(
length: widget.itemCount!,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation!.addListener(onScroll);
_currentCount = widget.itemCount!;
super.initState();
}
#override
void didUpdateWidget(CustomTabView oldWidget) {
if (_currentCount != widget.itemCount) {
controller.animation!.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
if (widget.initPosition != null) {
_currentPosition = widget.initPosition!;
}
if (_currentPosition > widget.itemCount! - 1) {
_currentPosition = widget.itemCount! - 1;
_currentPosition = _currentPosition < 0 ? 0 :
_currentPosition;
if (widget.onPositionChange is ValueChanged<int>) {
WidgetsBinding.instance!.addPostFrameCallback((_){
if(mounted) {
widget.onPositionChange!(_currentPosition);
}
});
}
}
_currentCount = widget.itemCount!;
setState(() {
controller = TabController(
length: widget.itemCount!,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation!.addListener(onScroll);
});
} else if (widget.initPosition != null) {
controller.animateTo(widget.initPosition!);
}
super.didUpdateWidget(oldWidget);
}
#override
void dispose() {
controller.animation!.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
if (widget.itemCount! < 1) return widget.stub ?? Container();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
alignment: Alignment.center,
child: TabBar(
isScrollable: true,
controller: controller,
labelColor: Theme.of(context).primaryColor,
unselectedLabelColor: Theme.of(context).hintColor,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).primaryColor,
width: 2,
),
),
),
tabs: List.generate(
widget.itemCount!,
(index) => widget.tabBuilder!(context, index),
),
),
),
Expanded(
child: TabBarView(
controller: controller,
children: List.generate(
widget.itemCount!,
(index) => widget.pageBuilder!(context, index),
),
),
),
],
);
}
onPositionChange() {
if (!controller.indexIsChanging) {
_currentPosition = controller.index;
if (widget.onPositionChange is ValueChanged<int>) {
widget.onPositionChange!(_currentPosition);
}
}
}
onScroll() {
if (widget.onScroll is ValueChanged<double>) {
widget.onScroll!(controller.animation!.value);
}
}
}