Controller not updating UI in good time to display appropriate progress - flutter

In a personal flutter project am working with Getx state management whereby am fetching data from a parse server to then save it to my local db on the app. Everything works fine in the backend in terms of fetching the data and later saving it locally as you can see in my controller class below.
However once I get the data from the server I want to display my custom progress but the ui is frozed with the circular progress and when it unfreezes the activity that was to update the progress is done at 99%. Am using Drift a wrapper around Sqlite.
class ProgressController extends GetxController {
String classid = '';
int progressValue = 0;
List<Student>? students = [];
MyDatabase? db;
#override
void onInit() {
super.onInit();
}
#override
void onReady() {
super.onReady();
}
#override
void onClose() {
super.onClose();
}
/// Get the list of students from the server
Future<List<Student>?> fetchStudentsData() async {
bool isConnected = await hasReliableInternetConnectivity();
if (isConnected) {
final EventObject eventObject = await httpGet(
client: http.Client(),
url: '${ApiConstants.students}?where={"class":$classid}&order=studentid&limit=1000',
);
try {
if (eventObject.id == EventConstants.requestSuccessful) {
final StudentsResponse jsonResponse = StudentsResponse.fromJson(
json.decode(eventObject.response),
);
students = jsonResponse.results;
if (students!.isNotEmpty) {
saveStudentsData();
} else {
showToast(
text: "No data was found",
state: ToastStates.error,
);
}
}
} catch (exception) {
students = null;
}
} else {
showToast(
text: "You don't seem to have reliable internet connection",
state: ToastStates.error,
);
students = null;
}
update();
return students;
}
/// Save students data to the local db
Future<void> saveStudentsData() async {
for (int i = 0; i < students!.length; i++) {
int progress = (i / students!.length * 100).toInt();
if (progress > progressValue) {
progressValue = progress;
update();
}
db!.saveNewStudent(students![i]);
}
update();
Get.offAll(() => HomeView());
}
}
class ProgressView extends StatelessWidget {
final ProgressController controller = Get.put(ProgressController());
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
margin: const EdgeInsets.all(30),
child: GetBuilder<ProgressController>(
builder: (controller) => FutureBuilder<List<Student>?>(
future: controller.fetchStudentsData(),
builder: (BuildContext context,
AsyncSnapshot<List<Student>?> snapshot) {
if (snapshot.hasData) {
if (snapshot.data!.isNotEmpty) {
return LineProgress(
progressSize: 100,
progressVl: controller.progressValue,
borderColor: Colors.black,
progressColor: AppColors.primaryColor,
backgroundColor: AppColors.secondaryColor,
);
} else {
return const Text('You are not connected to the internet');
}
} else if (snapshot.hasError) {
return const Text('An unexpected error occured');
} else {
return const CircularProgress();
}
},
),
),
),
),
);
}
}
My ui in the save view is as shown in the 2 frames below
The first frame is when fetching data from the server and the second is when the data has been received and saved to the db. It simply freezes until when it's done.

Related

How to prevent making more URL request after an image is already downloaded via Internet?

var alreadyDdl = false;
getLogoUrl(context) async {
if(!alreadyDdl) {
final db = Localstore.instance;
final data = db.collection('inputs').doc("1").get();
var database = (await data)["content"].toString();
var form = new DGForm("project/getwebsitelogo", {"database": database});
var ret = await form.urlGET(context);
ResponseObject responseObject =
ResponseObject.fromJson(json.decode(ret.body));
print("hola");
var hola = (responseObject.datas[0][0].toString());
bandeauDuClient = hola;
print(hola);
return hola;
}
}
getLogoUrl(context).then((val) {
setState(() =>
logoUrl = val
);
alreadyDdl = true;
});
Will never display me the server downloaded image in the widget build
(logoUrl != null) ? Image.network(logoUrl): Image.asset('assets/none.png')
And so, when I removed all alreadyDdl variables from my code, It will make an http request every 15 miliseconds. I want to stop the http request once the image is really downloaded...
You can use a future builder to create widgets based on the latest snapshot of interaction with a Future. You can use it in combination with cached_network_image package as suggested above.
Here's a sample code that demonstrates so:
import "package:flutter/material.dart";
//Your other imports...
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _MyApp();
}
}
class _MyApp extends State<MyApp> {
var db;
#override
initState() {
db = Localstore.instance;
}
getLogoUrl(context) async {
final data = db.collection('inputs').doc("1").get();
var database = (await data)["content"].toString();
var form = new DGForm("project/getwebsitelogo", {"database": database});
var ret = await form.urlGET(context);
ResponseObject responseObject =
ResponseObject.fromJson(json.decode(ret.body));
print("hola");
var hola = (responseObject.datas[0][0].toString());
bandeauDuClient = hola;
print(hola);
return hola;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: getLogoUrl(context),
builder: (ctx, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// if we got our data
if (snapshot.hasData) {
return CachedNetworkImage(
imageUrl: snapshot.data,
progressIndicatorBuilder: (context, url, downloadProgress) =>
CircularProgressIndicator(value: downloadProgress.progress),
errorWidget: (context, url, error) => Icon(Icons.error),
);
} else {
// If we probably got an error check snapshot.hasError
return Center(
child: Text(
'${snapshot.error} occurred',
style: TextStyle(fontSize: 18),
),
);
}
} else {
return const CircularProgressIndicator();
}
},
),
);
}
}
Note: Never make networking calls in build method because build method is usually called 60 times per second to render. Make network calls in initState or in widgets like FutureBuilder which handle these things for you.

Can't call setState() after displaying CircularProgressIndicator()

I have the "standard" build code:
if (listOfLessons.length == 28)
return MaterialApp(...);
else
return MaterialApp(
theme: theme,
home: Scaffold(
appBar: AppBar(
title: Text('Getting Data from Belgium!'),
),
body: Container(
child: Center(
child: CircularProgressIndicator(),
)
),
),
);
I have this in initState()
#override
void initState() {
super.initState();
_listOfLessons = lessonController.getLessons();
setState(() {});
}
getLessons
List<Lesson> getLessons() {
List<Lesson> listOfLessons = [];
FirebaseLessonRepository lessonRepository = FirebaseLessonRepository();
var flistOfLessons = lessonRepository.fbgetLessons();
flistOfLessons.forEach((collectionSnapshot) {
collectionSnapshot.docs.forEach((documentSnapshot) {
FbLesson fbLesson = FbLesson.fromSnapshot(documentSnapshot);
List<DateTime> classOfferings = [];
fbLesson.classOfferings!.forEach((fbClassOffering) {
classOfferings.add(
DateTime.fromMillisecondsSinceEpoch(
fbClassOffering.seconds * 1000));
});
Lesson lesson = Lesson();
lesson.lessonCode = fbLesson.lessonCode;
lesson.prerequisite = fbLesson.prerequisite;
lesson.description = fbLesson.description;
lesson.online = fbLesson.online;
lesson.sequence = fbLesson.sequence;
lesson.classOfferings = classOfferings;
listOfLessons.add(lesson);
});
});
return listOfLessons;
}
FirebaseLessonRepository
class FirebaseLessonRepository {
static FirebaseFirestore _firebaseFirestore = FirebaseFirestore.instance;
final CollectionReference _lessonCollection
= FirebaseFirestore.instance.collection('Lessons');
Stream<QuerySnapshot> fbgetLessons() {
return _lessonCollection.snapshots();
}
}
From stepping through the debugger I see that in initState(),
_listOfLessons = lessonController.getLessons() returns before the query is finished loading the data, and then calls setState() right away.
I am not using Bloc or any other plugin other than firebase and dart:async;
My felling is that I should use a stream in getLessons() to trigger a setState();
something like this
class LessonsSpeaker {
final StreamController _streamController = StreamController<List<Lesson>>();
Stream<List<Lesson>> get stream {
return _streamController.stream as Stream<List<Lesson>>;
}
speak(List<Lesson> listOfLessons) async {
_streamController.add(listOfLessons);
_streamController.close();
}
}
But I am not sure how and where to invoke it.
Thanks for reading this long post.

Getx is not working properly with FutureBuilder for update list

I'm using the Getx controller in my project. I have create the controller for FutureBuilder for displaying list but .Obs is not set on Future Function. I'm sharing the code.
class PPHomeController extends GetxController {
Future<List<PPProductRenterModel>> listNearProduct;
// i want to set .Obs end of the "listNearProduct" but it's not working because of Future.
FetchNearProductList({#required int price}) async {
listNearProduct = CallGetNearProducts();// Http API Result
}
}
{
PPHomeController _homeController = Get.put(PPHomeController());
Widget mainProductListView() {
return FutureBuilder<List<PPProductRenterModel>>
(builder: (context, AsyncSnapshot<List<PPProductRenterModel>> projectSnap){
if(!projectSnap.hasData){
if(projectSnap.connectionState == ConnectionState.waiting){
return Container(
child: Loading(),
);
}
}
return ListView.builder(
itemCount: projectSnap.data.length,
itemBuilder: (context, index) {
PPProductRenterModel model = projectSnap.data[index];
PPPrint(tag: "CheckId",value: model.productId);
return ProductMainItemRow(model);
});
},
future: _homeController.listNearProduct,);
There is a cleaner way for implementing List in GetX without worrying about Type-Casting:
Instantiate it:
final myList = [].obs;
Assign it:
myList.assignAll( listOfAnyType );
(Reference) Flutter error when using List.value :
'value' is deprecated and shouldn't be used. List.value is deprecated.
use [yourList.assignAll(newList)]. Try replacing the use of the
deprecated member with the replacement.
Detailed code example
ProductController.dart
class ProductController extends GetxController {
final productList = [].obs;
#override
void onInit() {
fetchProducts();
super.onInit();
}
void fetchProducts() async {
var products = await HttpServices.fetchProducts();
if (products != null) {
productList.assignAll(products);
}
}
}
HttpServices.dart
class HttpServices {
static var client = http.Client();
static Future<List<Product>> fetchProducts() async {
var url = 'https://link_to_your_api';
var response = await client.get(url);
if (response.statusCode == 200) {
return productFromJson(response.body);
} else {
return null;
}
}
}
product.dart
class Product {
Product({
this.id,
this.brand,
this.title,
this.price,
....
});
....
}
Form the docs:
3 - The third, more practical, easier and preferred approach, just add
.obs as a property of your value:
final items = <String>[].obs;
Following that instruction, this should work:
final listNearProduct = Future.value(<PPProductRenterModel>[]).obs;
E.g.:
// controller
final list = Future.value(<String>[]).obs;
#override
void onInit() {
super.onInit();
fetchList();
}
Future<List<String>> callApi() async {
await Future.delayed(Duration(seconds: 2));
return ['test'];
}
void fetchList() async {
list.value = callApi();
}
// screen
#override
Widget build(BuildContext context) {
return GetX<Controller>(
init: Controller(),
builder: (controller) {
return FutureBuilder<List<String>>(
future: controller.list.value,
builder: (context, snapshot) {
if (snapshot.hasData) {
print(snapshot.data[0]); // Output: test
return Text(snapshot.data[0]);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner.
return CircularProgressIndicator();
},
);
},
);
};
You never actually call FetchNearProductList.
You need to call it in some place, preferably before the FutureBuilder uses that Future.

Provider rebuilds the widget, but nothing shows up until a "Hot restart"

I am building a flutter app and I get some data from a future, I also got the same data with a changenotifier. Well the logic is that while some object doesn't have data because its waiting on the future then display a spinning circle. I have already done this in the app and I have a widget called Loading() when the object has not received data. The problem I have run into is that I get the data, but it doesn't display anything.
the data displays correctly until I perform a hot refresh of the app. a capital R instead of a lowercase r. The difference is that it starts the app and deletes all aggregated data.
when this happens it seems that the data fills the object but I hypothesize that it is becoming not null meaning [] which is empty but not null and is displaying the data "too quickly" this in turn displays nothing for this widget until I restart "r" which shows me the above screenshot.
here is the offending code.
import 'package:disc_t/Screens/LoggedIn/Classes/classTile.dart';
import 'package:disc_t/Screens/LoggedIn/Classes/classpage.dart';
import 'package:disc_t/Screens/LoggedIn/Classes/classpageroute.dart';
import 'package:disc_t/Services/database.dart';
import 'package:disc_t/models/user.dart';
import 'package:disc_t/shared/loading.dart';
import 'package:flutter/material.dart';
import 'package:morpheus/page_routes/morpheus_page_route.dart';
import 'package:provider/provider.dart';
class ClassList extends StatefulWidget {
#override
_ClassListState createState() => _ClassListState();
}
class _ClassListState extends State<ClassList> {
#override
void initState() {
ClassDataNotifier classdatanotif =
Provider.of<ClassDataNotifier>(context, listen: false);
// final user = Provider.of<User>(context);
// getTheClasses(classdatanotif);
// List<ClassData> d = classes;
}
#override
Widget build(BuildContext context) {
ClassDataNotifier classdatanotif = Provider.of<ClassDataNotifier>(context);
List<ClassData> cData = Provider.of<List<ClassData>>(context);
bool rebd = false;
Widget checker(bool r) {
if (cData == null) {
return Loading();
} else {
if (rebd == false) {
setState(() {
rebd = true;
});
rebd = true;
return checker(rebd);
// return Text("Still Loading");
} else {
return PageView.builder(
scrollDirection: Axis.horizontal,
itemCount: cData.length,
// controller: PageController(viewportFraction: 0.8),
itemBuilder: (context, index) {
return Hero(
tag: cData[index],
child: GestureDetector(
onTap: () {
// Navigator.of(context).push(ClassPageRoute(cData[index]));
Navigator.push(
context,
MorpheusPageRoute(
builder: (context) =>
ClassPage(data: cData[index]),
transitionToChild: true));
},
child: ClassTile(
classname: cData[index].classname,
description: cData[index].classdescription,
classcode: cData[index].documentID,
),
),
);
});
}
}
}
return checker(rebd);
}
}
here is how the provider is implemented
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
// final DatabaseService ds = DatabaseService();
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<User>.value(
value: AuthService().user,
// child: MaterialApp(
// home: Wrapper(),
// ),
),
ChangeNotifierProvider<ClassDataNotifier>(
create: (context) => ClassDataNotifier(),
),
FutureProvider(
create: (context) => DatabaseService().fetchClassdata,
)
],
child: MaterialApp(home: Wrapper()),
);
}
}
and here is the function that is ran to get the data
Future<List<ClassData>> get fetchClassdata async {
QuerySnapshot snapshot = await classesCollection.getDocuments();
List<ClassData> _classList = List<ClassData>();
snapshot.documents.forEach((element) async {
QuerySnapshot pre = await Firestore.instance
.collection("Classes")
.document(element.documentID)
.collection("Pre")
.getDocuments();
List<Preq> _preList = List<Preq>();
pre.documents.forEach((preClass) {
Preq preqData = Preq.fromMap(preClass.data);
if (preClass.data != null) {
_preList.add(preqData);
}
});
ClassData data =
ClassData.fromMap(element.data, element.documentID, _preList);
if (data != null) {
_classList.add(data);
}
});
return _classList;
}
I think the logic of your provider is fine, the problem lies in the line
snapshot.documents.forEach((element) async {
...
}
The forEach is not a Future (what is inside it's a future because the async, but the method itself not) so the code runs the first time, it reaches the forEach which does its own future on each value and propagate to the next line of code, the return, but the list is empty because the forEach isn't done yet.
There is a special Future.forEach for this case so you can wait for the value method before running the next line
Future<List<ClassData>> get fetchClassdata async {
QuerySnapshot snapshot = await classesCollection.getDocuments();
List<ClassData> _classList = List<ClassData>();
await Future.forEach(snapshot.documents, (element) async {
QuerySnapshot pre = await Firestore.instance
.collection("Classes")
.document(element.documentID)
.collection("Pre")
.getDocuments();
List<Preq> _preList = List<Preq>();
pre.documents.forEach((preClass) {
Preq preqData = Preq.fromMap(preClass.data);
if (preClass.data != null) {
_preList.add(preqData);
}
});
ClassData data =
ClassData.fromMap(element.data, element.documentID, _preList);
if (data != null) {
_classList.add(data);
}
});
return _classList;
}
Here is a similar problem with provider with a forEach. Maybe it can help you understand a bit better

Flutter StreamBuilder ListView not reloading when stream data changes

I`m trying to build an app that loads an endless feed from a blog in a ListView. At the top, the user has a choice of filtering the feed according to a certain category through the "categorias" menu. When the user taps on the "categorias" menu, another ListView appears with all the available categories. When the user taps on the desired category, the app should return to the feed ListView display only the posts under that category.
Expecter Result:
App call API and retrieves the 10 latest posts
As user scrolls, the next 10 posts are retrieved through successive API calls
User taps on the "categorias" menu and ListView with categories opens.
User taps on the desired category and app returns to the feed Listview, makes an API
call to retrieve the first 10 posts of that category.
As user scrolls, the next 10 posts of that category are retrieved through successive API
calls.
Observed Result:
App call API and retrieves the 10 latest posts
As user scrolls, the next 10 posts are retrieved through successive API calls
User taps on the "categorias" menu and ListView with categories opens.
User taps on the desired category and app returns to the feed Listview, makes an API
call to retrieve the first 10 posts of that category.
Posts of the desired category are appended to the ListView and appear only after the posts
that had been loaded previously.
My question:
How do I have to modify my states or my Bloc, so that I can get the desired result?
Relevant Screenshots
My structure:
PostBloc - My bloc component, which contains the stream definition for Articles and ArticleCategory StreamBuilders. Also contains the methods for making the API calls to
get the articles and article categories.
class PostBloc extends Bloc<PostEvent, PostState> {
final http.Client httpClient;
int _currentPage = 1;
int _limit = 10;
int _totalResults = 0;
int _numberOfPages = 0;
int _categoryId;
bool hasReachedMax = false;
var cachedData = new Map<int, Article>();
PostBloc({#required this.httpClient}) {
//Listen to when user taps a category in the ArticleCategory ListView
_articleCategoryController.stream.listen((articleCategory) {
if (articleCategory.id != null) {
_categoryId = articleCategory.id;
_articlesSubject.add(UnmodifiableListView(null));
_currentPage = 1;
_fetchPosts(_currentPage, _limit, _categoryId)
.then((articles) {
_articlesSubject.add(UnmodifiableListView(articles));
});
_currentPage++;
dispatch(Fetch());
}
});
_currentPage++;
}
List<Article> _articles = <Article>[];
// Category Sink for listening to the tapped category
final _articleCategoryController = StreamController<ArticleCategory>();
Sink<ArticleCategory> get getArticleCategory =>
_articleCategoryController.sink;
//Article subject for populating articles ListView
Stream<UnmodifiableListView<Article>> get articles => _articlesSubject.stream;
final _articlesSubject = BehaviorSubject<UnmodifiableListView<Article>>();
//Categories subjet for the article categories
Stream<UnmodifiableListView<ArticleCategory>> get categories => _categoriesSubject.stream;
final _categoriesSubject = BehaviorSubject<UnmodifiableListView<ArticleCategory>>();
void dispose() {
_articleCategoryController.close();
}
#override
Stream<PostState> transform(
Stream<PostEvent> events,
Stream<PostState> Function(PostEvent event) next,
) {
return super.transform(
(events as Observable<PostEvent>).debounceTime(
Duration(milliseconds: 500),
),
next,
);
}
#override
get initialState => PostUninitialized();
#override
Stream<PostState> mapEventToState(PostEvent event) async* {
//This event is triggered when user taps on categories menu
if (event is ShowCategory) {
_currentPage = 1;
await _fetchCategories(_currentPage, _limit).then((categories) {
_categoriesSubject.add(UnmodifiableListView(categories));
});
yield PostCategories();
}
// This event is triggered when user taps on a category
if(event is FilterCategory){
yield PostLoaded(hasReachedMax: false);
}
// This event is triggered when app loads and when user scrolls to the bottom of articles
if (event is Fetch && !_hasReachedMax(currentState)) {
try {
//First time the articles feed opens
if (currentState is PostUninitialized) {
_currentPage = 1;
await _fetchPosts(_currentPage, _limit).then((articles) {
_articlesSubject.add(UnmodifiableListView(articles)); //Send to stream
});
this.hasReachedMax = false;
yield PostLoaded(hasReachedMax: false);
_currentPage++;
return;
}
//User scrolls to bottom of ListView
if (currentState is PostLoaded) {
await _fetchPosts(_currentPage, _limit, _categoryId)
.then((articles) {
_articlesSubject.add(UnmodifiableListView(articles));//Append to stream
});
_currentPage++;
// Check if last page has been reached or not
if(_currentPage > _numberOfPages){
this.hasReachedMax = true;
}
else{
this.hasReachedMax = false;
}
yield (_currentPage > _numberOfPages)
? (currentState as PostLoaded).copyWith(hasReachedMax: true)
: PostLoaded(
hasReachedMax: false,
);
}
} catch (e) {
print(e.toString());
yield PostError();
}
}
}
bool _hasReachedMax(PostState state) =>
state is PostLoaded && this.hasReachedMax;
Article _getArticle(int index) {
if (cachedData.containsKey(index)) {
Article data = cachedData[index];
return data;
}
throw Exception("Article could not be fetched");
}
/**
* Fetch all articles
*/
Future<List<Article>> _fetchPosts(int startIndex, int limit,
[int categoryId]) async {
String query =
'https://www.batatolandia.de/api/batatolandia/articles?page=$startIndex&limit=$limit';
if (categoryId != null) {
query += '&category_id=$categoryId';
}
final response = await httpClient.get(query);
if (response.statusCode == 200) {
final data = json.decode(response.body);
ArticlePagination res = ArticlePagination.fromJson(data);
_totalResults = res.totalResults;
_numberOfPages = res.numberOfPages;
for (int i = 0; i < res.data.length; i++) {
_articles.add(res.data[i]);
}
return _articles;
} else {
throw Exception('error fetching posts');
}
}
/**
* Fetch article categories
*/
Future<List<ArticleCategory>> _fetchCategories(int startIndex, int limit,
[int categoryId]) async {
String query =
'https://www.batatolandia.de/api/batatolandia/articles/categories?page=$startIndex&limit=$limit';
final response = await httpClient.get(query);
if (response.statusCode == 200) {
final data = json.decode(response.body);
ArticleCategoryPagination res = ArticleCategoryPagination.fromJson(data);
_totalResults = res.totalResults;
_numberOfPages = res.numberOfPages;
List<ArticleCategory> categories = <ArticleCategory>[];
categories.add(ArticleCategory(id: 0 , title: 'Todos', color: '#000000'));
for (int i = 0; i < res.data.length; i++) {
categories.add(res.data[i]);
}
return categories;
} else {
throw Exception('error fetching categories');
}
}
}
Articles - contains a BlocProvider to read the current state set in PostBloc and displays
the corresponding view.
class Articles extends StatelessWidget{
PostBloc _postBloc;
#override
Widget build(BuildContext context) {
return BlocProvider(
builder: (context) =>
PostBloc(httpClient: http.Client())..dispatch(Fetch()),
child: BlocBuilder<PostBloc, PostState>(
builder: (context, state){
_postBloc = BlocProvider.of<PostBloc>(context);
// Displays circular progress indicator while posts are being retrieved
if (state is PostUninitialized) {
return Center(
child: CircularProgressIndicator(),
);
}
// Shows the feed Listview when API responds with the posts data
if (state is PostLoaded) {
return ArticlesList(postBloc:_postBloc );
}
// Shows the Article categories Listview when user clicks on menu
if(state is PostCategories){
return ArticlesCategoriesList(postBloc: _postBloc);
}
//Shows error if there are any problems while fetching posts
if (state is PostError) {
return Center(
child: Text('Failed to fetch posts'),
);
}
return null;
}
)
);
}
}
ArticlesList - Contains a StreamBuilder, which reads the articles data from PostBloc and loads into the feed ListView.
class ArticlesList extends StatelessWidget {
ScrollController _scrollController = new ScrollController();
int currentPage = 1;
int _limit = 10;
int totalResults = 0;
int numberOfPages = 0;
final _scrollThreshold = 200.0;
Completer<void> _refreshCompleter;
PostBloc postBloc;
ArticlesList({Key key, this.postBloc}) : super(key: key);
#override
Widget build(BuildContext context) {
_scrollController.addListener(_onScroll);
_refreshCompleter = Completer<void>();
return Scaffold(
appBar: AppBar(
title: Text("Posts"),
),
body: StreamBuilder<UnmodifiableListView<Article>>(
stream: postBloc.articles,
initialData: UnmodifiableListView<Article>([]),
builder: (context, snapshot) {
if(snapshot.hasData && snapshot != null) {
if(snapshot.data.length > 0){
return Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
ArticlesFilterBar(),
Expanded(
child: RefreshIndicator(
child: ListView.builder(
itemBuilder: (BuildContext context,
int index) {
return index >= snapshot.data.length
? BottomLoader()
: ArticlesListItem(
article: snapshot.data.elementAt(
index));
},
itemCount: postBloc.hasReachedMax
? snapshot.data.length
: snapshot.data.length + 1,
controller: _scrollController,
),
onRefresh: _refreshList,
),
)
],
);
}
else if (snapshot.data.length==0){
return Center(
child: CircularProgressIndicator(),
);
}
}
else{
Text("Error!");
}
return CircularProgressIndicator();
}
)
);
}
#override
void dispose() {
_scrollController.dispose();
}
void _onScroll() {
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.position.pixels;
if (maxScroll - currentScroll <= _scrollThreshold) {
postBloc.dispatch(Fetch());
}
}
Future<void> _refreshList() async {
postBloc.dispatch(Fetch());
return null;
}
}
ArticlesCategoriesList - a StreamBuilder, which reads the categories from PostBloc and loads into a ListView.
class ArticlesCategoriesList extends StatelessWidget {
PostBloc postBloc;
ArticlesCategoriesList({Key key, this.postBloc}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Categorias"),
),
body:
SafeArea(
child: StreamBuilder<UnmodifiableListView<ArticleCategory>>(
stream: postBloc.categories,
initialData: UnmodifiableListView<ArticleCategory>([]),
builder: (context, snapshot) {
return ListView.separated(
itemBuilder: (BuildContext context, int index) {
return new Container(
decoration: new BoxDecoration(
color: Colors.white,
),
child: ListTile(
dense: true,
leading: Icon(Icons.fiber_manual_record,color: HexColor(snapshot.data[index].color)),
trailing: Icon(Icons.keyboard_arrow_right),
title: Text(snapshot.data[index].title),
onTap: () {
postBloc.getArticleCategory.add(snapshot.data[index]);
},
));
},
separatorBuilder: (context, index) => Divider(
color: Color(0xff666666),
height: 1,
),
itemCount: snapshot.data.length);
},
)));
}
}
And here I am answering my own question...
In the end, I got everything to run smoothly by clearing the _articles List whenever a category tap event was detected.
So here is the new PostBloc
class PostBloc extends Bloc<PostEvent, PostState> {
final http.Client httpClient;
int _currentPage = 1;
int _limit = 10;
int _totalResults = 0;
int _numberOfPages = 0;
int _categoryId;
bool hasReachedMax = false;
var cachedData = new Map<int, Article>();
List<Article> _articles = <Article>[];
PostBloc({#required this.httpClient}) {
//Listen to when user taps a category in the ArticleCategory ListView
_articleCategoryController.stream.listen((articleCategory) {
if (articleCategory.id != null) {
_categoryId = articleCategory.id;
_currentPage = 1;
_articles.clear();
_fetchPosts(_currentPage, _limit, _categoryId)
.then((articles) {
_articlesSubject.add(UnmodifiableListView(articles));
});
_currentPage++;
dispatch(FilterCategory());
}
});
}
// Category Sink for listening to the tapped category
final _articleCategoryController = StreamController<ArticleCategory>();
Sink<ArticleCategory> get getArticleCategory =>
_articleCategoryController.sink;
//Article subject for populating articles ListView
Stream<UnmodifiableListView<Article>> get articles => _articlesSubject.stream;
final _articlesSubject = BehaviorSubject<UnmodifiableListView<Article>>();
//Categories subjet for the article categories
Stream<UnmodifiableListView<ArticleCategory>> get categories => _categoriesSubject.stream;
final _categoriesSubject = BehaviorSubject<UnmodifiableListView<ArticleCategory>>();
void dispose() {
_articleCategoryController.close();
}
#override
Stream<PostState> transform(
Stream<PostEvent> events,
Stream<PostState> Function(PostEvent event) next,
) {
return super.transform(
(events as Observable<PostEvent>).debounceTime(
Duration(milliseconds: 500),
),
next,
);
}
#override
get initialState => PostUninitialized();
#override
Stream<PostState> mapEventToState(PostEvent event) async* {
//This event is triggered when user taps on categories menu
if (event is ShowCategory) {
_currentPage = 1;
await _fetchCategories(_currentPage, _limit).then((categories) {
_categoriesSubject.add(UnmodifiableListView(categories));
});
yield PostCategories();
}
// This event is triggered when user taps on a category
if(event is FilterCategory){
yield PostLoaded(hasReachedMax: false);
}
// This event is triggered when app loads and when user scrolls to the bottom of articles
if (event is Fetch && !_hasReachedMax(currentState)) {
try {
//First time the articles feed opens
if (currentState is PostUninitialized) {
_currentPage = 1;
await _fetchPosts(_currentPage, _limit).then((articles) {
_articlesSubject.add(UnmodifiableListView(articles)); //Send to stream
});
this.hasReachedMax = false;
yield PostLoaded(hasReachedMax: false);
_currentPage++;
return;
}
//User scrolls to bottom of ListView
if (currentState is PostLoaded) {
await _fetchPosts(_currentPage, _limit, _categoryId)
.then((articles) {
_articlesSubject.add(UnmodifiableListView(_articles));//Append to stream
});
_currentPage++;
// Check if last page has been reached or not
if(_currentPage > _numberOfPages){
this.hasReachedMax = true;
}
else{
this.hasReachedMax = false;
}
yield (_currentPage > _numberOfPages)
? (currentState as PostLoaded).copyWith(hasReachedMax: true)
: PostLoaded(
hasReachedMax: false,
);
}
} catch (e) {
print(e.toString());
yield PostError();
}
}
}
bool _hasReachedMax(PostState state) =>
state is PostLoaded && this.hasReachedMax;
Article _getArticle(int index) {
if (cachedData.containsKey(index)) {
Article data = cachedData[index];
return data;
}
throw Exception("Article could not be fetched");
}
/**
* Fetch all articles
*/
Future<List<Article>> _fetchPosts(int startIndex, int limit,
[int categoryId]) async {
String query =
'https://www.batatolandia.de/api/batatolandia/articles?page=$startIndex&limit=$limit';
if (categoryId != null) {
query += '&category_id=$categoryId';
}
final response = await httpClient.get(query);
if (response.statusCode == 200) {
final data = json.decode(response.body);
ArticlePagination res = ArticlePagination.fromJson(data);
_totalResults = res.totalResults;
_numberOfPages = res.numberOfPages;
List<Article> posts = <Article>[];
for (int i = 0; i < res.data.length; i++) {
_articles.add(res.data[i]);
posts.add(res.data[i]);
}
return posts;
} else {
throw Exception('error fetching posts');
}
}
/**
* Fetch article categories
*/
Future<List<ArticleCategory>> _fetchCategories(int startIndex, int limit,
[int categoryId]) async {
String query =
'https://www.batatolandia.de/api/batatolandia/articles/categories?page=$startIndex&limit=$limit';
final response = await httpClient.get(query);
if (response.statusCode == 200) {
final data = json.decode(response.body);
ArticleCategoryPagination res = ArticleCategoryPagination.fromJson(data);
_totalResults = res.totalResults;
_numberOfPages = res.numberOfPages;
List<ArticleCategory> categories = <ArticleCategory>[];
categories.add(ArticleCategory(id: 0 , title: 'Todos', color: '#000000'));
for (int i = 0; i < res.data.length; i++) {
categories.add(res.data[i]);
}
return categories;
} else {
throw Exception('error fetching categories');
}
}
}