Prevent repeated API calls upon Changing Screens in Flutter - 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.

Related

Will ProviderScope and riverpod providers be removed from memory?

I am confused that will Nested ProviderScope and all Providers be romoved from memory? And is following usecase good practice or bad practice?
I have idsProvider
final idsProvider = Provider((_) => List.generate(50, (i) => i));
and have itemIdProvider for every id of idsProvider
final itemIdProvider = Provider.autoDispose((_) => 0);
UI as follows:
class BuildListView extends ConsumerWidget {
const BuildListView({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
final ids = ref.watch(idsProvider);
return ListView.builder(
itemCount: ids.length,
itemBuilder: (context, index) {
return ProviderScope(
overrides: [
itemIdProvider.overrideWithValue(ids[index]),
],
child: const BuildItem(),
);
},
);
}
}
class BuildItem extends ConsumerWidget {
const BuildItem({super.key});
#override
Widget build(BuildContext context, WidgetRef ref) {
final itemState = ref.watch(itemProvider);
return itemState.when(
data: (id, data) => ListTile(
title: Text("ID: $id"),
subtitle: Text(data),
),
loading: () => const CircularProgressIndicator(),
error: (error) => Text(error.toString()),
);
}
}
Then I have stateNotifierProvider to manipulate the state of every item of the ListView:
final itemProvider = StateNotifierProvider.autoDispose<ItemNotifier, ItemState>(
(ref) => ItemNotifier(ref.watch(itemIdProvider)),
dependencies: [itemIdProvider],
);
class ItemNotifier extends StateNotifier<ItemState> {
ItemNotifier(this.id) : super(const ItemState.loading()) {
fetchData();
}
final int id;
Future<void> fetchData() async {
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
state = ItemState.data(id: id, data: "Data for $id");
}
}
// A lot of methods to change the state
// ...
// ...
}
#freezed
class ItemState with _$ItemState {
const factory ItemState.data({required int id, required String data}) = Data;
const factory ItemState.loading() = Loading;
const factory ItemState.error([String? message]) = Error;
}
I think it's perfectly acceptable. In addition, you may not have an initial value:
final itemIdProvider = Provider.autoDispose((_) => throw UnimplementedError());
This way it will be seen that the value will be implemented later.
About memory. ProviderScope is a StatefulWidget and has the following lines of code under the 'hood':
#override
void dispose() {
container.dispose();
super.dispose();
}
So you don't have to worry too much :)

Flutter FutureBuilder snapshot returns Instance of 'Object' instead of data

i am new to flutter and trying to display data from a http post
referencing from [1]https://flutter.dev/docs/cookbook/networking/background-parsing and [2]https://flutter.dev/docs/cookbook/networking/fetch-data
i tried to display data on a futurebuilder but it keeps displaying this from the Text('${snapshot.data}')
[Instance of 'DashBoardBanner', Instance of 'DashBoardBanner', Instance of 'DashBoardBanner']
Builder
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late Future<List<DashBoardBanner>> futureBanner;
#override
void initState() {
super.initState();
futureBanner = getBannerDataFromServer();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ListView(
children: [
Card(
child: FutureBuilder(
future: getBannerDataFromServer(),
builder: (context,snapshot){
if(snapshot.connectionState == ConnectionState.done){
if (snapshot.hasData) {
return Text('${snapshot.data}');
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
}
return const CircularProgressIndicator();
},
),
)
],
)),
);
}
}
Class and postreq
class DashBoardBanner {
final String MsgId;
final String MsgKey;
final String MsgPic;
const DashBoardBanner(
{required this.MsgId, required this.MsgKey, required this.MsgPic});
factory DashBoardBanner.fromJson(Map<String, dynamic> json) {
return DashBoardBanner(
MsgId: json['MsgId'] as String,
MsgKey: json['MsgKey'] as String,
MsgPic: json['MsgPic'] as String,
);
}
}
Future<List<DashBoardBanner>> getBannerDataFromServer() async {
final queryParameters = {
"ApiFunc": 'Banner',
"UserKey": getDeviceKey(),
"Token": getDeviceToken(),
"SubmitContent": json.encode({"MobileNo": getMobileNo1()})
};
final response = await http.post(
Uri.http('somesite.net', '/capi.aspx', queryParameters),
);
if (response.statusCode == 200) {
Map<String, dynamic> data = jsonDecode(response.body);
final splitoff = jsonEncode(data['RespContent']);
return compute(parseBanner, splitoff);
} else {
throw Exception('Failed to load Data');
}
}
List<DashBoardBanner> parseBanner(String responseBody) {
final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
return parsed
.map<DashBoardBanner>((json) => DashBoardBanner.fromJson(json))
.toList();
}
Edit : i rebuilt the file replicating reference[1] and it finally displayed the data i needed, it seems the issue stem from not having this 2nd widget which return the obj back , however how do i combine the 2nd build widget into the first without needing the whole widget as having a whole build widget to return 1 line seems pointless?
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body:Container(
child: FutureBuilder<List<DashBoardBanner>>(
future: getBannerDataFromServer(http.Client()),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(
child: Text('An error has occurred!'),
);
} else if (snapshot.hasData) {
print(snapshot.data!.length);
return DashBoardBannersList(dashboardBanners: snapshot.data!); <--- original issue due to not having this
} else {
return CircularProgressIndicator();
}
},
),
),
);
}
}
class DashBoardBannersList extends StatelessWidget {
const DashBoardBannersList({Key? key, required this.dashboardBanners}) : super(key: key);
final List<DashBoardBanner> dashboardBanners;
#override
Widget build(BuildContext context) {
return Text(dashboardBanners[0].MsgId);
}
}
This error is caused because of the sound null safety
snapshot.data might be null for some requests so you can't access the array at a certain index cause it can be null.
If you know for sure snapshot.data exists you can use the ! operator to tell dart the variable is not null for sure like that:
snapshot.data![index];
You can also check if the data is null before accessing it like that:
if (snapshot.data != null) {
// do something with snapshot.data[index]
}
I recommed to read more about sound null safety here
Check the Firestore docs.
Inside snapshot.data, there's docs (every document of your collection).
The code is from there:
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _usersStream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("Loading");
}
return ListView(
children: snapshot.data!.docs.map((DocumentSnapshot document) {
Map<String, dynamic> data = document.data()! as Map<String, dynamic>;
return ListTile(
title: Text(data['full_name']),
subtitle: Text(data['company']),
);
}).toList(),
);
},
);
}
The code above shows how to convert every doc (type DocumentSnapshot) to a JSON format (that can be represented with Map<String, dynamic>). To access to the doc id, you'll access with document.id, because it isn't inside the document.data() method.
You wanna retrieve a list of DashBoardBanner but you forget initialize the futurebuilder by adding a ListView.builder().
Try to use the following code idea :
FutureBuilder(
future: getBannerDataFromServer(http.Client()),
builder: (context, AsyncSnapshot snapshot) {
print(snapshot.hasData);
if (snapshot.hasError) {
return CircularProgressIndicator();
} else if (snapshot.hasData) {
return Expanded(
child: ListView.builder(
scrollDirection: Axis.vertical,
itemCount: snapshot.data!.length,
itemBuilder: (BuildContext context, int index) {
var data = snapshot.data![index];
return DashBoardBannersList(dashboardBanners: data);
},),
),},
},)

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) {

Flutter: StreamProvider returns null on list even when containing data

I am trying to establish a stream between the Firestore server and my app. I am trying to retrieve a list of notes from the server to display in a ListView widget. However, I keep getting an error saying that the List that is being returned is null. Through debugging, I noticed that the snapshot does in fact contain the data I am trying to access, so something wrong is happening before the _notesFromSnapshot function is able to map each snapshot to a Note object, which leads me to believe that the app renders the ListView before the Stream has enough time to fetch all the data.
Edit: The list works if I change the StreamProvider to listen to QuerySnapshot instead of a list of notes, but I need the convenience of accessing custom objects.
NoteListScreen.dart
class NoteListScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return Scaffold(
appBar: NavBar(title: 'My notes'),
body: StreamProvider<List<Note>>.value(
value: DatabaseService(uid: user.uid).notes,
child: NoteList(),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).pushNamed('/note/create');
},
child: Icon(Icons.add),
),
);
}
}
NoteList.dart
class NoteList extends StatefulWidget {
#override
State<StatefulWidget> createState() => _NoteListState();
}
class _NoteListStateextends State<NoteList> {
#override
Widget build(BuildContext context) {
final notes = Provider.of<List<Note>>(context);
return ListView.builder(
itemCount: notes.length,
^^^^^^^^^^^^ Returns error: 'The getter 'length' was called on null.'
itemBuilder: (context, index) {
return Card(
child: NoteItem(
name: notes[index].name,
),
);
},
);
}
}
DatabaseService.dart
class DatabaseService {
final String uid;
DatabaseService({this.uid});
// collection reference
final CollectionReference _userCollection = Firestore.instance.collection('users');
.....
List<Note> _notesFromSnapshot(QuerySnapshot snapshot) {
return snapshot.documents.map((doc) {
return Note(
name: doc.data['name'],
);
});
}
Stream<List<Note>> get notes {
return _userCollection
.document(uid)
.collection('notes')
.snapshots()
.map(_notesFromSnapshot);
}
}
I fixed the problem by adding .toList() at the end of this function:
List<Note> _notesFromSnapshot(QuerySnapshot snapshot) {
return snapshot.documents.map((doc) {
return Note(
name: doc.data['name'],
);
}).toList();
}

Flutter : Prevent FutureBuilder always refresh every change screen

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.