I'm using the sample below (taken from the CupertinoTabScaffold documentation page).
There is a "slide" transition when pushing a new route inside the tab, but when I click on a tabbar item, the content is brutally replaced. How can I have a transition when switching between tabs?
I would like implement something like that: https://github.com/Interactive-Studio/TransitionableTab
CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: [
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.home),
title: Text("Tab 0"),
),
BottomNavigationBarItem(
icon: Icon(CupertinoIcons.news),
title: Text("Tab 1"),
),
],
),
tabBuilder: (BuildContext context, int index) {
return CupertinoTabView(
builder: (BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Page 1 of tab $index'),
),
child: Center(
child: CupertinoButton(
child: const Text('Next page'),
onPressed: () {
Navigator.of(context).push(
CupertinoPageRoute<void>(
builder: (BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Page 2 of tab $index'),
),
child: Center(
child: CupertinoButton(
child: const Text('Back'),
onPressed: () { Navigator.of(context).pop(); },
),
),
);
},
),
);
},
),
),
);
},
);
},
)
May be this package will be help you page_transition: ^2.0.5
https://pub.dev/packages/page_transition
Try this -
It has info on how to animate tabBarView but should be similar to CupertinoTabView
class MyApp2 extends StatefulWidget {
#override
_MyApp2State createState() => _MyApp2State();
}
class _MyApp2State extends State<MyApp2> with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
_tabController = TabController(length: 4, vsync: this);
super.initState();
}
_tabBarView() {
return AnimatedBuilder(
animation: _tabController.animation,
builder: (BuildContext context, snapshot) {
return Transform.rotate(
angle: _tabController.animation.value * pi,
child: [
Container(
color: Colors.blue,
),
Container(
color: Colors.orange,
),
Container(
color: Colors.lightGreen,
),
Container(
color: Colors.red,
),
][_tabController.animation.value.round()],
);
},
);
}
_bottomTabBar() {
return TabBar(
controller: _tabController,
labelColor: Colors.black,
tabs: [
Tab(
icon: new Icon(Icons.home),
),
Tab(
icon: new Icon(Icons.public),
),
Tab(
icon: new Icon(Icons.group),
),
Tab(
icon: new Icon(Icons.person),
)
],
);
}
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(title: const Text('Bottom App Bar')),
body: _tabBarView(),
bottomNavigationBar: _bottomTabBar(),
),
);
}
}
The above code is taken from the following link-
Flutter - Change the animation of TabBarView
I'm quite new with Flutter and I'm coming from using the Angular framework. Currently, I'm experimenting with flutter to make a desktop application using the following flutter embedding project: https://github.com/Drakirus/go-flutter-desktop-embedder.
I was wondering if someone could explain to me the best way to implement the following:
The black box represents the application as a whole.
The red box represents the custom menu.
The green box represents the content of the page.
How would I go about routing between "widgets" inside of the green area without changing the widget holding the application?
I'd love some direction please.
I am contributing Drakirus 's go-flutter plugin.
This projecd had moved to https://github.com/go-flutter-desktop
The question you ask can use package responsive_scaffold
https://pub.dev/packages/responsive_scaffold
or
you can reference this doc https://iirokrankka.com/2018/01/28/implementing-adaptive-master-detail-layouts/
Basically, there two are different layouts, see comments for detail
class _MasterDetailContainerState extends State<MasterDetailContainer> {
// Track the currently selected item here. Only used for
// tablet layouts.
Item _selectedItem;
Widget _buildMobileLayout() {
return ItemListing(
// Since we're on mobile, just push a new route for the
// item details.
itemSelectedCallback: (item) {
Navigator.push(...);
},
);
}
Widget _buildTabletLayout() {
// For tablets, return a layout that has item listing on the left
// and item details on the right.
return Row(
children: <Widget>[
Flexible(
flex: 1,
child: ItemListing(
// Instead of pushing a new route here, we update
// the currently selected item, which is a part of
// our state now.
itemSelectedCallback: (item) {
setState(() {
_selectedItem = item;
});
},
),
),
Flexible(
flex: 3,
child: ItemDetails(
// The item details just blindly accepts whichever
// item we throw in its way, just like before.
item: _selectedItem,
),
),
],
);
}
For package responsive_scaffold
on-line demo https://fluttercommunity.github.io/responsive_scaffold/#/
github https://github.com/fluttercommunity/responsive_scaffold/
more template code snippets for layout
https://github.com/fluttercommunity/responsive_scaffold/tree/dev
more pictures and demo can found here https://github.com/fluttercommunity/responsive_scaffold/tree/dev/lib/templates/3-column
code snippet 1
import 'package:flutter/material.dart';
import 'package:responsive_scaffold/responsive_scaffold.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
var _scaffoldKey = new GlobalKey<ScaffoldState>();
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ResponsiveListScaffold.builder(
scaffoldKey: _scaffoldKey,
detailBuilder: (BuildContext context, int index, bool tablet) {
return DetailsScreen(
// appBar: AppBar(
// elevation: 0.0,
// title: Text("Details"),
// actions: [
// IconButton(
// icon: Icon(Icons.share),
// onPressed: () {},
// ),
// IconButton(
// icon: Icon(Icons.delete),
// onPressed: () {
// if (!tablet) Navigator.of(context).pop();
// },
// ),
// ],
// ),
body: Scaffold(
appBar: AppBar(
elevation: 0.0,
title: Text("Details"),
automaticallyImplyLeading: !tablet,
actions: [
IconButton(
icon: Icon(Icons.share),
onPressed: () {},
),
IconButton(
icon: Icon(Icons.delete),
onPressed: () {
if (!tablet) Navigator.of(context).pop();
},
),
],
),
bottomNavigationBar: BottomAppBar(
elevation: 0.0,
child: Container(
child: IconButton(
icon: Icon(Icons.share),
onPressed: () {},
),
),
),
body: Container(
child: Center(
child: Text("Item: $index"),
),
),
),
);
},
nullItems: Center(child: CircularProgressIndicator()),
emptyItems: Center(child: Text("No Items Found")),
slivers: <Widget>[
SliverAppBar(
title: Text("App Bar"),
),
],
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Text(index.toString()),
);
},
bottomNavigationBar: BottomAppBar(
elevation: 0.0,
child: Container(
child: IconButton(
icon: Icon(Icons.share),
onPressed: () {},
),
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
_scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text("Snackbar!"),
));
},
),
),
);
}
}
code snippet 2
import 'package:flutter/material.dart';
import 'package:responsive_scaffold/responsive_scaffold.dart';
class MultiColumnNavigationExample extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ThreeColumnNavigation(
title: Text('Mailboxes'),
showDetailsArrows: true,
backgroundColor: Colors.grey[100],
bottomAppBar: BottomAppBar(
elevation: 1,
child: Row(
children: <Widget>[
IconButton(
icon: Icon(
Icons.filter_list,
color: Colors.transparent,
),
onPressed: () {},
),
],
),
),
sections: [
MainSection(
label: Text('All Inboxes'),
icon: Icon(Icons.mail),
itemCount: 100,
itemBuilder: (context, index, selected) {
return ListTile(
leading: CircleAvatar(
child: Text(index.toString()),
),
selected: selected,
title: Text('Primary Information'),
subtitle: Text('Here are some details about the item'),
);
},
bottomAppBar: BottomAppBar(
elevation: 1,
child: Row(
children: <Widget>[
IconButton(
icon: Icon(Icons.filter_list),
onPressed: () {},
),
],
),
),
getDetails: (context, index) {
return DetailsWidget(
title: Text('Details'),
child: Center(
child: Text(
index.toString(),
),
),
);
},
),
MainSection(
label: Text('Sent Mail'),
icon: Icon(Icons.send),
itemCount: 100,
itemBuilder: (context, index, selected) {
return ListTile(
leading: CircleAvatar(
child: Text(index.toString()),
),
selected: selected,
title: Text('Secondary Information'),
subtitle: Text('Here are some details about the item'),
);
},
getDetails: (context, index) {
return DetailsWidget(
title: Text('Details'),
actions: [
IconButton(
icon: Icon(Icons.share),
onPressed: () {},
),
],
child: Center(
child: Text(
index.toString(),
),
),
);
},
),
],
);
}
}
I'm a noob so please take anything I say with a grain of salt.
I know 2 ways to navigate through widgets and you can find them both here
https://flutter.io/docs/development/ui/navigation
I believe the main difference I can perceive is if you want to
send data to the new 'route' or not (the named route way cannot, at least that I'm aware of);
said so you can keep your main 'screen' and change the red and green widget
using the state of the widget where they are contained
example
class BlackWidget extends StatefulWidget
bla bla bla => BlackWidgetState();
class BlackWidget extend State<BlackWidget>
Widget tallWidget = GreenWidget();
Widget bigWidget = RedWidget();
return
container, column.. etc
Row(
children:[tallWidget,bigWidget
])
button onTap => tallWidget = YellowWidget();
}
GreenWidget... bla bla bla...
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => RedWidget()),
);
}
sorry for the 'bla bla', the part you need is at the bottom,
just added the 'yellow' widget to underline that you can
actually swap the 'green widget' with anything you want
I am pretty new to Flutter and Dart and I can't seem to find any hints for this particular topic. I am trying to put 3 RadioListTiles in a Row like so:
Row(
children: [
Expanded(
child:RadioListTile<GoalSelection>(
title: Text(
'Net',
style: Theme.of(context).textTheme.body1,
),
value: GoalSelection.net,
groupValue: _goalSelection,
onChanged: (GoalSelection value) {
setState(() {
_goalSelection = value;
});
},
),
),
Expanded(
child: RadioListTile<GoalSelection>(
title: Text(
'Gross',
style: Theme.of(context).textTheme.body1,
),
value: GoalSelection.gross,
groupValue: _goalSelection,
onChanged: (GoalSelection value) {
setState(() {
_goalSelection = value;
});
},
),
),
Expanded(
child: RadioListTile<GoalSelection>(
title: Text(
'Salary',
style: Theme.of(context).textTheme.body1,
),
value: GoalSelection.salary,
groupValue: _goalSelection,
onChanged: (GoalSelection value) {
setState(() {
_goalSelection = value;
});
},
),
),
],
),
The buttons layout fine, but there seems to be a lot of wasted space for the label. I put a screenshot of what it currently looks like below. I have tried wrapping the Expanded, the RadioListTile, and the Text in Padding widgets (all one at a time) to manually set the padding to 0, but it didn't do anything. I have also tried to change Expanded to Flexible even though I didn't think that would change anything. I am at a loss now. Is there any way to get this layout to work? I am kind of assuming it is something really dumb that I am doing.
You can use Radio + text widget instead of RadioListTile. For removing internal padding in Radio widget set:
Radio(
visualDensity: const VisualDensity(
horizontal: VisualDensity.minimumDensity,
vertical: VisualDensity.minimumDensity),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
.....
),
You can use a Radio and Text widget in a row. But the Radio also has the same padding problem. To remove the padding you can put the Radio as a child of a SizedBox.
eg:- SizedBox(height: 20, width: 20, child: Radio(.......))
RadioListTile is used with the purpose of taking the full width in a vertical scroll list.
If you don't want this behavior, don't use it. Use Radio instead.
just set contentPadding: EdgeInsets.zero
RadioListTile(contentPadding: EdgeInsets.zero)
We can control the padding of the RadioListTile using Flexible widget. As you want to arrange 3 RadioListTiles inside a Row Widget. Please try with the below code, it will work.
Row(
children: <Widget>[
Flexible(
fit: FlexFit.loose,
child:
RadioListTile(
title: const Text('hello'),
onChanged: (value) {
setState(() {});
},
),
),
Flexible(
fit: FlexFit.loose,
child:
RadioListTile(
title: const Text('Lafayette'),
onChanged: (value) {
setState(() {});
},
),
)
],
),
Do, let me know. Once you tried with the above code. If it resolved you problem, please accept my answer as useful and provide your valuable comments.
I got the same problem. You could try to customize with Radio, Text, InkWell, Padding.
class LabeledRadio extends StatelessWidget {
const LabeledRadio({
this.label,
this.padding,
this.groupValue,
this.value,
this.onChanged,
});
final String label;
final EdgeInsets padding;
final bool groupValue;
final bool value;
final Function onChanged;
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
if (value != groupValue)
onChanged(value);
},
child: Padding(
padding: padding,
child: Row(
children: <Widget>[
Radio<bool>(
groupValue: groupValue,
value: value,
onChanged: (bool newValue) {
onChanged(newValue);
},
),
Text(label),
],
),
),
);
}
}
// ...
bool _isRadioSelected = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <LabeledRadio>[
LabeledRadio(
label: 'This is the first label text',
padding: const EdgeInsets.symmetric(horizontal: 5.0),
value: true,
groupValue: _isRadioSelected,
onChanged: (bool newValue) {
setState(() {
_isRadioSelected = newValue;
});
},
),
LabeledRadio(
label: 'This is the second label text',
padding: const EdgeInsets.symmetric(horizontal: 5.0),
value: false,
groupValue: _isRadioSelected,
onChanged: (bool newValue) {
setState(() {
_isRadioSelected = newValue;
});
},
),
],
),
);
}
The documentation: https://api.flutter.dev/flutter/material/RadioListTile-class.html#material.RadioListTile.3
This is how I fix the padding:
enum ContactSex { nam, nu, khac }
class CreateContactScreen extends StatefulWidget {
static const routeName = './create_contact';
#override
_CreateContactScreenState createState() => _CreateContactScreenState();
}
class _CreateContactScreenState extends State<CreateContactScreen> {
ContactSex _contaxtSex = ContactSex.nu;
final _form = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'TẠO LIÊN HỆ',
style: kHeaderTextStyle,
),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('XONG', style: TextStyle(color: Colors.white),),
)
],
),
body: Container(
padding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 20.0),
child: Form(
key: _form,
child: SingleChildScrollView(
child: Column(
children: <Widget>[
TextFormField(
decoration: InputDecoration(
labelText: 'Tên*',
),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
// TODO: when submit this text field
},
validator: (value) {
if (value.isEmpty) {
return 'Hãy nhập tên cho liên hệ.';
}
return null;
},
onSaved: (value) {
// TODO : when save the whole form
},
),
TextFormField(
decoration: InputDecoration(
labelText: 'Họ',
),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
// TODO: when submit this text field
},
// validator: (value) {
// if (value.isEmpty) {
// return null;
// }
// return null;
// },
onSaved: (value) {
// TODO : when save the whole form
},
),
TextFormField(
decoration: InputDecoration(
labelText: 'Số điện thoại*',
),
keyboardType: TextInputType.phone,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
// TODO: when submit this text field
},
validator: (value) {
if (value.isEmpty) {
return 'Hãy nhập số điện thoại cho liên hệ.';
}
return null;
},
onSaved: (value) {
// TODO : when save the whole form
},
),
TextFormField(
decoration: InputDecoration(
labelText: 'Email',
),
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
// TODO: when submit this text field
},
// validator: (value) {
// if (value.isEmpty) {
// return null;
// }
// return null;
// },
onSaved: (value) {
// TODO : when save the whole form
},
),
Container(
padding: EdgeInsets.only(top: 15.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Giới tính',
style: TextStyle(fontSize: 14.0),
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
LabeledRadio(
label: 'Nữ',
padding: EdgeInsets.all(0),
groupValue: _contaxtSex,
value: ContactSex.nu,
onChanged: (ContactSex newValue) {
setState(() {
_contaxtSex = newValue;
});
},
),LabeledRadio(
label: 'Nam',
padding: EdgeInsets.all(0),
groupValue: _contaxtSex,
value: ContactSex.nam,
onChanged: (ContactSex newValue) {
setState(() {
_contaxtSex = newValue;
});
},
),LabeledRadio(
label: 'Khác',
padding: EdgeInsets.all(0),
groupValue: _contaxtSex,
value: ContactSex.khac,
onChanged: (ContactSex newValue) {
setState(() {
_contaxtSex = newValue;
});
},
),
],
),
],
),
)
],
)),
),
),
);
}
}
class LabeledRadio extends StatelessWidget {
final String label;
final EdgeInsets padding;
final ContactSex groupValue;
final ContactSex value;
final Function onChanged;
const LabeledRadio(
{this.label, this.padding, this.groupValue, this.value, this.onChanged});
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
if (value != groupValue) {
onChanged(value);
}
},
child: Padding(
padding: padding,
child: Row(
children: <Widget>[
Radio<ContactSex>(
groupValue: groupValue,
value: value,
onChanged: (ContactSex newValue) {
onChanged(newValue);
},
),
Text(label),
],
),
),
);
}
}
You just need to set the "dense" property to true, example:
RadioListTile<String>(
title: "My radio",
dense: true, // <= here it is !
value: '1',
);
you should achieve this manually like
make a group of Radio() and Text() and wrap with InkWell() for state handling. now remove extra space of radio by materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, That's it. Get idea by sample code.
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: () {
setState(() {
_radioVenue = 0;
});
},
child: Row(
children: [
Radio(
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
activeColor: primaryColor,
groupValue: _radioVenue,
onChanged: (value) {},
value: 0,
),
Text('From our list')
],
),
),
InkWell(
onTap: () {
setState(() {
_radioVenue = 1;
});
},
child: Row(
children: [
Radio(
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
activeColor: primaryColor,
groupValue: _radioVenue,
onChanged: (value) {},
value: 1,
),
Text('From our list')
],
),
),
],
),
We covered both both the issues in this sample.
Removed extra spaces.
whole group is selectable radio + text, Now it behaves like RadioListTile().
Simply use RadioListTile and remove extra padding, by default it's 18
RadioListTile(contentPadding: EdgeInsets.symmetric(horizontal: 0.0)),
OR
RadioListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 0.0),
value: null,
groupValue: null,
onChanged: null,
),
glad to answer
I was looking for same question and ended up on Flutter Documentation
I was working on Column and RadioListTile and I faced same issue, there's a horizontal padding between content inside RadioListTile
So, here it's the answer
Looking for this documentation ! RadioListTile content padding
Just add contentPadding: EdgeInsets.symmetric(horizontal: 0) and here you go, there's no horizontal padding anymore
Just copy paste this code and enjoy
Container(
height:35,
child: Row(
children: [
Radio(
groupValue: data.selected,
value: e,
onChanged: (DataBindModel? value) {
listener.value = MultiChoiceData(selected: value, items: listener.value.items);
onChanged(value);
onSelected(value);
},
),
Text(
e.value,
style: body14,
)
],
),
)
Copy the RadioListTile code and create your on new new file and paste it in there.
Remove the imports causing errors:
import 'package:flutter/widgets.dart'; // leave it
import 'package:flutter/material.dart'; //add
import 'list_tile.dart'; //remove
import 'radio.dart'; //remove
import 'theme.dart'; //remove
import 'theme_data.dart'; //remove
Then add the following padding to it, like this:
//Inside the file locate this widget and Add the padding or remove it. I needed to remove it and add 5.
return MergeSemantics(
child: ListTileTheme.merge(
contentPadding: EdgeInsets.only( // Add this
left: 5,
right: 0,
bottom: 0,
top: 0
),
selectedColor: activeColor ?? Theme.of(context).accentColor,
child: ListTile(
leading: leading,
title: title,
subtitle: subtitle,
trailing: trailing,
isThreeLine: isThreeLine,
dense: dense,
enabled: onChanged != null,
onTap: onChanged != null && !checked ? () { onChanged(value); } : null,
selected: selected,
),
),
);
then Import the file into your project like this:
import 'package:Project_Name/common/customComponets/custom_radio_list_tile.dart' as CustomRadioListTile;
Then use it like this:
CustomRadioListTile.RadioListTile(); // and that's how I managed to do it. Thought I should share.
This is my way of reducing the space. I have three Radio in one row.
Row(
children: [
Expanded(
child: RadioListTile(
contentPadding: EdgeInsets.all(0.0),
value: DayoffType.Range,
groupValue: _dayoffType,
title: Transform.translate(offset: const Offset(-18, 0), child: Text('Range')),
onChanged: (DayoffType? val) {
setState(() {
_dayoffType = val!;
});
},
),
),
Expanded(...Radio2...),
Expanded(...Radio3...)
)
I have been trying to add listeners when using DefaultTabController. However, every time I add a TabController in order to get the current index in either TabBar and TabBarView, I lose sync between them.
This is my code below:
#override
Widget build(BuildContext context) {
return new DefaultTabController(
length: subPages.length,
child: new Scaffold(
appBar: appBar('homepage'),
body: new Center(
child: new NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
new SliverAppBar(
backgroundColor: Colors.white,
title: new TabBar(
labelColor: Colors.black,
indicatorColor: Colors.black,
labelStyle: new TextStyle(fontWeight: FontWeight.bold),
tabs: subPages.map((String str) => new Tab(text: str)).toList(),
),
),
];
},
body: new TabBarView(
children: subPages.map((String str) {
return new ListView(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
children: subPages.map((String str) {
return new Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: new Text(str),
);
}).toList(),
);
}).toList(),
),
),
),
floatingActionButton: new FloatingActionButton(
backgroundColor: Colors.black,
onPressed: null,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
),
);
}
I use this:
new DefaultTabController(
child: Builder(
builder: (context) {
final tabController = DefaultTabController.of(context)!;
tabController.addListener(() {
print("New tab index: ${tabController.index}");
});
return Scaffold(
...
);
}
),
);
Define a tabController and a listener (once changing tab, it will be triggered twice)
class _ScreenState extends State<Screen> with SingleTickerProviderStateMixin {
late TabController tabController;
#override
void initState() {
super.initState();
this.tabController = TabController(length: 3, vsync: this);
this.tabController.addListener(() {
if (this.tabController.indexIsChanging) {
print(this.tabController.index);
print(this.tabController.previousIndex);
}
});
}
}
Pass it to the TabBar and TabBarView, like:
TabBar(
controller: this.tabController,
tabs: [
Tab(text: "0"),
Tab(text: "1"),
Tab(text: "2"),
],
)
TabBarView(controller: this.tabController, children: [...])