I am trying to implement pagination in my Flutter app but this is the first time I have done it. My idea was to create a Stream of data that updates each time the user reaches the bottom of a list. I have failed to get this working. My current code has the logic for getting the new data and adding it to the existing data, but right now it's not even returning the first range of data so the snapshots are empty. As for the pagination functionality, I tried to use a ChangeNotifier to notify the Stream to update, but I don't know if that is working. Please take a look at the code below and let me know what should be changed.
The DataProvider class:
class DataProvider extends ChangeNotifier{
DataProvider() : super();
static var changeController = ChangeNotifier();
static void reload() {
changeController.notifyListeners();
}
static int lastRange = 0;
static List data = [];
static Stream<List> paginatedUsersAndPosts() async* {
List<UserSearchResult> usersList = data.first;
List<Post> postsList = data.last;
print('Notifier');
changeController.addListener(() async {
print('Change notified, getting data');
List<int> range() {
if (lastRange == 0) {
lastRange = 10;
return [0, 10];
} else {
// Example 0, 10 => 11, 20
int newMin = lastRange + 1;
int newMax = lastRange + 10;
lastRange = newMax;
return [newMin, newMax];
}
}
List<Map<String, dynamic>> postsDocs = await Supabase.db
.from('posts')
.select()
.order('date', ascending: false)
.range(range().first, range().last);
List<Post> newPostsList =
postsDocs.map((postDoc) => Post.fromJson(postDoc)).toList();
newPostsList.forEach((post) async {
postsList.add(post);
if (usersList.where((u) => u.uid == post.uid).isNotEmpty) {
Map<String, dynamic> userDoc =
await Supabase.db.from('profiles').select().single();
ProfileInfoObject profileInfo = ProfileInfoObject.fromJson(userDoc);
print('New profile: $profileInfo');
Profile profile = Profile(profileInfo, []);
profile.posts.add(post);
List blockedUsers = userDoc['blockedUsers'] as List;
UserSearchResult user = (UserSearchResult(
profile, userDoc['uid'].toString(), blockedUsers));
usersList.add(user);
}
});
});
yield [usersList, postsList];
}
}
The main widget that uses the stream:
class FloatingTabBarView extends StatefulWidget {
const FloatingTabBarView({Key? key}) : super(key: key);
#override
State<FloatingTabBarView> createState() => _FloatingTabBarViewState();
}
class _FloatingTabBarViewState extends State<FloatingTabBarView> {
#override
void initState() {
PermissionsService.checkPermissions(context);
DataProvider.reload();
super.initState();
}
Stream<List> stream = DataProvider.paginatedUsersAndPosts();
return StreamBuilder<List>(
stream: stream,
builder: (context, AsyncSnapshot<List<dynamic>> snapshot) {
...
});
}
#override
Widget build(BuildContext context) {
return floatingTabBarPageView();
}
}
Please take a look at pull_to_refresh package. This allows to implement pull to refresh and incrementally load data. Use the RefreshController to update when the data is refreshed or loaded.
Instantiate RefreshController
Wrap the ListView with SmartRefresher
Implement the onLoading callback to fetch data incrementally
Update the RefreshController on completion or error.
The package has a good usage example, do check it out.
The issue was the way I was creating the stream. Since I made the Stream a function, there was no way for me to call that function to reload. Instead, I moved the code to a static void that I can call from any page, created a StreamController, and used controller.add(data) at the end of the void which allows me to update the stream with the new data.
Related
I'm fairly new to Flutter providers. I use Riverpod.
I have a Future provider that provide some data from a JSON file - in the future it will be from a API response.
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/pokemon.dart';
final pokemonProvider = FutureProvider<List<Pokemon>>((ref) async {
var response =
await rootBundle.loadString('assets/mock_data/pokemons.json');
List<dynamic> data = jsonDecode(response);
return List<Pokemon>.from(data.map((i) => Pokemon.fromMap(i)));
});
I subscribe to with ref.watch in ConsumerState widgets, e.g.:
class PokemonsPage extends ConsumerStatefulWidget {
const PokemonsPage({Key? key}) : super(key: key);
#override
ConsumerState<PokemonsPage> createState() => _PokemonsPageState();
}
class _PokemonsPageState extends ConsumerState<PokemonsPage> {
#override
Widget build(BuildContext context) {
final AsyncValue<List<Pokemon>> pokemons =
ref.watch(pokemonProvider);
return pokemons.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
data: (pokemons) {
return Material(
child: ListView.builder(
itemCount: pokemons.length,
itemBuilder: (context, index) {
Pokemon pokemon = pokemons[index];
return ListTile(
title: Text(pokemon.name),
);
},
));
},
);
}
}
But in that case, what is the best practice to write/update data to the JSON file/API?
It seems providers are used for reading/providing data, not updating it, so I'm confused.
Should the same provider pokemonProvider be used for that? If yes, what is the FutureProvider method that should be used and how to call it? If not, what is the best practice?
I am new to riverpod too but I'll try to explain the approach we took.
The examples with FutureProviders calling to apis are a little bit misleading for me, because the provider only offers the content for a single api call, not access to the entire api.
To solve that, we found the Repository Pattern to be very useful. We use the provider to export a class containing the complete api (or a mock one for test purposes), and we control the state (a different object containing the different situations) to manage the responses and updates.
Your example would be something like this:
First we define our state object:
enum PokemonListStatus { none, error, loaded }
class PokemonListState {
final String? error;
final List<Pokemon> pokemons;
final PokemonListStatus status;
const PokemonListState.loaded(this.pokemons)
: error = null,
status = PokemonListStatus.loaded,
super();
const PokemonListState.error(this.error)
: pokemons = const [],
status = PokemonListStatus.error,
super();
const PokemonListState.initial()
: pokemons = const [],
error = null,
status = PokemonListStatus.none,
super();
}
Now our provider and repository class (abstract is optional, but let's take that approach so you can keep the example for testing):
final pokemonRepositoryProvider =
StateNotifierProvider<PokemonRepository, PokemonListState>((ref) {
final pokemonRepository = JsonPokemonRepository(); // Or ApiRepository
pokemonRepository.getAllPokemon();
return pokemonRepository;
});
///
/// Define abstract class. Useful for testing
///
abstract class PokemonRepository extends StateNotifier<PokemonListState> {
PokemonRepository()
: super(const PokemonListState.initial());
Future<void> getAllPokemon();
Future<void> addPokemon(Pokemon pk);
}
And the implementation for each repository:
///
/// Class to manage pokemon api
///
class ApiPokemonRepository extends PokemonRepository {
ApiPokemonRepository() : super();
Future<void> getAllPokemon() async {
try {
// ... calls to API for retrieving pokemon
// updates cached list with recently obtained data and call watchers.
state = PokemonListState.loaded( ... );
} catch (e) {
state = PokemonListState.error(e.toString());
}
}
Future<void> addPokemon(Pokemon pk) async {
try {
// ... calls to API for adding pokemon
// updates cached list and calls providers watching.
state = PokemonListState.loaded([...state.pokemons, pk]);
} catch (e) {
state = PokemonListState.error(e.toString());
}
}
}
and
///
/// Class to manage pokemon local json
///
class JsonPokemonRepository extends PokemonRepository {
JsonPokemonRepository() : super();
Future<void> getAllPokemon() async {
var response =
await rootBundle.loadString('assets/mock_data/pokemons.json');
List<dynamic> data = jsonDecode(response);
// updates cached list with recently obtained data and call watchers.
final pokemons = List<Pokemon>.from(data.map((i) => Pokemon.fromMap(i)));
state = PokemonListState.loaded(pokemons);
}
Future<void> addPokemon(Pokemon pk) async {
// ... and write json to disk for example
// updates cached list and calls providers watching.
state = PokemonListState.loaded([...state.pokemons, pk]);
}
}
Then in build, your widget with a few changes:
class PokemonsPage extends ConsumerStatefulWidget {
const PokemonsPage({Key? key}) : super(key: key);
#override
ConsumerState<PokemonsPage> createState() => _PokemonsPageState();
}
class _PokemonsPageState extends ConsumerState<PokemonsPage> {
#override
Widget build(BuildContext context) {
final statePokemons =
ref.watch(pokemonRepositoryProvider);
if (statePokemons.status == PokemonListStatus.error) {
return Text('Error: ${statePokemons.error}');
} else if (statePokemons.status == PokemonListStatus.none) {
return const CircularProgressIndicator();
} else {
final pokemons = statePokemons.pokemons;
return Material(
child: ListView.builder(
itemCount: pokemons.length,
itemBuilder: (context, index) {
Pokemon pokemon = pokemons[index];
return ListTile(
title: Text(pokemon.name),
);
},
));
}
}
}
Not sure if this is the best approach but it is working for us so far.
you can try it like this:
class Pokemon {
Pokemon(this.name);
final String name;
}
final pokemonProvider =
StateNotifierProvider<PokemonRepository, AsyncValue<List<Pokemon>>>(
(ref) => PokemonRepository(ref.read));
class PokemonRepository extends StateNotifier<AsyncValue<List<Pokemon>>> {
PokemonRepository(this._reader) : super(const AsyncValue.loading()) {
_init();
}
final Reader _reader;
Future<void> _init() async {
final List<Pokemon> pokemons;
try {
pokemons = await getApiPokemons();
} catch (e, s) {
state = AsyncValue.error(e, stackTrace: s);
return;
}
state = AsyncValue.data(pokemons);
}
Future<void> getAllPokemon() async {
state = const AsyncValue.loading();
/// do something...
state = AsyncValue.data(pokemons);
}
Future<void> addPokemon(Pokemon pk) async {}
Future<void> updatePokemon(Pokemon pk) async {}
Future<void> deletePokemon(Pokemon pk) async {}
}
I'm completely stuck with the task below.
So, the idea is to solve these steps using Riverpod
Fetch data from db with some kind of Future async while pausing the app (display SomeLoadingPage() etc.)
Once the data has loaded:
2.1 initialize multiple global StateNotifierProviders which utilize the data in their constructors and can further be used throughout the app with methods to update their states.
2.2 then show MainScreen() and the rest of UI
So far I've tried something like this:
class UserData extends StateNotifier<AsyncValue<Map>> { // just <Map> for now, for simplicity
UserData() : super(const AsyncValue.loading()) {
init();
}
Future<void> init() async {
state = const AsyncValue.loading();
try {
final HttpsCallableResult response =
await FirebaseFunctions.instance.httpsCallable('getUserData').call();
state = AsyncValue.data(response.data as Map<String, dynamic>);
} catch (e) {
state = AsyncValue.error(e);
}}}
final userDataProvider = StateNotifierProvider<UserData, AsyncValue<Map>>((ref) => UserData());
final loadingAppDataProvider = FutureProvider<bool>((ref) async {
final userData = await ref.watch(userDataProvider.future);
return userData.isNotEmpty;
});
class LoadingPage extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
return FutureBuilder(
future: ref.watch(loadingAppDataProvider.future),
builder: (ctx, AsyncSnapshot snap) {
// everything here is simplified for the sake of a question
final Widget toReturn;
if (snap.connectionState == ConnectionState.waiting) {
toReturn = const SomeLoadingPage();
} else {
snap.error != null
? toReturn = Text(snap.error.toString())
: toReturn = const SafeArea(child: MainPage());
}
return toReturn;},);}}
I intentionally use FutureBuilder and not .when() because in future i may intend to use Future.wait([]) with multiple futures
This works so far, but the troubles come when I want to implement some kind of update() methods inside UserData and listen to its variables through the entire app. Something like
late Map userData = state.value ?? {};
late Map<String, dynamic> settings = userData['settings'] as Map<String, dynamic>;
void changeLang(String lang) {
print('change');
for (final key in settings.keys) {
if (key == 'lang') settings[key] = lang;
state = state.whenData((data) => {...data});
}
}
SomeLoadingPage() appears on each changeLang() method call.
In short:
I really want to have several StateNotifierProviders with the ability to modify their state from the inside and listen to it from outside. But fetch the initial state from database and make the intire app wait for this data to be fetched and these providers to be initilized.
So, I guess I figured how to solve this:
final futureExampleProvider = FutureProvider<Map>((ref) async {
final HttpsCallableResult response =
await FirebaseFunctions.instance.httpsCallable('getUserData').call();
return response.data as Map;
});
final exampleProvider = StateNotifierProvider<Example, Map>((ref) {
// we get AsyncValue from FutureNotifier
final data = ref.read(futureExampleProvider);
// and wait for it to load
return data.when(
// in fact we never get loading state because of FutureBuilder in UI
loading: () => Example({'loading': 'yes'}),
error: (e, st) => Example({'error': 'yes'}),
data: (data) => Example(data),
);
});
class LoadingPage extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
return FutureBuilder(
// future: ref.watch(userDataProvider.future),
future: ref.watch(futureExampleProvider.future),
builder: (ctx, AsyncSnapshot snap) {
final Widget toReturn;
if (snap.data != null) {
snap.error != null
? toReturn = Text(snap.error.toString())
: toReturn = const SafeArea(child: MainPage());
} else {
// this is the only 'Loading' UI the user see before everything get loaded
toReturn = const Text('loading');
}
return toReturn;
},
);
}
}
class Example extends StateNotifier<Map> {
Example(this.initData) : super({}) {
// here comes initial data loaded from FutureProvider
state = initData;
}
// it can be used further to refer to the initial data, kinda like cache
Map initData;
// this way we can extract any parts of initData
late Map aaa = state['bbb'] as Map
// this method can be called from UI
void ccc() {
// modify and update data
aaa = {'someKey':'someValue'};
// trigger update
state = {...state};
}
}
This works for me, at least on this level of complexity.
I'll leave question unsolved in case there are some better suggestions.
This is what I'm trying to achieve using flutter GetX package but not working properly.
I have a Firestore document, if the document is changed I want to call an api and keep the data up to date as observable.
The code below seems to work but initial screen shows null error then it shows the data.
I don't know how I can make sure both fetchFirestoreUser() and fetchApiData() (async methods) returns data before I move to the home screen.
GetX StateMixin seems to help with async data load problem but then I don't know how I can refresh the api data when the firestore document is changed.
I'm not sure if any other state management would be best for my scenario but I find GetX easy compared to other state management package.
I would very much appreciate if someone would tell me how I can solve this problem, many thanks in advance.
Auth Controller.
class AuthController extends SuperController {
static AuthController instance = Get.find();
late Rx<User?> _user;
FirebaseAuth auth = FirebaseAuth.instance;
var _firestoreUser = FirestoreUser().obs;
var _apiData = ProfileUser().obs;
#override
void onReady() async {
super.onReady();
_user = Rx<User?>(auth.currentUser);
_user.bindStream(auth.userChanges());
//get firestore document
fetchFirestoreUser();
//fetch data from api
fetchApiData();
ever(_user, _initialScreen);
//Refresh api data if firestore document has changed.
_firestoreUser.listen((val) {
fetchApiData();
});
}
Rx<FirestoreUser?> get firestoreUser => _firestoreUser;
_initialScreen(User? user) {
if (user == null) {
Get.offAll(() => Login());
} else {
Get.offAll(() => Home());
}
}
ProfileUser get apiData => _apiData.value;
void fetchFirestoreUser() async {
Stream<FirestoreUser> firestoreUser =
FirestoreDB().getFirestoreUser(_user.value!.uid);
_firestoreUser.bindStream(firestoreUser);
}
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
#override
void onDetached() {}
#override
void onInactive() {}
#override
void onPaused() {}
#override
void onResumed() {
fetchApiData();
}
}
Home screen
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: Obx(() =>
Text("username: " + AuthController.instance.apiData.username!))),
),
);
}
}
To be honest, I never used GetX so I'm not too familiar with that syntax.
But I can see from your code that you're setting some mutable state when you call this method:
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
Instead, a more robust solution would be to make everything reactive and immutable. You could do this by combining providers if you use Riverpod:
final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
final authService = ref.watch(authRepositoryProvider);
return authService.authStateChanges();
});
final apiDataProvider = FutureProvider.autoDispose<APIData?>((ref) {
final userValue = ref.watch(authStateChangesProvider);
final user = userValue.value;
if (user != null) {
// note: this should also be turned into a provider, rather than using a static method
return RemoteService.getProfile(user.uid);
} else {
// decide if it makes sense to return null or throw and exception when the user is not signed in
return Future.value(null);
}
});
Then, you can just use a ConsumerWidget to watch the data:
#override
Widget build(BuildContext context, WidgetRef ref) {
// this will cause the widget to rebuild whenever the auth state changes
final apiData = ref.watch(apiDataProvider);
return apiData.when(
data: (data) => /* some widget */,
loading: () => /* some loading widget */,
error: (e, st) => /* some error widget */,
);
}
Note: Riverpod has a bit of a learning curve (worth it imho) so you'll have to learn it how to use it first, before you can understand how this code works.
Actually the reason behind this that you put your controller in the same page that you are calling so in the starting stage of your page Get.put() calls your controller and because you are fetching data from the API it takes a few seconds/milliseconds to get the data and for that time your Obx() renders the error. To prevent this you can apply some conditional logic to your code like below :
Obx(() => AuthController.instance.apiData != null ? Text("username: " + AuthController.instance.apiData.username!) : CircularProgressIndicator())) :
I'm learning Flutter and there is something I cannot grasp my head around.
I implemented a Infinite scroll pagination, with a package (infine_scroll_pagination),
it works fine, but the data this Package is getting, comes from a Future call, which takes data from the WEB, and parses it in my Provider Class.
My issue is, the data that is loaded by the Infinite Scroll widget, cannot be accessed, in its state, anywhere else.
Example:
Let's take a contact list, that loads 10 contacts at a time:
class ContactsBody extends StatefulWidget {
#override
_ContactsBodyState createState() => _ContactsBodyState();
}
class _ContactsBodyState extends State<ContactsBody> {
static const _pageSize = 10;
final PagingController<int, Contact> pagingController =
PagingController(firstPageKey: 0);
#override
void initState() {
super.initState();
pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
}
Future<void> _fetchPage(int pageKey) async {
try {
final newItems = await ContactsService().fetchContactsPaged(pageKey, _pageSize);
final isLastPage = newItems.length < _pageSize;
if (isLastPage) {
pagingController.appendLastPage(newItems.contacts);
} else {
final nextPageKey = pageKey + 1;
pagingController.appendPage(newItems.contacts, nextPageKey);
}
} catch (error) {
pagingController.error = error;
}
}
#override
Widget build(BuildContext context) {
return ContactsList(pagingController);
}
#override
void dispose() {
pagingController.dispose();
super.dispose();
}
So basically this Infinite Scroll package, will fetch my contacts, 10 at a time, and here my ContactsService call:
Future<Contacts> fetchContactsPaged(int pageKey, int pageSize) async {
final response = await http.get(.....);
if (response.statusCode == 200) {
return Contacts.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load contacts');
}
}
And finally, as you can see here above, it initializes my Provider class (Contacts), using its factory method, "fromJson()", and returns the parsed data.
Now my Provider class:
class Contacts extends ChangeNotifier {
List<Contact> _contacts = <Contact>[];
Contacts();
factory Contacts.fromJson(final Map<String, dynamic> json) {
final Contacts contacts = Contacts();
if (json['data'] != null) {
json['data'].forEach((contact) {
contacts.add(Contact.fromJson(contact));
});
}
return contacts;
}
void add(final Contact contact) {
this._contacts.add(contact);
this.notifyListeners();
}
The problem I'm having here is, when the Inifinite Scroll listView is loaded, and for example I change the state of a single contact (contacts can be set as favorite for example),
How can I access the SAME instance of the Contacts() class, that the FUTURE call initialized, so that I can access the current state of the data in that class?
Of course if I were to POST my changes onto the API, and refetch the new values where I need them, I would get the updated state of my data, but I want to understand how to access the same instance here and make the current data available inside the app everywhere
EDIT : I removed the original answer to give a better sample of what the OP wants to achieve.
I made a repo on GitHub to try to show you what you want to achieve: https://github.com/Kobatsu/stackoverflow_66578191
There are a few confusing things in your code :
When to create instances of your objects (ContactsService, Contacts)
Provider usage
(Accessing the list of the pagingController ?)
Parsing a JSON / using a factory method
The repository results in the following :
When you update the list (by scrolling down), the yellow container is updated with the number of contacts and the number of favorites.
If you click on a Contact, it becomes a favorite and the yellow container is also updated.
I commented the repository to explain you each part.
Note: the Contacts class in your code became ContactProvider in mine.
The ContactsService class to make the API call :
class ContactsService {
static Future<List<Contact>> fetchContactsPaged(
int pageKey, int pageSize) async {
// Here, you should get your data from your API
// final response = await http.get(.....);
// if (response.statusCode == 200) {
// return Contacts.fromJson(jsonDecode(response.body));
// } else {
// throw Exception('Failed to load contacts');
// }
// I didn't do the backend part, so here is an example
// with what I understand you get from your API:
var responseBody =
"{\"data\":[{\"name\":\"John\", \"isFavorite\":false},{\"name\":\"Rose\", \"isFavorite\":false}]}";
Map<String, dynamic> decoded = json.decode(responseBody);
List<dynamic> contactsDynamic = decoded["data"];
List<Contact> listOfContacts =
contactsDynamic.map((c) => Contact.fromJson(c)).toList();
// you can return listOfContacts, for this example, I will add
// more Contacts for the Pagination plugin since my json only has 2 contacts
for (int i = pageKey + listOfContacts.length; i < pageKey + pageSize; i++) {
listOfContacts.add(Contact(name: "Name $i"));
}
return listOfContacts;
}
}
Usage of Provider :
Consumer<ContactProvider>(
builder: (_, foo, __) => Container(
child: Text(
"${foo.contacts.length} contacts - ${foo.contacts.where((c) => c.isFavorite).length} favorites"),
padding: EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
color: Colors.amber,
)),
Expanded(child: ContactsBody())
]),
)
Fetch page method in the ContactsBody class, where we add the contact to our ContactProvider :
Future<void> _fetchPage(int pageKey) async {
try {
// Note : no need to make a ContactsService, this can be a static method if you only need what's done in the fetchContactsPaged method
final newItems =
await ContactsService.fetchContactsPaged(pageKey, _pageSize);
final isLastPage = newItems.length < _pageSize;
if (isLastPage) {
_pagingController.appendLastPage(newItems);
} else {
final nextPageKey = pageKey + newItems.length;
_pagingController.appendPage(newItems, nextPageKey);
}
// Important : we add the contacts to our provider so we can get
// them in other parts of our app
context.read<ContactProvider>().addContacts(newItems);
} catch (error) {
print(error);
_pagingController.error = error;
}
}
ContactItem widget, in which we update the favorite statuts and notify the listeners :
class ContactItem extends StatefulWidget {
final Contact contact;
ContactItem({this.contact});
#override
_ContactItemState createState() => _ContactItemState();
}
class _ContactItemState extends State<ContactItem> {
#override
Widget build(BuildContext context) {
return InkWell(
child: Padding(child: Row(children: [
Expanded(child: Text(widget.contact.name)),
if (widget.contact.isFavorite) Icon(Icons.favorite)
]), padding: EdgeInsets.symmetric(vertical: 8, horizontal: 10),),
onTap: () {
// the below code updates the item
// BUT others parts of our app won't get updated because
// we are not notifying the listeners of our ContactProvider !
setState(() {
widget.contact.isFavorite = !widget.contact.isFavorite;
});
// To update other parts, we need to use the provider
context.read<ContactProvider>().notifyContactUpdated(widget.contact);
});
}
}
And the ContactProvider :
class ContactProvider extends ChangeNotifier {
final List<Contact> _contacts = [];
List<Contact> get contacts => _contacts;
void addContacts(List<Contact> newContacts) {
_contacts.addAll(newContacts);
notifyListeners();
}
void notifyContactUpdated(Contact contact) {
// You might want to update the contact in your database,
// send it to your backend, etc...
// Here we don't have these so we just notify our listeners :
notifyListeners();
}
}
I'm currently implementing pagination using pagewise package. But to use the pagecontroller, I have to define a static controller and static future function that will connect to my api using http. My problem is, I also need the current user id as a parameter in my API request retrieved using provider. And I also need the BuildContext to show dialog box on API request return. Is it ok to save the id and context globally or outside the class just like the sample code below? Please teach me how to do this the correct way.
int id;
BuildContext currentContext;
class MyWidgetView extends StatefulWidget {
#override
_MyWidgetViewState createState() => _MyWidgetViewState();
}
class _MyWidgetViewState extends State<MyWidgetView> {
bool _empty = false;
#override
void initState() {
super.initState();
currentContext = context;
id = Provider.of<User>(context, listen: false).id;
this._pageLoadController.addListener(() {
if (this._pageLoadController.noItemsFound) {
setState(() {
this._empty = this._pageLoadController.noItemsFound;
});
}
});
}
final _pageLoadController = PagewiseLoadController(
pageSize: PAGE_SIZE, pageFuture: (pageIndex) => getPage(pageIndex));
static Future<List<page>> getPage(int pageIndex) async {
final APIService _pageService = APIService();
final pages = await _pageService.getPage(id: id);
if (pages.error == false) {
return pages.data;
} else {
dialogBox(
title: 'Error',
message: pages.errorMessage,
context: currentContext,
isModal: true,
function: () => Navigator.pop(currentContext));
}
}
That static is a rabbit hole you should not follow. Just don't make it static in the first place.
PagewiseLoadController _pageLoadController;
#override
void initState() {
super.initState();
_pageLoadController = PagewiseLoadController(pageSize: PAGE_SIZE, pageFuture: (pageIndex) => getPage(pageIndex));
// rest of your method here
}
Now your getPage method can be a normal, non-static class method.