I think I have the design of my app wrong. I am new to flutter/dart and am finding myself confused by previous experience with other languages (specifically C# and JavaScript).
I have an app that currently consists of a 3 x 3 GridView of 9 colored circular tiles, named Tiled_Surface. Each tile is assigned the following onTap handler:
void on_tile_tapped(int index) {
setState(() {
tile_tapped = index;
});
} // on_tile_tapped
where index has an arbitrary value in the range [0..8). Whenever a tile is tapped, the color of the tile changes to a lighter value (actually the "accent color" of the tile's color). All of that works file.
The AppBar contains a title ("Tiled Surface Demo") and two actions that consist of two IconButtons (Icons.swap_horiz and Icons.replay). It is intended that when the swap icon is tapped that the tile colors are shuffled into a new random order. And when the replay icon is tapped the tile colors are restored to their original order. When the two AppBar icons are tapped there is no apparent change to the display until a tile is tapped. Then, the changes made by the AppBar taps are displayed.
This is not the desired effect. My problem is how to render Tiled_Surface when the AppBar icons are tapped.
The code for the app follows. Thanks for your thoughts.
// ignore_for_file: camel_case_types
// ignore_for_file: constant_identifier_names
// ignore_for_file: non_constant_identifier_names
import 'package:flutter/material.dart';
import 'dart:math';
const int NUMBER_TILES = 9;
final int cross_axis_count = (sqrt (NUMBER_TILES)).toInt();
final double cross_axis_spacing = 4.0;
final double main_axis_spacing = cross_axis_spacing;
List<int> indices = [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ];
List normal_colors = [
Colors.red,
Colors.orange,
Colors.yellow,
Colors.green,
Colors.blue,
Colors.purple,
Colors.amber,
Colors.cyan,
Colors.indigo,
]; // normal_colors
List bright_colors = [
Colors.pinkAccent,
Colors.orangeAccent,
Colors.yellowAccent,
Colors.lightGreenAccent,
Colors.blue.shade200,
Colors.purpleAccent,
Colors.amberAccent,
Colors.cyanAccent,
Colors.indigoAccent,
]; // bright_colors
void reinitialize_tiles() {
indices.clear();
for (int i = 0; (i < NUMBER_TILES); i++) {
indices.add(i);
}
} // reinitialize_tiles
void randomize_tiles() {
var random = new Random();
indices.clear();
for (int i = 0; (i < NUMBER_TILES); i++) {
var varient = random.nextInt(9);
if (indices.length > 0) {
while (indices.contains(varient)) {
varient = random.nextInt(9);
}
}
indices.add(varient);
}
} // randomize_tiles
void main() => runApp(new MyApp());
class Tiled_Surface extends StatefulWidget {
Tiled_Surface({Key key}) : super(key: key);
#override // Tiled_Surface
Tiled_Surface_State createState() => Tiled_Surface_State();
}
class Tiled_Surface_State extends State<Tiled_Surface> {
List<GridTile> grid_tiles = <GridTile>[];
int tile_tapped = -1;
void on_tile_tapped(int index) {
setState(() {
tile_tapped = index;
});
} // on_tile_tapped
GridTile new_surface_tile(Color tile_color, int index) {
GridTile tile = GridTile(
child: GestureDetector(
onTap: () => on_tile_tapped(index),
child: Container(
decoration: BoxDecoration(
color: tile_color,
shape: BoxShape.circle,
),
),
)
);
return (tile);
} // new_surface_tile
List<GridTile> create_surface_tiles() {
grid_tiles = new List<GridTile>();
for (int i = 0; (i < NUMBER_TILES); i++) {
Color tile_color = ( tile_tapped == i ) ?
bright_colors[indices[i]] :
normal_colors[indices[i]];
grid_tiles.add(new_surface_tile(tile_color, i));
}
return (grid_tiles);
} // create_surface_tiles
#override // Tiled_Surface_State
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: cross_axis_count,
childAspectRatio: 1.0,
padding: const EdgeInsets.all(4.0),
mainAxisSpacing: main_axis_spacing,
crossAxisSpacing: cross_axis_spacing,
children: create_surface_tiles(),
);
}
} // class Tiled_Surface_State
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Tiled Surface Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Tiled Surface Demo'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.swap_horiz),
onPressed: () {
randomize_tiles();
},
),
IconButton(
icon: Icon(Icons.replay),
onPressed: () {
reinitialize_tiles();
},
)
]
),
body: Column(
children: [
Tiled_Surface(),
],
),
),
);
}
}
Problem:
Flutter widgets(Stateful) will react to state variables only. Not for global and local. In your example indices is a global variable.
I updated the code with
Moved indices into MyApp as
Mutable global variables are not good
We want our MyApp to reflect for changes in indices
As MyApp started holding state changed it as StatefulWidget
Moved randomize_tiles and reinitialize_tiles into _MyAppState and added setState on change of indices so that widgets will get re-rendered.
As Tiled_Surface also need indices, injecting(passing) them in the constructor.
Please have a look
import 'package:flutter/material.dart';
import 'dart:math';
const int NUMBER_TILES = 9;
final int cross_axis_count = (sqrt(NUMBER_TILES)).toInt();
final double cross_axis_spacing = 4.0;
final double main_axis_spacing = cross_axis_spacing;
List normal_colors = [
Colors.red,
Colors.orange,
Colors.yellow,
Colors.green,
Colors.blue,
Colors.purple,
Colors.amber,
Colors.cyan,
Colors.indigo,
]; // normal_colors
List bright_colors = [
Colors.pinkAccent,
Colors.orangeAccent,
Colors.yellowAccent,
Colors.lightGreenAccent,
Colors.blue.shade200,
Colors.purpleAccent,
Colors.amberAccent,
Colors.cyanAccent,
Colors.indigoAccent,
]; // bright_colors
void main() => runApp(new MyApp());
class Tiled_Surface extends StatefulWidget {
List<int> indices;
Tiled_Surface(this.indices, {Key key}) : super(key: key);
#override // Tiled_Surface
Tiled_Surface_State createState() => Tiled_Surface_State(indices);
}
class Tiled_Surface_State extends State<Tiled_Surface> {
List<GridTile> grid_tiles = <GridTile>[];
int tile_tapped = -1;
List<int> indices;
Tiled_Surface_State(this.indices);
void on_tile_tapped(int index) {
setState(() {
tile_tapped = index;
});
} // on_tile_tapped
GridTile new_surface_tile(Color tile_color, int index) {
GridTile tile = GridTile(
child: GestureDetector(
onTap: () => on_tile_tapped(index),
child: Container(
decoration: BoxDecoration(
color: tile_color,
shape: BoxShape.circle,
),
),
));
return (tile);
} // new_surface_tile
List<GridTile> create_surface_tiles() {
grid_tiles = new List<GridTile>();
for (int i = 0; (i < NUMBER_TILES); i++) {
Color tile_color = (tile_tapped == i)
? bright_colors[indices[i]]
: normal_colors[indices[i]];
grid_tiles.add(new_surface_tile(tile_color, i));
}
return (grid_tiles);
} // create_surface_tiles
#override // Tiled_Surface_State
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: cross_axis_count,
childAspectRatio: 1.0,
padding: const EdgeInsets.all(4.0),
mainAxisSpacing: main_axis_spacing,
crossAxisSpacing: cross_axis_spacing,
children: create_surface_tiles(),
);
}
} // class Tiled_Surface_State
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
List<int> indices = [0, 1, 2, 3, 4, 5, 6, 7, 8];
void randomize_tiles() {
var random = new Random();
indices.clear();
for (int i = 0; (i < NUMBER_TILES); i++) {
var varient = random.nextInt(9);
if (indices.length > 0) {
while (indices.contains(varient)) {
varient = random.nextInt(9);
}
}
indices.add(varient);
}
setState(() {});
}
void reinitialize_tiles() {
indices.clear();
for (int i = 0; (i < NUMBER_TILES); i++) {
indices.add(i);
}
setState(() {});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Tiled Surface Demo',
home: Scaffold(
appBar: AppBar(title: Text('Tiled Surface Demo'), actions: <Widget>[
IconButton(
icon: Icon(Icons.swap_horiz),
onPressed: () {
randomize_tiles();
},
),
IconButton(
icon: Icon(Icons.replay),
onPressed: () {
reinitialize_tiles();
},
)
]),
body: Column(
children: [
Tiled_Surface(indices),
],
),
),
);
}
}
Related
I have a dashboard, represented by grid, that supposed to delete item on long press event (using flutter_bloc), but it deletes last item instead of selected. All debug prints show, that needed element actually removed from list, but view layer still keeps it.
My build function code:
Widget build(BuildContext context) {
double pyxelRatio = MediaQuery.of(context).devicePixelRatio;
double width = MediaQuery.of(context).size.width * pyxelRatio;
return BlocProvider(
bloc: _bloc,
child: BlocBuilder<Request, DataState>(
bloc: _bloc,
builder: (context, state) {
if (state is EmptyDataState) {
print("Uninit");
return Center(
child: CircularProgressIndicator(),
);
}
if (state is ErrorDataState) {
print("Error");
return Center(
child: Text('Something went wrong..'),
);
}
if (state is LoadedDataState) {
print("empty: ${state.contracts.isEmpty}");
if (state.contracts.isEmpty) {
return Center(
child: Text('Nothing here!'),
);
} else{
print("items count: ${state.contracts.length}");
print("-------");
for(int i = 0; i < state.contracts.length; i++){
if(state.contracts[i].isFavorite)print("fut:${state.contracts[i].name} id:${state.contracts[i].id}");
}
print("--------");
List<Widget> testList = new List<Widget>();
for(int i = 0; i < state.contracts.length; i++){
if(state.contracts[i].isFavorite) testList.add(
InkResponse(
enableFeedback: true,
onLongPress: (){
showShortToast();
DashBLOC dashBloc = BlocProvider.of<DashBLOC>(context);
dashBloc.dispatch(new UnfavRequest(state.contracts[i].id));
},
onTap: onTap,
child:DashboardCardWidget(state.contracts[i])
)
);
}
return GridView.count(
crossAxisCount: width >= 900 ? 2 : 1,
padding: const EdgeInsets.all(2.0),
children: testList
);
}
}
})
);
}
full class code and dashboard bloc
Looks like grid rebuilds itself, but don't rebuild its tiles.
How can I completely update grid widget with all its subwidgets?
p.s i've spent two days fixing it, pls help
I think you should use a GridView.builderconstructor to specify a build function which will update upon changes in the list of items, so when any update occur in your data the BlocBuilder will trigger the build function inside yourGridView.
I hope this example makes it more clear.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Test(),
);
}
}
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
List<int> testList = List<int>();
#override
void initState() {
for (int i = 0; i < 20; i++) {
testList.add(i);
}
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
//Here we can remove an item from the list and using setState
//or BlocBuilder will rebuild the grid with the new list data
onPressed: () => setState(() {testList.removeLast();})
),
body: GridView.builder(
// You must specify the items count of your grid
itemCount: testList.length,
// You must use the GridDelegate to specify row item count
// and spacing between items
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
childAspectRatio: 1.0,
crossAxisSpacing: 1.0,
mainAxisSpacing: 1.0,
),
// Here you can build your desired widget which will rebuild
// upon changes using setState or BlocBuilder
itemBuilder: (BuildContext context, int index) {
return Text(
testList[index].toString(),
textScaleFactor: 1.3,
);
},
),
);
}
}
Your code is always sending the last value of int i.
So instead of
for(int i = 0; i < state.contracts.length; i++){
if(state.contracts[i].isFavorite) testList.add(
InkResponse(
enableFeedback: true,
onLongPress: (){
showShortToast();
DashBLOC dashBloc = BlocProvider.of<DashBLOC>(context);
dashBloc.dispatch(new UnfavRequest(state.contracts[i].id));
},
onTap: onTap,
child:DashboardCardWidget(state.contracts[i])
)
);
Do
List<Widget> testList = new List<Widget>();
state.contracts.forEach((contract){
if(contract.isFavorite) testList.add(
InkResponse(
enableFeedback: true,
onLongPress: (){
showShortToast();
DashBLOC dashBloc = BlocProvider.of<DashBLOC>(context);
dashBloc.dispatch(new UnfavRequest(contract.id));
},
onTap: onTap,
child:DashboardCardWidget(contract)
)
));
Is it actually rebuilds? I'm just don't understand why you use the State with BLoC. Even if you use the State you should call the setState() method to update the widget with new data.
On my opinion the best solution to you will be to try to inherit your widget from StatelessWidget and call the dispatch(new UpdateRequest()); in the DashBLOC constructor.
Also always keep in mind this link about the bloc, there are lots of examples:
https://felangel.github.io/bloc/#/
just give children a key
return GridView.builder(
itemCount: children.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(3),
itemBuilder: (context, index) {
return Container(
key: ValueKey(children.length+index),
);
});
How do i create a gridview-layout with multi-select feature in Flutter, like android photo app? I was looking for an existing widget but couldn't find one.
What I have at the moment: a gridview-layout with n rows and 2 columns. The cells contain a GridTile-widget with some information and a header text. Now i want to have a functionality like in android photo app, after a long press on one of these tiles, a check-circle appears on the left top corner for all items.
Do i have to build this on my own, or is there an existing Flutter-widget which i didn't find so far?
I also don't know an existing widget, but perhaps this will help you:
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.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',
debugShowCheckedModeBanner: false,
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> {
List<String> _imageList = List();
List<int> _selectedIndexList = List();
bool _selectionMode = false;
#override
Widget build(BuildContext context) {
List<Widget> _buttons = List();
if (_selectionMode) {
_buttons.add(IconButton(
icon: Icon(Icons.delete),
onPressed: () {
_selectedIndexList.sort();
print('Delete ${_selectedIndexList.length} items! Index: ${_selectedIndexList.toString()}');
}));
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
actions: _buttons,
),
body: _createBody(),
);
}
#override
void initState() {
super.initState();
_imageList.add('https://picsum.photos/800/600/?image=280');
_imageList.add('https://picsum.photos/800/600/?image=281');
_imageList.add('https://picsum.photos/800/600/?image=282');
_imageList.add('https://picsum.photos/800/600/?image=283');
_imageList.add('https://picsum.photos/800/600/?image=284');
}
void _changeSelection({bool enable, int index}) {
_selectionMode = enable;
_selectedIndexList.add(index);
if (index == -1) {
_selectedIndexList.clear();
}
}
Widget _createBody() {
return StaggeredGridView.countBuilder(
crossAxisCount: 2,
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
primary: false,
itemCount: _imageList.length,
itemBuilder: (BuildContext context, int index) {
return getGridTile(index);
},
staggeredTileBuilder: (int index) => StaggeredTile.count(1, 1),
padding: const EdgeInsets.all(4.0),
);
}
GridTile getGridTile(int index) {
if (_selectionMode) {
return GridTile(
header: GridTileBar(
leading: Icon(
_selectedIndexList.contains(index) ? Icons.check_circle_outline : Icons.radio_button_unchecked,
color: _selectedIndexList.contains(index) ? Colors.green : Colors.black,
),
),
child: GestureDetector(
child: Container(
decoration: BoxDecoration(border: Border.all(color: Colors.blue[50], width: 30.0)),
child: Image.network(
_imageList[index],
fit: BoxFit.cover,
),
),
onLongPress: () {
setState(() {
_changeSelection(enable: false, index: -1);
});
},
onTap: () {
setState(() {
if (_selectedIndexList.contains(index)) {
_selectedIndexList.remove(index);
} else {
_selectedIndexList.add(index);
}
});
},
));
} else {
return GridTile(
child: InkResponse(
child: Image.network(
_imageList[index],
fit: BoxFit.cover,
),
onLongPress: () {
setState(() {
_changeSelection(enable: true, index: index);
});
},
),
);
}
}
}
I used staggered grid view to show a grid and grid tiles with a header to have a space for the selection icon. Hope that helps!
This is a plugin from flutter package You can use this
https://pub.dev/packages/drag_select_grid_view
I'm writing widget testing the Cupertino Picker for the different values chosen by the use. I can't find any good tutorial. I followed this https://github.com/flutter/flutter/blob/master/packages/flutter/test/cupertino/picker_test.dart but this won't work for my case. In my case when the user chooses the value from the picker the test case should check whether the user chooses the correct value or default value.
Cupertino Picker code :
List<String> ages1 = ["-- select --"];
List<String> ages2 = List<String>.generate(
45, (int index) => (21 + index).toString(),
growable: false);
List<String> ages = [ages1, ages2].expand((f) => f).toList();
picker.dart:
Widget _buildAgePicker(BuildContext context) {
final FixedExtentScrollController scrollController =
FixedExtentScrollController(initialItem: _selectedAgeIndex);
return GestureDetector(
key: Key("Age Picker"),
onTap: () async {
await showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return _buildBottomPicker(
CupertinoPicker(
key: Key("Age picker"),
scrollController: scrollController,
itemExtent: dropDownPickerItemHeight,
backgroundColor: Theme.of(context).canvasColor,
onSelectedItemChanged: (int index) {
setState(() {
_selectedAgeIndex = index;
ageValue = ages[index];
if (ageValue == S.of(context).pickerDefaultValue) {
ageDividerColor = Theme.of(context).errorColor;
errorText = S.of(context).pickerErrorMessage;
ageDividerWidth = 1.2;
} else {
ageDividerColor = Colors.black87;
errorText = "";
ageDividerWidth = 0.4;
}
});
},
children: List<Widget>.generate(ages.length, (int index) {
return Center(
child: Text(ages[index]),
);
}),
),
);
},
);
},
child: _buildMenu(
<Widget>[
Text(
S.of(context).Age,
style: TextStyle(fontSize: 17.0),
),
Text(
ages[_selectedAgeIndex],
),
],
),
);
}
Widget _buildMenu(List<Widget> children) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).canvasColor,
),
height: 44.0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SafeArea(
top: false,
bottom: false,
child: DefaultTextStyle(
style: const TextStyle(
color: Colors.black,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: children,
),
),
),
),
);
}
Widget _buildBottomPicker(Widget picker) {
return Container(
height: dropDownPickerSheetHeight,
padding: const EdgeInsets.only(top: 6.0),
color: Theme.of(context).canvasColor,
child: DefaultTextStyle(
style: const TextStyle(
color: Colors.black,
fontSize: 22.0,
),
child: GestureDetector(
key: Key("picker"),
onTap: () {},
child: SafeArea(
top: false,
child: picker,
),
),
),
);
}
test code :
testWidgets("picker test",(WidgetTester tester)async{
await tester.tap(find.byKey(Key("Age Picker")));
await tester.drag(find.byKey(Key("Age Picker")), Offset(0.0,70.0));
await tester.pumpAndSettle();
expect(ages[1], "21");
});
I used a similar example in my golden test. I modified it a little in order to fit your case. However, the most important part is calling both methods inclusive: fling and drag. If you call only one of them it won't work. At least in my case, that was what happened.
testWidgets("cupertino picker test", (WidgetTester tester) async{
// Find the gesture detector that invoke the cupertino picker
final gestureDetectorFinder = find.byKey(Key('Age Picker'));
await tester.tap(gestureDetectorFinder);
await tester.pump();
// Find the default option (the first one)
final ageFinder = find.text('21').last;
expect(ageFinder, findsOneWidget);
// Apply an offset to scroll
const offset = Offset(0, -10000);
// Use both methods: fling and drag
await tester.fling(
ageFinder,
offset,
1000,
warnIfMissed: false,
);
await tester.drag(
ageFinder,
offset,
warnIfMissed: false,
);
});
What errors did you receive? I've tried creating a minimal repro from the CupertinoPicker snippet you've provided and I did encounter some issues in testWidgets().
Some of the issues that I've noticed is that CupertinoPicker has "Age picker" as its key and GestureDetector has "Age Picker" key set. Note that the key is case-sensitive. Since you're going to test CupertinoPicker, the key set on GestureDetector seems to be unnecessary.
Aside from that, no widget was built for the test. I suggest going through the official docs for Flutter testing to get started https://flutter.dev/docs/cookbook/testing/widget/introduction
Here's the repro I've created from the snippets you've provided.
Code for testing widgets
void main(){
var ages = [18, 19, 20, 21, 22, 24, 24, 25];
testWidgets("CupertinoPicker test", (WidgetTester tester) async {
// build the app for the test
// https://flutter.dev/docs/cookbook/testing/widget/introduction#4-build-the-widget-using-the-widgettester
await tester.pumpWidget(MyApp());
// key should match the key set in the widget
await tester.tap(find.byKey(Key("Age Picker")));
await tester.drag(find.byKey(Key("Age Picker")), Offset(0.0, 70.0));
await tester.pumpAndSettle();
expect(ages[3], 21);
});
}
Sample code for the app
import 'package:flutter/cupertino.dart';
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,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
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> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child:
testPicker(),
),
);
}
var _selectedAgeIndex = 0;
var scrollController = FixedExtentScrollController();
var dropDownPickerItemHeight = 50.0;
var ageValue;
var ages = [18, 19, 20, 21, 22, 24, 24, 25];
testPicker(){
return CupertinoPicker(
key: Key("Age Picker"),
scrollController: scrollController,
itemExtent: dropDownPickerItemHeight,
backgroundColor: Theme.of(context).canvasColor,
onSelectedItemChanged: (int index) {
setState(() {
_selectedAgeIndex = index;
ageValue = ages[index];
print('CupertinoPicker age[$index]: ${ages[index]}');
// if (ageValue == S.of(context).pickerDefaultValue) {
// ageDividerColor = Theme.of(context).errorColor;
// errorText = S.of(context).pickerErrorMessage;
// ageDividerWidth = 1.2;
// } else {
// ageDividerColor = Colors.black87;
// errorText = "";
// ageDividerWidth = 0.4;
// }
});
},
children: List<Widget>.generate(ages.length, (int index) {
return Center(
child: Text('${ages[index]}'),
);
}),
);
}
}
I'm starting with Flutter and I cannot make drag and drop functionality to work. I followed the documentation but have no idea what I'm doing wrong.
This sample app displays three squares and the blue is draggable. The other ones have DragTarget set, one inside the square and one outside the square. When I drag the blue square it prints info that the drag started but there is no print info when dragging or dropping over the DragTargets.
Here is the 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.red,
),
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> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Container(
constraints: BoxConstraints.expand(),
color: Colors.grey[900],
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
width: 100,
height: 100,
color: Colors.red,
child: DragTarget(
onWillAccept: (d) => true,
onAccept: (d) => print("ACCEPT 1!"),
onLeave: (d) => print("LEAVE 1!"),
builder: (a,data,c) {
print(data);
return Center();
},
),
),
DragTarget(
onWillAccept: (d){return true;},
onAccept:(d) => print("ACCEPT 2!"),
onLeave: (d) => print("LEAVE 2!"),
builder: (context, candidateData, rejectedData){
return Container(
width: 150,
height: 150,
color: Colors.purple
);
}
),
Draggable(
data: ["SOME DATA"],
onDragStarted: () => print("DRAG START!"),
onDragCompleted: () => print("DRAG COMPLETED!"),
onDragEnd: (details) => print("DRAG ENDED!"),
onDraggableCanceled: (data, data2) => print("DRAG CANCELLED!"),
feedback: SizedBox(
width: 100,
height: 100,
child: Container(
margin: EdgeInsets.all(10),
color: Colors.green[800],
),
),
child: SizedBox(
width: 100,
height: 100,
child: Container(
margin: EdgeInsets.all(10),
color: Colors.blue[800],
),
),
),
],
)
),
)
);
}
}
Apparently the Draggable and DragTarget need to have the generic type specified if you are passing data, otherwise the onAccept and onWillAccept will not be fired.
For example, if you want to pass data as int then use Draggable<int> and DragTarget<int> — this also applies to onAccept and onWillAccept, they need to accept int as a parameter.
You should setState when you call onAccept and add a boolean value to your stateful widget.
bool accepted = false;
onAccept: (data){
if(data=='d'){
setState(() {
accepted = true;
});
},
I used ChangeNotifyProvider and a model to manage my Draggable and Dragable Target multiplication challenge and results. I built a simple multiplication game using ChangeNotify that updates the Provider that is listening for changes. The GameScore extends the ChangeNotifier which broadcast to the provider when changes occur in the model. The Provider can either be listening or not listening. If the user get the right answer than the Model updates its score and notifies the listeners. The score is then displayed in a text box. I think the provider model is a simplier way to interact with the widget for managing state.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'dart:math';
class Multiplication
{
int value1;
int value2;
int result;
int answerKey;
int fakeResult;
Multiplication(this.value1,this.value2,this.result,this.answerKey,this.fakeResult);
}
class GameScore with ChangeNotifier
{
int score=0;
int number=0;
List<Multiplication> lstMultiplication=[];
late Multiplication currentMultiplication;
GameScore()
{
var rng = Random();
for(int i=0; i<=25; i++)
{
for(int j=0; j<=25; j++)
{
var answerKey=rng.nextInt(2);
var fakeAnswer=rng.nextInt(25)*rng.nextInt(25);
lstMultiplication.add(Multiplication(i,j,i*j,answerKey,fakeAnswer));
}
}
}
int getChallengeValue(int key)
{
int retVal=0;
if (currentMultiplication.answerKey==key)
{
retVal=currentMultiplication.result;
}
else
{
retVal=currentMultiplication.fakeResult;
}
return retVal;
}
String displayMultiplication()
{
String retVal="";
if (currentMultiplication!=null)
{
retVal=currentMultiplication.value1.toString()+ " X "+currentMultiplication.value2.toString();
}
return retVal;
}
nextMultiplication()
{
var rng = Random();
var index=rng.nextInt(lstMultiplication.length);
currentMultiplication= lstMultiplication[index];
}
changeAcceptedData(int data) {
var rng = Random();
score += 1;
number=rng.nextInt(100);
notifyListeners();
}
changeWrongData(int data) {
var rng = Random();
score -= 1;
number=rng.nextInt(100);
notifyListeners();
}
}
void main() {
runApp(const MyApp());
//runApp(Provider<GameScore>(create: (context) => GameScore(), child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: //TestDraggableWidget(),
ChangeNotifierProvider(create:(context)=>GameScore(),child: TestDraggableWidget())
);
}
}
class TestDraggableWidget extends StatefulWidget {
TestDraggableWidget({Key? key}) : super(key: key);
#override
State<TestDraggableWidget> createState() => _TestDraggableWidgetState();
}
class _TestDraggableWidgetState extends State<TestDraggableWidget> {
#override
Widget build(BuildContext context) {
Provider.of<GameScore>(context,listen:false).nextMultiplication();
return Scaffold(appBar: AppBar(title:Text("Draggable")),body:
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
EvenContainerWidget(),
NumberContainerWidget(),
OddContainerWidget(),
SizedBox(width:100,child:Text("Score: ${Provider.of<GameScore>(context, listen: true).score}",style:TextStyle(color:Colors.green,fontSize:14)))
],)
],));
}
}
class EvenContainerWidget extends StatefulWidget {
EvenContainerWidget({Key? key}) : super(key: key);
#override
State<EvenContainerWidget> createState() => _EvenContainerWidgetState();
}
class _EvenContainerWidgetState extends State<EvenContainerWidget> {
int? valueAccepted;
_onAccept(BuildContext context, int data)
{
if (data==valueAccepted){
Provider.of<GameScore>(context, listen: false).changeAcceptedData(data);
setState(() {
valueAccepted=data;
});
}
else
{
Provider.of<GameScore>(context, listen: false).changeWrongData(data);
}
}
bool _willAccept(int? data)
{
return true;
}
#override
Widget build(BuildContext context) {
valueAccepted=Provider.of<GameScore>(context, listen: false).getChallengeValue(1);
return Container(
width:60,
height:60,
decoration:BoxDecoration(borderRadius: BorderRadius.circular(10),color:Colors.blueAccent),
child:
DragTarget<int>(
onAccept: (data)=> _onAccept(context,data),
onWillAccept: _willAccept,
builder:(context, candidateData, rejectedData) {
return Center(child:Text("Choice 1: ${valueAccepted==null?'':valueAccepted.toString()}"));
},
)
);
}
}
class OddContainerWidget extends StatefulWidget {
OddContainerWidget({Key? key}) : super(key: key);
#override
State<OddContainerWidget> createState() => _OddContainerWidgetState();
}
class _OddContainerWidgetState extends State<OddContainerWidget> {
int? valueAccepted;
_onAccept(BuildContext context, int data)
{
if(data==valueAccepted)
{
Provider.of<GameScore>(context, listen: false).changeAcceptedData(data);
setState(() {
valueAccepted=data;
});
}
else
{
Provider.of<GameScore>(context, listen: false).changeWrongData(data);
}
}
bool _willAccept(int? data)
{
/*if (data!.isOdd)
{
setState(() {
valueAccepted=data;
});
}*/
return true;
}
#override
Widget build(BuildContext context) {
valueAccepted=Provider.of<GameScore>(context, listen: false).getChallengeValue(0);
return Container(
width:60,
height:60,
decoration:BoxDecoration(borderRadius: BorderRadius.circular(10),color:Colors.blueAccent),
child:
DragTarget<int>(
onAccept: (data)=> _onAccept(context,data),
onWillAccept: _willAccept,
builder:(context, candidateData, rejectedData) {
return Center(child:Text("Choice 2: ${valueAccepted==null?'':valueAccepted.toString()}"));
},
)
);
}
}
class NumberContainerWidget extends StatelessWidget {
const NumberContainerWidget({Key? key}) : super(key: key);
_dragCompleted(BuildContext context){
}
#override
Widget build(BuildContext context) {
return Draggable(
//information dropped by draggable at dragtarget
data: Provider.of<GameScore>(context, listen: true).currentMultiplication.result,
onDragCompleted: _dragCompleted(context) ,
//Widget to be displayed when drag is underway
feedback: Container(
width:60,
height:60,
decoration:BoxDecoration(borderRadius: BorderRadius.circular(10),color:Colors.black26),
child: Center(child:Text("${Provider.of<GameScore>(context, listen: false).displayMultiplication()}",style:TextStyle(color:Colors.green,fontSize:14))),
),
child:
Container(
width:60,
height:60,
decoration:BoxDecoration(borderRadius: BorderRadius.circular(10),color:Colors.black26),
child: Center(child:Text("${Provider.of<GameScore>(context, listen: false).displayMultiplication()}",style:TextStyle(color:Colors.blue,fontSize:14))),
));
}
}
How would I create a circular ListView in Flutter?
I want something that allows me to have a list of widgets rotate around an origin.
Something similar to this:
Any help would be appreciated.
Circular List View Demo. Which Is helpful for You May be.
Main.dart
import 'package:master/numbers_list.dart';
import 'package:master/radial_list.dart';
import 'package:meta/meta.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyHomePage());
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: new ThemeData(
accentColor: Colors.blue,
),
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
RadialListViewModel radialList;
HomePage({
#required this.radialList
});
#override
HomePageState createState() {
return new HomePageState();
}
}
class HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
title: Text("My Home Page"),
),
body: Stack(
children: <Widget>[
RadialList(
radialList: radialNumbers,
radius: 150.00,
),
],
)
);
}
}
numbers_list.dart
import 'package:master/radial_list.dart';
final RadialListViewModel radialNumbers = new RadialListViewModel(
items: [
new RadialListItemViewModel(
number: 1,
isSelected: true,
),
new RadialListItemViewModel(
number: 2,
isSelected: false,
),
new RadialListItemViewModel(
number: 3,
isSelected: false,
),new RadialListItemViewModel(
number: 4,
isSelected: false,
),
new RadialListItemViewModel(
number: 5,
isSelected: false,
),
new RadialListItemViewModel(
number: 6,
isSelected: false,
),
new RadialListItemViewModel(
number: 7,
isSelected: false,
),
new RadialListItemViewModel(
number: 8,
isSelected: false,
),
new RadialListItemViewModel(
number: 9,
isSelected: false,
),
new RadialListItemViewModel(
number: 10,
isSelected: false,
),
new RadialListItemViewModel(
number: 11,
isSelected: false,
),new RadialListItemViewModel(
number: 12,
isSelected: false,
),
]
);
radial_list.dart
import 'package:flutter/material.dart';
import 'dart:math';
import 'package:master/radial_position.dart';
class RadialList extends StatefulWidget {
final RadialListViewModel radialList;
final double radius;
RadialList({
this.radialList,
this.radius,
});
List<Widget> _radialListItems(){
final double firstItemAngle = pi;
final double lastItemAngle = pi;
final double angleDiff = (firstItemAngle + lastItemAngle) / (radialList.items.length);
double currentAngle = firstItemAngle;
return radialList.items.map((RadialListItemViewModel viewModel){
final listItem = _radialListItem(viewModel,currentAngle);
currentAngle += angleDiff;
return listItem;
}).toList();
}
Widget _radialListItem(RadialListItemViewModel viewModel, double angle){
return Transform(
transform: new Matrix4.translationValues(
180.0,
250.0,
0.0
),
child: RadialPosition(
radius: radius,
angle: angle,
child: new RadialListItem(
listItem: viewModel,
),
),
);
}
#override
RadialListState createState() {
return new RadialListState();
}
}
class RadialListState extends State<RadialList> {
#override
Widget build(BuildContext context) {
return new Stack(
children: widget._radialListItems(),
);
}
}
class RadialListItem extends StatefulWidget {
final RadialListItemViewModel listItem;
RadialListItem({
this.listItem
});
#override
RadialListItemState createState() {
return new RadialListItemState();
}
}
class RadialListItemState extends State<RadialListItem> {
#override
Widget build(BuildContext context) {
return Transform(
transform: new Matrix4.translationValues(-30.0, -30.0, 0.0),
child: Container(
width: 60.0,
height: 60.0,
decoration: new BoxDecoration(
shape: BoxShape.circle,
color: Colors.deepPurpleAccent,
border: new Border.all(
color: Colors.red,
width: 2.0
)
),
child: Padding(
padding: const EdgeInsets.all(0.0),
child: OutlineButton(
shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(60.0)),
color: Colors.transparent,
onPressed: () {
setState(() {
widget.listItem.isSelected = true;
//widget.listItem.number = widget.listItem.number + 1;
});
},
child: new Text(
widget.listItem.number.toString(),
style: new TextStyle(
color: widget.listItem.isSelected ? Colors.red : Colors.yellow,
fontSize: 20.0
),
),
),
),
),
);
}
}
class RadialListViewModel{
final List<RadialListItemViewModel> items;
RadialListViewModel({
this.items = const [],
});
}
class RadialListItemViewModel{
int number;
bool isSelected;
RadialListItemViewModel({
this.isSelected=false,
this.number,
});
}
radial_position.dart
import 'package:flutter/material.dart';
import 'dart:math';
class RadialPosition extends StatefulWidget {
final double radius;
final double angle;
final Widget child;
RadialPosition({
this.angle,
this.child,
this.radius,
});
#override
RadialPositionState createState() {
return new RadialPositionState();
}
}
class RadialPositionState extends State<RadialPosition> {
#override
Widget build(BuildContext context) {
final x = cos(widget.angle) * widget.radius;
final y = sin(widget.angle) * widget.radius;
return Transform(
transform: new Matrix4.translationValues(x, y, 0.0),
child: widget.child,
);
}
}
A simpler way to do it is to use this package
circular_motion
Here's an example using that package
CircularMotion.builder(
itemCount: 18,
centerWidget: Text('Center'),
builder: (context, index) {
return Text('$index');
}
)