In my Flutter app the user can create new tasks and see them in the homepage, but right now I am fetching all the tasks from Firebase at once, and I wish I could do that using infinite scroll. I googled how to do this, but I really couldn't figure it out.
In my API project I have the following:
async getTasksByFilter(filters: Array<IFilter>): Promise<Array<ITask>> {
let tasksUser: Array<ITask> = [];
let collectionQuery: Query<DocumentData> = this.db.collection(
this.taskCollection,
);
let query = collectionQuery;
return new Promise((resolve, reject) => {
filters.forEach(entry => {
switch (entry.searchType) {
case 'where':
query = query.where(
entry.field,
entry.condition as WhereFilterOp,
entry.value,
);
break;
default:
break;
}
});
query
.orderBy('createdAt', 'asc')
.get()
.then(query => {
if (query.docs.length > 0) {
query.docs.forEach(doc => {
let task: ITask = this.transformDate(doc.data());
tasksUser.push(task);
});
}
return resolve(tasksUser);
})
.catch(error => {
return reject(error);
});
});
}
In my app I use this function to fetch the tasks
Future<List<Task>> getUserTasks(String _extension, Filter filters) async {
final Response response =
await client.post(Uri.parse("$BASE_URL$_extension"),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(filters.toJson()),
);
Iterable l = jsonDecode(response.body);
List<Task> tasks = List<Task>.from(l.map((model) => Task.fromJson(model)));
return tasks;
}
So when the tasks page is opened, the cubit changes its state to InitTaskListState, start to fetch all the tasks data and show a loading spinner for the user. When its done the state changes to LoadedTaskListState and the task list is displayed.
This is my code for it:
BlocConsumer<TaskListCubit, TaskListState>(
listenWhen: (previous, current) =>
current is FatalErrorTaskListState,
listener: (context, state) {
if (state is FatalErrorTaskListState) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(state.title ?? 'Error'),
content: Text(state.message ?? 'Error'),
actions: <Widget>[
TextButton(
child: const Text('Ok'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
},
builder: (context, state) {
if (state is InitTaskListState ||
state is LoadingTaskListState) {
return const ProgressView(message: 'Loading tasks');
}
if (state is LoadedTaskListState) {
final tasks = state.tasks;
return taskList(context, cubit, state, tasks);
}
return const Text('Unknown error');
},
)
TabBarView taskList(BuildContext context, TaskListCubit cubit,
LoadedTaskListState state, List<Task> tasks) {
return TabBarView(
physics: const NeverScrollableScrollPhysics(),
children: List<Widget>.generate(
cubit.tabNames.length,
(int index) {
if (index == state.tabIndex) {
return Center(
child: tasks.isEmpty
? setEmptyListText(state.tabName)
: Column(
children: [
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
final task = tasks[index];
return TaskCard(
task,
state.tabName,
state.tabType,
cubit,
onClick: () {
push(
context,
TaskDetailsContainer(task, state.tabType),
);
},
);
},
itemCount: tasks.length,
),
),
],
),
);
} else {
return const Text('');
}
},
),
);
}
Does someone can explain me how to implement the infinite scroll in my project?
I think this will give you a pretty good idea of how to do it:
https://medium.com/flutter-community/flutter-infinite-list-tutorial-with-flutter-bloc-2fc7a272ec67
What you need to do is that each time reach the bottom of the list you will increment the limit of your current query, which can be easily done with firebase.
In your api call you can do like this:
firestore.collection("products").limit(_myLimit).get();
Make sure to update your limit as you reach the bottom of the screen and change states accordingly. Your bloc could keep track of the current limit and then you just increase it as you scroll down.
Related
I have a general configuration screen, with a button that syncs the data
(...)
appBar: AppBar(
actions: [
Row(
children: [
const Text(ConfigurationsStringsUI.updateGeneral),
IconButton(
icon: const Icon(Icons.sync),
onPressed: () {
ref.read(listProductController.notifier).syncProducts();
ref.read(listEmployedController.notifier).syncEmployees();
},
),
],
)
],
),
(...)
In the case of products, it has a specific screen that is responsible for managing them, basically a CRUD. When I press the sync button, the idea is to connect to supabase and update the data. While this is happening display a loadign. The problem is that the loading does not appear.
products_page.dart
GetIt sl = GetIt.instance;
class CRUDProduct extends ConsumerWidget {
#override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop();
},
),
actions: [
IconButton(
onPressed: () {
ref.read(listProductController.notifier).syncProducts();
},
icon: const Icon(Icons.update),
)
],
),
floatingActionButton: ref.watch(isVisibleFabProducts)
? FloatingActionButton(
onPressed: () {
showDialog(
context: scaffoldKey.currentContext!,
builder: (context) => AddProductDialog(),
);
},
child: const Icon(Icons.fastfood),
)
: null,
body: ref.watch(listProductController).when(
data: (products) {
if (products.isEmpty) {
return const Center(
child: Text(ProductStringsUI.emptyList),
);
} else {
return NotificationListener<UserScrollNotification>(
onNotification: (notification) {
if (notification.direction == ScrollDirection.forward) {
ref.read(isVisibleFabProducts.notifier).state = true;
}
if (notification.direction == ScrollDirection.reverse) {
ref.read(isVisibleFabProducts.notifier).state = false;
}
return true;
},
child: ListView.separated(
shrinkWrap: true,
itemBuilder: (context, index) {
return ItemProductList(product: products[index]);
},
separatorBuilder: (_, __) => const Divider(
color: Colors.grey,
),
itemCount: products.length),
);
}
},
error: (error, stackTrace) {
return const Center(
child: Text(ProductStringsUI.errorList),
);
},
loading: () {
return const Center(child: CircularProgressIndicator());
},
));
}
}
Product provider:
final listProductController =
StateNotifierProvider<ProductController, AsyncValue<List<LocalProduct>>>(
(ref) => ProductController(ref));
product_controller.dart
class ProductController extends StateNotifier<AsyncValue<List<LocalProduct>>> {
ProductController(this._ref) : super(const AsyncValue.loading()) {
getProducts();
}
final Ref _ref;
Future<void> getProducts() async {
try {
final employees = await sl.get<ListProductUseCase>().getProducts();
if (mounted) {
state = AsyncValue.data(employees);
}
} catch (e) {
state = AsyncValue.error(e, StackTrace.current);
}
}
Future<void> syncProducts() async {
try {
_ref.read(listCategoryController.notifier).state =
const AsyncValue.loading();
_ref.read(listEmployedController.notifier).state =
const AsyncValue.loading();
state = const AsyncValue.loading();
await _ref.read(listCategoryController.notifier).syncCategory();
final employees = await sl.get<SyncProductUseCase>().syncProducts();
state.whenData((value) {
if (mounted) {
state = AsyncValue.data([...value, ...employees]);
}
});
_ref.invalidate(listProductController);
} catch (e) {
state = AsyncValue.error(e, StackTrace.current);
}
}
}
In the case of products, it has a specific screen that is responsible for managing them, basically a CRUD. When I press the sync button, the idea is to connect to supabase and update the data. While this is happening display a loadign. The problem is that the loading does not appear. There are two scenarios:
1-I open the app, I press the sync button on the configuration screen, I enter the screen in charge of managing the products, I see the loaded products, and at the moment it updates me with the new data, when I should see the loading and then the new ones data.
In this scenario is where my biggest doubt about the strange behavior is.
2-I open the app, I enter the screen in charge of managing the products, I go to the configuration screen, I press sync, and in that case if I go to enter if the loading appears
The same goes for employees.
When you have an async provider in Riverpod, you should tell it to load:
Future<void> addTopic(String name) async {
state = const AsyncValue.loading(); // Here
state = await AsyncValue.guard(() async { // And then you guard the value
// Inside the brackets do all the logic of the function
final currentId = ref.watch(currentSubjectProvider);
final topicRepo = ref.watch(topicRepoProvider);
await topicRepo.addTopic(currentId!, name);
return _getTopics();
});
}
This example of mine, is a real project I am working on, and this is loading as expected, but I should mention that I am using the Riverpod generator, so if the generator did something, I am unaware of it.
If you set the state to loading and guard the value, all listerners of that provider should be loading correctly.
I have two screens, the first one fetches the data from the database and shows it on the screen. And the second screen creates a new user/course and sends it to the database. But when I use a pop to go back to the first screen, I want to update its data. How can I do this?
How can I update the screen data when returning from the second screen?
First Screen:
class CourseList extends StatefulWidget {
const CourseList({super.key});
#override
State<CourseList> createState() => _CourseListState();
}
class _CourseListState extends State<CourseList> {
Future<List<Course>?> listCourses = Future.value([]);
#override
initState() {
super.initState();
Future.delayed(Duration.zero, () {
setState(() {
var course = CoursesData();
listCourses = course.getCourses();
});
});
}
#override
Widget build(BuildContext context) {
var futureBuilder = FutureBuilder(
future: listCourses,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return createScreen(context, snapshot);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
return const Center(child: CircularProgressIndicator());
});
return futureBuilder;
}
Widget createScreen(BuildContext context, AsyncSnapshot snapshot) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context)
.pushNamed('/courseForm', arguments: null);
},
child: const Text("Adicionar Curso"))
],
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.length,
itemBuilder: ((context, index) {
return CourseTile(snapshot.data[index]);
}),
))
],
);
}
}
Second screen:
IconButton(
onPressed: () {
final isValid = _fomr.currentState!.validate();
Navigator.pop(context, true);
},
icon: const Icon(Icons.save))
EDITED:
ElevatedButton(
onPressed: () async {
var result = await Navigator.of(context)
.pushNamed('/courseForm', arguments: null);
if (result != null) {
setState(() {
var course = CoursesData();
listCourses = course.getCourses();
});
}
},
child: const Text("Adicionar Curso"))
You can navigate to second screen like this:
var result = await Navigator.of(context).push(MaterialPageRoute(builder: (context) => SecondScreen()));
if(result != null && result){
setState(() {
var course = CoursesData();
listCourses = course.getCourses();
});
}
then in your second screen when you pop like this
Navigator.pop(context, true);
as you are doing right now you pass back a bool variable to first screen which act like a flag and with that you can find when it is the time to reload the data. Also don forgot to await for the Navigator because of that you can receive the data from your second screen. Now when you come back from second screen, its going to update the ui.
The documentation covers this example in detail. So I'll paste in the relevant part:
Future<void> _navigateAndDisplaySelection(BuildContext context) async {
// Navigator.push returns a Future that completes after calling
// Navigator.pop on the Selection Screen.
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SelectionScreen()),
);
The key is in this line:
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SelectionScreen()),
);
The result of navigator.push will be stored in result.
So in your case, you should do this after getting the result (as #eamirho3ein has answered first, so I'm explaining) :
setState(() {
var course = CoursesData();
listCourses = course.getCourses();
});
Take a look at the Flutter example for returning data
The pagebuilder in GoRoute has the the only place I can grab the state.params, I want to update my StateNotifier when the route changes if it is different.
final LibraryRoutes = Provider<RouteBase>((ref) {
return ShellRoute(
navigatorKey: _LibraryKey,
builder: (context, state, child) {
return LibraryHomePage(
child: child,
);
},
routes: [
//dashboard
GoRoute(
path: "/library/:LibraryKey/Dashboard",
pageBuilder: (context, state) {
final String passedValue = state.params['LibraryKey']!;
final newLibrary = LibraryReadDto(LibraryKey: passedValue);
//this line error's out because its during lifecycle method
ref.read(AsyncLibraryProvidor.notifier).updateLibrary(newLibrary);
final AsyncLibraryNotifier = ref.watch(AsyncLibraryProvidor);
return AsyncLibraryNotifier.when(data: (data) {
return NoTransitionPage(
child: Text("dashboard"),
);
}, error: (_, __) {
return NoTransitionPage(
child: const Text("An error occurred"),
);
}, loading: () {
return NoTransitionPage(
child: CircularProgressIndicator(),
);
});
}),
]);
});
I've managed to put the update in a future to get around the problem is there a more elegant solution as the library is used in many different places.
GoRoute(
path: "/library/:LibraryKey/Dashboard",
pageBuilder: (context, state) {
if (ref.read(LibraryProvider) == null) {
final String passedValue = state.params['LibraryKey']!;
try {
//can't update during widget tree delay by 150 micoseconds so we can update after future
Future.delayed(
const Duration(microseconds: 150),
() {
final newLibrary = LibraryReadDto(LibraryKey: passedValue);
ref.read(LibraryProvider.notifier).updateLibrary(newLibrary);
},
);
} on Exception catch (ex) {
print(ex.toString());
}
}
return NoTransitionPage(
child: Text("dashboard"),
);
}),
Right now It's not possible to use await within the page Builder,
Write Some Logic in your page side to prefetch the values before the page renders.
StreamController<UserModel> _controller = StreamController<UserModel>.broadcast();
getFriendsName() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var token = prefs.getString("token");
var username = prefs.getString("username");
final response = await http
.post(Uri.parse("http://192.168.0.111:3000/friendNames"),
headers: {
"Content-Type": "application/json",
"authorization": "$token"
},
body: jsonEncode({"username": username}))
.then((value) => value)
.catchError((e) => print(e));
UserModel usermodel = UserModel.fromJson(json.decode(response.body));
return _controller.sink.add(usermodel);
//return usermodel;
}
i created an infinite loop that reload data every 0 second
void initState() {
Timer.periodic(Duration(seconds: 0), (_) => getFriendsName());
super.initState();
}
here is the stream builder
StreamBuilder<UserModel>( /
stream: _controller.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
if (snapshot.data!.msg == "no friends to chat with") {
return Center(child: Text("No friends found."));
} else {
return ListView.builder(
itemCount: snapshot.data!.msg.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(snapshot.data!.msg[index]),
subtitle:
Text("${snapshot.data!.msg1![index]}"),
leading: CircleAvatar(
backgroundColor: Colors.orange,
backgroundImage: NetworkImage(
"http://192.168.0.111:3000/getImage/${snapshot.data!.msg[index]}?v=${Random().nextInt(100)}",
),
),
onTap: () async {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return (ChatRoom(
snapshot.data!.msg[index]));
}));
},
);
});
}
} else {
return Center(
child: CircularProgressIndicator(),
);
}
}),
What im asking for is a way to use streambuilder and listen to changes without the need of looping the stream infinitly.
so any propositions
i solved the problem by changing Timer to Stream and adding as .asBroadcastStream()
and it should look like this
return Stream.periodic(Duration(seconds: 0))
.asyncMap((event) => getFriendsName0()).asBroadcastStream();
My dashboard code looks like this,
Here I am doing get req in getReport method, I have added the RefreshIndicator in the code which when pulled down inside container should do the refresh, there I am calling my getData(), But I am not getting the refreshed content, I am adding my code below, let me know if anywhere I made a mistake.
below my dashboard.dart
class Window extends StatefulWidget {
#override
_WindowState createState() => _WindowState();
}
class _WindowState extends State<Window> {
Future reportList;
#override
void initState() {
super.initState();
reportList = getReport();
}
Future<void> getReport() async {
http.Response response =
await http.get(reportsListURL, headers: {"token": "$token"});
switch (response.statusCode) {
case 200:
String reportList = response.body;
var collection = json.decode(reportList);
return collection;
case 403:
break;
case 401:
return null;
default:
return 1;
}
}
getRefreshScaffold() {
return Center(
child: RaisedButton(
onPressed: () {
setState(() {
reportList = getReport();
});
},
child: Text('Refresh, Network issues.'),
),
);
}
getDashBody(var data) {
double maxHeight = MediaQuery.of(context).size.height;
return Column(
children: <Widget>[
Container(
height: maxHeight - 800,
),
Container(
margin: new EdgeInsets.all(0.0),
height: maxHeight - 188,
child: new Center(
child: new RefreshIndicator( //here I am adding the RefreshIndicator
onRefresh:getReport, //and calling the getReport() which hits the get api
child: createList(context, data),
),),
),
],
);
}
Widget createList(BuildContext context, var data) {
Widget _listView = ListView.builder(
itemCount: data.length,
itemBuilder: (context, count) {
return createData(context, count, data);
},
);
return _listView;
}
createData(BuildContext context, int count, var data) {
var metrics = data["statistic_cards"].map<Widget>((cardInfo) {
var cardColor = getColorFromHexString(cardInfo["color"]);
if (cardInfo["progress_bar"] != null && cardInfo["progress_bar"]) {
return buildRadialProgressBar(
context: context,
progressPercent: cardInfo["percentage"],
color: cardColor,
count: cardInfo["value"],
title: cardInfo["title"],
);
} else {
return buildSubscriberTile(context, cardInfo, cardColor);
}
}).toList();
var rowMetrics = new List<Widget>();
for (int i = 0; i < metrics.length; i += 2) {
if (i + 2 < metrics.length)
rowMetrics.add(Row(children: metrics.sublist(i, i + 2)));
else
rowMetrics.add(Row(children: [metrics[metrics.length - 1], Spacer()]));
}
return SingleChildScrollView(
child: LimitedBox(
// maxHeight: MediaQuery.of(context).size.height / 1.30,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: rowMetrics,
),
),
);
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: reportList,
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
return Center(
child: CircularProgressIndicator(),
);
case ConnectionState.done:
var data = snapshot.data;
if (snapshot.hasData && !snapshot.hasError) {
return getDashBody(data);
} else if (data == null) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text("Timeout! Log back in to continue"),
Padding(
padding: EdgeInsets.all(25.0),
),
RaisedButton(
onPressed: () {
setState(() {
token = null;
});
Navigator.of(context).pushReplacement(
CupertinoPageRoute(
builder: (BuildContext context) => LoginPage()),
);
},
child: Text('Login Again!'),
),
],
),
);
} else {
getRefreshScaffold();
}
}
},
);
}
}
Basic Example
Below is a State class of a StatefulWidget, where:
a ListView is wrapped in a RefreshIndicator
numbersList state variable is its data source
onRefresh calls _pullRefresh function to update data & ListView
_pullRefresh is an async function, returning nothing (a Future<void>)
when _pullRefresh's long running data request completes, numbersList member/state variable is updated in a setState() call to rebuild ListView to display new data
import 'package:flutter/material.dart';
import 'dart:math';
class PullRefreshPage extends StatefulWidget {
const PullRefreshPage();
#override
State<PullRefreshPage> createState() => _PullRefreshPageState();
}
class _PullRefreshPageState extends State<PullRefreshPage> {
List<String> numbersList = NumberGenerator().numbers;
#override
Widget build(BuildContext context) {
return Scaffold(
body: RefreshIndicator(
onRefresh: _pullRefresh,
child: ListView.builder(
itemCount: numbersList.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(numbersList[index]),
);
},),
),
);
}
Future<void> _pullRefresh() async {
List<String> freshNumbers = await NumberGenerator().slowNumbers();
setState(() {
numbersList = freshNumbers;
});
// why use freshNumbers var? https://stackoverflow.com/a/52992836/2301224
}
}
class NumberGenerator {
Future<List<String>> slowNumbers() async {
return Future.delayed(const Duration(milliseconds: 1000), () => numbers,);
}
List<String> get numbers => List.generate(5, (index) => number);
String get number => Random().nextInt(99999).toString();
}
Notes
If your async onRefresh function completes very quickly, you may want to add an await Future.delayed(Duration(seconds: 2)); after it, just so the UX is more pleasant.
This gives time for the user to complete a swipe / pull down gesture & for the refresh indicator to render / animate / spin indicating data has been fetched.
FutureBuilder Example
Here's another version of the above State<PullRefreshPage> class using a FutureBuilder, which is common when fetching data from a Database or HTTP source:
class _PullRefreshPageState extends State<PullRefreshPage> {
late Future<List<String>> futureNumbersList;
#override
void initState() {
super.initState();
futureNumbersList = NumberGenerator().slowNumbers();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<List<String>>(
future: futureNumbersList,
builder: (context, snapshot) {
return RefreshIndicator(
child: _listView(snapshot),
onRefresh: _pullRefresh,
);
},
),
);
}
Widget _listView(AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(snapshot.data[index]),
);
},);
}
else {
return Center(
child: Text('Loading data...'),
);
}
}
Future<void> _pullRefresh() async {
List<String> freshNumbers = await NumberGenerator().slowNumbers();
setState(() {
futureNumbersList = Future.value(freshNumbers);
});
}
}
Notes
slowNumbers() function is the same as in the Basic Example above, but the data is wrapped in a Future.value() since FutureBuilder expects a Future, but setState() should not await async data
according to RĂ©mi, Collin & other Dart/Flutter demigods it's good practice to update Stateful Widget member variables inside setState() (futureNumbersList in FutureBuilder example & numbersList in Basic example), after its long running async data fetch functions have completed.
see https://stackoverflow.com/a/52992836/2301224
if you try to make setState async, you'll get an exception
updating member variables outside of setState and having an empty setState closure, may result in hand-slapping / code analysis warnings in the future
Not sure about futures, but for refresh indicator you must return a void so
Use something like
RefreshIndicator(
onRefresh: () async {
await getData().then((lA) {
if (lA is Future) {
setState(() {
reportList = lA;
});
return;
} else {
setState(() {
//error
});
return;
}
});
return;
},
Try this and let me know!
EDIT:
Well, then just try this inside you refresh method
setState(() {
reportList = getReport();
});
return reportList;
Try this:
onRefresh: () {
setState(() {});
}}
instead of onRefresh:getReport
reportList field is Future which returns its value once. So, when you call getReport again it changes nothing. Actually, more correctly it'll be with Stream and StreamBuilder instead of Future and FutureBuilder. But for this code it can be shortest solution
Easy method: you can just use Pull Down to Refresh Package - https://pub.dev/packages/pull_to_refresh
In Non-scrollable list view, RefreshIndicator does not work, so you have to wrap your widget with Stack for implementing pull down to refresh.
RefreshIndicator(
onRefresh: () {
// Refresh Functionality
},
child: Stack(
children: [
ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
children: [
SizedBox(
height: MediaQuery.of(context).size.height,
)
],
),
// Your Widget
],
);
),
I am working on a huge project which contains CustomScrollView, NestedScrollView, ListView, etc I tried every answer above and all of the answers use RefreshIndicator from flutter SDK. It doesn't work entirely with my app because I also have horizontal scroll views. So in order to implement it I had to use NestedScrollView on almost every screen. Then I came to know about liquid_pull_to_refresh, applied it to the top widget, and WOLAAH! If you need a separate logic for each screen then use it at the top of each screen but in my case, I'm refreshing the whole project's data.