So I have 2 widgets in a column. 1 is a listview of containers and another is a Column of TabBar and TabBarView. I wrote this code which helps me store the current index is the TabView selected
TabController _tabController;
void initState() {
super.initState();
_tabController = new TabController(vsync: this, length: 4);
_tabController.addListener(() {
globals.tabIndex = _tabController.index;
});
}
As per the image I want to change the color of the tile as the active listview changes (Active container is blue). The containers have an onTap functions which changes their color as they are pressed. Is there anyway I can rebuild the top containers when the listview Index changes?
You can simply call the setState function to rebuild the widget;
import 'dart:ui';
import 'package:flutter/material.dart';
import 'dart:async';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Example(),
),
);
}
}
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> with SingleTickerProviderStateMixin {
TabController _tabController;
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: 4);
_tabController.addListener(() {
setState(() {});
});
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Row(
children: <Widget>[
_Item(
isActive: _tabController.index == 0,
onTap: () => _tabController.animateTo(0),
),
_Item(
isActive: _tabController.index == 1,
onTap: () => _tabController.animateTo(1),
),
_Item(
isActive: _tabController.index == 2,
onTap: () => _tabController.animateTo(2),
),
_Item(
isActive: _tabController.index == 3,
onTap: () => _tabController.animateTo(3),
),
],
),
TabBar(
controller: _tabController,
tabs: <Widget>[
_TabItem(
'one',
),
_TabItem('two'),
_TabItem('three'),
_TabItem('four'),
],
)
],
);
}
}
class _TabItem extends StatelessWidget {
final String title;
const _TabItem(this.title, {Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Text(
title,
style: TextStyle(color: Colors.black),
);
}
}
class _Item extends StatelessWidget {
final bool isActive;
final Function onTap;
const _Item({Key key, this.isActive, this.onTap}) : super(key: key);
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 80,
width: 50,
color: isActive ? Colors.blue : Colors.grey,
child: Text('hey'),
),
);
}
}
Hey there created a simple example to show how you would achieve this. Uses all the components you mentioned, tabBar, TabBarView, ListViewBuilder to create the list.
And onTap of the element use the tabController to animate to the Tab. And this subsequently trigger the addListener callaback which updates the currentSelected Index.
https://dartpad.dev/5f5475b790fe9a75b26c1f686adab96e
Related
I use the following code snippet to create a tab bar with 20 tabs along with their views (you can copy-paste the code to try it out, it complies with no problems):
import 'package:flutter/material.dart';
class TabBody extends StatefulWidget {
final int tabNumber;
const TabBody({required this.tabNumber, Key? key}) : super(key: key);
#override
State<TabBody> createState() => _TabBodyState();
}
class _TabBodyState extends State<TabBody> {
#override
void initState() {
print(
'inside init state for ${widget.tabNumber}'); //<--- I want this line to execute only once
super.initState();
}
getDataForTab() {
//getting data for widget.tabNumber
}
#override
Widget build(BuildContext context) {
return Center(
child: Container(
color: Colors.grey,
child: Text('This is tab #${widget.tabNumber} body')),
);
}
}
class MainPage extends StatefulWidget {
const MainPage({Key? key}) : super(key: key);
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
List<Text> get _tabs {
var list = [for (var i = 0; i < 20; i += 1) i];
List<Text> tabs = list.map((i) => Text('Tab Title $i')).toList();
return tabs;
}
List<TabBody> get _tabsBodies {
var list = [for (var i = 0; i < 20; i += 1) i];
List<TabBody> bodies = list.map((i) => TabBody(tabNumber: i)).toList();
return bodies;
}
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: _tabs.length,
child: Column(
children: <Widget>[
Container(
width: double.infinity,
height: 50,
color: Colors.black,
child: TabBar(
isScrollable: true,
tabs: _tabs,
),
),
Expanded(
child: TabBarView(
children: _tabsBodies, //<--- i want this to be one child only
),
)
],
),
);
}
}
I need to do the following but couldn't find a way for that:
I want to let the TabBarView to have only one child of type TabBody not a list of _tabsBodies, i.e. the print statement in initState should execute once.
I want to execute the function getDataForTab every time the tab is changed to another tab.
so in general I need to refresh the tab body page for each tab selection, in contrast to the default implementation of the DefaultTabController widget which requires to have n number of tab bodies for n number of tabs.
You'll need to do three things:
Remove the TabBarView. You don't need it if you want to have a single widget. (Having a TabBar does not require you to have a TabBarView)
Create your own TabController so you can pass the current index to the TabBody.
Listen to the TabController and update the state to pass the new index to TabBody.
Here's a fully runnable example and that you can copy and paste to DartPad
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
const MyHomePage({
Key? key,
required this.title,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: MainPage(),
);
}
}
class MainPage extends StatefulWidget {
const MainPage({Key? key}) : super(key: key);
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage>
with SingleTickerProviderStateMixin {
late final tabController =
TabController(length: 20, vsync: this, initialIndex: 0);
#override
void initState() {
super.initState();
tabController.addListener(() {
if (tabController.previousIndex != tabController.index && !tabController.indexIsChanging) {
print('setting state'); // <~~ will print one time now
setState(() {});
}
});
}
List<Text> get _tabs {
var list = [for (var i = 0; i < 20; i += 1) i];
List<Text> tabs = list.map((i) => Text('Tab Title $i')).toList();
return tabs;
}
#override
void dispose() {
tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
width: double.infinity,
height: 50,
color: Colors.black,
child: TabBar(
isScrollable: true, tabs: _tabs, controller: tabController),
),
Expanded(
child: TabBody(tabNumber: tabController.index),
)
],
);
}
}
class TabBody extends StatefulWidget {
final int tabNumber;
const TabBody({required this.tabNumber, Key? key}) : super(key: key);
#override
State<TabBody> createState() => _TabBodyState();
}
class _TabBodyState extends State<TabBody> {
#override
void initState() {
print(
'inside init state for ${widget.tabNumber}'); //<--- I want this line to execute only once
super.initState();
}
getDataForTab() {
//getting data for widget.tabNumber
}
#override
Widget build(BuildContext context) {
return Center(
child: Container(
color: Colors.grey,
child: Text('This is tab #${widget.tabNumber} body')),
);
}
}
Few notes about the example:
when you create a TabController, you'll need a ticker. You can use SingleTickerProviderStateMixin to make the class itself a ticker (hence: vsync: this). Alternatively, you can create your own and pass it to TabController.vsync parameter.
class _MainPageState extends State<MainPage> with SingleTickerProviderStateMixin {
late final tabController = TabController(length: 20, vsync: this, initialIndex: 0);
Here we are listening to the tab controller whenever the tabs changes:
#override
void initState() {
super.initState();
tabController.addListener(() {
if (tabController.previousIndex != tabController.index && !tabController.indexIsChanging) {
print('setting state'); // <~~ will print one time now
setState(() {});
}
});
}
edit: you'll also need to dispose the tabController. I updated the code above.
All data is contains in Parent widget and showing in child widget
I want to call function in child widget when parent state change
child widget function which is stateFull widget
void changeSelectedJodi(i) {
_jodiScrollController.animateTo(50.0 * i,
duration: Duration(seconds: 2), curve: Curves.fastOutSlowIn);
}
Parent widget
child: JodiDataWidget(
this._jodies, <= data is here
this.selectedJodi, <= call function changeSelectedJodi in child widget when change
),
how to achieve this method
You still don't want to access your child from your parent. Flutter control flow goes only one way and there are good reasons for that, which your should respect.
The question is, then, how can my child know that my parent has changed? For this, you have to update the child from the parent (as always) then use didUdpdateWidget in the child to catch a widget change and react to it.
Here is a small example:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: ParentWidget()));
}
class ParentWidget extends StatefulWidget {
#override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
/// The item of the list to display
///
/// This will be changed randomly by clicking the button
int selectedIndex = 0;
#override
Widget build(BuildContext context) {
return Material(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Center(
child: ChildWidget(
selectedIndex: selectedIndex,
),
),
),
Padding(
padding: const EdgeInsets.all(15.0),
child: ElevatedButton(
onPressed: () => setState(() => selectedIndex = Random().nextInt(100)),
child: Center(
child: Text('Press my to move the list'),
),
),
),
],
),
);
}
}
class ChildWidget extends StatefulWidget {
/// The item of the list to display
///
/// Changed randomly by the parent
final int selectedIndex;
const ChildWidget({
Key? key,
required this.selectedIndex,
}) : super(key: key);
#override
State<ChildWidget> createState() => _ChildWidgetState();
}
class _ChildWidgetState extends State<ChildWidget> {
/// The colors of the items in the list
final _itemsColors = List.generate(
100,
(index) => getRandomColor(),
);
static Color getRandomColor() =>
Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
final _controller = PageController();
void functionOfChildWidget() {
_controller.animateToPage(
widget.selectedIndex,
duration: Duration(milliseconds: 200),
curve: Curves.easeIn,
);
}
/// Here is the important part: When data is set from the parent,
/// move this widget
#override
void didUpdateWidget(covariant ChildWidget oldWidget) {
// If you want to react only to changes you could check
// oldWidget.selectedIndex != widget.selectedIndex
functionOfChildWidget();
super.didUpdateWidget(oldWidget);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return SizedBox(
height: 200,
child: PageView.builder(
controller: _controller,
padEnds: false,
itemBuilder: (context, index) {
return Container(
margin: EdgeInsets.all(50),
color: _itemsColors[index],
width: 100,
);
},
itemCount: _itemsColors.length,
),
);
}
}
You can use something like this:
import 'package:flutter/widgets.dart';
class ChangeCallWidget extends StatefulWidget {
final dynamic value;
final VoidCallback onChange;
final Widget child;
const ChangeCallWidget({
super.key,
required this.value,
required this.onChange,
required this.child,
});
#override
State<ChangeCallWidget> createState() => _ChangeCallWidgetState();
}
class _ChangeCallWidgetState extends State<ChangeCallWidget> {
#override
void didUpdateWidget(oldWidget) {
if (oldWidget.value != widget.value) widget.onChange();
super.didUpdateWidget(oldWidget);
}
#override
Widget build(BuildContext context) => widget.child;
}
And use it like this to monitor changes of the _jodies and trigger the onChange if it changes:
ChangeCallWidget(
value: _jodies,
onChange: selectedJodi,
)
Let's suppose that I have a Main screen (stateful widget) where there is a variable count as state. In this Main screen there is a button and another stateful widget (let's call this MyListWidget. MyListWidget initialize it's own widgets in the initState depending by the value of the count variable. Obviously if you change the value of count and call SetState, nothing will happen in MyListWidget because it create the values in the initState. How can I force the rebuilding of MyListWidget?
I know that in this example we can just move what we do in the initState in the build method. But in my real problem I can't move what I do in the initState in the build method.
Here's the complete code example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int count = 5;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
Expanded(
child: MaterialButton(
child: Text('Click me'),
color: Colors.red,
onPressed: () {
setState(() {
count++;
});
},
),
),
MyListWidget(count),
],
));
}
}
class MyListWidget extends StatefulWidget {
final int count;
const MyListWidget(this.count, {Key? key}) : super(key: key);
#override
_MyListWidgetState createState() => _MyListWidgetState();
}
class _MyListWidgetState extends State<MyListWidget> {
late List<int> displayList;
#override
void initState() {
super.initState();
displayList = List.generate(widget.count, (int index) => index);
}
#override
Widget build(BuildContext context) {
return Expanded(
child: ListView.builder(
itemBuilder: (BuildContext context, int index) => ListTile(
title: Text(displayList[index].toString()),
),
itemCount: displayList.length,
),
);
}
}
I don't think the accepted answer is accurate, Flutter will retain the state of MyListWidget because it is of the same type and in the same position in the widget tree as before.
Instead, force a widget rebuild by changing its key:
class _MyHomePageState extends State<MyHomePage> {
int count = 5;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
Expanded(
child: MaterialButton(
child: Text('Click me'),
color: Colors.red,
onPressed: () {
setState(() {
count++;
});
},
),
),
MyListWidget(count, key: ValueKey(count)),
],
),
);
}
}
Using a ValueKey in this example means the state will only be recreated if count is actually different.
Alternatively, you can listen to widget changes in State.didUpdateWidget, where you can compare the current this.widget with the passed in oldWidget and update the state if necessary.
USE THIS:
class _MyHomePageState extends State<MyHomePage> {
int count = 5;
MyListWidget myListWidget = MyListWidget(5);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
Expanded(
child: MaterialButton(
child: Text('Click me'),
color: Colors.red,
onPressed: () {
setState(() {
count++;
myListWidget = MyListWidget(count);
});
},
),
),
myListWidget,
],
));
}
}
i am studying key in flutter. and in explanation, when i want swap widget in statefulWidget i need to add key value. because when flutter check element structure if type, state are not same they don't response. this is how i understand.
void main() => runApp(new MaterialApp(home: PositionedTiles()));
class PositionedTiles extends StatefulWidget {
#override
State<StatefulWidget> createState() => PositionedTilesState();
}
class PositionedTilesState extends State<PositionedTiles> {
List<Widget> tiles = [
StatefulColorfulTile(key: UniqueKey()), // Keys added here
StatefulColorfulTile(key: UniqueKey()),
];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(children: tiles),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.sentiment_very_satisfied), onPressed: swapTiles),
);
}
swapTiles() {
setState(() {
tiles.insert(1, tiles.removeAt(0));
});
}
}
class StatefulColorfulTile extends StatefulWidget {
StatefulColorfulTile({Key key}) : super(key: key); // NEW CONSTRUCTOR
#override
ColorfulTileState createState() => ColorfulTileState();
}
class ColorfulTileState extends State<ColorfulTile> {
Color myColor;
#override
void initState() {
super.initState();
myColor = UniqueColorGenerator.getColor();
}
#override
Widget build(BuildContext context) {
return Container(
color: myColor,
child: Padding(
padding: EdgeInsets.all(70.0),
));
}
}
but i saw this code.
Widget build(BuildContext context) {
return Column(
children: [
value
? const SizedBox()
: const Placeholder(),
GestureDetector(
onTap: () {
setState(() {
value = !value;
});
},
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
!value
? const SizedBox()
: const Placeholder(),
],
);
}
this code is also use statefulWidget. in this code when user taps Box it's changed but i think there're no key value and in element structure there are different type(one is SizedBox and the other is placeHolder) so i think there aren't changed. why they're changed? what i misunderstand?
I have two tabs, the left tab having a list of tiles and the right tab having nothing. The user can drag the screen from right-to-left or left-to-right to get from one tab to the other.
The left tab has a list of dismissible tiles that only have "direction: DismissDirection.startToEnd" (from left-to-right) enabled so that the user can still theoretically drag (from right-to-left) to go to the right tab.
However, I believe the Dismissible widget still receives the right-to-left drag information which is disabling the TabView drag to change tabs.
In essence, how do I allow the right-to-left drag to be detected by only the TabView and not the Dismissible item?
If an explicit solution/example with code snippets can be given, I would very very much appreciate the help!
Here's a paste for your main.dart file:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/semantics.dart';
void main() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(),
home: MainPage(),
);
}
}
class MainPage extends StatefulWidget {
#override
State<StatefulWidget> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage>
with SingleTickerProviderStateMixin {
TabController _tabController;
#override
void initState() {
_tabController = TabController(vsync: this, length: 2, initialIndex: 1);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
color: Colors.black,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
child: TabBarView(
controller: _tabController,
children: <Widget>[
TabWithSomething(),
TabWithNothing(),
],
),
),
],
),
),
),
);
}
}
class TabWithNothing extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Container(
child: Text("Swipe from left-to-right!"),
),
);
}
}
class TabWithSomethingItem implements Comparable<TabWithSomethingItem> {
TabWithSomethingItem({this.index, this.name, this.subject, this.body});
TabWithSomethingItem.from(TabWithSomethingItem item)
: index = item.index,
name = item.name,
subject = item.subject,
body = item.body;
final int index;
final String name;
final String subject;
final String body;
#override
int compareTo(TabWithSomethingItem other) => index.compareTo(other.index);
}
class TabWithSomething extends StatefulWidget {
const TabWithSomething({Key key}) : super(key: key);
static const String routeName = '/material/leave-behind';
#override
TabWithSomethingState createState() => TabWithSomethingState();
}
class TabWithSomethingState extends State<TabWithSomething> {
List<TabWithSomethingItem> TabWithSomethingItems;
void initListItems() {
TabWithSomethingItems =
List<TabWithSomethingItem>.generate(10, (int index) {
return TabWithSomethingItem(
index: index,
name: 'Item $index',
subject: 'Swipe from left-to-right to delete',
body: "Swipe from right-to-left to go back to old tab");
});
}
#override
void initState() {
super.initState();
initListItems();
}
void _handleDelete(TabWithSomethingItem item) {
setState(() {
TabWithSomethingItems.remove(item);
});
}
#override
Widget build(BuildContext context) {
Widget body;
body = ListView(
children:
TabWithSomethingItems.map<Widget>((TabWithSomethingItem item) {
return _TabWithSomethingListItem(
item: item,
onDelete: _handleDelete,
dismissDirection: DismissDirection.startToEnd,
);
}).toList());
return body;
}
}
class _TabWithSomethingListItem extends StatelessWidget {
const _TabWithSomethingListItem({
Key key,
#required this.item,
#required this.onDelete,
#required this.dismissDirection,
}) : super(key: key);
final TabWithSomethingItem item;
final DismissDirection dismissDirection;
final void Function(TabWithSomethingItem) onDelete;
void _handleDelete() {
onDelete(item);
}
#override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Semantics(
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
const CustomSemanticsAction(label: 'Delete'): _handleDelete,
},
child: Dismissible(
key: ObjectKey(item),
direction: dismissDirection,
onDismissed: (DismissDirection direction) => _handleDelete(),
background: Container(
color: theme.primaryColor,
child: const ListTile(
leading: Icon(Icons.delete, color: Colors.white, size: 36.0))),
child: Container(
decoration: BoxDecoration(
color: theme.canvasColor,
border: Border(bottom: BorderSide(color: theme.dividerColor))),
child: ListTile(
title: Text(item.name),
subtitle: Text('${item.subject}\n${item.body}'),
isThreeLine: true),
),
),
);
}
}
UPDATE:
I'm thinking we could change the "dismissible.dart" file to change the "TabControlller", but i'm not sure how I might do that.
In the "dismissible.dart" file:
...
void _handleDragUpdate(DragUpdateDetails details) {
if (!_isActive || _moveController.isAnimating)
return;
final double delta = details.primaryDelta;
if (delta < 0) print(delta); // thinking of doing something here
...