using setstate to update my database changes with refresh button - flutter

i use RefreshIndicator and its working well but couldnt make it functional for my database, i think my setstate is wrong , can you help me to use setstate to make it functional , here is my product page..
class FeaturedProducts extends StatefulWidget {
#override
_FeaturedProductsState createState() => _FeaturedProductsState();-
}
class _FeaturedProductsState extends State<FeaturedProducts> {
final keyRefresh = GlobalKey<RefreshIndicatorState>();
List<ProductModel> products = [];
#override
void initState() {
super.initState();
loadProducts();
}
Future loadProducts() async {
keyRefresh.currentState?.show();
await Future.delayed(Duration(milliseconds: 4000));
setState(() => this.products = products);,
}
#override
Widget build(BuildContext context) {
final productProvider = Provider.of<ProductProvider>(context);
return RefreshIndicator(
keyRefresh: keyRefresh,
onRefresh: loadProducts,
child: ListView.builder(
scrollDirection: Axis.vertical,
itemCount: productProvider.products.length,
itemBuilder: (_, index) {
return FeaturedCard(
product: productProvider.products[index],
);
}));
}
}

Related

Force a widget tree build using a Hook Widget in 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: ...
);
},
),
);
}

Flutter View does not update view while Provider call notifyListeners()

I want to the View update list item when provider function called. But it does not work.
Before changing normal View to FutureBuilder, it worked. I tried to use StreamBuilder instead, does not work either.
How can I solve this?
Here is the Provider:
class SelectArtistProvider extends ChangeNotifier {
List<Map> artists = [];
...
Future initArtistListVM() async { //for Future Builder
var allArtists =
await Firestore.instance.collection('artists').getDocuments();
List list = allArtists.documents
.map((artist) => {
'id': artist.documentID,
'name': artist.data['name'],
'image': "artist.data['image'],
'selected': "false",
})
.toList();
artists = list;
notifyListeners();
return artists;
}
void toggleSelected(Map item, int index) {
artists[index]['selected'] = !item['selected'];
notifyListeners();
}
View
// list view
class SelectArtist extends StatelessWidget {
Widget build(BuildContext context) {
final selectArtistProvider = Provider.of<SelectArtistProvider>(context);
return FutureBuilder(
future: selectArtistProvider.initArtistListVM(), // initial data from api
builder: (context, snapshot) {
return ListView.builder(
...
child: ArtistItem(index, selectArtistProvider.artists[index])
// item view
class ArtistItem extends StatelessWidget {
final int index;
final Map artist;
Widget build(BuildContext context) {
final selectArtistProvider = Provider.of<SelectArtistProvider>(context);
return GestureDetector(
onTap: () => selectArtistProvider.toggleSelected(artist, index),
child:
...
Visibility(
visible: artist['selected'],
child: SomeWidget()
...
// root view to place providers
class InitialProviders extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
...
ChangeNotifierProvider(create: (_) => SelectArtistProvider()),
],
child: ...
Solved This!
Change initArtistListVM() some part.
if (artists.isEmpty) {
artists = list;
notifyListeners();
}

Flutter PageView keep rebuilding the main widget on setState value

currently flutter app structure
StackedHome has a pageview with 2 children
Pageview(parent):
HomeScreen(child#1)
Vertical PageView
bottom navigation bar
UserProfilePage(child#2)
HomeScreen should pass the index value to UserProfilePage, so when scrolling horizontally, we will get user profilescreen with id passed to that received from HomeScreen. based on the id passed i will display related user profile
Here is sample video showing the problem :
https://drive.google.com/file/d/1tIypNOHewcFSo2Pf-F97hsQGfDgNVqfW/view?usp=sharing
Problem:
i managed to do that and its working fine, but my problem on setState of that variable
setState(() {
_postIndex = postIndex;
});
on each HomeScreen > onPageChanged call i am updating the index value pass it to the parent (StackedHome) class, and since there is a setState to update profile index (UserProfilePage)...the whole app will be rebuild on each pageview change...
What i need is to disable that main widget to be rebuilt again and again on value update..
StackedHome
class StackedHome extends StatefulWidget {
final int data;
final Function(int) onDataChange;
const StackedHome({
this.data,
this.onDataChange,
Key key,
}) : super(key: key);
#override
_StackedHomeState createState() => _StackedHomeState();
}
class _StackedHomeState extends State<StackedHome>{
PageController pageController;
int _count = 0;
int _postIndex = 0;
void _postId(int postIndex) {
//This cuasing main screen to be rebuilt everytime on pageview scroll
//but getting the value correctly
setState(() {
_postIndex = postIndex;
});
}
#override
void initState() {
super.initState();
pageController = PageController();
}
#override
void dispose() {
pageController.dispose();
super.dispose();
}
int index = 0;
#override
Future<void> _refreshPosts() async {
PostApi postApi = PostApi();
setState(() {
postApi.fetchAllPosts();
});
}
Widget build(BuildContext context) {
PostApi postApi = PostApi();
return FutureBuilder(
future: postApi.fetchAllPosts(),
builder: (BuildContext context, AsyncSnapshot<List<Post>> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return apiError('No Connection Made');
break;
case ConnectionState.waiting:
case ConnectionState.active:
return ApiLoading(color:0xff000000);
break;
case ConnectionState.done:
if (snapshot.hasError) {
return apiError(snapshot.error.toString());
}
if (snapshot.hasData) {
return _drawPostsList(snapshot.data, context);
}
break;
}
return Container();
},
);
}
Widget _drawPostsList(List<Post> posts, BuildContext context) {
return PageView(
reverse: true,
children: <Widget>[
HomeScreen(
posts: posts,
index: index,
postId: _postId,//function Passed
),
UserProfilePage(
posts: posts,
index: _postIndex,
)
],
);
}
}
HomeScreen
class HomeScreen extends StatefulWidget {
#override
final List posts;
final int index;
final Function(int) postId;
int getPage() {
return value;
}
void setPage(int page) {
value = page;
}
HomeScreen({Key key, this.posts, this.index, this.postId}) : super(key: key);
HomeScreenState createState() => HomeScreenState();
}
class HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin {
final PageController _controller = PageController();
PageController _pageController = PageController();
int index = 0;
#override
void initState() {
super.initState();
//Set pageview inital page
_pageController = PageController(
keepPage: true,
initialPage: widget.getPage(),
);
}
#override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: _refreshPosts,
child: Stack(children: <Widget>[
PageView.builder(
controller: _pageController,
onPageChanged: (index) => setState(() {
.
widget.postId(index);//I am calling parent class and updating the vlaue with new index value
.
}),
scrollDirection: Axis.vertical,
itemBuilder: (context, position) {
//Build image lists
return _homeList(widget.posts, position);
},
),
BottomNavigation("light"),
]),
);
}
}
i hope my problem is clear enough....i need to pass the value to parent so i can pass it to second child which is the profile screen so it will show user profile realted to that post
Ohh wow, managed to solve this problem using provider and consumer, by listening to any update on index id... this post helped me to solve it https://medium.com/flutter-nyc/a-closer-look-at-the-provider-package-993922d3a5a5

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

How to rebuild the Widget onpress of floatingactionbutton?

i'm trying to pull new data from firebase cloud firestore and rebuild the widget on onPress of floating action button. i'm not sure how to rebuild the whole widget. Tried to call getList from the onPressed and setState() but still not rebulding widget evening nameList was updated.
class MyList extends StatefulWidget {
static const String id = 'test';
#override
_MyListState createState() => _MyListState();
}
class _MyListState extends State<MyList> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('TEST'),),
body: MainList(),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.refresh),
backgroundColor: Colors.teal,
onPressed: () {
}),
);
}
}
class MainList extends StatefulWidget {
#override
_MainListState createState() => _MainListState();
}
class _MainListState extends State<MainList> {
List<Test> nameList = [];
#override
void initState() {
super.initState();
getList();
}
getList() async {
final _name = await
Firestore.instance.collection('test').getDocuments();
nameList.clear();
for (var name in _name.documents) {
Test addName = new Test(
name.data['name'],
);
nameList.add(addName);
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: nameList.length,
itemBuilder: (BuildContext context, int index) {
return Container(
child: Text(nameList[index].name),
);
});
}
}
Once a widget is created initState isn't called again. So your listview is still reflecting the old data.
You could getList in the onPressed which would then update your nameList. You could then pass this nameList to MainList.
class MyList extends StatefulWidget {
static const String id = 'test';
#override
_MyListState createState() => _MyListState();
}
class _MyListState extends State<MyList> {
List<Test> nameList = [];
getList() async {
final _name = await
Firestore.instance.collection('test').getDocuments();
nameList.clear();
for (var name in _name.documents) {
Test addName = new Test(
name.data['name'],
);
nameList.add(addName);
setState(() {});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('TEST'),),
body: MainList(nameList: nameList),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.refresh),
backgroundColor: Colors.teal,
onPressed: () {
getList();
}),
);
}
}
Your MainList widget would then look like:
class MainList extends StatefulWidget {
final List nameList;
MainList({this.nameList});
#override
_MainListState createState() => _MainListState();
}
class _MainListState extends State<MainList> {
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: nameList.length,
itemBuilder: (BuildContext context, int index) {
return Container(
child: Text(nameList[index].name),
);
});
}
}
Just calling setState() is not enough. You'll have to tell Dart what you are going to set. Sample code :
setState ( ()=> nameList = _fetchedList ) ;
In the above code, the variable nameList is assigned within setState().
In your code, you've two options.
Option 1 :
setState(() {
nameList.clear();
for (var name in _name.documents) {
Test addName = new Test(
name.data['name'],
);
nameList.add(addName);
}
});
Or option 2, better way, use for loop to add in the data in another list and use setState with one line as below :
List<Test> _fetchedList ;
for (var name in _name.documents) {
Test addName = new Test(
name.data['name'],
);
_fetchedList.add(addName);
}
setState( ()=> nameList = _fetchedList ) ;