I’m using the gsheets package to asynchronously read and write data to a Google spreadsheet. I click a button on an AppBar()
on the home page to add a row to the database. The row is successfully added to the spreadsheet. Here is the code:
// import statements
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool viewNotebooks = false;
bool viewList = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leadingWidth: 650,
leading: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
// code irrelevant to this question, then
TextButton.icon(
icon: const Icon(Icons.note_add_outlined),
label: const Text("New Note"),
onPressed: () async {
await Data.addNote(
title: 'New Note', content: '[content goes here]');
setState(() {});
}),
// more code
],
),
),
body: Row(
children: [
// more code
viewList
? const Expanded(
flex: 1,
child: NotesListView(),
)
: Container(),
// more code
],
),
);
}
}
A ListView.builder()
should then rebuild to include the new row’s data in its items. Here is the relevant code, in a separate .dart file:
import 'package:flutter/material.dart';
import '../providers/sheets_api.dart';
class NotesListView extends StatefulWidget {
const NotesListView({super.key});
@override
State<NotesListView> createState() => _NotesListViewState();
}
class _NotesListViewState extends State<NotesListView> {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: Data.getRows(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasData) {
return ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(snapshot.data![index][1]),
subtitle: Text(snapshot.data![index][2]),
onTap: () async {
var id = snapshot.data![index][0];
// print(await Data.getNoteById(id));
});
},
);
} else {
return const Center(
// render the loading indicator
child: CircularProgressIndicator(),
);
}
},
);
}
}
The widget, however, does not rebuild when the spreadsheet content changes. But when I put the code in the app’s main.dart
file, even in a widget outside _NotesListViewState
, the UI does rebuild as expected.
Widget notesListView() {
return FutureBuilder (
// rest of code
I’m struggling to understand what’s going on. I’ve read about BuildContexts and widget trees, and about keys, and I assume it’s something about the BuildContext for NotesListView
being outside the main app’s widget tree? But I don’t really understand and, if that’s the case, I don’t know how to have NotesListView
be separate (with its own BuildContext) and yet still be seen within the main app’s context. I know I could use something like Provider
to control when widgets rebuild, or just keep the custom notesListView
widget in HomePage, but I’d like to understand what’s going on here.
2
Answers
I assume that the reason is
const
word here:As an option, you could add key:
It will rebuild widget every time on parent rebuild. Or you could override method
didUpdateWidget()
in theNotesListView
and call future :when parent widget rebuilds.
Flutter will rebuild the affected widgets when it decides that a change has been made. For widget state changes you use, e.g.
within a stateful widget to notify Flutter that you are changing state and that it should rebuild the widget tree for affected widgets. It is worth mentioning that Flutter will compare the current value of myState in the widget tree and the new value and will only rebuild if they differ.
Crucially, it is Flutter which decides whether a rebuild is required. You may have called setState() but you didn’t change any local state within that method. Flutter would have analysed the widget tree and decided that there were no changes. It is not enough to have made changes to your data source in the way that you have. In general terms, your data source or provider needs to tell Flutter that it has changed by way of a notification which mutates inherited (think global) state. This can be done manually with InheritedWidget, or you can use packages like provider or riverpod or get. These packages handle this reactive communication for you so you don’t have to do it manually. In riverpod for example you make a watch() call at the top of the build() method of a widget which says ‘when this data source changes I want you to rebuild’. You then mutate your data source via dedicated mutation methods, which then kick off the notification to Flutter to rebuild. I think you need to plump for one of these packages for an easy life. I went for riverpod which has a bit of a learning curve, but nothing insurmountable. Once you understand this reactive component, which is really crucial, then everything should fall into place.