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'm currently working on a Flutter mobile app which is supposed to work on Android and IOS.
The issue I'm having is about ListView and updating it.
I know I'm doing wrong with a lot of things but I'm learning and I would like to learn properly. So if you have any comments, tips about the code pls give them :)
Basically here is what it does :
The main player will choose who will play with him at the game via an AlertDialog which has a CheckboxList inside it and every time he selects a player, it will update a list called choosenPlayers which has all Player objects choosen in it.
Then what I want to do is to display a list of all selected players (a reorderable list to change the order of players) and update it everytime the choosenPlayers list is updated.
I managed to display these players but I have to reload the page by going in the drawer menu and clicking on page link to see added players.
I use a stateful widget for my players reorderable list and I pass to the parent the list of players (This is not the rigth way to do it I know) :
import 'package:flutter/material.dart';
import 'package:mollky/models/player.dart';
class ChoosenPlayers extends StatefulWidget {
_ChoosenPlayersState _choosenPlayersState = _ChoosenPlayersState();
List<Player> choosenPlayers = [];
ChoosenPlayers({Key key, this.choosenPlayers}) : super(key: key);
#override
_ChoosenPlayersState createState() => _choosenPlayersState;
}
class _ChoosenPlayersState extends State<ChoosenPlayers> {
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return ReorderableListView(
onReorder: onReorder,
children: getListItems(),
);
}
List<ListTile> getListItems() => widget.choosenPlayers
.asMap()
.map((i, item) => MapEntry(i, buildTenableListTile(item, i)))
.values
.toList();
ListTile buildTenableListTile(Player item, int index) {
return ListTile(
key: ValueKey(item.id),
title: Text(item.nickname + " " + item.name),
leading: Text("#${index + 1}"),
);
}
void onReorder(int oldIndex, int newIndex) {
if (newIndex > oldIndex) {
newIndex -= 1;
}
setState(() {
Player reOrderedPlayer = widget.choosenPlayers[oldIndex];
widget.choosenPlayers.removeAt(oldIndex);
widget.choosenPlayers.insert(newIndex, reOrderedPlayer);
});
}
}
Here is the code of the main page where reorderable list is displayed and AlertDialog showed.
Sorry, couldn't format with Dart, don't run the code snipped obviously xD
class NewGame extends StatefulWidget {
#override
State<StatefulWidget> createState() => NewGameState();
}
class NewGameState extends State<NewGame> {
List<Player> players = [];
NewGameState() {
this.players.add(
Player(id: 0, name: "Dupont", nickname: "julien", picture: "test"));
this
.players
.add(Player(id: 1, name: "Dpont", nickname: "julien", picture: "test"));
this
.players
.add(Player(id: 2, name: "Dunt", nickname: "juen", picture: "test"));
}
static List<Player> _choosenPlayers = [];
ChoosenPlayers choosenPlayersObject = ChoosenPlayers(
choosenPlayers: _choosenPlayers,
);
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: DrawerWidget(),
appBar: AppBar(title: Text("Nouvelle partie")),
body: Column(children: <Widget>[
Card(
child: ListTile(
leading: Icon(Icons.people),
title: Text("Choisissez les joueurs"),
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("Les joueurs existants"),
content:
Stack(overflow: Overflow.visible, children: <
Widget>[
Positioned(
right: -40.0,
top: -40.0,
child: InkResponse(
onTap: () {
Navigator.of(context).pop();
},
child: CircleAvatar(
child: Icon(Icons.close),
backgroundColor: Colors.lightBlue,
),
),
),
Positioned(
child: StatefulBuilder(
builder: (BuildContext context,
StateSetter setState) {
return Container(
width: 350.0,
height: 150.0,
child: ListView.builder(
itemCount: players.length,
itemBuilder:
(context, playerIndex) {
return CheckboxListTile(
title: Text(players[playerIndex]
.nickname +
" " +
players[playerIndex].name),
value: _choosenPlayers.contains(
players[playerIndex]),
onChanged: (bool value) {
if (!_choosenPlayers.contains(
players[playerIndex])) {
_choosenPlayers.add(
players[playerIndex]);
setState(() {});
} else {
_choosenPlayers.remove(
players[playerIndex]);
setState(() {});
}
},
secondary: const Icon(
Icons.hourglass_empty),
);
}),
);
},
),
),
]));
});
})),
Container(
width: 350.0,
height: 150.0,
child: choosenPlayersObject,
),
]));
}
}
I've seen nothing on forums about updating list without triggering a callback like onRefresh which is not what I want.
It is a real nightmare xD. Sorry for french words btw I can translate if needed but they are not important, simple text.
Here are two screenshots of the list and alert dialog :
Thank you in advance :)
The state of parent widget is not updated. That's why, even though the payer is added to the list. But not shown to in parent widget.
The setState you called only update the state of StatefulBuilder not of the NewGame.
Check out the below 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: NewGameScreen(),
);
}
}
class NewGameScreen extends StatefulWidget {
#override
_NewGameScreenState createState() => _NewGameScreenState();
}
class _NewGameScreenState extends State<NewGameScreen> {
List<Player> _availablePlayers = [];
List<Player> _selectedPlayers = [];
#override
void initState() {
super.initState();
_availablePlayers = [
Player(id: 0, name: "Ross", nickname: "Geller", picture: "test"),
Player(id: 1, name: "Rachel", nickname: "Green", picture: "test"),
Player(id: 2, name: "Chandler", nickname: "Bing", picture: "test"),
];
}
_selectPlayer() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("Existing players"),
content: Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
right: -40.0,
top: -40.0,
child: InkResponse(
onTap: () {
Navigator.of(context).pop();
},
child: CircleAvatar(
child: Icon(Icons.close),
backgroundColor: Colors.lightBlue,
),
),
),
StatefulBuilder(
builder: (BuildContext context, StateSetter alertState) {
return Container(
width: 350.0,
height: 150.0,
child: ListView.builder(
itemCount: _availablePlayers.length,
itemBuilder: (context, playerIndex) {
return CheckboxListTile(
title:
Text(_availablePlayers[playerIndex].nickname + " " + _availablePlayers[playerIndex].name),
value: _selectedPlayers.contains(_availablePlayers[playerIndex]),
onChanged: (bool value) {
if (_selectedPlayers.contains(_availablePlayers[playerIndex])) {
_selectedPlayers.remove(_availablePlayers[playerIndex]);
} else {
_selectedPlayers.add(_availablePlayers[playerIndex]);
}
setState(() {});//ALSO UPDATE THE PARENT STATE
alertState(() {});
},
secondary: const Icon(Icons.hourglass_empty),
);
},
),
);
},
),
],
),
);
},
);
}
_onReorder(int oldIndex, int newIndex) {
if (newIndex > oldIndex) {
newIndex -= 1;
}
print('oldIndex:$oldIndex');
print('newIndex:$newIndex');
setState(() {
Player player = _selectedPlayers[newIndex];
_selectedPlayers[newIndex] = _selectedPlayers[oldIndex];
_selectedPlayers[oldIndex] = player;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("New Game")),
body: Column(
children: <Widget>[
Card(
child: ListTile(
leading: Icon(Icons.people),
title: Text("Choose players"),
onTap: _selectPlayer,
),
),
Flexible(
child: ReorderableListView(
onReorder: _onReorder,
children: _selectedPlayers.map((player) {
return ListTile(
key: ValueKey(player.id),
title: Text(player.nickname + " " + player.name),
leading: Text("#${_selectedPlayers.indexOf(player) + 1}"),
);
}).toList(),
),
),
],
),
);
}
}
class Player {
int id;
String name;
String nickname;
String picture;
Player({this.id, this.name, this.nickname, this.picture});
}
Hope it helps :)
I am trying to use TabBar and Bottom Navigation bar together. I am able to save state of tabview when I switch to different page using bottom navigation bar. When I switch back, the position of the selected tab and pageview do not match.
What could be possibly wrong? Please help me with example code of using tab bar and bottom navigation bar together.
Set initialIndex, Take a variable which update your initial and last selected tab position, This variable must be update with the new value when you change any tab.
int lastTabPosition=0;
DefaultTabController(
length: 3,
initialIndex: lastTabPosition,
child: Scaffold(
appBar: AppBar(
title: Text("Tabs"),
bottom: TabBar(
tabs: <Widget>[
Tab(text: 'One'),
Tab(text: 'Two'),
Tab(text: 'Three'),
],
),
),
body: TabBarView(
children: <Widget>[
],
),
),
);
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: new MyHomePage(),
));
}
class TabbedPage extends StatefulWidget {
TabbedPage({Key key, this.pageIndex, this.tabCount}) : super(key: key);
final int pageIndex;
final int tabCount;
_TabbedPageState createState() => new _TabbedPageState();
}
class _TabbedPageState extends State<TabbedPage> with TickerProviderStateMixin {
TabController _tabController;
int _getInitialIndex() {
int initialIndex = PageStorage.of(context).readState(
context,
identifier: widget.pageIndex,
) ??
0;
print("Initial Index ${initialIndex}");
return initialIndex;
}
#override
void initState() {
_tabController = new TabController(
length: widget.tabCount,
vsync: this,
initialIndex: _getInitialIndex(),
);
_tabController.addListener(() {
print("New Index ${_tabController.index}");
PageStorage.of(context).writeState(
context,
_tabController.index,
identifier: widget.pageIndex,
);
});
super.initState();
}
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new Container(
color: Theme.of(context).primaryColor,
child: new TabBar(
controller: _tabController,
isScrollable: true,
tabs: new List<Tab>.generate(widget.tabCount, (int tabIndex) {
var name = 'Tab ${widget.pageIndex}-${tabIndex}';
return new Tab(text: name);
}),
),
),
new Expanded(
child: new TabBarView(
controller: _tabController,
children:
new List<Widget>.generate(widget.tabCount, (int tabIndex) {
return new ListView.builder(
key: new PageStorageKey<String>(
'TabBarView:${widget.pageIndex}:$tabIndex'),
itemCount: 20,
itemExtent: 60.0,
itemBuilder: (BuildContext context, int index) => new Text(
'View ${widget.pageIndex}-${tabIndex}-${index}'));
}),
),
),
],
);
}
}
class MyHomePage extends StatefulWidget {
_MyHomePageState createState() => new _MyHomePageState();
}
const List<int> tabCounts = const <int>[5, 8];
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
PageController _controller = new PageController();
int currentIndex = 0;
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(),
body: new PageView(
controller: _controller,
children: new List<Widget>.generate(tabCounts.length, (int index) {
return new TabbedPage(
pageIndex: index,
tabCount: tabCounts[index],
);
}),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentIndex,
onTap: (int index) {
setState(() {
currentIndex = index;
_controller.jumpToPage(index);
});
},
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
title: Text("Settings"),
),
],
),
);
}
}
reference : https://github.com/flutter/flutter/issues/20341
When I use TextEditingController in CupertinoTextField, and change to another widget(page) and return, the previous state in that page is lost.
When I uncomment //controller: textController, everything works fine.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'test',
home: DefaultTabController(
length: 2,
child: Scaffold(
body: TabBarView(
children: [new Search(), new Setting(),
],
),
bottomNavigationBar: Container(
height: 60,
child: new TabBar(
tabs: [
Tab(icon: new Icon(Icons.search)),
Tab(icon: new Icon(Icons.settings)),
],
labelColor: Colors.blue,
unselectedLabelColor: Colors.grey,
),
)
),
),
);
}
}
class Setting extends StatelessWidget {
#override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(Icons.check),
onPressed: () {
Navigator.push(context, CupertinoPageRoute(
builder: (context) =>
new Scaffold(
appBar: AppBar(title: Text('3'),),
)));
});
}
}
class Search extends StatefulWidget {
#override
createState() => new SearchState();
}
class SearchState extends State<Search> {
String currentWord = '';
final TextEditingController textController = new TextEditingController();
#override
void dispose() {
textController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Row(
children: <Widget>[
new Expanded(
child: new CupertinoTextField(
style: TextStyle(color: Colors.white),
cursorColor: Colors.white,
//controller: textController,
maxLines: 1,
clearButtonMode: OverlayVisibilityMode.editing,
onChanged: (text) {
setState(() {
currentWord = text;
});
},
),
),
],
),
),
body: ListView.builder(
itemCount: 5,
itemBuilder: (context, i) {
return Text(currentWord);
})
);
}
}
The expected result(without controller set):get back and the state keeps the same.
Actual results(with controller set): get back and the state lost
The explanation for the observed behavior is the following:
CupertinoTextField uses an internal TextEditingController for which the framework automatically sets an AutomaticKeepAlive. This keepAlive is responsible for keeping the state.
If you use your own controller, you are in charge of attaching the AutomaticKeepAlive because the framework doesn't do it for you.
The following snippet adds the keepAlive to your code:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'test',
home: DefaultTabController(
length: 2,
child: Scaffold(
body: TabBarView(
children: [
new Search(),
new Setting(),
],
),
bottomNavigationBar: Container(
height: 60,
child: new TabBar(
tabs: [
Tab(icon: new Icon(Icons.search)),
Tab(icon: new Icon(Icons.settings)),
],
labelColor: Colors.blue,
unselectedLabelColor: Colors.grey,
),
)),
),
);
}
}
class Setting extends StatelessWidget {
#override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(Icons.check),
onPressed: () {
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) => new Scaffold(
appBar: AppBar(
title: Text('3'),
),
)));
});
}
}
class Search extends StatefulWidget {
#override
createState() => new SearchState();
}
class SearchState extends State<Search> with AutomaticKeepAliveClientMixin {
String currentWord = '';
final TextEditingController textController = new TextEditingController();
#override
void initState() {
super.initState();
textController?.addListener(updateKeepAlive);
}
#override
void dispose() {
textController?.removeListener(updateKeepAlive);
textController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context); // See AutomaticKeepAliveClientMixin.
return new Scaffold(
appBar: new AppBar(
title: new Row(
children: <Widget>[
new Expanded(
child: new CupertinoTextField(
style: TextStyle(color: Colors.white),
cursorColor: Colors.white,
controller: textController,
maxLines: 1,
clearButtonMode: OverlayVisibilityMode.editing,
onChanged: (text) {
setState(() {
currentWord = text;
});
},
),
),
],
),
),
body: ListView.builder(
itemCount: 5,
itemBuilder: (context, i) {
return Text(currentWord);
}));
}
#override
bool get wantKeepAlive => textController?.text?.isNotEmpty == true;
}
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);
}
}
}