I have a ListView.builder inside a page that contains multiple tabs.
After a list item is removed (by swiping left or right), going to another tab and then going back to the first tab results in a range error.
Going to another page and coming back does not result in an error. Perhaps going to another page causes the builder to re-build again, and tab doesn't?
Am I doing something wrong here or is this a bug with TabView? Fully reproducible code below.
To reproduce the issue swipe an item to delete it, then go to second tab and then back to first tab.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'List Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
List<int> list = [1, 2, 3, 4, 5];
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.recycling_outlined)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
),
body: TabBarView(
children: [
ListView.builder(
padding: const EdgeInsets.all(4.0),
itemCount: list.length,
itemBuilder: (context, index) {
var item = list[index];
return Dismissible(
key: Key(item.toString()),
onDismissed: (direction) {
list.removeAt(index);
},
child: ListTile(
title: Text(item.toString()),
),
);
},
),
const Icon(Icons.directions_transit),
const Icon(Icons.directions_bike),
],
),
),
);
}
}
Separating the ListView solves tree the issue. To save the state of list, I am using AutomaticKeepAliveClientMixin, you can use others state-management property.
body: TabBarView(
children: [
FirstChild(),
const Icon(Icons.directions_transit),
const Icon(Icons.directions_bike),
],
),
ListView
class FirstChild extends StatefulWidget {
FirstChild({Key? key}) : super(key: key);
#override
State<FirstChild> createState() => _FirstChildState();
}
class _FirstChildState extends State<FirstChild>
with AutomaticKeepAliveClientMixin {
List<int> list = [1, 2, 3, 4, 5];
#override
Widget build(BuildContext context) {
super.build(context);
return ListView.builder(
padding: const EdgeInsets.all(4.0),
itemCount: list.length,
itemBuilder: (context, index) {
var item = list[index];
return Dismissible(
key: Key(list[index].toString()),
onDismissed: (direction) {
setState(() {
list.removeAt(index);
});
},
child: ListTile(
title: Text(item.toString()),
),
);
},
);
}
#override
bool get wantKeepAlive => true;
}
Use setState to update list and also remove List declaration from build method
class _MyHomePageState extends State<MyHomePage> {
List<int> list = [1, 2, 3, 4, 5];
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.recycling_outlined)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
),
body: TabBarView(
children: [
ListView.builder(
padding: const EdgeInsets.all(4.0),
itemCount: list.length,
itemBuilder: (context, index) {
var item = list[index];
return Dismissible(
key: Key(item.toString()),
onDismissed: (direction) {
setState(() {
list.removeAt(index);
});
},
child: ListTile(
title: Text(item.toString()),
),
);
},
),
const Icon(Icons.directions_transit),
const Icon(Icons.directions_bike),
],
),
),
);
}
}
Related
I am currently working in a page where I have to show a list of apps in the device which should be half a screen only, for that i have a used a bottom drawer widget to display that ,
the main problem is every time i open the app it needs to get pulled, i don't want to do it so can some help me to display the apps without pulling the bottom drawer widget ? or else is there any other widget to do that ?
You can try bottom modal sheet or draggableScrollableSheet which has a min size
import 'package:flutter/material.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 MyStatelessWidget(),
),
);
}
}
class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
child: const Text('showModalBottomSheet'),
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return Container(
height: 200,
color: Colors.amber,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('Modal BottomSheet'),
ElevatedButton(
child: const Text('Close BottomSheet'),
onPressed: () => Navigator.pop(context),
)
],
),
),
);
},
);
},
),
);
}
}
draggableScrollableSheet
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('DraggableScrollableSheet'),
),
body: SizedBox.expand(
child: DraggableScrollableSheet(
builder: (BuildContext context, ScrollController scrollController) {
return Container(
color: Colors.blue[100],
child: ListView.builder(
controller: scrollController,
itemCount: 25,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text('Item $index'));
},
),
);
},
),
),
);
}
}
You can use DraggableScrollableSheet with minChildSize: .5, on this case.
return Scaffold(
bottomNavigationBar: DraggableScrollableSheet(
minChildSize: .5,
maxChildSize: 1,
builder: (context, scrollController) {
return Column(
children: [
Center(
child: Text("Title"),
),
Expanded(
child: ListView.builder(
controller: scrollController,
itemCount: 25,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text('Item $index'));
},
),
),
],
);
},
));
More about DraggableScrollableSheet
Is it possible to Bring down or display the appbar only when the draggable sheet reaches the top of the page in flutter?
And having another doubt.
Is it possible to navigate from the home page to another page that has a transparent background and displays the home page as the background? Something like widgets placed on the stack...
You can add a listener to the draggableScrollableSheet and get its offset. Then based on the offset you can hide or show the Appbar.. To get a transparent screen use PageRouteBuilder with opaque: false. Check this demo
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: CustomPaintIssue()));
class CustomPaintIssue extends StatelessWidget {
const CustomPaintIssue({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool showAppbar = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: showAppbar ? AppBar() : null,
body: Container(
child: Stack(
children: [
Center(
child: InkWell(
onTap: () {
Navigator.push(
context,
PageRouteBuilder(
opaque: false,
pageBuilder: (context, _, __) => SecondPage()));
},
child: Container(
width: 50,
height: 50,
color: Colors.blue,
),
),
),
DraggableScrollableSheet(
maxChildSize: 1,
builder: (context, scrollController) {
return SingleChildScrollView(
controller: scrollController
..addListener(() {
if (scrollController.hasClients &&
scrollController.offset > 50) {
setState(() {
showAppbar = true;
});
} else {
setState(() {
showAppbar = false;
});
}
}),
child: Container(
width: MediaQuery.of(context).size.width,
height: 1500,
color: Colors.red,
),
);
})
],
),
),
);
}
}
class SecondPage extends StatelessWidget {
const SecondPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Column(
children: List.generate(
5,
(index) => Container(
margin: EdgeInsets.symmetric(vertical: 10),
width: MediaQuery.of(context).size.width,
height: 50,
color: Colors.green,
)),
),
),
);
}
}
I want to have reorderable list in flutter with custom drag handle (that works immediately, without long press first).
To achieve that I did:
buildDefaultDragHandles: false,
and I used ReorderableDragStartListener.
code:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final List<int> _items = List<int>.generate(50, (int index) => index);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: ReorderableListView(
buildDefaultDragHandles: false,
children: <Widget>[
for (int index = 0; index < _items.length; index++)
Container(
key: Key('$index'),
color: _items[index].isOdd ? Colors.blue[100] : Colors.red[100],
child: Row(
children: <Widget>[
Container(
width: 64,
height: 64,
padding: const EdgeInsets.all(8),
child: ReorderableDragStartListener(
index: index,
child: Card(
color: Colors.green,
elevation: 2,
),
),
),
Text('Item ${_items[index]}'),
],
),
),
],
onReorder: (int oldIndex, int newIndex) {
print('oldIndex $oldIndex, newIndex $newIndex');
},
),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
On desktop (e.g. when run in Edge) it works as expected, drag handle is clicked (mouse down) and dragged up or down to change order.
The problem is on mobile device. When I tap down, and I move finger up or down - the scroll is performed. When however I tap down, and move finger little left or right, and then up/down -> then reordering happens. (tested in android emulator and real android device).
Question is - why on mobile I need to do this little annoying additional left/right move before chaining order? How to fix it?
How it works on desktop (Edge):
How it work on Android (bug!):
I solved it using custom ReorderableDragStartListener, when I set tap delay to 1ms. Since this approach does not require moving finger left/right before dragging, and 1ms is low time, it works like a charm.
code:
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
class CustomReorderableDelayedDragStartListener extends ReorderableDragStartListener {
final Duration delay;
const CustomReorderableDelayedDragStartListener({
this.delay = kLongPressTimeout,
Key? key,
required Widget child,
required int index,
bool enabled = true,
}) : super(key: key, child: child, index: index, enabled: enabled);
#override
MultiDragGestureRecognizer createRecognizer() {
return DelayedMultiDragGestureRecognizer(delay: delay, debugOwner: this);
}
}
usage:
CustomReorderableDelayedDragStartListener(
delay: const Duration(milliseconds: 1), // or any other duration that fits you
index: widget.index, // passed from parent
child: Container(
child: const Icon( // or any other graphical element
Icons.drag_handle
),
),
)
Try this
import 'package:flutter/material.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 MyStatefulWidget(),
),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({Key? key}) : super(key: key);
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
final List<int> _items = List<int>.generate(20, (int index) => index);
#override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final Color oddItemColor = colorScheme.primary.withOpacity(0.05);
final Color evenItemColor = colorScheme.primary.withOpacity(0.15);
return ReorderableListView(
buildDefaultDragHandles: false,
children: <Widget>[
for (int index = 0; index < _items.length; index++)
Container(
key: Key('$index'),
color: _items[index].isOdd ? oddItemColor : evenItemColor,
child: Row(
children: <Widget>[
Container(
width: 64,
height: 64,
padding: const EdgeInsets.all(8),
child: ReorderableDragStartListener(
index: index,
child: Card(
color: colorScheme.primary,
elevation: 2,
),
),
),
Text('Item ${_items[index]}'),
],
),
),
],
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final int item = _items.removeAt(oldIndex);
_items.insert(newIndex, item);
});
},
);
}
}
I have a Native Android Fragment and I need to use inside a flutter project.
Inside a Android Project when you need to use a fragment something like
supportFragmentManager.beginTransaction()
.replace(R.id.content_fragment, it1,
"name")
.commit()
I would like to embed this fragment along with a BottonNavigationBar (second option for example).
I tried to follow some tutorials as:
https://medium.com/flutter-community/flutter-platformview-how-to-create-flutter-widgets-from-native-views-366e378115b6
https://60devs.com/how-to-add-native-code-to-flutter-app-using-platform-views-android.html
But I wasn`t able to adapt these tutorials for fragment or even activities becase they talk about Views.
Does anyone have any suggestions?
Obs: Just to clarify, I need to use a native screen inside a flutter screen.
But you can use flutter BottomNavigationBar
here is a demo of BottomNavigationBar
and it looks same as Bottomnavigation with fragment in android
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int index = 0;
void currentindex(value) {
setState(() {
this.index = value;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: Icon(
MdiIcons.flower,
color: Colors.white,
),
title: Text(
widget.title,
style: TextStyle(color: Colors.white),
),
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(
icon: Icon(MdiIcons.home),
title: Text("Home"),
),
BottomNavigationBarItem(
icon: Icon(MdiIcons.human),
title: Text("User"),
),
BottomNavigationBarItem(
icon: Icon(MdiIcons.imageAlbum),
title: Text("Theme"),
),
],
onTap: (index) => currentindex(index),
elevation: 19.0,
currentIndex: index,
),
body: Navtabwidget()[this.index],
);
}
List<Widget> Navtabwidget() {
return [
Homewidget(),
Userlistwidget(),
Settingwidget(),
];
}
}
i hope it helps
I used this, in a similar problem:
class DetailsScreen extends StatefulWidget {
#override
DetailsScreenState createState() {
return DetailsScreenState();
}
}
class DetailsScreenState extends State<DetailsScreen>
with SingleTickerProviderStateMixin {
TabController tabController;
#override
void initState() {
tabController = new TabController(length: 4, vsync: this);
super.initState();
}
#override
void dispose() {
tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(67.0),
child: AppBar(
elevation: 10.0,
automaticallyImplyLeading: false,
flexibleSpace: Padding(
padding: const EdgeInsets.only(top: 0.0),
child: SafeArea(
child: getTabBar(),
),
),
),
),
body: getTabBarPages());
}
Widget getTabBar() {
return TabBar(controller: tabController, tabs: [
Tab(
text: AppLocalizations.of(context).translate("nav_dieta"),
icon: Icon(MdiIcons.silverwareForkKnife)),
Tab(
text: AppLocalizations.of(context).translate("nav_exercise"),
icon: Icon(MdiIcons.dumbbell)),
Tab(
text: AppLocalizations.of(context).translate("_news"),
icon: Icon(MdiIcons.newspaper)),
Tab(
text: AppLocalizations.of(context).translate("nav_info"),
icon: Icon(MdiIcons.accountDetails)),
]);
}
Widget getTabBarPages() {
return TabBarView(
controller: tabController,
physics: NeverScrollableScrollPhysics(),
children: <Widget>[
MealPlanScreen(),
ExerciseScreen(),
Container(color: Colors.blue),
Container(color: Colors.yellow)
]);
}
}
Where MealPlanScreen and ExerciseScreen are StatefulWidget and those two Containers will be replaced with other classes that contain StatefulWidget.
I am using page_indicator: ^0.1.3 plugin to show an indicator for the page view.
PageIndicatorContainer(
pageView: PageView.builder(
controller: PageController(),
itemBuilder: (context, position) {
);
},
scrollDirection: Axis.horizontal,
itemCount: widgetView.widgetItemList.length,
),
align: IndicatorAlign.bottom,
length: widgetView.widgetItemList.length,
indicatorColor: Colors.white,
indicatorSelectorColor: Colors.grey,
size: 5.0,
indicatorSpace: 10.0,
))
but it shows the indicator over the image like.
I did not find any option or plugin to set the indicator below image
like
PageIndicatorContainer doesn't seem to have a property where you could set the indicator outside PageView. A workaround for this case is to add padding on the pages of your PageView.
Complete sample
import 'package:flutter/material.dart';
import 'package:page_indicator/page_indicator.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: 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();
}
class _MyHomePageState extends State<MyHomePage> {
PageController controller;
GlobalKey<PageContainerState> key = GlobalKey();
#override
void initState() {
super.initState();
controller = PageController();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
Flexible(
flex: 5,
child: PageIndicatorContainer(
key: key,
child: PageView.builder(itemCount: 4,itemBuilder: (context, position) {
return Container(
// Padding will help prevent overlap on the page indicator
padding: EdgeInsets.fromLTRB(5, 5, 5, 30),
child: Container(
color: Colors.greenAccent,
child: Center(child: Text('${position + 1}')),
),
);
}),
align: IndicatorAlign.bottom,
length: 4,
indicatorSpace: 20.0,
padding: const EdgeInsets.all(10),
indicatorColor: Colors.grey,
indicatorSelectorColor: Colors.blue,
shape: IndicatorShape.circle(size: 12),
),
),
Flexible(flex: 1, child: Container()),
Flexible(
flex: 5,
child: Container(
color: Colors.cyan,
))
],
),
);
}
}