Force a widget tree build using a Hook Widget in Flutter - flutter

I have a page that dynamically accepts a future list and a callback to get the future list to receive data and be able to refresh it through on refresh. a simplified version looks like this:
class ListSearchPage<T> extends StatefulWidget {
final Future<List<T>> itemsFuture;
final ValueGetter<Future<List<T>>> getItemsFuture;
const ListSearchPage({Key key, this.getItemsFuture, this.itemsFuture})
: super(key: key);
#override
_ListSearchPageState createState() => _ListSearchPageState();
}
class _ListSearchPageState<T> extends State<ListSearchPage> {
Future<List<T>> itemsFuture;
TextEditingController _controller;
#override
void initState() {
itemsFuture = widget.itemsFuture;
_controller = TextEditingController();
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future:
itemsFuture != null ? itemsFuture : widget.getItemsFuture(),
builder: (context, snapshot) {
return RefreshIndicator(
onRefresh: () async {
setState(() {
itemsFuture = null;
_controller.text = '';
});
},
child: ...
);
});
}
}
So the first time, the page loads with the future already loaded. when the user refreshes, I mark the future as null so the callback gets called and the data can be re-fetched.
I'm trying to implement flutter_hooks throughout the app now and I've refactored this widget to be like this (simplified version):
class ListSearchPage<T> extends HookWidget {
final Future<List<T>> itemsFuture;
final ValueGetter<Future<List<T>>> getItemsFuture;
const ListSearchPage({Key key, this.getItemsFuture, this.itemsFuture})
: super(key: key);
#override
Widget build(BuildContext context) {
final itemsFutureNotifier = useState(this.itemsFuture);
final TextEditingController _controller = useTextEditingController();
return FutureBuilder(
future:
itemsFutureNotifier.value != null ? itemsFutureNotifier.value : getItemsFuture(),
builder: (context, snapshot) {
return RefreshIndicator(
onRefresh: () async {
itemsFutureNotifier.value = null;
_controller.text = '';
},
child: ...
);
});
}
}
This works the first time, however after that the value keeps on getting assigned to null, and therefore the value notifier does not get notified about the change. How can I force the widget to rebuild in this case like before? and as a bonus, do you see a better solution for this?
Thanks in advance.
update
This is itemsFuture
final future = useMemoized(() => repository.fetchData());
This is getItemsFuture
() => repository.fetchData()
The idea behind it is to fetch the data before the search page is opened. In my use case works.
I've found a solution to my problem, but I won't post it as an answer because I don't believe is clean and I rather see if someone finds the proper way of doing it.
current solution
#override
Widget build(BuildContext context) {
// feels like a dirty solution for rebuilding on refresh
final counterNotifier = useState(0);
final itemsFutureNotifier = useState(this.itemsFuture);
final TextEditingController _controller = useTextEditingController();
return ValueListenableBuilder(
valueListenable: counterNotifier,
builder: (context, value, child) {
return FutureBuilder(
future:
itemsFutureNotifier.value != null ? itemsFutureNotifier.value : getItemsFuture(),
builder: (context, snapshot) {
return RefreshIndicator(
onRefresh: () async {
counterNotifier.value++;
itemsFutureNotifier.value = null;
_controller.text = '';
},
child: ...
);
});
});
As you can see I now have a counter notifier that will actually rebuild the ValueListenableBuilder and will make the FutureBuilder fetch the data

I think itemsFuture is not necessary to set to null (because it can be a initial statement inside useState).
#override
Widget build(BuildContext context) {
final fetchData = useState(itemsFuture ?? getItemsFuture());
return Scaffold(
body: FutureBuilder(
future: fetchData.value,
builder: (context, snapshot) {
return RefreshIndicator(
onRefresh: () async {
fetchData.value = getItemsFuture();
},
child: ...
);
},
),
);
}

Related

How to modify a variable using async method before it use in widget build?

I have a variable named userName,which depends on databse query,so async is a must.
My older code can be concluded liks this
class IndexScreen extends StatefulWidget {
#override
_IndexScreenState createState() => _IndexScreenState();
}
//use database query function
Future<void> initUser() async{
UserTable().getUserInfo(curUserEmail).then((value)=>null);
}
//show page
class _IndexScreenState extends State<IndexScreen> {
#override
Widget build(BuildContext context) {
initUser().then((value){
final theme = Theme.of(context);
return WillPopScope(
onWillPop: () =>router.navigateTo(context, '/welcome'),
child: SafeArea(
child: Scaffold(
//The static global variable is used in Body in other files
body: Body()
),
),
);
});
}
}
It warns that miss return,I dont knwo how to amend my code.
Thanks!!
You can achive this by using the FutureBuilder widget. Please refer the code below.
class IndexScreen extends StatefulWidget {
#override
_IndexScreenState createState() => _IndexScreenState();
}
//use database query function
Future<Map> initUser() async {
final data =
await UserTable().getUserInfo(curUserEmail);
return data;
}
//show page
class _IndexScreenState extends State<IndexScreen> {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: initUser(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
final theme = Theme.of(context);
return WillPopScope(
onWillPop: () => router.navigateTo(context, '/welcome'),
child: SafeArea(
child: Scaffold(
body: Body(),
),
),
);
} else {
// Returns empty container untill the data is loaded
Container();
}
},
);
}
}

prevent some items from Refreshing data in Listview.builder

I have a Listview.builder inside a FutureBuilder which taking some data from an http request.i have a bool closed i want to prevent some items from refreshing if status bool is true
how can I do that
You can achieve this by placing your call in initState. In this way you can make sure that it will get the data only once.
example:
class FutureSample extends StatefulWidget {
// Create instance variable
#override
_FutureSampleState createState() => _FutureSampleState();
}
class _FutureSampleState extends State<FutureSample> {
Future myFuture;
Future<String> _fetchData() async {
await Future.delayed(Duration(seconds: 10));
return 'DATA';
}
#override
void initState() {
// assign this variable your Future
myFuture = _fetchData();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: FutureBuilder(
future: myFuture,
builder: (ctx, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.toString());
}
return CircularProgressIndicator();
},
),
),
);
}
}
In that way you don't need a bool value. There are also different ways to achieve or extend your request. You can check this article for more informations: https://medium.com/flutterworld/why-future-builder-called-multiple-times-9efeeaf38ba2

How to use both FutureBuilder and setState method?(Flutter)

I tried to load data from the database by using FutureBuilder like this code.
// counter application
import 'package:flutter/material.dart';
class SetStateScreen extends StatefulWidget {
#override
_SetStateScreenState createState() => _SetStateScreenState();
}
class _SetStateScreenState extends State<SetStateScreen> {
Future<int> getData() async {
int initNumFromDB = 0; // fetch some asynchronous initial value
return initNumFromDB;
}
#override
Widget build(BuildContext context) {
int counter;
return Container(
color: Colors.white,
child: FutureBuilder(
future: getData(),
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
Widget childWidget;
if (snapshot.hasData) {
counter = snapshot.data;
childWidget = Center(
child: Container(
child: FlatButton(
onPressed: () {
setState(() {
counter += 1;
});
},
child: Text("$counter"),
)),
);
} else {
childWidget = Container();
}
return childWidget;
}),
);
}
}
But, when pushing the button and running the setState method, FutureBuilder loads initial data again and reset the counter number. How to load initial data once, and run this code correctly?
Try this is the proper way to use
class SetStateScreen extends StatefulWidget {
#override
_SetStateScreenState createState() => _SetStateScreenState();
}
class _SetStateScreenState extends State<SetStateScreen> {
Future<int> getData() async {
int initNumFromDB = 0; // fetch some asynchronous initial value
return initNumFromDB;
}
#override
void initState() {
super.initState();
getData();
}
}
You need to call you future method inside the initstate method . As such it will run only once when the widget is built and not everytime when setstate() is called.

Flutter/Dart won't display FutureBuilder snapshot when resolved

I'm attempting to set a field value from SharedPreferences like so:
FutureBuilder<String>(
future: _getUsername(),
initialData: 'Bruh',
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
print(snapshot);
print(snapshot.data);
print(snapshot.hasData);
return snapshot.hasData ? TextFormField(initialValue: snapshot.data, onChanged: _setUsername) : TextFormField();
}
),
This is the _getUsername() future I'm using:
Future<String> _getUsername() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString('username') ?? 'hmm';
return "Fine";
}
And this is the console output:
Reloaded 0 of 567 libraries in 352ms.
I/flutter (28961): AsyncSnapshot<String>(ConnectionState.done, tesy, null)
I/flutter (28961): tesy
I/flutter (28961): true
As you can see, L2 of the output shows 'tesy', which is the value in SharedPreferences, but I only ever see 'Bruh' in the text field (the initial value).
In all the examples I can find, 'Bruh' would be displayed (extremely) briefly (if at all) before 'tesy' is then displayed in the input box. Where am I going wrong?
I guess the problem is in TextFormField itself. When the component is being rebuilt, changed initialValue is not taken into account. If you want to have a "dynamic" initial value, it's better to provide an explicit controller. Something like this:
class TestApp extends StatefulWidget {
#override
_TestAppState createState() => _TestAppState();
}
class _TestAppState extends State<TestApp> {
final TextEditingController _controller = TextEditingController();
#override
void initState() {
super.initState();
_controller.text = 'Initial';
_initUsername();
}
Future<void> _initUsername() async {
final username = await _getUsername();
_controller.value = _controller.value.copyWith(text: username);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: TextFormField(controller: _controller, onChanged: (_) {}),
),
);
}
Future<String> _getUsername() async {
await Future.delayed(Duration(seconds: 1));
return 'Loaded';
}
}
Try change
TextFormField(initialValue: snapshot.data, onChanged: _setUsername)
To
TextFormField(key: UniqueKey(), initialValue: snapshot.data, onChanged: _setUsername)
This is because TextFormField is "Stateful" and the widget tree doesn't detect the change, puting a unique key, does the job that you want, another option is update value using an TextEditingrController.

How to avoid reloading data every time navigating to page

I'm trying to avoid rebuilding FutureBuilder in flutter. I have tried solution suggested in below Q's.
How to parse JSON only once in Flutter
Flutter Switching to Tab Reloads Widgets and runs FutureBuilder
still my app fires API every time I navigate to that page. Please point me where I'm going wrong.
Util.dart
//function which call API endpoint and returns Future in list
class EmpNetworkUtils {
...
Future<List<Employees>> getEmployees(data) async {
List<Employees> emps = [];
final response = await http.get(host + '/emp', headers: { ... });
final responseJson = json.decode(response.body);
for (var empdata in responseJson) {
Employees emp = Employees( ... );
emps.add(emp);
}
return emps;
}
}
EmpDetails.dart
class _EmpPageState extends State<EmpPage>{
...
Future<List<Employees>>_getAllEmp;
#override
initState() {
_getAllEmp = _getAll();
super.initState();
}
Future <List<Employees>>_getAll() async {
_sharedPreferences = await _prefs;
String authToken = AuthSessionUtils.getToken(_sharedPreferences);
return await EmpNetworkUtils().getEmployees(authToken);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar( ... ),
body: Container(
child: FutureBuilder(
future: _getAllEmp,
builder: (BuildContext context, AsyncSnapshot snapshot) { ... }
)))
}
}
Update:
I'm using bottomNavigationBar in my app, from which this page is loaded.
You are calling your getEmployees function in initState, which is meant to be called every time your widget is inserted into the tree. If you want to save the data after calling your function the first time, you will have to have a widget that persists.
An easy implementation would be using an InheritedWidget and a data class:
class InheritedEmployees extends InheritedWidget {
final EmployeeData employeeData;
InheritedEmployees({
Key key,
#required Widget child,
}) : assert(child != null),
employeeData = EmployeeData(),
super(key: key, child: child);
static EmployeeData of(BuildContext context) => (context.inheritFromWidgetOfExactType(InheritedEmployees) as InheritedEmployees).employeeData;
#override
bool updateShouldNotify(InheritedEmployees old) => false;
}
class EmployeeData {
List<Employees> _employees;
Future<List<Employees>> get employees async {
if (_employees != null) return _employees;
_sharedPreferences = await _prefs;
String authToken = AuthSessionUtils.getToken(_sharedPreferences);
return _employees = await EmpNetworkUtils().getEmployees(authToken);
}
}
Now, you would only have to place your InheritedEmployees somewhere that will not be disposed, e.g. about your home page, or if you want, even about your MaterialApp (runApp(InheritedEmployees(child: MaterialApp(..));). This way the data is only fetched once and cached after that. You could also look into AsyncMemoizer if that suits you better, but the example I provided should work fine.
Now, you will want to call this employees getter in didChangeDependencies because your _EmpPageState is dependent on InheritedEmployees and you need to look that up, which cannot happen in initState:
class _EmpPageState extends State<EmpPage>{
Future<List<Employees>>_getAllEmp;
#override
void didChangeDependencies() {
_getAllEmp = InheritedEmployees.of(context).employees;
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar( ... ),
body: Container(
child: FutureBuilder(
future: _getAllEmp,
builder: (BuildContext context, AsyncSnapshot snapshot) { ... }
)))
}
}
I mentioned that your State is now dependent on your InheritedWidget, but that does not really matter as updateShouldNotify always returns false (there are not going to be any additional builds).
I got another way to solve this issue and apply to my app also
Apply GetX controller to call API and render response data
Remove FutureBuilder to call API data
Apply GetX controller to call API data, Like
class NavMenuController extends GetxController {
Api api = new Api();
var cart = List<NavMenu>().obs;
#override
void onInit() {
// TODO: implement onInit
getNavMenuData();
super.onInit();
}
Future<List<NavMenu>> getNavMenuData() async {
var nav_data = await api.getNavMenus();
if(nav_data!=null) {
cart.value = nav_data;
}
return cart;
}
}
Call API using controller on initState() into desired class
class NavMenuDrawer extends StatefulWidget {
#override
_NavMenuDrawerState createState() => _NavMenuDrawerState();
}
class _NavMenuDrawerState extends State<NavMenuDrawer> {
final NavMenuController navMenuController = Get.put(NavMenuController());
#override
void initState() {
// TODO: implement initState
super.initState();
navMenuController.getNavMenuData();
}
Remove below FutureBuilder code for calling API, [if you use FutureBuilder/StreamBuilder whatever]
return FutureBuilder<List<NavMenu>>(
future: api.getNavMenus(),
builder: (context, snapshot) {
return ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
physics: ScrollPhysics(),
itemCount: snapshot.data?.length ?? 0,
itemBuilder: (context, index) {
return Column(
children: [
ListTile(
title: Text("${snapshot.data[index].title}"),
Just use GetX controller to get data, like
return ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
physics: ScrollPhysics(),
itemCount: navMenuController.cart?.length ?? 0,
itemBuilder: (context, index) {
return Column(
children: [
ListTile(
title: Obx(() {
return Text(
"${navMenuController.cart.value[index].title}");
}),
Note : For more info you can search on how to apply GetX