I'm trying to get my head around how I should manage reading data from a database in Flutter (or any API/backend).
Without a database and just using state that's local to the widget I'd expect to write something similar to the following. I am printing out a list of strings in a ListView
and adding a new one when a FloatingActionButton
is pressed.
class _ListScreenState extends State<ListScreen> {
var _myStrings = ["string1", "string2"];
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: _myStrings.length,
itemBuilder: (BuildContext context, int index) {
return Text(_myStrings[index]);
}
),
floatingActionButton: FloatingActionButton(
onPressed: () => {
setState(() {
_myStrings.add("another string");
});
},
child: const Icon(Icons.add),
),
);
}
}
However, when reading from a database, I've extracted the database logic behind a repository class and so I'm using a FutureBuilder.
class _ListScreenState extends State<ListScreen> {
final _repo = FooRepository();
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: _repo.getAll(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.data == null) {
return const Center(child: Text("Loading..."));
}
return ListView<String, String>(
elements: snapshot.data,
itemBuilder: (context, element) {
return Text(element);
}
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => {
_repo.add("newstring");
},
child: const Icon(Icons.add),
),
);
}
}
This seems to go against the idea of the flutter widget being reactive to changes in its local state. Am I doing something wrong here? I don't know whether calling setState()
will cause the FutureBuilder
to redraw but it also seems wasteful to re-get all the records from the database when one is added. Should/can I hold a copy of the strings locally to the widget and add to both the database but also the local list?
When using a FutureBuilder
you should not need to use setState
, as the actual update of the state, based on the Future's response, will be managed by the FutureBuilder
itself.
The problem here is that FutureBuilder
is used when you want to retrieve some data once, but from your example, you would like that once you press your button and a new String
is added to the Database
, that the widget displays reactively the new data.
In this case probably you want to use a StreamBuilder
, which is very similiar to a FutureBuilder
, but it's used when you want to display some data that can change over time.
With your example, using a FutureBuilder
, when you add some data to the Database
the only way to get the new data would be to refetch it via the _repo.getAll()
method.
This can work if it's what you want, you simply can force a rebuild once the button is pressed, and the future
will run again.
To answer your final question, if you want you can manage your state locally, in this case, I would suggest to refactor your code, and add some state management logic, for example a simple Provider
.
There you can initialize your data by calling once _repo.getAll()
, and in your Widget, you would not use anymore a FutureBuilder
or StreamBuilder
, but you would listen to the changes coming from the Provider
, via a Consumer
, then when your button is pressed, the Provider
can add that piece of data to the object
and your UI
would see immediately the changes.
I can provide you a simple example:
your provider, that manages the state:
class YourDataProvider extends ChangeNotifier {
bool isLoading = true;
List<String> myData = [];
Future<void> getAll() async {
isLoading = true;
notifyListeners();
myData = await yourAsyncCall();
isLoading = false;
notifyListeners();
}
Future<void> add(String value) async {
isLoading = true;
notifyListeners();
await yourAddAsyncCall(value);
myData.add(value);
notifyListeners();
}
}
your UI widget
.....
@override
void initState() {
context.read<YourDataProvider>.getAll();
super.initState();
}
class _ListScreenState extends State<ListScreen> {
@override
Widget build(BuildContext context) {
YourDataProvider provider = context.watch<YourDataProvider>;
return Scaffold(
body: provider.isLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: provider.myData.length,
itemBuilder: (context, index) => Text(provider.myData[index]),
)
),
floatingActionButton: FloatingActionButton(
onPressed: () => provider.add("newstring"),
child: const Icon(Icons.add),
),
);
}
}
As you can see, the Provider
will fetch once the data, and then it will hold the state, and once a new String
is added, it will only submit it to the backend, but it won't be refetching all the data again.