I have model which implements ChangeNotifier
class DataModel with ChangeNotifier{
List<Data> data = List<Data>();
void addData(Data data){
data.add(data);
notifyListeners();
}
}
and a ListView which listens to those changes:
class DataListView extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<DataModel>(
builder: (context, model, child) {
return ListView.builder(
itemCount: model.data.length,
itemBuilder: (context, index) {
return Text(model.data[index].value);
},
);
},
);
}
}
so far so good, when an item is added to the list in the model, the change notification triggers a rebuild of the Listview and I see the new data. But I cant wrap my head around using this with a AnimatedList instead of a ListView. Preferably id like to keep my model as it is, seeing as the animation is a concern of the ui and not of my logic.
The changenotifier always gives me a uptodate version of my data, but what i really need is a "item added" or "item removed" notification.
Is there a best practice way of doing this?
This is the result of my trial.
It's a riverpod version, but I think it's the same for providers.
There are two points.
Initialize the state in the parent widget of the widget that uses
AnimatedList.
Add / delete AnimatedList and add / delete states asynchronously by using async.
main.dart
import 'package:animatedlist_riverpod_sample/provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:hooks_riverpod/all.dart';
void main() {
runApp(ProviderScope(child: 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: Home(),
);
}
}
class Home extends HookWidget {
const Home({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final todoList = useProvider(todoListProvider.state);
return Scaffold(appBar: AppBar(title: Text('Todo[${todoList.length}]')), body: TodoListView());
}
}
class TodoListView extends HookWidget {
TodoListView({Key key}) : super(key: key);
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
final todoList = useProvider(todoListProvider.state);
#override
Widget build(BuildContext context) {
return AnimatedList(
key: _listKey,
initialItemCount: todoList.length,
itemBuilder: (context, index, animation) =>
_buildItem(todoList[index], animation, index, context),
);
}
Slidable _buildItem(Todo todo, Animation<double> animation, int index, BuildContext context) {
return Slidable(
actionPane: SlidableDrawerActionPane(),
child: SizeTransition(
sizeFactor: animation,
axis: Axis.vertical,
child: ListTile(title: Text(todo.description), subtitle: Text(todo.id), onTap: () => {})),
secondaryActions: <Widget>[
IconSlideAction(
caption: 'Delete',
color: Colors.red,
icon: Icons.delete,
onTap: () {
_listKey.currentState.removeItem(
index, (context, animation) => _buildItem(todo, animation, index, context),
duration: Duration(milliseconds: 200));
_removeItem(context, todo);
},
),
],
);
}
void _removeItem(BuildContext context, Todo todo) async {
await Future.delayed(
Duration(milliseconds: 200), () => context.read(todoListProvider).remove(todo));
}
}
provider.dart
import 'package:hooks_riverpod/all.dart';
final todoListProvider = StateNotifierProvider<TodoList>((ref) {
return TodoList([
Todo(id: '0', description: 'Todo1'),
Todo(id: '1', description: 'Todo2'),
Todo(id: '2', description: 'Todo3'),
]);
});
class Todo {
Todo({
this.id,
this.description,
});
final String id;
final String description;
}
class TodoList extends StateNotifier<List<Todo>> {
TodoList([List<Todo> initialTodos]) : super(initialTodos ?? []);
void add(String description) {
state = [
...state,
Todo(description: description),
];
}
void remove(Todo target) {
state = state.where((todo) => todo.id != target.id).toList();
}
}
sample repository is here.
I recently started to learn Flutter and was surprised to find that this topic isn't covered properly anywhere. I came up with two approaches which I called Basic and Advanced. Let's start from Basic. It's named like that because Provider is called within the same widget where AnimatedList is built.
class Users extends ChangeNotifier {
final _list = ['0', '1', '2', '3', '4'];
int get length => _list.length;
operator [](index) => _list[index];
int add() {
final int index = length;
_list.add('$index');
notifyListeners();
return index;
}
String removeAt(int index) {
String user = _list.removeAt(index);
notifyListeners();
return user;
}
}
class BasicApp extends StatelessWidget {
const BasicApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ChangeNotifierProvider(create: (_) => Users(), child: AnimatedListDemo()));
}
}
class AnimatedListDemo extends StatelessWidget {
final GlobalKey<AnimatedListState> _listKey = GlobalKey();
AnimatedListDemo({Key? key}) : super(key: key);
void addUser(Users users) {
final int index = users.add();
_listKey.currentState!.insertItem(index, duration: const Duration(seconds: 1));
}
void deleteUser(Users users, int index) {
String user = users.removeAt(index);
_listKey.currentState!.removeItem(
index,
(context, animation) {
return SizeTransition(sizeFactor: animation, child: _buildItem(users, user));
},
duration: const Duration(seconds: 1),
);
}
Widget _buildItem(Users users, String user, [int? removeIndex]) {
return ListTile(
key: ValueKey<String>(user),
title: Text(user),
leading: const CircleAvatar(
child: Icon(Icons.person),
),
trailing: (removeIndex != null)
? IconButton(
icon: const Icon(Icons.delete),
onPressed: () => deleteUser(users, removeIndex),
)
: null,
);
}
#override
Widget build(BuildContext context) {
Users users = Provider.of<Users>(context, listen: false);
return Scaffold(
appBar: AppBar(
title: const Text('Basic AnimatedList Provider Demo'),
),
body: AnimatedList(
key: _listKey,
initialItemCount: users.length,
itemBuilder: (context, index, animation) {
return FadeTransition(
opacity: animation,
child: _buildItem(users, users[index], index),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => addUser(users),
tooltip: 'Add an item',
child: const Icon(Icons.add),
),
);
}
}
Advanced approach differs in that it encapsulates AnimatedListState. I took this idea from Flutter's AnimatedList docs.
typedef RemovedItemBuilder = Widget Function(
String user, BuildContext context, Animation<double> animation);
class Users extends ChangeNotifier {
final _list = ['0', '1', '2', '3', '4'];
final GlobalKey<AnimatedListState> _listKey = GlobalKey();
final RemovedItemBuilder _removedItemBuilder;
Users(this._removedItemBuilder);
int get length => _list.length;
operator [](index) => _list[index];
GlobalKey<AnimatedListState> get listKey => _listKey;
int add() {
final int index = length;
_list.add('$index');
_listKey.currentState!.insertItem(index, duration: const Duration(seconds: 1));
notifyListeners();
return index;
}
String removeAt(int index) {
String user = _list.removeAt(index);
_listKey.currentState!.removeItem(
index,
(BuildContext context, Animation<double> animation) {
return _removedItemBuilder(user, context, animation);
},
duration: const Duration(seconds: 1),
);
notifyListeners();
return user;
}
}
class AdvancedApp extends StatelessWidget {
const AdvancedApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(home: AnimatedListDemo());
}
}
class AnimatedListDemo extends StatelessWidget {
const AnimatedListDemo({Key? key}) : super(key: key);
Widget _buildItem(BuildContext context, String user, [int? removeIndex]) {
Users users = Provider.of<Users>(context, listen: false);
return ListTile(
key: ValueKey<String>(user),
title: Text(user),
leading: const CircleAvatar(
child: Icon(Icons.person),
),
trailing: (removeIndex != null)
? IconButton(
icon: const Icon(Icons.delete),
onPressed: () => users.removeAt(removeIndex),
)
: null,
);
}
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(create: (_) => Users((user, context, animation) {
return SizeTransition(sizeFactor: animation, child: _buildItem(context, user));
}), child: Scaffold(
appBar: AppBar(
title: const Text('Advanced AnimatedList Provider Demo'),
),
body: Consumer<Users>(builder: (BuildContext context, Users users, _){
return AnimatedList(
key: users.listKey,
shrinkWrap: true,
initialItemCount: users.length,
itemBuilder: (context, index, animation) {
return FadeTransition(
opacity: animation,
child: _buildItem(context, users[index], index),
);
},
);
}),
floatingActionButton: const AddButtonSeparateWidget(),
));
}
}
class AddButtonSeparateWidget extends StatelessWidget {
const AddButtonSeparateWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
Users users = Provider.of<Users>(context, listen: false);
return FloatingActionButton(
onPressed: users.add,
tooltip: 'Add an item',
child: const Icon(Icons.add),
);
}
}
All code is published on Github. Now I want to elaborate a bit on your proposition of having "item added" or "item removed" notifications. From what I understand it goes against Flutter's philosophy where widget is a UI config. When a widget's state changes, Flutter diffs against its previous state and magically applies the diff to UI. That's why I didn't use "item added", "item removed" notifications in my implementations. However I think it should be possible to do because I saw a similar approach in Firestore subscription to document changes although for now I can't figure how to implement the same with Provider. Provider's documentation is kind of poor. After a careful reading I can't say how to implement partial updates with Provider. May be ProxyProvider with its update could help or may be ListenableProvider. Let me know if you could find the solution to your proposition.
Related
I am trying to use Flutter Riverpod together with the Date Range Picker.
I have a list of Trips which when clicked on, open up the Trip Page.
On the trip page, the user can view the start / end dates
They can then click on a button, which brings up the Date Range Picker widget
I want the user to be able to change the date range, and this be reflected on the screen (and eventually saved back against the Trip object)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Riverpod DateRange Picker',
home: HomeScreen(),
);
}
}
class Trip {
final String name;
final DateTimeRange dateTimeRange;
Trip(this.name, this.dateTimeRange);
}
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
Set<Trip> trips = {
Trip('Test 1',
DateTimeRange(start: DateTime(2023, 1, 3), end: DateTime(2023, 1, 6)))
};
return Scaffold(
appBar: AppBar(title: const Text('Riverpod DateRange Picker')),
body: ListView(
children: [
for (final trip in trips)
ListTile(
title: Text(trip.name),
onTap: () {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (BuildContext context) =>
TripScreen(trip: trip)));
},
),
],
),
);
}
}
class TripScreen extends ConsumerWidget {
const TripScreen({Key? key, required this.trip}) : super(key: key);
final Trip trip;
#override
Widget build(BuildContext context, WidgetRef ref) {
void _showDateRangePicker() async {
final DateTimeRange? result = await showDateRangePicker(
context: context,
locale: const Locale('en', 'GB'),
initialDateRange: trip.dateTimeRange,
saveText: 'Done',
firstDate: DateTime(2022, 1, 1),
lastDate: DateTime(2030, 12, 31),
);
if (result != null) {
print(result);
}
}
return WillPopScope(
onWillPop: () {
Navigator.pop(context);
return Future.value(false);
},
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text(trip.name),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
IconButton(
icon: const Icon(Icons.calendar_month),
onPressed: () {
_showDateRangePicker();
},
),
Text(
'Start: ${DateFormat('dd/MM/yyyy').format(trip.dateTimeRange.start)}'),
Text(
'End: ${DateFormat('dd/MM/yyyy').format(trip.dateTimeRange.end)}'),
],
))));
}
}
Any help would be much appriciated.
Thanks.
I've created this using Provider but I would like to use Riverpod.
I created "tripsProvider" instance of StateProvider that holds all trip data, like in your case only one value. Home screen watching that provider and displaying data in ListView, when user select item in list I pass index of item and watch that single item on new screen (copy of yours TripScreen).
When user update the data of single item I update that list, so update is available on TripScreen and HomeScreen. I also still learning riverpod but hope my answer can help you.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}
final tripsProvider = StateProvider<List<Trip>>(
(ref) => [
Trip(
'Test 1',
DateTimeRange(start: DateTime(2023, 1, 3), end: DateTime(2023, 1, 6)),
),
],
);
class Trip {
final String name;
final DateTimeRange dateTimeRange;
const Trip(this.name, this.dateTimeRange);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Riverpod DateRange Picker',
home: HomeScreen(),
);
}
}
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
final trips = ref.watch(tripsProvider);
return Scaffold(
appBar: AppBar(title: const Text('Riverpod DateRange Picker')),
body: ListView.builder(
itemCount: trips.length,
itemBuilder: (context, index) {
Trip trip = trips[index];
return ListTile(
title: Text(trip.name),
onTap: () {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (BuildContext context) => TripScreen(index: index),
),
);
},
);
},
),
);
}
}
class TripScreen extends ConsumerWidget {
int index;
TripScreen({Key? key, required this.index}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final trip = ref.watch(tripsProvider)[index];
void _showDateRangePicker() async {
final DateTimeRange? result = await showDateRangePicker(
context: context,
locale: const Locale('en', 'GB'),
initialDateRange: trip.dateTimeRange,
saveText: 'Done',
firstDate: DateTime(2022, 1, 1),
lastDate: DateTime(2030, 12, 31),
);
if (result != null) {
print(result);
Trip updatedTrip = Trip(trip.name, DateTimeRange(start: result.start, end: result.end));
ref.read(tripsProvider.notifier).update(
(state) {
List<Trip> updatedList = [];
for (int i = 0; i < state.length; i++) {
if (i == index) {
updatedList.add(updatedTrip);
} else {
updatedList.add(state[i]);
}
}
return updatedList;
},
);
}
}
return WillPopScope(
onWillPop: () {
Navigator.pop(context);
return Future.value(false);
},
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text(trip.name),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
IconButton(
icon: const Icon(Icons.calendar_month),
onPressed: () {
_showDateRangePicker();
},
),
Text('Start: ${DateFormat('dd/MM/yyyy').format(trip.dateTimeRange.start)}'),
Text('End: ${DateFormat('dd/MM/yyyy').format(trip.dateTimeRange.end)}'),
],
),
),
),
);
}
}
I am trying to get a new historyTile() to be called to the Scaffold() each second. I am unsure how to make the void function connect.
Any advice and feedback is appreciated!
Code:
class activityTab extends StatefulWidget {
const activityTab({Key? key}) : super(key: key);
#override
State<activityTab> createState() => _activityTabState();
}
class _activityTabState extends State<activityTab> {
#override
void historyTile() {
final now = DateTime.now();
String tileTime = DateFormat.yMMMMd().add_jm().format(now);
ListView.builder(
shrinkWrap: true,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.backup_outlined),
title: Text('Synced my_script.pdf with the cloud.'),
subtitle: Text('${tileTime}'),
tileColor: Colors.greenAccent,
);
}
);
}
#override
void initState() {
super.initState();
Timer.periodic(Duration(seconds: 1), (Timer t) => historyTile());
}
#override
Widget build(BuildContext context) {
return Scaffold(
body:
Container(
child: SingleChildScrollView(
child: historyTile(); // ERROR HERE
),
),
);
}
}
You can try creating periodic streams with a Stream Builder widget. If not, the simplest method is to put your widget in scaffold and try calling the setState function periodically with a 1-second timer.
In the StreamBuilder example you should change the widget a bit. Sending the parameter you want to update to the widget from outside will add a little more flexibility to you.
return Scaffold(
body: StreamBuilder<String>(
stream: Stream.periodic(const Duration(seconds: 1), (x) {
// Your Action Here
final now = DateTime.now();
return DateFormat.yMMMMd().add_jm().format(now);
}),
builder: (context, snapshot) {
String param = "";
if (snapshot.hasData) param = snapshot.data!;
return _historyTile(txt = param);
}
),
);
Or you could use your widget in Scaffold Body and periodically set the widgets state in timer callback.
class _activityTabState extends State<activityTab> {
String tileTime = "";
...
Timer.periodic(Duration(seconds: 1), () {
setState(() {
final now = DateTime.now();
tileTime = DateFormat.yMMMMd().add_jm().format(now);
});
};
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: SingleChildScrollView(
child: historyTile(tileName);
),
),
);
}
or just
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: SingleChildScrollView(
child: ListTile(
leading: Icon(Icons.backup_outlined),
title: Text('Synced my_script.pdf with the cloud.'),
subtitle: Text('$tileTime'),
tileColor: Colors.greenAccent,
),
),
),
);
}
Create your historyTile widget as a custom widget
class HistoryTile extends StatefulWidget {
const HistoryTile({Key? key, required this.txt}) : super(key: key);
final String txt;
#override
State<HistoryTile> createState() => _HistoryTileState();
}
class _HistoryTileState extends State<HistoryTile> {
#override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(Icons.backup_outlined),
title: Text('Synced my_script.pdf with the cloud.'),
subtitle: Text(widget.txt),
tileColor: Colors.greenAccent,
);
}
}
there is some issues in you ListView.Builder. You do not put itemCount there. And you need to use setState in timer. So codes are below. Please check.
class activityTab extends StatefulWidget {
const activityTab({Key? key}) : super(key: key);
#override
State<activityTab> createState() => _activityTabState();
}
class _activityTabState extends State<activityTab> {
String _now;
Timer _everySecond;
#override
historyTile() {
final now = DateTime.now();
String tileTime = DateFormat.yMMMMd().add_jms().format(now);
return ListView.builder(
shrinkWrap: true,
itemCount: 1,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.backup_outlined),
title: Text('Synced my_script.pdf with the cloud.'),
subtitle: Text('${tileTime}'),
tileColor: Colors.greenAccent,
);
});
}
void _timer() {
Future.delayed(Duration(seconds: 1)).then((_) {
setState(() {
_timer();
});
});
}
#override
void initState() {
super.initState();
_timer();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
height: 500,
child: SingleChildScrollView(
child: historyTile(),
),
),
);
}
}
I am trying to make my list animated, so when i delete task it plays animation. I watched few tutorials however i am not sure how to implement this into my code because they had the animatedlist and void deleteitem in the same script, where i am having in 2 different one.
Here is my code from the task_data script file
void removeItem(Task task) {
//removedTask = task;
final item = _tasks.remove(task);
listKey.currentState!.removeItem(
task,
(context, animation) => TaskTile(
//taskTitle: task.name,
//isChecked: task.isDone,
//checkboxCallback: (checkboxState) {
//taskData.updateTask(task);
taskTitle: task.name,
isChecked: task.isDone,
animation: animation, checkboxCallback: (bool) {},
longPressCallback: () {},
));
notifyListeners();
saveData();
}
Here is my code from task_tile script
class TaskTile extends StatelessWidget {
final bool isChecked;
final String taskTitle;
final Function(bool?) checkboxCallback;
final VoidCallback longPressCallback;
final Animation<double> animation;
TaskTile({
required this.isChecked,
required this.taskTitle,
required this.checkboxCallback,
required this.longPressCallback,
required this.animation,
});
And here is my code from tasks_list script file
#override
Widget build(BuildContext context) {
return Consumer<TaskData>(
builder: (context, taskData, child) {
return AnimatedList(
key: _listKey,
initialItemCount: taskData.tasks.length,
itemBuilder: (context, index, animation) {
return TaskTile(
animation: animation,
taskTitle: taskData.tasks[index].name,
//isChecked: Provider.of<TaskData>(context).tasks[index].isDone,
//Provider.of<TaskData>(context).tasks = task_data. we would use LHS when we did not wrap with Consumer
isChecked: taskData.tasks[index].isDone,
checkboxCallback: (checkboxState) {
HapticFeedback.mediumImpact();
taskData.updateTask(taskData.tasks[index]);
},
longPressCallback: () {
ScaffoldMessenger.of(context).showSnackBar(snackBar(taskData));
taskData.deleteTask(taskData.tasks[index]);
HapticFeedback.heavyImpact();
},
);
},
//itemCount: taskData.taskCount,
);
},
);
Would really appreciate if someone can help me with this!
EDIT
--- I am getting this error right now "The argument type 'Task' can't be assigned to the parameter type 'int'."
on the actual screen it displays red box range error and I'm not sure how to fix those
Use it as a global key to access the keys from other classes
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
//Create it as Global key
final myListKey = GlobalKey<AnimatedListState>();
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, required this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final widgets = [
Container(color: Colors.red),
Container(color: Colors.green),
Container(color: Colors.yellow),
];
int currentIndex = 0;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: AnimatedList(
key: myListKey,
initialItemCount: widgets.length,
itemBuilder: (_, index, animation) {
return Container(
height: 100,
child: widgets[index],
);
},
),
);
}
}
I am new to flutter and I would like to add element every 5 seconds to my list view. I have list view and I think I have the true adding method. However, I do not know how to update my list view every 5 seconds.
void randomCity(){
List <int> colors = [yellow,green,blue,red,black,white];
List <String> countryNames = ["Gdańsk","Warszawa","Poznań","Białystok","Wrocław","Katowice","Kraków"];
List <String> countryImages = [gdanskPic,warszawaPic,poznanPic,bialystokPic,wroclawPic,katowicePic,krakowPic];
Random random = new Random();
DateTime now = new DateTime.now();
Future.delayed(Duration(seconds: 5), (){
setState(() {
int randomCity = random.nextInt(countryNames.length);
int randomColor = random.nextInt(colors.length);
countrylist.add(Country(
countryNames[randomCity], countryImages[randomCity],
colors[randomColor], now.toString()));
});
});
}
In this code I am adding new element to my list view.
randomCity();
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
backgroundColor: Colors.grey[100],
elevation: 0.0,
title: Text(
"Random City App",
style: TextStyle(fontSize: 20.0, color: Colors.black),
),
centerTitle: true,
actions: <Widget>[
IconButton(
icon: Icon(
Icons.add,
color: Colors.black,
size: 32,
),
onPressed: () {})
],
),
body: ListView.builder(
itemCount: countrylist.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => CountryDetails(countryName: countrylist[index].name,
appBarColor: countrylist[index].color, date: countrylist[index].date, image: countrylist[index].image,))
);
},
title: Text(countrylist[index].name + " ${countrylist[index].date}"),
tileColor: Color(countrylist[index].color),
),
);
},
));
}
And this is my ListView.Builder.
You have to convert your widget into StatefulWidget and then rebuild it with setState (more info on ways to manage state https://flutter.dev/docs/development/data-and-backend/state-mgmt/options)
class MyApp extends StatelessWidget { // your main widget
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: MyWidget(),
),
);
}
}
class MyWidget extends StatefulWidget { // create new StatefulWidget widget
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
List<Country> countrylist = []; // mover other variables in here
...
void randomCity(){
...
setState(() {}); // this will rebuild your widget again and again
}
#override
Widget build(BuildContext context) {
Future.delayed(Duration(seconds: 5), (){
randomCity();
});
return ListView.builder(
itemCount: countrylist.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
onTap: () {},
title: Text(countrylist[index]),
),
);
},
);
}
}
You have to tell the ListView to rebuild which you can do with the setState method (if you are using a StefulWidget). Also, I would use Timer instead of Future.delayed for periodic updates. Here would be a simplified example of your usecase:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Timer timer;
final countryNames = ['Germany', 'Croatia', 'Turkey', 'USA'];
List<String> countryList = [];
#override
void initState() {
Timer.periodic(Duration(seconds: 5), (timer) {
int randomCity = Random().nextInt(countryNames.length);
countryList.add(countryNames[randomCity]);
setState(() {});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('List Updater'),
),
body: ListView.builder(
itemBuilder: (context, index) {
return Card(
child: Text(countryList[index]),
);
},
itemCount: countryList.length,
),
);
}
#override
void dispose() {
timer?.cancel();
super.dispose();
}
}
I try to create a basic notes app to study about flutter and I do not quite understand how to notify my NotesContainer that the button has been pressed. I tried to create a ref to it but the adding function is in the state class that I'm not sure how to reach.
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
final NotesContainer Notes = new NotesContainer();
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('My Notes'),
backgroundColor: Color.fromRGBO(223, 175, 117, 1),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: (){
Notes.add()
},
)
],
),
body: Notes
);
}
}
class NoteData{
String title;
String content;
NoteData(this.title, this.content);
NoteData.noContent(t){
title = t;
content ='';
}
}
class NotesContainer extends StatefulWidget{
#override
State<StatefulWidget> createState(){
return new _NotesContainer();
}
}
class _NotesContainer extends State<NotesContainer>{
final _notes = <NoteData>[new NoteData('title','thing to do'), new NoteData('title2','thing to do2')];
void add({String title='1'}){ //just to test adding
setState(() {
_notes.add(new NoteData.noContent(title));
});
}
Widget build(BuildContext context){
return _buildNotesContainer();
}
_buildNotesContainer(){
return new ListView.separated(
itemCount: _notes.length,
separatorBuilder: (BuildContext context, int index) => Divider(),
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(_notes[index].title),
);
},
padding: const EdgeInsets.all(10.0),
);
}
}
I guess the solution is somehow exposing the _function in the _NotesContainer via the stateful NotesContainer class. I wonder if there is a more elegant solution for this.
Thanks, Or
I think it makes more sense delegating the responsibility of adding a element further up in the widget tree. I modified your code to show how this works.
However, if you eventually get a deep widget tree and the children widgets require the _notes list, then I would recommend that you look into using a Inherited widget and add the _notes list to it, so you can access it without passing the state around too much.
import 'package:flutter/material.dart';
// Note the name change
class NotesPage extends StatefulWidget {
#override
_NotesPageState createState() => _NotesPageState();
}
class _NotesPageState extends State<NotesPage> {
final List<NoteData> _notes = <NoteData>[NoteData('title','thing to do'), NoteData('title2','thing to do2')];
void add({String title='1'}){ //just to test adding
setState(() {
_notes.add(NoteData.noContent(title));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('My Notes'),
backgroundColor: Color.fromRGBO(223, 175, 117, 1),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: (){
add();
},
)
],
),
body: NotesContainer(notes: _notes)
);
}
}
class NoteData{
String title;
String content;
NoteData(this.title, this.content);
NoteData.noContent(t){
title = t;
content ='';
}
}
class NotesContainer extends StatelessWidget{
final List<NoteData> notes;
const NotesContainer({Key key, this.notes}) : super(key: key);
#override
Widget build(BuildContext context){
return ListView.separated(
itemCount: notes.length,
separatorBuilder: (BuildContext context, int index) => Divider(),
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(notes[index].title),
);
},
padding: const EdgeInsets.all(10.0),
);
}
}
Hope it helps :-)