Flutter : Prevent FutureBuilder always refresh every change screen - flutter

I have FutureBuilder to fetch User profil from API and code to fetch user like this :
Future<List<UserModel>> getUserByUsername({#required String username}) async {
try {
final response =
await _client.get("$_baseUrl/getUserByUsername?username=$username");
final Map<String, dynamic> responseJson = json.decode(response.body);
if (responseJson["status"] == "ok") {
List userList = responseJson['data'];
final result = userList
.map<UserModel>((json) => UserModel.fromJson(json))
.toList();
return result;
} else {
throw CustomError(responseJson['message']);
}
} catch (e) {
return Future.error(e.toString());
}
}
If you can see in above GIF, My FutureBuilder are inside BottomNavigationBar. Every i change the screen/page from BottomNavigationBar and come back to my FutureBuilder is always refresh !
How can i fixed it to only once to refresh ?
Home Screen
class _HomeScreenState extends State<HomeScreen> {
#override
Widget build(BuildContext context) {
final username = Provider.of<SharedPreferencesFunction>(context).username;
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CardTime(),
FutureBuilder(
future: userApi.getUserByUsername(username: username),
builder: (BuildContext context,
AsyncSnapshot<List<UserModel>> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Center(
child: Text(
snapshot.error.toString(),
),
);
} else {
final user = snapshot.data[0];
return CardProfil(
imageUrl: "${userApi.baseImageUrl}/${user.fotoUser}",
detailProfil: [
Text(
user.namaUser,
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(user.idDevice),
],
);
}
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
),
],
),
);
}
}
Shared Preferences Function
import 'package:flutter/cupertino.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SharedPreferencesFunction extends ChangeNotifier {
SharedPreferencesFunction() {
initialSharedPreferences();
getUsername();
}
String _username;
String get username => _username;
void initialSharedPreferences() {
getUsername();
}
Future updateUsername(String username) async {
SharedPreferences pref = await SharedPreferences.getInstance();
await pref.setString("username", username);
//! It's Important !!! After update / remove sharedpreferences , must called getUsername() to updated the value.
getUsername();
notifyListeners();
}
Future removeUsername() async {
SharedPreferences pref = await SharedPreferences.getInstance();
final result = await pref.remove("username");
//! It's Important !!! After update / remove sharedpreferences , must called getUsername() to updated the value.
getUsername();
print(result);
notifyListeners();
}
Future getUsername() async {
SharedPreferences pref = await SharedPreferences.getInstance();
final result = pref.getString("username");
_username = result;
notifyListeners();
}
}
final sharedpref = SharedPreferencesFunction();
Update Question
I already try Initialize FutureBuilder and use initState and didChangeDependencies . But new problem is , if i initialize inside initState my profil not rebuild because Provider listen=false.
If i using didChangeDependencies my FutureBuilder still refresh every i change screen.
Something wrong ?
Using initState
Using didChangeDependencies

Initialize the Future during initState or didChangeDependencies instead.
class _HomeScreenState extends State<HomeScreen> {
Future<List<UserModel>> user;
#override
void initState() {
super.initState();
// must use listen false here
final username = Provider.of<SharedPreferencesFunction>(context, listen: false).username;
user = userApi.getUserByUsername(username: username);
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
final username = Provider.of<SharedPreferencesFunction>(context).username;
user = userApi.getUserByUsername(username: username);
}
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FutureBuilder(
future: user,
builder: (context, snapshot) {
// ...
},
),
],
),
);
}
}

I faced a similar case and use AutomaticKeepAliveClientMixin on each view / page / tab bar view / widget / child to keep the page not refreshing every time I go back and forth through the tab bar.
class YourClass extends StatefulWidget {
YourClass({
Key key
}): super(key key);
#override
_YourClassState createState() => _YourClassState();
}
// Must include AutomaticKeepAliveClientMixin
class _YourClassState extends State<YourClass> with AutomaticKeepAliveClientMixin {
Future resultGetData;
void getData() {
setState(() {
resultGetData = getDataFromAPI();
});
}
// Must include
#override
bool get wantKeepAlive => true;
#override
void initState() {
getData();
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context); // Must include
return FutureBuilder(
future: resultGetAllByUserIdMCId,
builder: (context, snapshot) {
// ...
// Some Code
// ...
}
);
}
}
If you want to refresh the data you could use RefreshIndicator that runs the getData() function. Put this code inside FutureBuilder. The key: PageStorageKey(widget.key) will keep the scroll in the exact same place where you left of.
return RefreshIndicator(
onRefresh: () async {
getData();
},
child: ListView.separated(
key: PageStorageKey(widget.key),
itemCount: data.length,
separatorBuilder: (BuildContext context, int index) {
return Divider(height: 0);
},
itemBuilder: (context, index) {
return ...;
},
),
);

Use IndexedStack as the parent of tabbar.

You have to put your Future Builder in a Stateful Widget then define a
late final Future myFuture;
then you have to initialize it in the initstate so the future will be executed only one time.

Related

Flutter #3: I have some async problem in flutter

I have a piece of code to scan and read device information. I have printed the elements in the list in onScan function, however I don't know how to get that information and put it in a listview.
Can someone help me?
List<Data> listDevice = [];
Future<void> getData() async {
var apiEndpoint = TTAPI.shared;
await apiEndpoint.devideScan(((data) => onScan(data)));
}
Future<void> onScan(dynamic data) async {
var dataResponse = DataResponse.fromJson(data);
print(dataResponse.toJson());
List<dynamic> dt = jsonDecode(jsonEncode(dataResponse.data).toString());
dt.forEach((element) {
var item = Data.fromJson(element);
print(item.modelName);
listDevice.add(item);
});
var connectRequest = {
'serialNumber': 'DEVICE_SERIAL',
'modelName': 'DEVICE_MODEL',
'ipAddr': 'DEVICE_IP'
};
var apiEndpoint = TTAPI.shared;
await apiEndpoint.connectDevice(connectRequest);
}
Future<List<Data>> getList() async {
return listDevice;
}
You can see more of my code here: https://docs.google.com/document/d/1ntxaDpyNCLD1MyzJOTmZsrh7-Jfim8cm0Va86IQZGww/edit?usp=sharing
As for the current code structure, listDevice is populated inside Future. So you can call setState to update the UI after getting the list at the end of onScan.
Future<void> getData() async {
var apiEndpoint = TTAPI.shared;
await apiEndpoint.devideScan(((data) => onScan(data)));
setState((){});
}
But it would be great to use FutureBuilder and return list from getData.
Current question pattern example
class TextFW extends StatefulWidget {
const TextFW({super.key});
#override
State<TextFW> createState() => _TextFWState();
}
class _TextFWState extends State<TextFW> {
//for current question way
List<int> listDevice = [];
Future<void> getData() async {
await Future.delayed(Duration(seconds: 2));
/// others async method
listDevice = List.generate(10, (index) => index);
setState(() {}); //here or `getData().then()`
}
#override
void initState() {
super.initState();
getData();
// or this getData().then((value) => setState((){}));
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: listDevice.length,
itemBuilder: (context, index) => ListTile(
title: Text("${listDevice[index]}"),
),
),
);
}
}
Using FutureBuilder
class TextFW extends StatefulWidget {
const TextFW({super.key});
#override
State<TextFW> createState() => _TextFWState();
}
class _TextFWState extends State<TextFW> {
/// method will provide data by scanning
Future<List<int>> getData() async {
await Future.delayed(Duration(seconds: 2));
return List.generate(10, (index) => index);
}
late final fututre = getData();
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<List<int>>(
future: fututre,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text("${snapshot.error}");
}
if (snapshot.hasData) {
final listDevice = snapshot.data;
return ListView.builder(
itemCount: listDevice?.length,
itemBuilder: (context, index) => ListTile(
title: Text("${listDevice![index]}"),
),
);
}
return CircularProgressIndicator();
},
),
);
}
}

Prevent repeated API calls upon Changing Screens in Flutter

I was looking for alternatives in which the data is loaded from API only once and stays that way if I move to and fro that screen and I found one in using InheritedWidget. However, I'm now getting the below error and I cannot figure out how to get rid of this.
The getter 'users' was called on null.
Receiver: null
Tried calling: users
The errors are marked as comments in the below:
My code:
InheritedWidget Class
class InheritedUsers extends InheritedWidget {
final UsersList users;
InheritedUsers({required Key key, required Widget child})
: assert(child != null),
users = UsersList(),
super(key: key, child: child);
static UsersList of(BuildContext context) =>
(context.dependOnInheritedWidgetOfExactType(aspect: InheritedUsers)
as InheritedUsers)
.users;
#override
bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
}
class UsersList {
late List<User> listOfUsers;
Future<List<User>> get userList async {
return listOfUsers = await UsersApi.getUsers();
}
}
class UsersApi with ChangeNotifier {
static Future<List<User>> getUsers() async {
// List<User> list = [];
// late final body;
final url =
'https://firebasestorage.googleapis.com/v0/b/web-johannesmilke.appspot.com/o/other%2Fvideo126%2Fusers.json?alt=media';
final response = await http.get(Uri.parse(url));
final body = json.decode(response.body);
return body.map<User>(User.fromJson).toList();
}
}
UserNetworkPage widget
class UserNetworkPage extends StatefulWidget {
UserNetworkPageState createState() => UserNetworkPageState();
}
class UserNetworkPageState extends State<UserNetworkPage> {
late final Future<List<User>> result;
#override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
result = InheritedUsers.of(context).userList; //This is where the error gets thrown
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) => Scaffold(
body: FutureBuilder<List<User>>(
future: result,
builder: (context, snapshot) {
final users = snapshot.data;
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
default:
if (snapshot.hasError) {
return Center(child: Text('Some error occurred!'));
} else {
return buildUsers(users!);
}
}
},
),
);
Widget buildUsers(List<User> users) => ListView.builder(
physics: BouncingScrollPhysics(),
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) => UserPage(user),
)),
leading: CircleAvatar(
backgroundImage: NetworkImage(user.urlAvatar),
),
title: Text(user.username),
subtitle: Text(user.email),
);
},
);
}
What you need at this point is a state management solution. There's a lot of them, and I'm pretty sure they all use InheritedWidget underneath.
Introduction to state management
List of state management approaches
I personally recommend Riverpod, Provider or BLoC.

Unable to get data from the List to build the ListView.builder()

I'm trying to fetch data from the jsonplaceholder todos API, Once I retrieve the data I'm storing it into a List and notifying all the listeners. But something weird is happening.
class Todos with ChangeNotifier {
List<Todo> _items = [];
List<Todo> get item {
return [..._items];
}
Future fetchAndSetData() async {
try {
const url = 'https://jsonplaceholder.typicode.com/todos';
final List<dynamic> response =
json.decode((await http.get(Uri.parse(url))).body);
List<Todo> extractedTodo =
response.map((dynamic item) => Todo.fromJson(item)).toList();
_items = extractedTodo;
print(_items.length); // Getting 200 which is exact length I'm expecting
notifyListeners();
} catch (err) {
print(err);
}
}
}
The above code is where I'm making a get request and storing the data into the List. The following code is where I'm calling the fetchAndSetData with the help of Provider.
class _HomeScreenState extends State<HomeScreen> {
var _isLoading = true;
#override
void didChangeDependencies() {
Provider.of<Todos>(context, listen: false).fetchAndSetData().then((_) {
setState(() {
_isLoading = false;
});
});
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: _isLoading
? const Center(
child: CircularProgressIndicator(),
)
: TodoList(),
);
}
}
The following is where I'm trying to get the todos from the items list.
#override
Widget build(BuildContext context) {
final todos = Provider.of<Todos>(context, listen: false).item;
print(todos.length);
return ListView.builder(
itemCount: todos.length,
itemBuilder: (ctx, index) {
return Card(
child: ListTile(
title: Text(todos[index].title),
),
);
},
);
}
Once didChangeDependencies, it will call the fetchAndSetData and will set the List, so the print statement on the Todos class will print 200 as the length of items I'm expecting but in the TodoList class where I'm calling the getter, the length I'm receiving is 0.
Now the weird part is when I removed the listen: false in the didChangeDependencies, the print statement on the fetchAndSetData getting called again and again. With that I mean the length for the todos is 200 but the print goes beyond 200. As, there is no way that the data gets updated, so I mark those as listen: false
Please help me
Please fetch your data in
Future<List<dynamic>> _post;
#override
void initState() {
super.initState();
_post = fetchAndSetData();
}
and then use a FutureBuilder like this
return FutureBuilder(
future: _post,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasData) {

how to make this lazyload scrolling working with provider

it take about 7 days trying make a working example for lazyload listview with provider in flutter with real world example and it's still not working because i think something is missing
As a note : the first load , works good and when i scroll it's print (scroll) but nothing happened it's still in the same page
if i try to return _todolist variable in the _onScrollUpdated it not change page correctly and after three times i see this error
E/flutter ( 7713): [ERROR:flutter/lib/ui/ui_dart_state.cc(166)]
Unhandled Exception: type 'String' is not a subtype of type
'List' E/flutter ( 7713): #0 TodoService.fetchTodos
(package:flutter_todo_provider/services/todo_service.dart:32:21)
json example
https://jsonformatter.org/52c83e
todos_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_todo_provider/helpers/http_exception.dart';
import 'package:provider/provider.dart';
import 'package:flutter_todo_provider/.env.dart';
import 'package:flutter_todo_provider/services/todo_service.dart';
class TodosScreen extends StatefulWidget {
#override
_TodosScreenState createState() => _TodosScreenState();
}
class _TodosScreenState extends State<TodosScreen> {
ScrollController _controller;
List<dynamic> _todoList;
bool _isLoading ;
#override
void initState() {
super.initState();
_controller = ScrollController();
_controller.addListener(_onScrollUpdated);
}
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(Configuration.AppName),
),
body: FutureBuilder(
future: _fetchListItems(),
builder: (context, snapshot){
if(snapshot.hasData){
return _listItems(snapshot.data);
}
return _buildProgressIndicator();
}
),
);
}
_fetchListItems() async {
try {
await Provider.of<TodoService>(context, listen: false).loadNextPage();
_todoList = Provider.of<TodoService>(context, listen: false).items;
} on HttpException catch (e) {
EasyLoading.showError(e.message);
}
return _todoList ;
}
Widget _listItems(data){
_isLoading = Provider.of<TodoService>(context, listen: false).isLoading ;
return ListView.builder(
controller: _controller,
itemCount: data.length ,
itemBuilder: (context, index) {
return ListTile(
title: Text(data[index].title),
subtitle:Text(data[index].content),
trailing: Icon(Icons.print),
);
},
);
}
Future<void> _onScrollUpdated() async {
print("Scroll11");
var maxScroll = _controller.position.maxScrollExtent;
var currentPosition = _controller.position.pixels;
if (currentPosition == maxScroll ) {
try {
await Provider.of<TodoService>(context, listen: false).loadNextPage();
_todoList = Provider.of<TodoService>(context, listen: false).items;
// return _todoList ; if use this line i see the error
} on HttpException catch (e) {
EasyLoading.showError(e.message);
}
}
}
Widget _buildProgressIndicator() {
_isLoading = Provider.of<TodoService>(context, listen: false).isLoading ;
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: _isLoading ? 1.0 : 00,
child: new CircularProgressIndicator(),
),
),
);
}
}
todo_service.dart
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_todo_provider/.env.dart';
import 'package:flutter_todo_provider/models/todo.dart';
class TodoService with ChangeNotifier {
bool isLoading = false;
bool isFetching = false;
int currentPage = 1;
int totalRows = 10;
List<Todo> items = [];
loadNextPage() async {
await fetchTodos(currentPage);
currentPage++;
notifyListeners();
}
Future fetchTodos(int currentPage) async {
try {
//404
var options = Options(headers: {
HttpHeaders.authorizationHeader: 'Basic ${Configuration.authToken}'
});
Map<String, dynamic> qParams = {
'current_page': currentPage,
};
Response response = await Dio().get('${Configuration.ApiUrl}/todos/my_todos', options: options, queryParameters: qParams);
List<dynamic> responseBode = response.data["data"];
responseBode.forEach(( dynamic json) {
items.add(Todo.fromJson(json));
});
notifyListeners();
} on DioError catch (e) {
print("Error Message" + e.response.statusMessage);
return items=[];
}
}
}
Here is the code:
class TodoScreen extends StatefulWidget {
// Your service goes here
// (the class extending ChangeNotifier)
#override
_TodoScreenState createState() => _TodoScreenState();
}
class _TodoScreenState extends State<TodoScreen> {
final TodoService todoService = TodoService();
ScrollController _controller;
#override
void initState() {
super.initState();
_controller = ScrollController();
_controller.addListener(_onScrollUpdated);
loadNextPage();
}
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Configuration.AppName'),
),
body: ChangeNotifierProvider.value(
value: todoService,
child: Consumer<TodoService>(builder: (_, ctl, __) {
if (todoService.isLoading) {
return _buildProgressIndicator();
} else {
return _listItems(todoService.items);
}
}),
),
);
}
Widget _listItems(data) {
return ListView.builder(
controller: _controller,
itemCount: data.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(data[index].title),
subtitle: Text(data[index].content),
trailing: Icon(Icons.print),
);
},
);
}
Widget _buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: CircularProgressIndicator(),
),
);
}
Future<void> _onScrollUpdated() async {
var maxScroll = _controller.position.maxScrollExtent;
var currentPosition = _controller.position.pixels;
if (currentPosition == maxScroll) {
todoService.loadNextPage();
}
}
}
Note that i didn't make changes to your service. The notifyListeners will do all the job for us.
When you are using Provider, the idea is to keep all your data inside the controller or service (the class that extends ChangeNitifier) and just use the variables with notifyListeners to change the behavior of your screen.
The screen needs to be listening for changes, for this we use the pair ChangeNotifierProvider.value with Consumer(builder: (_, ctl, __) {}).
Use ChangeNotifierProvider in some upper level of the widget tree and use Consumer only where you need the widget to be updated. You can even use more than one Consumer, they all just need to be under ChangeNotifierProvider.

FutureBuilder class argument future is an async function with arguments

I'm developing a Flutter mobile application which uses Google APIs. In one of the screens of my application I want to let the user type in a place (city, address, ...) and call the Google Places API to generate a list of suggestions based on user input. Whenever the text input changes a new GET request is issued.
To handle user input I am using a TextEditingController and in order to have a better user experience I want to use FutureBuilder class in order to show a loading spinner when the data is not ready. This is the code:
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
class Info extends StatefulWidget {
static const routeName = '/info';
#override
_InfoState createState() => _InfoState();
}
class _InfoState extends State<Info> {
final controller = TextEditingController();
#override
void initState() {
// Start listening to changes.
controller.addListener(buildPredictionList);
super.initState();
}
#override
void dispose() {
// Clean up the controller when the widget is disposed.
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Info'),
),
body: Column(
children: <Widget>[
TextField(
controller: controller,
),
Container(
height: 200,
child: buildPredictionList(),
),
],
),
);
}
Widget buildPredictionList() {
return FutureBuilder(
future: fetchPredictions, // <-- Error! fetchPredictions expects a parameter!
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
Prediction pred = snapshot.data[index];
return Card(
child: ListTile(
leading: Icon(Icons.pin_drop),
title: Text('${pred.description}'),
),
);
},
);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return CircularProgressIndicator();
},
);
}
}
class Prediction {
final String placeId;
final String description;
Prediction({this.placeId, this.description});
factory Prediction.fromJson(Map<String, dynamic> json) {
return Prediction(
placeId: json['place_id'],
description: json['description'],
);
}
}
Future<List<Prediction>> fetchPredictions(String query) async {
const GOOGLE_API_KEY = '...';
final lat = 40.758058;
final lng = -73.985626;
final radius = 2000;
final lang = 'en';
var url =
'https://maps.googleapis.com/maps/api/place/autocomplete/json?input=$query&key=$GOOGLE_API_KEY&location=$lat,$lng&radius=$radius&language=$lang&strictbounds=true';
final response = await http.get(url);
if (response.statusCode == 200) {
var predictionsJson = json.decode(response.body)['predictions'] as List;
List<Prediction> predictions = predictionsJson
.map((predictionJson) => Prediction.fromJson(predictionJson))
.toList();
return predictions;
} else {
throw Exception('Failed to fetch Predictions');
}
}
My async function fetchPredictions expects an argument, which is the query string used for the GET request (so the input address, city, ...). But I cannot wrap this in an anonymous function because the future argument is expecting a return type of Future.
Thanks in advance!