flutter tabbar swippable issue - flutter

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);
});
});
}

Related

Using Floating action button with PageView

I'm trying to use Flutter FAB in PageView, but since the FAB takes a variable that is initialized in the page 2, it creates an error.
So how can i show the FAB only after the second page?
You can use PageView on top level, and use scaffold on every item.It can also be done by adding listener to the pageController.
class FabG extends StatefulWidget {
const FabG({super.key});
#override
State<FabG> createState() => _FabGState();
}
class _FabGState extends State<FabG> {
bool isFabActivePage = false;
late final PageController controller = PageController()
..addListener(() {
if (controller.hasClients && (controller.page ?? 0) < 2) { //your logic here
isFabActivePage = false;
setState(() {});
} else if (isFabActivePage == false) {
isFabActivePage = true;
setState(() {});
}
});
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton:
isFabActivePage ? FloatingActionButton(onPressed: () {}) : null,
body: PageView.builder(
controller: controller,
itemBuilder: (context, index) => Center(child: Text("page ${index+1}")),
),
);
}
}
The solution I used was to set a callback with the bool floating.
on the home page:
`
bool floating = false;
showFAB(show) {
setState(() {
floating = show;
});
}`
floatingActionButton:(floating) ? FloatingMenu(pageController: pageController) : null,
then on the initState function of second page used the callback to change the bool value

Flutter: Lazy Loading with low amount of data

I try to use lazy load to show the order of the customer by using the ScrollController.
Of course, the new user has a low number of orders and those items are not enough to take up the entire screen. So the ScrollController doesn't work. What I can do?
This code will show a basic lazy load. You can change the _initialItemsLength to a low value like 1 to see this issue.
You can try this at api.flutter.dev
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const Center(
child: MyStatefulWidget(),
),
),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
late List myList;
ScrollController _scrollController = ScrollController();
int _initialItemsLength = 10, _currentMax = 10;
#override
void initState() {
super.initState();
myList = List.generate(_initialItemsLength, (i) => "Item : ${i + 1}");
_scrollController.addListener(() {
print("scrolling: ${_scrollController.position.pixels}");
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_getMoreData();
}
});
}
_getMoreData() {
print("load more: ${myList.length}");
for (int i = _currentMax; i < _currentMax + 10; i++) {
myList.add("Item : ${i + 1}");
}
_currentMax = _currentMax + 10;
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
controller: _scrollController,
itemBuilder: (context, i) {
if (i == myList.length) {
return CupertinoActivityIndicator();
}
return ListTile(
title: Text(myList[i]),
);
},
itemCount: myList.length + 1,
),
);
}
}
First, start _initialItemsLength with 10. The scroller will be available and you will see it in the console. After that, change _initialItemsLength to 1. The console will be blank.
scroll listener will be triggered only if user try to scroll
as an option you need to check this condition _scrollController.position.pixels == _scrollController.position.maxScrollExtent after build method executed and each time when user scroll to bottom
just change a bit initState and _getMoreData methods
#override
void initState() {
super.initState();
myList = List.generate(_initialItemsLength, (i) => 'Item : ${i + 1}');
_scrollController.addListener(() => _checkIsMaxScroll());
WidgetsBinding.instance.addPostFrameCallback((_) => _checkIsMaxScroll());
}
void _checkIsMaxScroll() {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
_getMoreData();
}
}
_getMoreData() {
print('load more: ${myList.length}');
for (int i = _currentMax; i < _currentMax + 10; i++) {
myList.add('Item : ${i + 1}');
}
_currentMax = _currentMax + 10;
setState(() => WidgetsBinding.instance.addPostFrameCallback((_) => _checkIsMaxScroll()));
}
You can set your ListView with physics: AlwaysScrollableScrollPhysics(), and thus it will be scrollable even when the items are not too many. This will lead the listener to be triggered.
Key code part:
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
itemBuilder: (context, i) {
if (i == myList.length) {
return CupertinoActivityIndicator();
}
return ListTile(
title: Text(myList[i]),
);
},
itemCount: myList.length + 1,
),
);
}
The point is 'Find some parameter that can tell whether scroll is enabled or not. If not just load more until the scroll is enabled. Then use a basic step for a lazy load like the code in my question.'
After I find this parameter on google, I don't find this. But I try to check any parameter as possible. _scrollController.any until I found this.
For someone who faces this issue like me.
You can detect the scroll is enabled by using _scrollController.position.maxScrollExtent == 0 with using some delay before that.
This is my code. You can see it works step by step in the console.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class PageStackoverflow72734370 extends StatefulWidget {
const PageStackoverflow72734370({Key? key}) : super(key: key);
#override
State<PageStackoverflow72734370> createState() => _PageStackoverflow72734370State();
}
class _PageStackoverflow72734370State extends State<PageStackoverflow72734370> {
late final List myList;
final ScrollController _scrollController = ScrollController();
final int _initialItemsLength = 1;
bool isScrollEnable = false, isLoading = false;
#override
void initState() {
super.initState();
print("\ninitState work!");
print("_initialItemsLength: $_initialItemsLength");
myList = List.generate(_initialItemsLength, (i) => 'Item : ${i + 1}');
_scrollController.addListener(() {
print("\nListener work!");
print("position: ${_scrollController.position.pixels}");
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) _getData();
});
_helper();
}
Future _helper() async {
print("\nhelper work!");
while (!isScrollEnable) {
print("\nwhile loop work!");
await Future.delayed(Duration.zero); //Prevent errors from looping quickly.
try {
print("maxScroll: ${_scrollController.position.maxScrollExtent}");
isScrollEnable = 0 != _scrollController.position.maxScrollExtent;
print("isScrollEnable: $isScrollEnable");
if (!isScrollEnable) _getData();
} catch (e) {
print(e);
}
}
print("\nwhile loop break!");
}
void _getData() {
print("\n_getData work!");
if (isLoading) return;
isLoading = true;
int i = myList.length;
int j = myList.length + 1;
for (i; i < j; i++) {
myList.add("Item : ${i + 1}");
}
print("myList.length: ${myList.length}");
isLoading = false;
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
controller: _scrollController,
itemBuilder: (context, i) {
if (i == myList.length) {
return const CupertinoActivityIndicator();
}
return ListTile(title: Text(myList[i]));
},
itemCount: myList.length + 1,
),
);
}
}
You can test in my test. You can change the initial and incremental values at ?initial=10&incremental=1.
I know, this case is rare. Most applications show more data widget height than the height of the screen or the data fetching 2 turns that enough for making these data widget height than the height of the screen. But I put these data widgets in the wrap for users that use the desktop app. So, I need it.

Flutter tabcontroller index does not respond to changes in the tabbarview

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:

Dynamically build a Flutter PageView

I need some suggestions on how to use the PageView. I have an online project that allows users to build custom forms, complete with conditions that hide or show questions depending on the answers in other questions. The mobile project allows users to fill out these forms. I've been playing with the PageView, which works for this, but I'm struggling to figure out how to indicate the end to the PageView. Right now, it will allow scrolling to continue forever.
return PageView.builder(
controller: _controller,
onPageChanged: (int index) {
FocusScope.of(context).requestFocus(FocusNode());
},
itemBuilder: (BuildContext context, int index) {
if (index >= _form.controls.length) {
print("Returning null");
return null;
}
return FormControlFactory.createFormControl(
_form.controls[index], null);
},
);
Since I'm not sure until the end of the form how many elements how do end the scrolling?
Update: In my example, I try returning null, but it still scrolls past the end.
Update: Here is where I'm currently at:
class _FormViewerControllerState extends State<FormViewerController> {
int _currentIndex = 0;
List<FormGroupController> _groups = List();
List<StreamSubscription> _subscriptions = List();
Map<int, FormControlController> _controllerMap = Map();
bool _hasVisibilityChanges = false;
#override
void initState() {
super.initState();
for (var i = 0; i < widget.form.controls.length; i++) {
var control = widget.form.controls[i];
if (control.component == ControlType.header) {
_groups.add(FormGroupController(
form: widget.form,
formResponses: widget.responses,
headerIndex: i));
}
}
_controllerMap[_currentIndex] = _getControl(_currentIndex);
_subscriptions.add(FormsEventBus()
.on<FormControlVisibilityChanging>()
.listen(_onControlVisibilityChanging));
_subscriptions.add(FormsEventBus()
.on<FormControlVisibilityChanged>()
.listen(_onControlVisibilityChanged));
}
#override
Widget build(BuildContext context) {
print("Building pageview, current index: $_currentIndex");
return PageView.builder(
controller: PageController(
initialPage: _currentIndex,
keepPage: true,
),
onPageChanged: (int index) {
print("Page changed: $index");
_currentIndex = index;
FocusScope.of(context).requestFocus(FocusNode());
},
itemBuilder: (BuildContext context, int index) {
print("Building $index");
_controllerMap[index] = _getControl(index);
return _controllerMap[index].widget;
},
itemCount: _groups
.map((g) => g.visibleControls)
.reduce((curr, next) => curr + next),
);
}
#override
void dispose() {
_subscriptions.forEach((sub) => sub.cancel());
_groups.forEach((g) => g.dispose());
super.dispose();
}
FormControlController _getControl(int index) {
for (var group in _groups) {
// We want to reduce the index so it can be local to group
if (index >= group.visibleControls) {
index -= group.visibleControls;
continue;
}
for (var instance in group.instances) {
// We want to reduce the index so it can be local to the instance
if (index >= instance.visibleControls) {
index -= instance.visibleControls;
continue;
}
return instance.controls.where((c) => c.visible).elementAt(index);
}
}
throw StateError("Weird, the current control doesn't exist");
}
int _getControlIndex(FormControlController control) {
var index = 0;
for (var group in _groups) {
if (control.groupInstance.group.groupId != group.groupId) {
index += group.visibleControls;
continue;
}
for (var instance in group.instances) {
if (control.groupInstance.groupInstanceId != instance.groupInstanceId) {
index += instance.visibleControls;
continue;
}
for (var c in instance.controls.where((c) => c.visible)) {
if (c.control.id != control.control.id) {
index++;
continue;
}
return index;
}
}
}
throw StateError("Weird, can't find the control's index");
}
_onControlVisibilityChanging(FormControlVisibilityChanging notification) {
_hasVisibilityChanges = true;
}
_onControlVisibilityChanged(FormControlVisibilityChanged notification) {
if (!_hasVisibilityChanges) {
return;
}
setState(() {
print("Setting state");
var currentControl = _controllerMap[_currentIndex];
_controllerMap.clear();
_currentIndex = _getControlIndex(currentControl);
_controllerMap[_currentIndex] = currentControl;
});
_hasVisibilityChanges = false;
}
}
The problem now is that if the changes result in a new page before the current one, in order to stay on the same page, the page index has to change so that it stays on the current one and that part isn't working. The build method is getting called multiple times and ends up showing the original index for some reason.
Here some sample print statements that show what I mean:
flutter: Control Text Box 2 (5) visible: true
flutter: Group instance Section 2 (1) visible controls: 1 -> 2
flutter: Group 1 clearing visible controls count
flutter: Setting state
flutter: Building pageview, current index: 2
flutter: Building 1
flutter: Building 2
flutter: Building pageview, current index: 2
flutter: Building 1
So I'm on index 1 at the beginning. I choose something on that view that results in a new page being inserted before index 1, so a NEW index 1. I call set state to set the current index to 2 since that is the new index of the current view. As you can see, the build method in the widget gets called twice, the first once renders index 1 and 2 in the page view, but the next one only renders index 1 even though the initial index is set to 2.
Since I'm unable to run the minimal repro you've posted. I tried to replicate the behavior locally from a sample app. From my tests, returning null on itemBuilder works as expected. I'm using Flutter stable channel version 1.22.0
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
int pageViewIndex;
class _MyHomePageState extends State<MyHomePage> {
ActionMenu actionMenu;
final PageController pageController = PageController();
int currentPageIndex = 0;
int pageCount = 1;
#override
void initState() {
super.initState();
actionMenu = ActionMenu(this.addPageView, this.removePageView);
}
addPageView() {
setState(() {
pageCount++;
});
}
removePageView(BuildContext context) {
if (pageCount > 1)
setState(() {
pageCount--;
});
else
Scaffold.of(context).showSnackBar(SnackBar(
content: Text("Last page"),
));
}
navigateToPage(int index) {
pageController.animateToPage(
index,
duration: Duration(milliseconds: 300),
curve: Curves.ease,
);
}
getCurrentPage(int page) {
pageViewIndex = page;
}
createPage(int page) {
return Container(
child: Center(
child: Text('Page $page'),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
actions: <Widget>[
actionMenu,
],
),
body: Container(
child: PageView.builder(
controller: pageController,
onPageChanged: getCurrentPage,
// itemCount: pageCount,
itemBuilder: (context, position) {
if (position == 5) return null;
return createPage(position + 1);
},
),
),
);
}
}
enum MenuOptions { addPageAtEnd, deletePageCurrent }
List<Widget> listPageView = List();
class ActionMenu extends StatelessWidget {
final Function addPageView, removePageView;
ActionMenu(this.addPageView, this.removePageView);
#override
Widget build(BuildContext context) {
return PopupMenuButton<MenuOptions>(
onSelected: (MenuOptions value) {
switch (value) {
case MenuOptions.addPageAtEnd:
this.addPageView();
break;
case MenuOptions.deletePageCurrent:
this.removePageView(context);
break;
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
PopupMenuItem<MenuOptions>(
value: MenuOptions.addPageAtEnd,
child: const Text('Add Page at End'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.deletePageCurrent,
child: Text('Delete Current Page'),
),
],
);
}
}
Here how the sample app looks.

How to display RefreshIndicator at top with ListView.builder reverse:true

I'm building a simple messaging system, where the user will see a list of messages.
I have a ListView.Builder with reverse:true since I want the list to appear at the bottom when they load the messaging page.
When they pull down to scroll all the way to the top I want a refresh indicator to appear so they can load previous messages, like most popular chat applications do.
However due to having reverse:true on the list they have to pull up at the bottom of the screen to load previous messages while using a RefreshIndicator.
Is there a way to make the RefreshIndicator trigger when pulling down rather than up when using reverse:true?
In my opinion,do you want to load more at the bottom of the listview,i think you just need to add one load more view to the last item of the listview,like the following code:
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(new MaterialApp(
home: new Scaffold(
body: new LoadMoreListView(enableLoadMore: true, count: 30,),
),
));
}
class LoadMoreListView extends StatefulWidget {
bool enableLoadMore;
int count;
LoadMoreListView({this.enableLoadMore = true, this.count = 15});
#override
State<StatefulWidget> createState() {
return new LoadMoreListViewState();
}
}
class LoadMoreListViewState extends State<LoadMoreListView> {
ScrollController _scrollController = new ScrollController();
bool isRequesting = false;
#override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
///load more when the listView attached the bottom
loadMore();
}
});
}
Future<Null> loadMore() async {
if (isRequesting) {
///if is requesting ,return the next action
return null;
}
setState(() {
isRequesting = true;
});
///loading your data from any where,eg:network
return null;
}
#override
void dispose() {
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new ListView.builder(
itemCount: _count(),
itemBuilder: _buildItem);
}
_count() {
if (widget.enableLoadMore) {
return widget.count + 1;
}
return widget.count;
}
Widget _buildItem(BuildContext context, int index) {
if (index == widget.count) {
return _buildLoadMoreView();
}
return new Container(
height: 36.0,
child: new Center(
child: new Text("I am the $index item"),
),
);
}
Widget _buildLoadMoreView() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: 1.0,
child: new CircularProgressIndicator(),
),
),
);
}
}