Flutter how to save position of the reorderables package? - flutter

i tried the reorderables package https://pub.dev/packages/reorderables
I successed to move my dashboard blocs but when I restart app, my moves are removed.
So the solution can only be a sharedpref solution.
But I dont found how to save this information
I tried to save and load newIndex but without success
I tried to save and load List _tiles; but sharedpref can't save List
Here is my code example
List<Widget> _tiles;
void _onReorder(int oldIndex, int newIndex) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
Widget row = _tiles.removeAt(oldIndex);
_tiles.insert(newIndex, row);
//prefs.setListWidget('indexList', _tiles); not working
// prefs.setInt('index', newIndex ); not working
});
}
#override
void initState() {
super.initState();
_tiles = <Widget>[
//my widget1
//my widget2
//my widget3
//my widget4
//my widget5
//my widget6
//my widget7
//my widget8
//my widget9
]
}
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
ReorderableWrap(
spacing: 0.0,
runSpacing:0,
maxMainAxisCount: 3,
minMainAxisCount: 3,
padding: const EdgeInsets.all(5),
children:_tiles,
onReorder: _onReorder,
onNoReorder: (int index) {
//this callback is optional
debugPrint('${DateTime.now().toString().substring(5, 22)} reorder cancelled. index:$index');
},
onReorderStarted: (int index) {
//this callback is optional
debugPrint('${DateTime.now().toString().substring(5, 22)} reorder started: index:$index');
}
)
]
);
}
}
edit : here is Widget 1. other widget are same
new Container (
width: SizeConfig.safeBlockHorizontal * 32,
height: 160,
child :
new Card(
elevation: 8,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
color: Colors.white,
child:
Ink(
child: InkResponse(
splashFactory: InkRipple.splashFactory,
radius: 100,
onTap: () {
},
child:
Padding(
padding: const EdgeInsets.only(top:10),
child:
new Container(
padding: const EdgeInsets.all(0),
child :
new Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new Container(
height:25,
child :
new Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(Icons.keyboard_arrow_right, color: Colors.white, size: 15.0),
Text('Planning',textAlign:TextAlign.center, style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w500,
fontSize: SizeConfig.safeBlockHorizontal * 4),
),
Icon(Icons.keyboard_arrow_right, color: Colors.grey, size: 15.0),
]
),
),
Padding(
padding: const EdgeInsets.all(5),),
Icon(Icons.event_available, color:Color(0xffff9a7b), size: 70.0),
],
),
))
),
)),
)
Here is my design
Edit #2
Now I tried to add my assets in the model, but I don't know how to do
void initState() {
// default models list
models = [
Model(index: 0, new Container (
width:90,
height: 90,
child :new FlareActor("assets/pierre.flr", alignment:Alignment.center, fit:BoxFit.contain, animation:"pierre")
), title: 'Coach'),
Model(index: 1, new Image.asset(
"assets/cup.png",
width: 50,
), title: 'Victories'),
Model(index: 2, icon: Icon(Icons.card_giftcard), title: 'Jeux'),
Model(index: 3, icon: Icon(Icons.wb_sunny), title: 'Sunny'),
Model(index: 4, icon: Icon(Icons.cloud), title: 'Cloud'),
Model(index: 5, icon: Icon(Icons.tv), title: 'TV'),
Model(index: 6, icon: Icon(Icons.place), title: 'Location'),
Model(index: 8, icon: Icon(Icons.music_note), title: 'Music'),
// More customization
Model(
index: 7,
icon: Icon(Icons.event_available, color: Color(0xffff9a7b)),
title: 'Planning'),
];
config();
super.initState();
}

This solution isn't a good one and I don't like it, but it works :D
I really appreciate it if anyone would refer a better one
Maybe for this project, it's good to use DB instead of SharedPreferences but here I used SharedPreferences.
The question is how to save the order of some widget(each time on reordering, the order of widget changes and we want to save the order of them, after restarting the app the saved order should be fetched).
SharedPreferences can also save a list of string, so what I did here was:
In the beginning, there should be a default list, that contains the initial order of widget's of the app.
Because widgets are somehow the same and only some of their info is different, I decided to define a model and work with models, instead of a whole complicated widget, I mean when I want to remove or change indexes I do it for a list of models rather than a list of widgets.
Here I supposed the model only contains a title, I also defined an index for it, so all I do is that when I reorder the widget, it reorders the list of models, to save the order, I save the index of models in any order they are now,
for example, if the initial order was [0, 1, 2, 3] let's say after reordering it's now [3, 0, 1, 2], I save this order, and for the next boot, I fetch the saved order([3, 0, 1, 2]) and then reorder the default list based on this fetched order.
Another solution would be to change the model's index and then show an ordered list of models based on their index.
Here is the code:
import 'package:flutter/material.dart';
import 'package:reorderables/reorderables.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: Page(),
),
),
),
);
class Page extends StatefulWidget {
const Page({Key key}) : super(key: key);
#override
_PageState createState() => _PageState();
}
class _PageState extends State<Page> {
SharedPreferences prefs;
List<Model> models;
#override
void initState() {
// default models list
models = [
Model(index: 0, title: 'Item 0'),
Model(index: 1, title: 'Item 1'),
Model(index: 2, title: 'Item 2'),
Model(index: 3, title: 'Item 3'),
];
config();
super.initState();
}
void config() async {
// Here we reset the default model based on saved order
await SharedPreferences.getInstance().then((pref) {
prefs = pref;
List<String> lst = pref.getStringList('indexList');
List<Model> list = [];
if (lst != null && lst.isNotEmpty) {
list = lst
.map(
(String indx) => models
.where((Model item) => int.parse(indx) == item.index)
.first,
)
.toList();
models = list;
}
setState(() {});
});
}
void _onReorder(int oldIndex, int newIndex) async {
Model row = models.removeAt(oldIndex);
models.insert(newIndex, row);
setState(() {
prefs.setStringList(
'indexList', models.map((m) => m.index.toString()).toList());
});
}
#override
Widget build(BuildContext context) {
return ReorderableWrap(
scrollDirection: Axis.vertical,
direction: Axis.vertical,
spacing: 0.0,
runSpacing: 0,
maxMainAxisCount: 3,
minMainAxisCount: 3,
padding: const EdgeInsets.all(5),
children: models
.map((m) => Card(
child: Container(
child: Text('${m.index} - ${m.title}'),
padding: EdgeInsets.all(24.0),
),
))
.toList(),
onReorder: _onReorder,
onNoReorder: (int index) {
//this callback is optional
debugPrint('${DateTime.now().toString().substring(5, 22)} ' +
'reorder cancelled. index:$index');
},
onReorderStarted: (int index) {
//this callback is optional
debugPrint('${DateTime.now().toString().substring(5, 22)} ' +
'reorder started: index:$index');
});
}
}
class Model {
int index;
String title;
Model({this.index, this.title});
#override
String toString() {
return '$index : $title';
}
}
Changes After Edit to the main question:
This version is based on the editing to the main question, I decided to keep the first answer unchanged because it's a more simple version and may help another viewer.
For the new model, as far as I could get, it has an icon, title, and an onTap functionality, I changed the model to have icon and title, but for the onTap, I wrote my own card version that gets a model and onTap functionality, I could add onTap to the model, but I thought it's better for future use or to use in other places, so I separated the onTap from the model, I also chose Icon for the model, it could be IconData (benefit of IconData is that you can choose customization for each icon and etc).
On my Card version (MyCard), I simply used a GestureDetector and Card to simulate the taps and card.
I wrote a FakePage that gets a model and if you Tap on each card it navigates to this page and shows some message based on the received model.
To clean the previously saved model in SharedPreferences, you should comment the part that fetches models order in config() and on the next refresh, you should uncomment it again.
Here is the new version of code:
import 'package:flutter/material.dart';
import 'package:reorderables/reorderables.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(
MaterialApp(
home: Scaffold(
body: Center(
child: Page(),
),
),
),
);
class Page extends StatefulWidget {
const Page({Key key}) : super(key: key);
#override
_PageState createState() => _PageState();
}
class _PageState extends State<Page> {
SharedPreferences prefs;
List<Model> models;
#override
void initState() {
// default models list
models = [
Model(index: 0, icon: Icon(Icons.people), title: 'Coach'),
Model(index: 1, icon: Icon(Icons.wb_incandescent), title: 'Victories'),
Model(index: 2, icon: Icon(Icons.card_giftcard), title: 'Jeux'),
Model(index: 3, icon: Icon(Icons.wb_sunny), title: 'Sunny'),
Model(index: 4, icon: Icon(Icons.cloud), title: 'Cloud'),
Model(index: 5, icon: Icon(Icons.tv), title: 'TV'),
Model(index: 6, icon: Icon(Icons.place), title: 'Location'),
Model(index: 8, icon: Icon(Icons.music_note), title: 'Music'),
// More customization
Model(
index: 7,
icon: Icon(Icons.event_available, color: Color(0xffff9a7b)),
title: 'Planning'),
];
config();
super.initState();
}
void config() async {
// Here we reset the default model based on saved order
await SharedPreferences.getInstance().then((pref) {
prefs = pref;
List<String> lst = pref.getStringList('indexList');
List<Model> list = [];
if (lst != null && lst.isNotEmpty) {
list = lst
.map(
(String indx) => models
.where((Model item) => int.parse(indx) == item.index)
.first,
)
.toList();
models = list;
}
setState(() {});
});
}
void _onReorder(int oldIndex, int newIndex) async {
Model row = models.removeAt(oldIndex);
models.insert(newIndex, row);
setState(() {
prefs.setStringList(
'indexList', models.map((m) => m.index.toString()).toList());
});
}
#override
Widget build(BuildContext context) {
return ReorderableWrap(
spacing: 0.0,
runSpacing: 0,
maxMainAxisCount: 3,
minMainAxisCount: 3,
padding: const EdgeInsets.all(5),
children: <Widget>[
MyCard(
model: models[0],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[0]),
),
),
),
MyCard(
model: models[1],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[1]),
),
),
),
MyCard(
model: models[2],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[2]),
),
),
),
MyCard(
model: models[3],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[3]),
),
),
),
MyCard(
model: models[4],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[4]),
),
),
),
MyCard(
model: models[5],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[5]),
),
),
),
MyCard(
model: models[6],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[6]),
),
),
),
MyCard(
model: models[7],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[7]),
),
),
),
MyCard(
model: models[8],
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => FakePage(model: models[8]),
),
),
),
],
onReorder: _onReorder,
onNoReorder: (int index) {
//this callback is optional
debugPrint('${DateTime.now().toString().substring(5, 22)} ' +
'reorder cancelled. index:$index');
},
onReorderStarted: (int index) {
//this callback is optional
debugPrint('${DateTime.now().toString().substring(5, 22)} ' +
'reorder started: index:$index');
});
}
}
// ---------------------- Model --------------------------
class Model {
int index;
String title;
Icon icon;
Model({this.index, this.title, this.icon});
#override
String toString() {
return '$index : $title';
}
}
// ------------------------ MyCard ----------------------------
class MyCard extends StatelessWidget {
final Model model;
final void Function() onTap;
const MyCard({Key key, this.onTap, #required this.model})
: assert(model != null),
super(key: key);
#override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
return GestureDetector(
onTap: onTap,
child: Card(
elevation: 8.0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: _child(width),
),
);
}
Widget _child(double width) {
return Container(
width: width / 4,
height: width / 3,
margin: EdgeInsets.all(5.0),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
flex: 3,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
model.title,
maxLines: 1,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w500,
fontSize: 15.0,
),
overflow: TextOverflow.ellipsis,
),
Icon(
Icons.arrow_forward_ios,
color: Colors.grey.shade400,
size: 15.0,
),
],
),
),
Expanded(
flex: 5,
child: Padding(
padding: EdgeInsets.all(8.0),
child: FittedBox(
fit: BoxFit.contain,
child: model.icon,
),
),
),
],
),
);
}
}
// ----------------------- FAKE PAGE ---------------------------
class FakePage extends StatelessWidget {
final Model model;
const FakePage({Key key, this.model}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepOrangeAccent,
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
'You Clicked on Card : ${model.title}',
style: TextStyle(fontSize: 20.0),
),
Padding(
padding: EdgeInsets.only(top: 24.0),
child: Icon(
model.icon.icon,
size: 70.0,
),
),
],
),
),
);
}
}

Related

Deleting Item out of List, Listview.build shows wrong data

I have a Stateful widget that i pass a list to (for example 2 items).
After I delete an item, the widget should rebuild itself.
Unfortunately, the deleted item is still displayed and the other one is not.
When I re-enter the widget, the correct item is loaded.
There is a similar problem List not updating on deleting item
but maybe someone can explain me what i did wrong and why provider is helping me here instead of setState?
My code is:
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import 'package:trip_planner/util/dialog_box.dart';
import 'package:trip_planner/util/previewUrl.dart';
class BookingPage extends StatefulWidget {
final List toDoList;
BookingPage({
super.key,
required this.toDoList,
});
#override
State<BookingPage> createState() => _BookingPageState();
}
class _BookingPageState extends State<BookingPage> {
//text controller
final _controller = TextEditingController();
final _database = FirebaseDatabase.instance.ref();
//Liste is an example what i have in my list
List toDoList2 = [
["https://www.booking.com/Share-Rnv2Kf", true],
["https://www.booking.com/Share-3hKQ0r", true],
];
void initState(){
super.initState();
}
void deleteTask(int index){
setState(() {
widget.toDoList.removeAt(index);
});
//DatabaseReference _testRef = _database.child("Hotel:");
//_testRef.set(widget.toDoList.toString());
}
//save new Item
void saveNewItem(){
setState(() {
widget.toDoList.add([_controller.text, false]);
//DatabaseReference _testRef = _database.child("Hotel:");
//_testRef.set(widget.toDoList.toString());
_controller.clear();
});
Navigator.of(context).pop();
}
void createNewItem(){
showDialog(
context: context,
builder: (context){
return DialogBox(
controller: _controller,
onSave: saveNewItem,
onCancel: () => Navigator.of(context).pop(),
);
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Booking Seiten'),
elevation: 0,
),
floatingActionButton: FloatingActionButton(
onPressed: createNewItem,
child: Icon(Icons.add),
),
body: ListView.builder(
itemCount: widget.toDoList.length,
itemBuilder: (context, index){
return PreviewUrl(
url2: widget.toDoList[index][0],
deleteFunction: (context) => setState(() => deleteTask(index)),
);
},
),
);
}
}
i thought setState does the same thing as when i re-enter the widget, but it doesn't.
import 'package:any_link_preview/any_link_preview.dart';
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:url_launcher/url_launcher.dart';
class PreviewUrl extends StatelessWidget {
final String url2;
//Function(bool?)? onChanged;
Function(BuildContext)? deleteFunction;
PreviewUrl({
super.key,
required this.url2,
required this.deleteFunction,
//required this.onChanged,
});
Future openBrowserURL({
required String url,
bool inApp = false,
}) async {
if(await canLaunch(url)){
await launch(
url,
forceSafariVC: inApp, //iOS
forceWebView: inApp, //Android
enableJavaScript: true, //Android
);
}
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(1.0),
child: Slidable(
endActionPane: ActionPane(
motion: StretchMotion(),
children: [
SlidableAction(
onPressed: deleteFunction,
icon: Icons.delete,
backgroundColor: Colors.red.shade300,
borderRadius: BorderRadius.circular(12),
)
],
),
child: Container(
child: AnyLinkPreview.builder(
link: url2,
itemBuilder: (context, metadata, imageProvider) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (imageProvider != null)
GestureDetector(
onTap: () async {
final url = url2;
openBrowserURL(url: url, inApp: true);
},
child: Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.width *0.25,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12)),
image: DecorationImage(
image: imageProvider,
fit: BoxFit.cover,
),
),
),
),
Container(
width: double.infinity,
color: Theme.of(context).primaryColor.withOpacity(0.6),
padding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (metadata.title != null)
Text(
metadata.title!,
maxLines: 1,
style:
const TextStyle(fontWeight: FontWeight.w500),
),
const SizedBox(height: 5),
if (metadata.desc != null)
Text(
metadata.desc!,
maxLines: 1,
style: Theme.of(context).textTheme.bodySmall,
),
Text(
metadata.url ?? url2,
maxLines: 1,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
],
),
),
),
),
);
}
}
If you run the simplified version of your code in DartPad - it will work:
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
List toDoList = [
["Button 1", true],
["Button 2", true],
];
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: BookingPage(toDoList: toDoList),
),
),
);
}
}
class BookingPage extends StatefulWidget {
final List toDoList;
const BookingPage({
super.key,
required this.toDoList,
});
#override
State<BookingPage> createState() => _BookingPageState();
}
class _BookingPageState extends State<BookingPage> {
//Liste is an example what i have in my list
List toDoList2 = [
["Button 1", true],
["Button 2", true],
];
#override
void initState() {
super.initState();
}
void deleteTask(int index) {
setState(() {
widget.toDoList.removeAt(index);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Booking Seiten'),
elevation: 0,
),
body: ListView.builder(
itemCount: widget.toDoList.length,
itemBuilder: (context, index) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.lightBlue,
padding: const EdgeInsets.all(12),
textStyle: const TextStyle(fontSize: 22),
),
child: Text(widget.toDoList[index][0]!),
onPressed: () => setState(() => deleteTask(index)),
);
},
),
);
}
}
Which tells me that the problem is your PreviewUrl. My guess is - it is a statful widget, and when the tree rebuilds - it will link the old State object to the first item.
Using Keys might help, something like:
return PreviewUrl(
key: ObjectKey(widget.toDoList[index]),
url2: widget.toDoList[index][0],
deleteFunction: (context) => setState(() => deleteTask(index)),
);

Why isn't Navigator.pop() refreshing data?

Hi guys I'm trying to build an app with flutter, so I have two screens HomeScreen() and RoutineScreen(). The first one is a Scaffold and in the body has a child Widget (a ListView called RoutinesWidget()) with all the routines. And the second one is to create a routine. The thing is, that when I create the routine, I use a button to pop to the HomeScreen() but it doesn't refresh the ListView (I'm guessing that it's because when I use Navigator.pop() it refreshes the Scaffold but not the child Widget maybe?)
HomeScreen() code here:
import 'package:flutter/material.dart';
import 'package:workout_time/constants.dart';
import 'package:workout_time/Widgets/routines_widget.dart';
import 'package:workout_time/Widgets/statistics_widget.dart';
import 'package:workout_time/Screens/settings_screen.dart';
import 'package:workout_time/Screens/routine_screen.dart';
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
List<Widget> _views = [
RoutinesWidget(),
StatisticsWidget(),
];
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: kThirdColor,
appBar: AppBar(
leading: Icon(Icons.adb),
title: Text("Workout Time"),
actions: <Widget>[
IconButton(
icon: Icon(Icons.settings),
onPressed: () => Navigator.push(context,
MaterialPageRoute(builder: (context) => SettingsScreen()))),
],
),
body: _views[_selectedIndex],
floatingActionButton: (_selectedIndex == 1)
? null
: FloatingActionButton(
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RoutineScreen(null)));
setState(() {});
},
child: Icon(
Icons.add,
color: kSecondColor,
size: 30.0,
),
elevation: 15.0,
),
bottomNavigationBar: BottomNavigationBar(
items: <BottomNavigationBarItem>[
bottomItems(Icon(Icons.fitness_center_rounded), "Routines"),
bottomItems(Icon(Icons.leaderboard_rounded), "Statistics"),
],
currentIndex: _selectedIndex,
onTap: (int index) => setState(() => _selectedIndex = index),
),
);
}
}
BottomNavigationBarItem bottomItems(Icon icon, String label) {
return BottomNavigationBarItem(
icon: icon,
label: label,
);
}
RoutinesWidget() code here:
import 'package:flutter/material.dart';
import 'package:workout_time/Services/db_crud_service.dart';
import 'package:workout_time/Screens/routine_screen.dart';
import 'package:workout_time/constants.dart';
import 'package:workout_time/Models/routine_model.dart';
class RoutinesWidget extends StatefulWidget {
#override
_RoutinesWidgetState createState() => _RoutinesWidgetState();
}
class _RoutinesWidgetState extends State<RoutinesWidget> {
DBCRUDService helper;
#override
void initState() {
super.initState();
helper = DBCRUDService();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: helper.getRoutines(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
Routine routine = Routine.fromMap(snapshot.data[index]);
return Card(
margin: EdgeInsets.all(1.0),
child: ListTile(
leading: CircleAvatar(
child: Text(
routine.name[0],
style: TextStyle(
color: kThirdOppositeColor,
fontWeight: FontWeight.bold),
),
backgroundColor: kAccentColor,
),
title: Text(routine.name),
subtitle: Text(routine.exercises.join(",")),
trailing: IconButton(
icon: Icon(Icons.delete_rounded),
color: Colors.redAccent,
onPressed: () {
setState(() {
helper.deleteRoutine(routine.id);
});
},
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RoutineScreen(routine))),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0)),
color: kSecondColor,
);
},
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
);
}
}
RoutineScreen() code here:
import 'package:flutter/material.dart';
import 'package:workout_time/Models/routine_model.dart';
import 'package:workout_time/Widgets/type_card_widget.dart';
import 'package:workout_time/constants.dart';
import 'package:workout_time/Services/db_crud_service.dart';
class RoutineScreen extends StatefulWidget {
final Routine _routine;
RoutineScreen(this._routine);
#override
_RoutineScreenState createState() => _RoutineScreenState();
}
class _RoutineScreenState extends State<RoutineScreen> {
DBCRUDService helper;
final _nameController = TextEditingController();
final _descriptionController = TextEditingController();
bool _type = true;
int _cycles = 1;
int _restBetweenExercises = 15;
int _restBetweenCycles = 60;
#override
void initState() {
super.initState();
helper = DBCRUDService();
}
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
title: widget._routine != null
? Text(widget._routine.name)
: Text("Create your routine"),
actions: [
IconButton(
icon: Icon(Icons.done_rounded),
onPressed: createRoutine,
)
],
bottom: TabBar(
tabs: [
Tab(
text: "Configuration",
),
Tab(
text: "Exercises",
),
],
),
),
body: TabBarView(children: [
//_routine == null ? ConfigurationNewRoutine() : Text("WIDGET N° 1"),
ListView(
children: [
Container(
padding: EdgeInsets.all(15.0),
child: Row(
children: [
Text(
"Name:",
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
),
),
SizedBox(
width: 40.0,
),
Expanded(
child: TextField(
textAlign: TextAlign.center,
controller: _nameController,
),
),
],
),
),
SizedBox(
height: 20.0,
),
Card(
margin: EdgeInsets.all(15.0),
color: kSecondColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
),
child: Container(
padding: EdgeInsets.all(15.0),
child: Column(
children: [
Text(
"Type",
style: TextStyle(fontSize: 25.0),
),
Row(
children: [
Expanded(
child: TypeCard(
Icons.double_arrow_rounded,
_type == true ? kFirstColor : kThirdColor,
() => setState(() => _type = true),
"Straight set",
),
),
Expanded(
child: TypeCard(
Icons.replay_rounded,
_type == false ? kFirstColor : kThirdColor,
() => setState(() => _type = false),
"Cycle",
),
),
],
),
],
),
),
),
SizedBox(
height: 20.0,
),
Container(
padding: EdgeInsets.all(15.0),
child: Row(
children: [
Text(
"N° cycles:",
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
),
),
SizedBox(
width: 40.0,
),
Expanded(
child: Text("Hello"),
),
],
),
),
SizedBox(
height: 20.0,
),
],
),
Text("WIDGET N° 2"),
]),
),
);
}
void createRoutine() {
List<String> _exercises = ["1", "2"];
List<String> _types = ["t", "r"];
List<String> _quantities = ["30", "20"];
Routine routine = Routine({
'name': _nameController.text,
'description': "_description",
'type': _type.toString(),
'cycles': 1,
'numberExercises': 2,
'restBetweenExercises': 15,
'restBetweenCycles': 60,
'exercises': _exercises,
'types': _types,
'quantities': _quantities,
});
setState(() {
helper.createRoutine(routine);
Navigator.pop(context);
});
}
}
Any idea what can I do to make it work? Thank you
Make it simple
use Navigator.pop() twice
so that the current class and old class in also removed
from the stack
and then use Navigator.push()
When you push a new Route, the old one still stays in the stack. The new route just overlaps the old one and forms like a layer above the old one. Then when you pop the new route, it will just remove the layer(new route) and the old route will be displayed as it was before.
Now you must be aware the Navigator.push() is an asynchronous method and returns a Future. How it works is basically when you perform a Navigator.push(), it will push the new route and will wait for it to be popped out. Then when the new route is popped, it returns a value to the old one and that when the future callback will be executed.
Hence the solution you are looking for is add a future callback like this after your Navigator.push() :
Navigator.push(context,
MaterialPageRoute(builder: (context) => SettingsScreen())
).then((value){setState(() {});}); /// A callback which is executed after the new route will be popped. In that callback, you simply call a setState and refresh the page.

Passing value to previous widget

I have simple form , inside it have CircularAvatar when this is pressed show ModalBottomSheet to choose between take picture from gallery or camera. To make my widget more compact , i separated it to some file.
FormDosenScreen (It's main screen)
DosenImagePicker (It's only CircularAvatar)
ModalBottomSheetPickImage (It's to show ModalBottomSheet)
The problem is , i don't know how to passing value from ModalBottomSheetPickImage to FormDosenScreen. Because value from ModalBottomSheetPickImage i will use to insert operation.
I only success passing from third Widget to second Widget , but when i passing again from second Widget to first widget the value is null, and i think the problem is passing from Second widget to first widget.
How can i passing from third Widget to first Widget ?
First Widget
class FormDosenScreen extends StatefulWidget {
static const routeNamed = '/formdosen-screen';
#override
_FormDosenScreenState createState() => _FormDosenScreenState();
}
class _FormDosenScreenState extends State<FormDosenScreen> {
String selectedFile;
#override
Widget build(BuildContext context) {
final detectKeyboardOpen = MediaQuery.of(context).viewInsets.bottom;
print('trigger');
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('Tambah Dosen'),
actions: <Widget>[
PopupMenuButton(
itemBuilder: (_) => [
PopupMenuItem(
child: Text('Tambah Pelajaran'),
value: 'add_pelajaran',
),
],
onSelected: (String value) {
switch (value) {
case 'add_pelajaran':
Navigator.of(context).pushNamed(FormPelajaranScreen.routeNamed);
break;
default:
}
},
)
],
),
body: Stack(
fit: StackFit.expand,
children: <Widget>[
SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
SizedBox(height: 20),
DosenImagePicker(onPickedImage: (file) => selectedFile = file),
SizedBox(height: 20),
Card(
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextFormFieldCustom(
onSaved: (value) {},
labelText: 'Nama Dosen',
),
SizedBox(height: 20),
TextFormFieldCustom(
onSaved: (value) {},
prefixIcon: Icon(Icons.email),
labelText: 'Email Dosen',
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 20),
TextFormFieldCustom(
onSaved: (value) {},
keyboardType: TextInputType.number,
inputFormatter: [
// InputNumberFormat(),
WhitelistingTextInputFormatter.digitsOnly
],
prefixIcon: Icon(Icons.local_phone),
labelText: 'Telepon Dosen',
),
],
),
),
),
SizedBox(height: kToolbarHeight),
],
),
),
Positioned(
child: Visibility(
visible: detectKeyboardOpen > 0 ? false : true,
child: RaisedButton(
onPressed: () {
print(selectedFile);
},
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
color: colorPallete.primaryColor,
child: Text(
'SIMPAN',
style: TextStyle(fontWeight: FontWeight.bold, fontFamily: AppConfig.headerFont),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
textTheme: ButtonTextTheme.primary,
),
),
bottom: kToolbarHeight / 2,
left: sizes.width(context) / 15,
right: sizes.width(context) / 15,
)
],
),
);
}
}
Second Widget
class DosenImagePicker extends StatefulWidget {
final Function(String file) onPickedImage;
DosenImagePicker({#required this.onPickedImage});
#override
DosenImagePickerState createState() => DosenImagePickerState();
}
class DosenImagePickerState extends State<DosenImagePicker> {
String selectedImage;
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.center,
child: InkWell(
onTap: () async {
await showModalBottomSheet(
context: context,
builder: (context) => ModalBottomSheetPickImage(
onPickedImage: (file) {
setState(() {
selectedImage = file;
widget.onPickedImage(selectedImage);
print('Hellooo dosen image picker $selectedImage');
});
},
),
);
},
child: CircleAvatar(
foregroundColor: colorPallete.black,
backgroundImage: selectedImage == null ? null : MemoryImage(base64.decode(selectedImage)),
radius: sizes.width(context) / 6,
backgroundColor: colorPallete.accentColor,
child: selectedImage == null ? Text('Pilih Gambar') : SizedBox(),
),
),
);
}
}
Third Widget
class ModalBottomSheetPickImage extends StatelessWidget {
final Function(String file) onPickedImage;
ModalBottomSheetPickImage({#required this.onPickedImage});
#override
Widget build(BuildContext context) {
return SizedBox(
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Wrap(
alignment: WrapAlignment.spaceEvenly,
children: <Widget>[
InkWell(
onTap: () async {
final String resultBase64 =
await commonFunction.pickImage(quality: 80, returnFile: ReturnFile.BASE64);
onPickedImage(resultBase64);
},
child: CircleAvatar(
foregroundColor: colorPallete.white,
backgroundColor: colorPallete.green,
child: Icon(Icons.camera_alt),
),
),
InkWell(
onTap: () async {
final String resultBase64 =
await commonFunction.pickImage(returnFile: ReturnFile.BASE64, isCamera: false);
onPickedImage(resultBase64);
},
child: CircleAvatar(
foregroundColor: colorPallete.white,
backgroundColor: colorPallete.blue,
child: Icon(Icons.photo_library),
),
),
],
),
),
);
}
}
The cleanest and easiest way to do this is through Provider. It is one of the state management solutions you can use to pass values around the app as well as rebuild only the widgets that changed. (Ex: When the value of the Text widget changes). Here is how you can use Provider in your scenario:
This is how your model should look like:
class ImageModel extends ChangeNotifier {
String _base64Image;
get base64Image => _base64Image;
set base64Image(String base64Image) {
_base64Image = base64Image;
notifyListeners();
}
}
Don't forget to add getters and setters so that you can use notifyListeners() if you have any ui that depends on it.
Here is how you can access the values of ImageModel in your UI:
final model=Provider.of<ImageModel>(context,listen:false);
String image=model.base64Image; //get data
model.base64Image=resultBase64; //set your image data after you used ImagePicker
Here is how you can display your data in a Text Widget (Ideally, you should use Selector instead of Consumer so that the widget only rebuilds if the value its listening to changes):
#override
Widget build(BuildContext context) {
//other widgets
Selector<ImageModel, String>(
selector: (_, model) => model.base64Image,
builder: (_, image, __) {
return Text(image);
},
);
}
)
}
You could achieve this easily. If you are using Blocs.

How to navigate to another page using the widgets generated from a 'for loop' in Flutter?

I'm testing out this Flutter project which generates a movie list from a for loop:for (int i = 0; i < 3; i++) {...}.
The result of the loop is 3 cards which I'd like to add an onTap function to and navigate to the corresponding page as a result.
Github:https://github.com/devefy/Flutter-Streaming-Service-App-UI/blob/master/lib/main.dart
After the padding on line 222, I added a ListTile() with the onTap:(){}
widget. This enabled the tap widget for the bottom half of the card.
// Line 219 to 222
Padding(
padding: EdgeInsets.only(top: 3.0),
child: Text(i == 0 ? "Season 2" : ""),
),// Padding
ListTile(onTap: (){
debugPrint('${[i]} was tapped!');
//Navigator.push(context, route)
My results when tapping the 3 cards.
flutter: [0] was tapped!
flutter: [1] was tapped!
flutter: [2] was tapped!
Where I get lost is how to actually navigate to the detail page of the movie depending on which card I tap on.
Any help is appreciated...Thank You All!!!
The best practice for something like this would be to create 2 pages, a movie list, and a details page.
The movie list will loop through all of the movies, then the on tap would point to the details page. The key here is that you can pass data to the details page when navigating. Whether that be an id or slug for the movie allowing you to fetch specific data or just an index to a list for a simpler example.
Navigator.push( context, MaterialPageRoute( builder: (context) => DetailScreen(todo: todos[index]),),);
Here is an example regarding a todo list and a details screen. I would try running this so you can understand further what I mean.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class Todo {
final String title;
final String description;
Todo(this.title, this.description);
}
void main() {
runApp(MaterialApp(
title: 'Passing Data',
home: TodosScreen(
todos: List.generate(
20,
(i) => Todo(
'Todo $i',
'A description of what needs to be done for Todo $i',
),
),
),
));
}
class TodosScreen extends StatelessWidget {
final List<Todo> todos;
TodosScreen({Key key, #required this.todos}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Todos'),
),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todos[index].title),
// When a user taps the ListTile, navigate to the DetailScreen.
// Notice that you're not only creating a DetailScreen, you're
// also passing the current todo through to it.
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(todo: todos[index]),
),
);
},
);
},
),
);
}
}
class DetailScreen extends StatelessWidget {
// Declare a field that holds the Todo.
final Todo todo;
// In the constructor, require a Todo.
DetailScreen({Key key, #required this.todo}) : super(key: key);
#override
Widget build(BuildContext context) {
// Use the Todo to create the UI.
return Scaffold(
appBar: AppBar(
title: Text(todo.title),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Text(todo.description),
),
);
}
}
Here is the app running.
Let me know if you have any questions!
Finally found a solution I was happy with. Thanks to flutter_ui_challenge.
import 'package:flutter/material.dart';
import 'package:flutter_youtube/flutter_youtube.dart';
class YouTubeVideoList extends StatelessWidget {
#override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text("YouTube Video List"),
backgroundColor: Colors.lightBlue,
elevation: 2,
actions: <Widget>[
Container(
padding: EdgeInsets.all(10),
)
],
),
body: Lists(),
);
}
}
class Item {
final String title;
final String category;
final String place;
final Function onTap;
final String image;
Item(
{this.title,
this.category,
this.place,
this.onTap,
this.image});
}
class Lists extends StatelessWidget {
final List<Item> _data = [
Item(
onTap: playYoutubeVideo1,
title: 'Gardens By the Bay',
category: "Gardens",
place: "Singapore",
image: "https://images.pexels.com/photos/672142/pexels-photo-672142.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940"),
Item(
onTap: playYoutubeVideo2,
title: 'Singapore Zoo',
category: "Parks",
place: "Singapore",
image: "https://images.pexels.com/photos/1736222/pexels-photo-1736222.jpeg?cs=srgb&dl=adult-adventure-backpacker-1736222.jpg&fm=jpg"),
Item(
onTap: playYoutubeVideo3,
title: 'National Orchid Garden',
category: "Parks",
place: "Singapore",
image: "https://images.pexels.com/photos/62403/pexels-photo-62403.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940"),
];
#override
Widget build(BuildContext context) {
return ListView.builder(
padding: EdgeInsets.all(6),
itemCount: _data.length,
itemBuilder: (BuildContext context, int index) {
Item item = _data[index];
return GestureDetector(
onTap: item.onTap,
child: Card(
elevation: 3,
child: Row(
children: <Widget>[
Container(
height: 125,
width: 110,
padding:
EdgeInsets.only(left: 0, top: 10, bottom: 70, right: 20),
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(item.image),
fit: BoxFit.cover)),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
item.title,
style: TextStyle(
color: Colors.deepOrange,
fontWeight: FontWeight.w700,
fontSize: 15),
),
Text(
item.category,
style: TextStyle(fontSize: 12, color: Colors.black87),
),
Text(
item.place,
style: TextStyle(fontSize: 10, color: Colors.black87),
),
SizedBox(
height: 10,
),
],
),
)
],
),
),
);
},
);
}
var youtube = new FlutterYoutube();
static playYoutubeVideo1() {
FlutterYoutube.playYoutubeVideoByUrl(
apiKey: "YOUR_API_KEY",
videoUrl: "YOUTUBE_VIDEO_URL",
);
}
static playYoutubeVideo2() {
FlutterYoutube.playYoutubeVideoByUrl(
apiKey: "YOUR_API_KEY",
videoUrl: "YOUTUBE_VIDEO_URL",
);
}
static playYoutubeVideo3() {
FlutterYoutube.playYoutubeVideoByUrl(
apiKey: "YOUR_API_KEY",
videoUrl: "YOUTUBE_VIDEO_URL",
);
}
}

onTap go to next list item (Flutter)

I have a ListView.builder showing a list, when i click on an item it shows details of that item on the next page (FlashcardDetailsPage).
I'd like to show the next list item when i tap the IconButton in the class FlashcardDetailsPage. So i'd like this button to skip to the next list item. Any ideas?
class FlashcardListView extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: allFlashcards.length,
itemBuilder: (context, int index) {
return ListTile(
title: Text(allFlashcards[index].actReg),
subtitle: Text(allFlashcards[index].question),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FlashcardDetailPage(
flashcardToShow: allFlashcards[index]),
),
);
},
);
});
}
}
class FlashcardDetailPage extends StatefulWidget {
final Flashcard flashcardToShow;
FlashcardDetailPage({Key key, #required this.flashcardToShow})
: super(key: key);
#override
_FlashcardDetailPageState createState() => _FlashcardDetailPageState();
}
class _FlashcardDetailPageState extends State<FlashcardDetailPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color.fromRGBO(242, 242, 242, 1),
appBar: AppBar(
centerTitle: true,
title: Text(widget.flashcardToShow.actReg),
),
body: Column(
children: <Widget>[
Container(
child: Card(
margin: EdgeInsetsDirectional.fromSTEB(20, 20, 20, 0),
child: Center(
child: Text(
widget.flashcardToShow.question,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 30),
),
)),
),
Container(
height: 100.0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
IconButton(
icon: Icon(Icons.skip_next),
iconSize: 32.0,
),
],
),
),
],
),
);
}
}
You could just replace the screen with another one showing the next card:
IconButton(
icon: Icon(Icons.skip_next),
iconSiz: 32,
onTap: () {
int currentIndex = allFlashcards.indexOf(widget.flashcardToShow);
if (currentIndex >= allFlashcards.length) return;
var nextFlashcard = allFlashcards[currentIndex + 1];
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (ctx) => FlashDetailsPage(flashcardToShow: nextFlashcard)
));
},
)
Thanks Marcel for the direction! I used your logic for a method. To avoid opening a new page every time I pressed the button, i did this & it's working:
void _skipFlashcard () {
setState(() {
int currentIndex = allFlashcards.indexOf(widget.flashcardToShow);
var nextFlashcard = allFlashcards[currentIndex + 1];
widget.flashcardToShow = nextFlashcard;
});
}
IconButton(
icon: Icon(Icons.skip_next),
iconSize: 32.0,
onPressed: _skipFlashcard,