How to implement load more data with flutter_bloc without reload every time: I have this:
post_bloc.dart:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scroool/post_events.dart';
import 'package:scroool/post_repo.dart';
import 'package:scroool/post_states.dart';
class PostsBloc extends Bloc<PostEvents, PostsStates> {
PostRepo repo;
int page = 1;
ScrollController controller = ScrollController();
PostsBloc(this.repo) : super(PostInitState()){
on<FetchPosts>((event, emit) async{
emit(PostLoadingState());
final posts = await repo.fetchPosts(page);
emit(PostLoadedState(posts: posts));
});
on<LoadMore>((event, emit) async{
if (controller.position.pixels ==
controller.position.maxScrollExtent) {
emit(LoadMoreState());
page++;
final posts = await repo.fetchPosts(page);
emit(PostLoadedState(posts: posts));
// isLoadingMore = false;
} else {
print("not called");
}
});
}
}
And at home.dart:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scroool/post_bloc.dart';
import 'package:scroool/post_events.dart';
import 'package:scroool/post_states.dart';
class Scroool extends StatelessWidget {
List posts = [];
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocConsumer<PostsBloc, PostsStates>(
listener: (context, state){},
builder: (context, state) {
if(state is PostLoadingState) {
return const Center(child: CircularProgressIndicator(),);
} else if(state is PostLoadedState) {
posts = posts + state.posts;
return ListView.builder(
controller: context.read<PostsBloc>().controller
..addListener(() => context.read<PostsBloc>().add(LoadMore())),
itemCount: state is LoadMoreState
? posts.length + 1 : posts.length ,
itemBuilder: (context, index) {
if(index < posts.length) {
final post = posts[index];
return Card(
child: ListTile(
leading: CircleAvatar(child: Text("${index + 1}"),),
title: Text(post['author'].toString()),
subtitle: Text(post['title']['rendered']),
)
);
} else {
return Center(child: CircularProgressIndicator(),);
}
},
);
} else {
return Container();
}
},
)
);
}
}
It is working without any problem, but every time I need to load more is just re-emit the state and display data from beginning, I need just continue with more data not reload and add all data
Change your BLoC state so the parent state will have property posts. You can set it to empty list for states that do not use it.
abstract class PostState {
final List<Post> posts;
PostState(this.posts);
}
class PostLoadingState extends PostState {
LoadMoreState() : super ([]);
}
class LoadMoreState extends PostState {
LoadMoreState({required List<Post> posts}) : super (posts);
}
class PostLoadedState extends PostState {
PostLoadedState({required List<Post> posts}) : super (posts);
}
Then change your BLoC accordingly:
on<LoadMore>((event, emit) async{
if (controller.position.pixels ==
controller.position.maxScrollExtent) {
emit(LoadMoreState(posts: state.posts));
page++;
final posts = await repo.fetchPosts(page);
emit(PostLoadedState(posts: [...state.posts, ...posts]));
} else {
print("not called");
}
});
In your widget remove the posts variable and use posts from bloc's state. Since your parent state has property posts, you can access posts for every state, including LoadMoreState. So change the if condition to return ListView for all states except PostLoadingState.
class Scroool extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocConsumer<PostsBloc, PostsStates>(
listener: (context, state){},
builder: (context, state) {
if(state is PostLoadingState) {
return const Center(child: CircularProgressIndicator(),);
} else {
final posts = state.posts;
return ListView.builder(
controller: context.read<PostsBloc>().controller
..addListener(() => context.read<PostsBloc>().add(LoadMore())),
itemCount: state is LoadMoreState
? posts.length + 1 : posts.length ,
itemBuilder: (context, index) {
if(index < posts.length) {
final post = posts[index];
return Card(
child: ListTile(
leading: CircleAvatar(child: Text("${index + 1}"),),
title: Text(post['author'].toString()),
subtitle: Text(post['title']['rendered']),
)
);
} else {
return Center(child: CircularProgressIndicator(),);
}
},
);
}
},
)
);
}
}
Related
I want to load more when the user scrolls, but the problem is how to keep track of the course id when I want to load more answers, since each course has many answers, below all the code for both (courses bloc and repository) and (answers bloc and repository).
class AnswersRepository {
final _url = "https://awers.osamline.com/answs";
Future<List<AnswerData>> getAnswers({int cid = 1, int page=1}) async {
final response = await get(Uri.parse("$_url/$cid?page=$page"));
final answers = answerFromJson(response.body);
return answers.data;
}
}
and here the events:
abstract class AnswersEvent {}
class FetchAnswers extends AnswersEvent {
final int cid;
FetchAnswers({required this.cid});
}
class LoadMoreEvent extends AnswersEvent {
final int cid;
LoadMoreEvent({required this.cid});
}
and states:
abstract class AnswersState {
final answers;
AnswersState(this.answers);
}
class AnswersLoadingState extends AnswersState {
AnswersLoadingState(super.answers);
}
class AnswersSuccessState extends AnswersState {
AnswersSuccessState({required answers}) : super(answers);
}
and the bloc:
class AnswersBloc extends Bloc<AnswersEvent, AnswersState> {
int page = 0;
int cid = 0;
bool isFetching = false;
bool isLoadingMore = false;
final AnswersRepository repo;
ScrollController scrolController = ScrollController();
AnswersBloc(this.repo) : super(AnswersLoadingState(null)) {
scrolController.addListener(() => add(LoadMoreEvent(cid: cid)));
on<FetchAnswers>((event, emit) async{
emit(AnswersLoadingState(null));
try {
final answers = await repo.getAnswers(cid: event.cid,
page: page);
emit(AnswersSuccessState(answers: answers));
// page++;
}catch(e) {
emit(AnswersErrorState(msg: e.toString()));
}
});
on<LoadMoreEvent>((event, emit) async{
if(scrolController.position.pixels ==
scrolController.position.maxScrollExtent) {
isLoadingMore = true;
page++;
var answers = await repo.getAnswers(cid: cid, page: page);
emit(AnswersSuccessState(answers: [...state.answers, ...answers]));
}
});
and in the UI:
child: BlocConsumer<AnswersBloc, AnswersState>(
listener: (context, state) {},
builder: (context, state) {
if (state is AnswersLoadingState) {
return const Center(child: CircularProgressIndicator(),);
} else if (state is AnswersSuccessState) {
var answers = state.answers;
return GridView.builder(
controller: context
.read<AnswersBloc>()
.scrolController,
itemCount: context
.read<AnswersBloc>()
.isLoadingMore
? answers.length +1 : answers.length,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2),
itemBuilder: (context, index) {
if (index >= answers.length) {
return const Center(child: CircularProgressIndicator(),);
} else {
var answer = answers[index];
return DetailCardWidget(
title: answer.title,
link: answer.link);
}
}
);
The problem is how can I keep track of cid when making the load more? Because I have another bloc for courses like this:
repo:
class CoursesRepository {
final _url = "https://answers.osa.com/ap/coures";
Future<List<CoursesData>> getCourses() async {
final response = await get(Uri.parse(_url),
headers: {});
final courses = courseFromJson(response.body);
return courses.data;
}
}
event:
abstract class CoursesEvent {}
class FetchCourses extends CoursesEvent {}
state:
abstract class CoursesState {}
class CoursesLoadingState extends CoursesState {}
class CoursesSuccessState extends CoursesState {
List<CoursesData> courses;
CoursesSuccessState({required this.courses});
}
and bloc:
class CoursesBloc extends Bloc<CoursesEvent, CoursesState> {
final CoursesRepository repo;
final AnswersRepository answerRepo;
CoursesBloc(this.repo, this.answerRepo)
: super(CoursesLoadingState()) {
on<CoursesEvent>((event, emit) async{
emit(CoursesLoadingState());
try {
final courses = await repo.getCourses();
emit(CoursesSuccessState(courses: courses));
}catch(e) {
emit(CoursesErrorState(msg: e.toString()));
}
});
and in UI to display the courses:
return BlocBuilder<CoursesBloc, CoursesState>(
builder: (context, state) {
if(state is CoursesLoadingState) {
return const Center(child: CircularProgressIndicator(),);
} else if(state is CoursesSuccessState) {
return ListView.builder(
scrollDirection: direction,
itemCount:state.courses.length,
itemBuilder: (context, index) {
return CategoryCardWidget(
onClick: () {
context.read<AnswersBloc>()
.add(FetchAnswers(cid: state.courses[index].id));
//context.read<AnswersBloc>().cid = state.courses[index].id;
},
color: Colors.black,
image: Image.network(state.courses[index].image ?? "",
fit: BoxFit.fitHeight,),
title: Text(state.courses[index].title,
style: const TextStyle(fontSize: 15,
fontWeight: FontWeight.bold,
color: Colors.black),));
},
);
and main like below:
Scaffold(
resizeToAvoidBottomInset: false,
body: Container(
color: Colors.black12,
child: MultiRepositoryProvider(
providers: [
RepositoryProvider<AnswersRepository>(
create: (context)=> AnswersRepository()
),
RepositoryProvider<CoursesRepository>(
create: (context)=> CoursesRepository()
),
],
child: MultiBlocProvider(
providers: [
BlocProvider<AnswersBloc>(
create: (context)
=> AnswersBloc(
context.read<AnswersRepository>())
..add(FetchAnswers(cid: 2))),
BlocProvider<CoursesBloc>(
create: (context)
=> CoursesBloc(context.read<CoursesRepository>(),
context.read<AnswersRepository>())
..add(FetchCourses()))
],
child: HomeScreen()
),
),
I am trying to create a ListView Builder from Stream builder in Flutter.
TodoList Class
import 'package:isar/isar.dart';
part 'todo_list.g.dart';
#Collection()
class TodoList {
Id id = Isar.autoIncrement;
late String todoTitle;
}
Isar Services
class IsarService {
late Future<Isar> db;
IsarService() {
db = openDB();
}
//Return IsarDB, if not found, then create
Future<Isar> openDB() async {
if (Isar.instanceNames.isEmpty) {
return await Isar.open(
[TodoListSchema],
inspector: true,
);
}
return Future.value(Isar.getInstance());
}
Stream<List<TodoList>> listenToTodoList() async* {
final isar = await db;
yield* isar.todoLists.where().watch(fireImmediately: true);
}
}
ListView Builder from above Streambuilder
class ListScreen extends StatefulWidget {
final IsarService service;
const ListScreen(this.service, {super.key});
#override
State<ListScreen> createState() => _ListScreenState();
}
class _ListScreenState extends State<ListScreen> {
//Text Controller
final _textController = TextEditingController();
final service = IsarService();
//Let's build List Title from snapshot
final List<TodoList> _todoList = [];
//Root widget of the class
#override
Widget build(BuildContext context) {
return Scaffold(
//List Screen Body Section
body: StreamBuilder(
stream: service.listenToTodoList(),
builder: (context, snapshot) {
if (snapshot.hasError) {
AlertDialog(
content: Text(snapshot.error.toString()),
);
} else if (snapshot.hasData) {
_todoList.add(snapshot.data); //Error happen in this line
}
return const CircularProgressIndicator();
},
),
);
}
}
The Error is
The argument type 'List?' can't be assigned to the parameter type 'TodoList'.
I'am trying to assign the snapshot data in final List<TodoList> _todoList = []; and use them ListView Builder
Try this code:
return Scaffold(
body: StreamBuilder(
stream: service.listenToTodoList(),
builder: (context, snapshot) {
if (snapshot.hasError) {
AlertDialog(
content: Text(snapshot.error.toString()),
);
} else if (snapshot.hasData) {
final todos = snapshot.data;
if (todos != null) {
return ListView.builder(
itemCount: todos.length();
itemBuilder: (context, index) {
final todo = todos[index];
return Text(todo.todoTitle);
}
);
} else {
return const Center(child: Text('No data found!'));
}
}
return const CircularProgressIndicator();
},
),
);
I have this in PostBloc.dart:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scroool/post_events.dart';
import 'package:scroool/post_repo.dart';
import 'package:scroool/post_states.dart';
class PostsBloc extends Bloc<PostEvents, PostsStates> {
PostRepo repo;
bool isLoadingMore = false;
int page = 1;
List posts = [];
ScrollController controller = ScrollController();
PostsBloc(this.repo) : super(PostInitState()){
on<FetchPosts>((event, emit) async{
emit(PostLoadingState());
final posts = await repo.fetchPosts(page);
emit(PostLoadedState(posts: posts));
controller.addListener(scrolListener);
});
}
scrolListener() async {
//don' call the api if user scroll whil calling the api not finished yet
if (isLoadingMore) return;
if (controller.position.pixels ==
controller.position.maxScrollExtent) {
isLoadingMore = true;
page++;
final posts = await repo.fetchPosts(page);
emit(PostLoadedState(posts: posts));
isLoadingMore = false;
} else {
print("not called");
}
}
}
And at home.dart (UI):
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scroool/post_bloc.dart';
import 'package:scroool/post_states.dart';
class Scroool extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocConsumer<PostsBloc, PostsStates>(
listener: (context, state){},
builder: (context, state) {
if(state is PostLoadingState) {
return const Center(child: CircularProgressIndicator(),);
} else if(state is PostLoadedState) {
context.read<PostsBloc>().posts =
context.read<PostsBloc>().posts + state.posts;
} else {
return Container();
}
return ListView.builder(
controller: context.read<PostsBloc>().controller,
itemCount: context.read<PostsBloc>().isLoadingMore
? context.read<PostsBloc>().posts.length + 1
: context.read<PostsBloc>().posts.length,
itemBuilder: (context, index) {
if(index < context.read<PostsBloc>().posts.length) {
final post = context.read<PostsBloc>().posts[index];
return Card(
child: ListTile(
leading: CircleAvatar(child: Text("${index + 1}"),),
title: Text(post['author'].toString()),
subtitle: Text(post['title']['rendered']),
)
);
} else {
return Center(child: CircularProgressIndicator(),);
}
},
);
},
)
);
}
}
It is working, but when I scroll, I cannot see the CircularIndicator when I load more items.
When i refresh the data isnt fetching new data please help me
This is my method to fetch data from news org api
Future<List<Article>> getApi() async {
Response response = await get(Uri.parse(
"https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=apikey"));
if (response.statusCode == 200) {
Map<String, dynamic> json = jsonDecode(response.body);
List<dynamic> body = json['articles'];
List<Article> article = body.map((e) => Article.fromJson(e)).toList();
return article;
} else {
throw ("cant get the articles");
}
}
this is my builder to show data
body: FutureBuilder<List<Article>>(
future: futureWords,
builder: (context, AsyncSnapshot<List<Article>> snap) {
if (snap.hasData) {
List<Article> articles = snap.data!;
return RefreshIndicator(
onRefresh: () {
setState(() {});
return _pullRefresh();
},
child: ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
return customListTile(articles[index]);
}),
);
} else {
return Center(child: CircularProgressIndicator());
}
}),
this is my pullrefresh method
Future<List<Article>> _pullRefresh() async {
List<Article> freshWords = await news.getApi();
setState(() {
futureWords = Future.value(freshWords);
});
return futureWords!;
}
May be it'll help you. If not - post your full code)
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
class Test2 extends StatefulWidget {
const Test2({Key? key}) : super(key: key);
#override
_Test2State createState() => _Test2State();
}
class Article {
String header='';
}
class _Test2State extends State<Test2> {
Future <List<Article>>? futureWords;
#override
void initState() {
super.initState();
_getNews();
}
_getNews() async {
var response = await http.get(Uri.parse(
"https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=aa20aef042a14de5b99a7f7d32952504"));
if (response.statusCode == 200) {
Map<String, dynamic> json = jsonDecode(response.body);
List<dynamic> body = json['articles'];
//List<Article> articles = body.map((e) => Article.fromJson(e)).toList();
List<Article> articles = [];
for (var el in body) {
Article article = Article();
article.header = el['title'];
articles.add(article);
}
articles.shuffle();
setState(() {
futureWords = Future.value(articles);
});
} else {
throw ("cant get the articles. statusCode ${response.statusCode}");
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<List<Article>>(
future: futureWords,
builder: (context, AsyncSnapshot<List<Article>> snap) {
if (snap.hasData) {
List<Article> articles = snap.data!;
return
RefreshIndicator(
onRefresh: () {
_getNews();
return futureWords!;
},
child: ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return ListTile(title: Text(articles[index].header));
}),
);
} else {
return Center(child: CircularProgressIndicator());
}
}),
);
}
}
While fetching data from database in flutter snapShot.ConnectionState is always waiting and the circular progress indicator keeps on loading.
I am not getting any errors and I am using FutureBuilder to build my widget.
Class where I build my widget
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/event_provider.dart';
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: Provider.of<EventProviders>(context).fetchAndSetEvents(),
builder: (ctx, dataSnapshot) {
if (dataSnapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return Consumer<EventProviders>(
child: Text('Not found'),
builder: (ctx, eventData, ch) {
if (eventData.events.length <= 0) {
return ch;
} else {
return ListView.builder(
itemCount: eventData.events.length,
itemBuilder: (ctx, index) {
return Container(
child: Text(eventData.events[index].eventName),
);
},
);
}
},
);
}
},
);
}
}
My future class
Future<void> fetchAndSetEvents() async {
final dataList = await DBHelper.getData('user_events');
_events = dataList
.map(
(data) => EventProvider(
eventName: data['event'],
eventDate: data['date'],
id: data['id'],
),
)
.toList();
notifyListeners();
}
}
Some help will be highly appreciated
Set listen: false
future: Provider.of<EventProviders>(context, listen: false).fetchAndSetEvents(),