Flutter tabcontroller detects the change in the tabbar but does not know the change in the tabbarview.
Listener causes the text of the floatingactionbutton to change, but there is no response when the tabbarview changes.
class TabPageState extends State<TabPage> with SingleTickerProviderStateMixin {
TabController _controller;
int _currentIndex = 0;
#override
void initState() {
super.initState();
_controller = TabController(vsync: this, length: 2);
_controller.addListener(_handleTabSelection);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Tab'),
bottom: TabBar(
controller: _controller,
tabs: <Widget>[
Tab(icon: Icon(Icons.laptop_mac),),
Tab(icon: Icon(Icons.desktop_mac),),
],
),
),
body: TabBarView(
controller: _controller,
children: <Widget>[
Center(child: Text('laptop'),),
Center(child: Text('desctop'),),
],
),
floatingActionButton: FloatingActionButton(
onPressed: (){},
child: Text('$_currentIndex'),
),
);
}
_handleTabSelection() {
if (_controller.indexIsChanging) {
setState(() {
_currentIndex = _controller.index;
});
}
}
}
just remove the condition :
if (_controller.indexIsChanging) {
Because every time you start changing from previousIndex to the currentIndex, you rebuild the widget and your _controller.index is the same as your initial index.
This should work :
_handleTabSelection() {
setState(() {
_currentIndex = _controller.index;
});
}
Doc Says:
indexIsChanging : True while we're animating from [previousIndex] to [index] as a
consequence of calling [animateTo]. This value is true
during the [animateTo] animation that's triggered when /// the user
taps a [TabBar] tab. It is false when [offset] is changing as a ///
consequence of the user dragging (and "flinging") the [TabBarView].
bool get indexIsChanging => _indexIsChangingCount != 0;
int _indexIsChangingCount = 0;
Code:
TabController _controller;
int _selectedIndex = 0;
List<Widget> list = [
Tab(icon: Icon(Icons.card_travel)),
Tab(icon: Icon(Icons.add_shopping_cart)),
];
#override
void initState() {
// TODO: implement initState
super.initState();
// Create TabController for getting the index of current tab
_controller = TabController(length: list.length, vsync: this);
_controller.addListener(() {
setState(() {
_selectedIndex = _controller.index;
});
print("Selected Index: " + _controller.index.toString());
});
}
Sample: https://github.com/jitsm555/Flutter-Problems/tree/master/tab_bar_tricks
Output:
Related
I have a floating action button at the right bottom of a SingleChildScrollView that I will like to disappear when scrolling down and appear when scrolling up like the attached gif file :
My code is below and will appear with any suggestion
final dataKey = new GlobalKey();
initState(){
super.initState();
_isVisible = true;
_hideButtonController = new ScrollController();
_hideButtonController.addListener((){
if(_hideButtonController.position.userScrollDirection == ScrollDirection.reverse){
if(_isVisible == true) {
/* only set when the previous state is false
* Less widget rebuilds
*/
print("**** ${_isVisible} up"); //Move IO away from setState
setState((){
_isVisible = false;
});
}
} else {
if(_hideButtonController.position.userScrollDirection == ScrollDirection.forward){
if(_isVisible == false) {
/* only set when the previous state is false
* Less widget rebuilds
*/
print("**** ${_isVisible} down"); //Move IO away from setState
setState((){
_isVisible = true;
});
}
}
}});
}
floatingActionButton: new Visibility(
visible: _isVisible,
child: new FloatingActionButton(backgroundColor: colorBlue,
onPressed: () => Scrollable.ensureVisible(dataKey.currentContext,duration: Duration(seconds: 1)),
child: Icon(Icons.arrow_upward),)),
body:SingleChildScrollView(
key: dataKey,
physics: BouncingScrollPhysics(),
controller: _hideButtonController,
)
As you can see that I have key: dataKey, that simply automatically scrolls to the top of the page when click you can see I tried using Visibility but it didn't work for me, and not sure what I did wrong but I will like the FAB to appear and disappear as shown in the attached GIF. Thanks in advance.
About the fab animation, you can use SlideTransition
Run on dartPad
class FabAnimation extends StatefulWidget {
const FabAnimation({Key? key}) : super(key: key);
#override
State<FabAnimation> createState() => _FabAnimationState();
}
class _FabAnimationState extends State<FabAnimation>
with SingleTickerProviderStateMixin {
late ScrollController _hideButtonController;
late final AnimationController _controller = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
)
..addListener(() {
setState(() {});
})
..forward();//first time load
late final Animation<Offset> _offsetAnimation = Tween<Offset>(
end: Offset.zero,
begin: const Offset(0, 2.0),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.linear,
));
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
initState() {
super.initState();
_hideButtonController = ScrollController();
_hideButtonController.addListener(() {
//add more logic for your case
if (_hideButtonController.position.userScrollDirection ==
ScrollDirection.reverse) {
if (_offsetAnimation.isCompleted) _controller.reverse();
}
if (_hideButtonController.position.userScrollDirection ==
ScrollDirection.forward) {
_controller.forward();
}
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: SlideTransition(
position: _offsetAnimation,
child: FloatingActionButton(
backgroundColor: Colors.blue,
onPressed: () {},
child: Icon(Icons.arrow_upward),
)),
body: SingleChildScrollView(
physics: BouncingScrollPhysics(),
controller: _hideButtonController,
child: Column(
children: List.generate(
33,
(index) => ListTile(
tileColor: index.isEven ? Colors.deepPurple : Colors.blue,
),
),
),
),
);
}
}
I am using Flutter to build an app and trying to show an input dialogue box when app is run for the first time. For this I use shared preferences library. However, when I open the app, the dialogue box is shown every time regardless if it is the first time or not. Also, as I type a single letter in the input box, another dialogue box opens up in front of the previous one. I have attached the code below:
class HomeMenu extends StatefulWidget {
const HomeMenu({Key? key}) : super(key: key);
#override
State<HomeMenu> createState() => _HomeMenuState();
}
class _HomeMenuState extends State<HomeMenu> with TickerProviderStateMixin {
late TabController _controller;
TextEditingController textController = TextEditingController();
var newLaunch;
Future<void> showDialogIfFirstLoaded(BuildContext thisContext) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool _newLaunch = prefs.getBool('newLaunch') ?? true;
newLaunch = _newLaunch;
if (newLaunch) {
showDialog(
context: thisContext,
builder: (BuildContext context) {
return AlertDialog(
content: TextField(
controller: textController,
onChanged: (value) =>
setState(() => name = textController.text),
),
actions: <Widget>[
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.pop(thisContext);
prefs.setBool(newLaunch, false);
},
),
],
);
},
);
}
}
#override
void initState() {
super.initState();
_controller = TabController(length: 3, vsync: this);
}
#override
Widget build(BuildContext context) {
Future.delayed(Duration.zero, () => showDialogIfFirstLoaded(context));
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _controller,
tabs: tabs,
),
),
body: TabBarView(
controller: _controller,
children: [
null
],
),
);
}
}
you are calling showDialogIfFirstLoaded in build method, and on TextFiled onChange method you are calling setState. Every time you call setState build method re-build again and it calling showDialogIfFirstLoaded on every setState.
I don't know your requirement but it not good to call setState in onChange method of TextField. You can use ValueListenableBuilder if you require to update some part of UI on every single char type in text filed
If you want's to open that dialog only one time then create a method to get whether it's first launch or not and if yes then call showDialogIfFirstLoaded method.
For more details see below code -
#override
void initState() {
super.initState();
_controller = TabController(length: 3, vsync: this);
_isFirstLaunch();
}
_isFirstLaunch()async{
final prefs = await SharedPreferences.getInstance();
final bool? isfirstLaunch = prefs.getBool('first_launch');
if(!isfirstLaunch ?? true){
await prefs.setBool('first_launch', true);
showDialogIfFirstLoaded(context);
}
and update your build method like this -
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _controller,
tabs: tabs,
),
),
body: TabBarView(
controller: _controller,
children: [
null
],
),
);
}
i am newbie to flutter and i created a tab bar and tab bar view by adding dynamic content as following code. when click on tab in tab bar it works fine. but swiping tab bar view new value always detect on another swipe for example move to tab one tab bar view says 0. move to tab 2 tab bar view says 1.
please help me friends.
#override
void initState() {
super.initState();
setState(() {
_isLoading = true;
num = 0;
for(int i=0;i<3;i++){
_tabs.add(Tab(text: '${i + 1}'));
tabView.add(getWidget());
}
_tabController = TabController(vsync: this, length: _tabs.length, initialIndex: 0);
_tabController.addListener((){
print('index${_tabController.index}');
setState(() {
num = _tabController.index ;
});
});
Widget getWidget(){
return Builder(builder: (BuildContext context) {
return Text('${num}');
});
}
#override
Widget build(BuildContext context) {
return
DefaultTabController(
length: _tabs.length,
child:Scaffold(
appBar: AppBar(
title: Text("cart"),
bottom: TabBar(
controller: _tabController,
tabs: _tabs,
),
),
body: TabBarView(
controller: _tabController,
children:tabView,
),
});
Use AutomaticKeepAliveClientMixin
override wantKeepAlive property and return true
Instead of this:
tabView.add(getWidget());
use:
tabView.add(Tabpageview(TabData(i)));
class Tabpageview extends StatefulWidget {
final tabData;
Tabpageview(this.tabData);
#override
_TabpageviewState createState() => _TabpageviewState();
}
class _TabpageviewState extends State<Tabpageview>
with AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Tab(child: Center(child: Text('Tab : ${widget.tabData.data}')));
}
}
class TabData {
int data;
TabData(this.data);
}
Modify your code with some smooth animation, plz modify setState() method as per your need.
final aniValue = _tabController.animation.value;
if (aniValue > 0.5 && index != 1) {
products.forEach((item) {
setState(() {
displayProducts.add(item);
});
});
} else if (aniValue <= 0.5 && index != 0) {
products.forEach((item) {
setState(() {
displayProducts.add(item);
});
});
}
How to solve the exception -
Unhandled Exception: 'package:flutter/src/widgets/page_view.dart': Failed assertion: line 179 pos 7: 'positions.isNotEmpty': PageController.page cannot be accessed before a PageView is built with it.
Note:- I used it in two screens and when I switch between screen it shows the above exception.
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _animateSlider());
}
void _animateSlider() {
Future.delayed(Duration(seconds: 2)).then(
(_) {
int nextPage = _controller.page.round() + 1;
if (nextPage == widget.slide.length) {
nextPage = 0;
}
_controller
.animateToPage(nextPage,
duration: Duration(milliseconds: 300), curve: Curves.linear)
.then(
(_) => _animateSlider(),
);
},
);
}
I think you can just use a Listener like this:
int _currentPage;
#override
void initState() {
super.initState();
_currentPage = 0;
_controller.addListener(() {
setState(() {
_currentPage = _controller.page.toInt();
});
});
}
I don't have enough information to see exactly where your problem is, but I just encountered a similar issue where I wanted to group a PageView and labels in the same widget and I wanted to mark active the current slide and the label so I was needing to access controler.page in order to do that. Here is my fix :
Fix for accessing page index before PageView widget is built using FutureBuilder widget
class Carousel extends StatelessWidget {
final PageController controller;
Carousel({this.controller});
/// Used to trigger an event when the widget has been built
Future<bool> initializeController() {
Completer<bool> completer = new Completer<bool>();
/// Callback called after widget has been fully built
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
completer.complete(true);
});
return completer.future;
} // /initializeController()
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
// **** FIX **** //
FutureBuilder(
future: initializeController(),
builder: (BuildContext context, AsyncSnapshot<void> snap) {
if (!snap.hasData) {
// Just return a placeholder widget, here it's nothing but you have to return something to avoid errors
return SizedBox();
}
// Then, if the PageView is built, we return the labels buttons
return Column(
children: <Widget>[
CustomLabelButton(
child: Text('Label 1'),
isActive: controller.page.round() == 0,
onPressed: () {},
),
CustomLabelButton(
child: Text('Label 2'),
isActive: controller.page.round() == 1,
onPressed: () {},
),
CustomLabelButton(
child: Text('Label 3'),
isActive: controller.page.round() == 2,
onPressed: () {},
),
],
);
},
),
// **** /FIX **** //
PageView(
physics: BouncingScrollPhysics(),
controller: controller,
children: <Widget>[
CustomPage(),
CustomPage(),
CustomPage(),
],
),
],
);
}
}
Fix if you need the index directly in the PageView children
You can use a stateful widget instead :
class Carousel extends StatefulWidget {
Carousel();
#override
_HomeHorizontalCarouselState createState() => _CarouselState();
}
class _CarouselState extends State<Carousel> {
final PageController controller = PageController();
int currentIndex = 0;
#override
void initState() {
super.initState();
/// Attach a listener which will update the state and refresh the page index
controller.addListener(() {
if (controller.page.round() != currentIndex) {
setState(() {
currentIndex = controller.page.round();
});
}
});
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Column(
children: <Widget>[
CustomLabelButton(
child: Text('Label 1'),
isActive: currentIndex == 0,
onPressed: () {},
),
CustomLabelButton(
child: Text('Label 2'),
isActive: currentIndex == 1,
onPressed: () {},
),
CustomLabelButton(
child: Text('Label 3'),
isActive: currentIndex == 2,
onPressed: () {},
),
]
),
PageView(
physics: BouncingScrollPhysics(),
controller: controller,
children: <Widget>[
CustomPage(isActive: currentIndex == 0),
CustomPage(isActive: currentIndex == 1),
CustomPage(isActive: currentIndex == 2),
],
),
],
);
}
}
This means that you are trying to access PageController.page (It could be you or by a third party package like Page Indicator), however, at that time, Flutter hasn't yet rendered the PageView widget referencing the controller.
Best Solution: Use FutureBuilder with Future.value
Here we just wrap the code using the page property on the pageController into a future builder, such that it is rendered little after the PageView has been rendered.
We use Future.value(true) which will cause the Future to complete immediately but still wait enough for the next frame to complete successfully, so PageView will be already built before we reference it.
class Carousel extends StatelessWidget {
final PageController controller;
Carousel({this.controller});
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
FutureBuilder(
future: Future.value(true),
builder: (BuildContext context, AsyncSnapshot<void> snap) {
//If we do not have data as we wait for the future to complete,
//show any widget, eg. empty Container
if (!snap.hasData) {
return Container();
}
//Otherwise the future completed, so we can now safely use the controller.page
return Text(controller.controller.page.round().toString);
},
),
//This PageView will be built immediately before the widget above it, thanks to
// the FutureBuilder used above, so whenever the widget above is rendered, it will
//already use a controller with a built `PageView`
PageView(
physics: BouncingScrollPhysics(),
controller: controller,
children: <Widget>[
AnyWidgetOne(),
AnyWidgetTwo()
],
),
],
);
}
}
Alternatively
Alternatively, you could still use a FutureBuilder with a future that completes in addPostFrameCallback in initState lifehook as it also will complete the future after the current frame is rendered, which will have the same effect as the above solution. But I would highly recommend the first solution as it is straight-forward
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
//Future will be completed here
// e.g completer.complete(true);
});
use this widget and modify it as you want:
class IndicatorsPageView extends StatefulWidget {
const IndicatorsPageView({
Key? key,
required this.controller,
}) : super(key: key);
final PageController controller;
#override
State<IndicatorsPageView> createState() => _IndicatorsPageViewState();
}
class _IndicatorsPageViewState extends State<IndicatorsPageView> {
int _currentPage = 0;
#override
void initState() {
widget.controller.addListener(() {
setState(() {
_currentPage = widget.controller.page?.toInt() ?? 0;
});
});
super.initState();
}
#override
void dispose() {
widget.controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
3,
(index) => IndicatorPageview(isActive: _currentPage == index, index: index),
),
);
}
}
class IndicatorPageview extends StatelessWidget {
const IndicatorPageview({
Key? key,
required this.isActive,
required this.index,
}) : super(key: key);
final bool isActive;
final int index;
#override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(right: 8),
width: 16,
height: 16,
decoration: BoxDecoration(color: isActive ?Colors.red : Colors.grey, shape: BoxShape.circle),
);
}
}
I am trying to make an app where a route has a tabbed layout with 5 tabs. In two of these tabs, I need to place a FAB to load a new screen.
However, by default (Using DefaultTabController), this is an all or nothing choice as there is no way to get the Tab index with this controller.
However, I followed this SO question and this one and added a manual TabController. However, now when the Tabs load, I don't see the FAB unless I click on an element in the Tab and navigate back to the tab.
Also, the FAB does not disappear when I swipe to a tab where there shouldn't be a FAB.
My code is as follows:
TabController controller;
#override
void initState(){
super.initState();
controller = new TabController(vsync: this, length: 5);
}
#override
void dispose(){
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text("My Clinic"), backgroundColor: Colors.blue,
bottom: new TabBar(
controller: controller,
tabs: <Tab>[
new Tab(icon: new Icon(Icons.report)),
new Tab(icon: new Icon(Icons.person)),
new Tab(icon: new Icon(Icons.assistant)),
new Tab(icon: new Icon(Icons.calendar_today)),
new Tab(icon: new Icon(Icons.settings))
]
)
),
body: new Container(
color: Colors.blue,
child : new TabBarView(
controller: controller,
children: <Widget>[
clinicInfo(doctor),
doctorInfo(),
assistantInfo(),
clinicSchedule(),
clinicOperations()
]
),
),
floatingActionButton: _bottomButtons(controller.index),
);
}
Here _bottomButtons is as follows:
Widget _bottomButtons(int index ) {
switch(index) {
case 0: // dashboard
return null;
break;
case 1: // doctors
return FloatingActionButton(
onPressed: null,
backgroundColor: Colors.redAccent,
child: Icon(
Icons.edit,
size: 20.0,
),
);
break;
case 2: // assistants
return FloatingActionButton(
onPressed: null,
backgroundColor: Colors.redAccent,
child: Icon(
Icons.edit,
size: 20.0,
),
);
break;
case 3: // sessions
return null;
break;
case 4: // settings
return null;
break;
}
}
As we can see, the FAB is only supposed to be visible on Tabs 1 and 2. What am I overlooking/doing wrong here?
Are you sure you change the state?
Maybe you need:
TabController controller;
#override
void initState() {
super.initState();
controller = new TabController(vsync: this, length: 5);
controller.addListener(updateIndex);
}
#override
void dispose() {
controller.removeListener(updateIndex);
controller.dispose();
super.dispose();
}
void updateIndex() {
setState(() {});
}
With this approach, you can create beautifully animated fabs for selected tabs:
class MultipleHidableFabs extends StatefulWidget {
#override
State<MultipleHidableFabs> createState() => _MultipleHidableFabsState();
}
class _MultipleHidableFabsState extends State<MultipleHidableFabs>
with SingleTickerProviderStateMixin {
// Index of initially opened tab
static const initialIndex = 0;
// Number of tabs
static const tabsCount = 3;
// List with current scales for each tab's fab
// Initialize with 1.0 for initial opened tab, 0.0 for others
final tabScales =
List.generate(tabsCount, (index) => index == initialIndex ? 1.0 : 0.0);
late TabController tabController;
#override
void initState() {
super.initState();
tabController = TabController(
length: tabsCount,
initialIndex: initialIndex,
vsync: this,
);
// Adding listener to animation gives us opportunity to track changes more
// frequently compared to listener of TabController itself
tabController.animation!.addListener(() {
setState(() {
// Current animation value. It ranges from 0 to (tabsCount - 1)
final animationValue = tabController.animation!.value;
// Simple rounding gives us understanding of what tab is showing
final currentTabIndex = animationValue.round();
// currentOffset equals 0 when tabs are not swiped
// currentOffset ranges from -0.5 to 0.5
final currentOffset = currentTabIndex - animationValue;
for (int i = 0; i < tabsCount; i++) {
if (i == currentTabIndex) {
// For current tab bringing currentOffset to range from 0.0 to 1.0
tabScales[i] = (0.5 - currentOffset.abs()) / 0.5;
} else {
// For other tabs setting scale to 0.0
tabScales[i] = 0.0;
}
}
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: tabController,
tabs: [
Tab(icon: Icon(Icons.one_k)),
Tab(icon: Icon(Icons.two_k)),
Tab(icon: Icon(Icons.three_k)),
],
),
),
body: SafeArea(
child: TabBarView(
controller: tabController,
children: [Icon(Icons.one_k), Icon(Icons.two_k), Icon(Icons.three_k)],
),
),
floatingActionButton: createScaledFab(),
);
}
Widget? createScaledFab() {
// Searching for index of a tab with not 0.0 scale
final indexOfCurrentFab = tabScales.indexWhere((fabScale) => fabScale != 0);
// If there are no fabs with non-zero opacity return nothing
if (indexOfCurrentFab == -1) {
return null;
}
// Creating fab for current index
final fab = createFab(indexOfCurrentFab);
// If no fab created return nothing
if (fab == null) {
return null;
}
final currentFabScale = tabScales[indexOfCurrentFab];
// Scale created fab with
// You can use different Widgets to create different effects of switching
// fabs. E.g. you can use Opacity widget or Transform.translate to create
// custom animation effects
return Transform.scale(scale: currentFabScale, child: fab);
}
// Create fab for provided index
// You can skip creating fab for any indexes you want
Widget? createFab(final int index) {
if (index == 0) {
return FloatingActionButton(
onPressed: () => print("On first fab clicked"),
child: Icon(Icons.one_k),
);
}
// Not created fab for 1 index deliberately
if (index == 2) {
return FloatingActionButton(
onPressed: () => print("On third fab clicked"),
child: Icon(Icons.three_k),
);
}
}
}
Advantages of this approach:
Synchronized animation between swiping and showing fabs
Tapping on tabs also animates in a right manner
Ability to easily skip creating fabs for selected indexes
See an example in action: