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.
Related
I'm trying to implement a search bar in my app that displays songs, which the user is then able to search through to find a specific song. I pass the List songs into my CustomSearchDelegate class, but I can't access the variable songTitle from the for loop in buildResults and buildSuggestions. Relevant code below:
import 'package:flutter/material.dart';
class Recordings extends StatelessWidget {
Recordings({Key? key, required this.title}) : super(key: key);
final String title;
//This will be replaced by data from the database
final List<Widget> songs = [
const Song(songTitle: "September"),
const Song(songTitle: "Don't Stop Me Now"),
const Song(songTitle: "Let It Go"),
const Song(songTitle: "Smoke on the Water"),
const Song(songTitle: "Don't Kill My Vibe"),
const Song(songTitle: "Mamma Mia"),
const Song(songTitle: "4'33"),
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
actions: [
IconButton(
onPressed: () {
showSearch(context: context, delegate: CustomSearchDelegate(searchTerms: songs));
},
icon: const Icon(Icons.search)),
],
),
body: Container(
color: Colors.grey[600],
child: Center(
child: ListView(
children: songs,
),
),
),
);
}
}
class Song extends StatelessWidget {
const Song({Key? key, required this.songTitle}) : super(key: key);
final String songTitle;
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(10),
child: Container(
alignment: Alignment.center,
color: Colors.lightBlue[300],
child: Text(
songTitle,
style: const TextStyle(color: Colors.black),
),
),
);
}
}
class CustomSearchDelegate extends SearchDelegate {
CustomSearchDelegate({required this.searchTerms});
final List<Widget> searchTerms;
#override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
onPressed: () {
query = "";
},
icon: const Icon(Icons.clear),
),
];
}
#override
Widget buildLeading(BuildContext context) {
return IconButton(
onPressed: () {
close(context, null);
},
icon: const Icon(Icons.arrow_back),
);
}
#override
Widget buildResults(BuildContext context) {
List<Widget> matchQuery = [];
//looping through each item in searchTerms
for (var song in searchTerms) {
if (song.title.toLowerCase().contains(query.toLowerCase())) { //ERROR OCCURS HERE
matchQuery.add(song);
}
}
return ListView.builder(
itemCount: matchQuery.length,
itemBuilder: ((context, index) {
var result = matchQuery[index].title;
return ListTile(
title: Text(result),
);
}),
);
}
#override
Widget buildSuggestions(BuildContext context) {
List<Widget> matchQuery = [];
//looping through each item in searchTerms
for (var song in searchTerms) {
if (song.title.toLowerCase().contains(query.toLowerCase())) { //ERROR OCCURS HERE
matchQuery.add(song);
}
}
return ListView.builder(
itemCount: matchQuery.length,
itemBuilder: ((context, index) {
var result = matchQuery[index].title;
return ListTile(
title: Text(result),
);
}),
);
}
}
Figured out how to fix this. I changed List<Widget> to List<Song> wherever I needed to access songTitle. I also added a getSongTitle to the Song class that returned songTitle.
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 have an app with three different pages. To navigate between the pages I use a bottom navigation. One of the pages contains a form. Before leaving the form page, I want to display a confirmation dialog. I know that I can intercept the back button of the device with WillPopScope. This works perfectly. But WillPopScope is not triggered when I leave the page via the bottom navigation.
I have also tried using the WidgetsBindingObserver, but both didPopRoute and didPushRoute are never triggered.
Any ideas what I can do?
My form page:
class TodayTimeRecordFormState extends State<TodayTimeRecordForm> with WidgetsBindingObserver{
final formKey = GlobalKey<FormState>();
TodayTimeRecordFormState();
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
Future<bool> didPopRoute() {
//never called
_onWillPop();
}
#override
Future<bool> didPushRoute(String route){
//never called
_onWillPop();
}
#override
Widget build(BuildContext context) {
return WillPopScope(
// called when the device back button is pressed.
onWillPop: _onWillPop,
child: Scaffold(...));
}
Future<bool> _onWillPop() {
return showDialog(
context: context,
child: new AlertDialog(
title: new Text('Are you sure?'),
content: new Text('Unsaved data will be lost.'),
actions: <Widget>[
new FlatButton(
onPressed: () => Navigator.of(context).pop(false),
child: new Text('No'),
),
new FlatButton(
onPressed: () => Navigator.of(context).pop(true),
child: new Text('Yes'),
),
],
),
);
}
}
Page containing the bottom navigation:
const List<Navigation> allNavigationItems = <Navigation>[
Navigation('Form', Icons.home, Colors.orange),
Navigation('Page2', Icons.work_off, Colors.orange),
Navigation('Page3', Icons.poll, Colors.orange)
];
class HomePage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return HomePageState();
}
}
class HomePageState extends State<HomePage> {
int selectedTab = 0;
final pageOptions = [
TodayTimeRecordForm(),
Page2(),
Page3(),
),
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppConfig.instance.values.title),
),
body: pageOptions[selectedTab],
bottomNavigationBar: BottomNavigationBar(
currentIndex: selectedTab,
unselectedItemColor: Colors.grey,
showUnselectedLabels: true,
selectedItemColor: Colors.amber[800],
onTap: (int index) {
setState(() {
selectedTab = index;
});
},
items: allNavigationItems.map((Navigation navigationItem) {
return BottomNavigationBarItem(
icon: Icon(navigationItem.icon),
backgroundColor: Colors.white,
label: navigationItem.title);
}).toList(),
),
);
}
}
You can do it by modifying your BottomNavigationBar's onTap.
onTap: (int index) {
if (selectedTab == 0 && index != 0){
// If the current tab is the first tab (the form tab)
showDialog(
context: context,
child: new AlertDialog(
title: new Text('Are you sure?'),
content: new Text('Unsaved data will be lost.'),
actions: <Widget>[
new FlatButton(
onPressed: () => Navigator.pop(context), // Closes the dialog
child: new Text('No'),
),
new FlatButton(
onPressed: () {
setState(()=>selectedTab = index); // Changes the tab
Navigator.pop(context); // Closes the dialog
},
child: new Text('Yes'),
),
],
),
);
} else {
// If the current tab isn't the form tab
setState(()=>selectedTab = index);
}
},
I have been trying to create backdrop menu with some navigation buttons each with his own stateless widget page . I want to pass the child widget page based on index position of the clicked menu.
backlayer.dart
class BackLayer extends StatefulWidget {
AnimationController controller;
Widget child;
BackLayer({this.controller});
_BackLayerState createState() => _BackLayerState();
}
class _BackLayerState extends State<BackLayer> {
var _currentPageIndex;
List<String> navlist = ['Home', 'About', 'Contact Us'];
bool get isPanelVisible {
final AnimationStatus status = widget.controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: navlist.length,
itemBuilder: (BuildContext context, int index) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
ListTile(
onTap: () {
setState(() {
_currentPageIndex = navlist[index];
});
// PanelClass(frontLayer: About(),);
print(navlist[index].toString());
widget.controller
.fling(velocity: isPanelVisible ? -1.0 : 1.0);
},
title: Text(
navlist[index],
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black87, fontWeight: FontWeight.bold),
),
)
],
);
I want to pass the reference of the clicked menu inside backdrop.dart ,where i am gonna pass that reference to panelClass body as the child .
Backdrop.dart
class BackDrop extends StatefulWidget {
_BackDropState createState() => _BackDropState();
}
class _BackDropState extends State<BackDrop> with TickerProviderStateMixin {
AnimationController controller;
#override
void initState() {
super.initState();
controller = AnimationController(
duration: Duration(seconds: 3), vsync: this, value: 1.0);
}
#override
void dispose() {
super.dispose();
controller.dispose();
}
bool get isPanelVisible {
final AnimationStatus status = controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0.0,
backgroundColor: Theme.of(context).accentColor,
title: Text('Home'),
leading: IconButton(
onPressed: () {
controller.fling(velocity: isPanelVisible ? -1.0 : 1.0);
},
icon: AnimatedIcon(
progress: controller.view,
icon: AnimatedIcons.close_menu,
),
),
),
body: PanelClass(controller: controller,frontLayer: 'Need to get reference here',),
);
}
}
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);
}
}
}