1. 依赖
dependencies:
flutter_bloc: ^2.1.1
equatable: ^1.0.1
http: ^0.12.0
2. 免费REST API
3. 数据模型
post.dart
import 'package:equatable/equatable.dart';
/// 文章模型
class Post extends Equatable {
/// 文章ID
final int id;
/// 文章标题
final String title;
/// 文章内容
final String body;
const Post({this.id, this.title, this.body});
@override
List<Object> get props => [id, title, body];
@override
String toString() => "Post { id: $id }";
}
注: 继承 Equatable 后, 当且仅当this和other是相同的实例时,相等运算符才返回true。
4. 文章事件
此处我们只有一个加载文章的事件
infinite_event.dart
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
/// 永动事件
@immutable
abstract class InfiniteEvent extends Equatable{
@override
List<Object> get props => [];
}
/// 加载事件
class Fetch extends InfiniteEvent {}
5. 文章状态
- PostUninitialized: 第一批文章被加载时需要呈现一个加载指示器
- PostLoaded: 有内容需要展示
- posts - 将被显示的文章列表List<Post>
- hasReachedMax - 是否达到最大文章数
- PostError: 获取文章异常
infinite_state.dart
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:state_manage/infinite_list/post.dart';
/// 文章的状态
@immutable
abstract class InfiniteState extends Equatable {
const InfiniteState();
@override
List<Object> get props => [];
}
/// 文章初始化状态
class PostUninitialized extends InfiniteState {}
/// 文章异常状态
class PostError extends InfiniteState {}
/// 有文章需要加载
class PostLoaded extends InfiniteState {
/// 文章列表
final List<Post> posts;
/// 是否还有更多文章
final bool hasReachedMax;
const PostLoaded({this.posts, this.hasReachedMax});
/// 拷贝对象
/// @param post 文章列表
/// @param hasReachMax 是否还有更多文章
PostLoaded copyWith({List<Post> posts, bool hasReachedMax}) {
return PostLoaded(
posts: posts ?? this.posts,
hasReachedMax: hasReachedMax ?? this.hasReachedMax);
}
@override
List<Object> get props => [posts, hasReachedMax];
@override
String toString() =>
"PostLoaded { posts: ${posts.length}, hasReachedMax: $hasReachedMax }";
}
copyWith以便我们可以复制的实例PostLoaded并方便地更新零个或多个属性。
6. 文章 Bloc 实现
- 设置初始化状态
- 实现mapEventToState方法
- 使用http进行网络请求
- 设置事件连续间隔
infinite_bloc.dart
import 'dart:async';
import 'dart:convert';
import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:state_manage/infinite_list/post.dart';
import './bloc.dart';
import 'package:http/http.dart' as Http;
import 'package:rxdart/rxdart.dart';
/// 永动列表 Bloc
class InfiniteBloc extends Bloc<InfiniteEvent, InfiniteState> {
/// 网络请求工具
final Http.Client httpClient;
InfiniteBloc({@required this.httpClient});
@override
InfiniteState get initialState => PostUninitialized();
@override
Stream<InfiniteState> mapEventToState(
InfiniteEvent event,
) async* {
// 当前状态
final currentState = state;
/// 判断状态
if (event is Fetch && !_hasReachMax(currentState)) {
try {
if (currentState is PostUninitialized) {
// 加载数据
final posts = await _fetchPosts(0, 20);
yield PostLoaded(posts: posts, hasReachedMax: false);
} else if (currentState is PostLoaded) {
// 加载数据
final posts = await _fetchPosts(currentState.posts.length, 20);
yield posts.isEmpty
? currentState.copyWith(hasReachedMax: true)
: PostLoaded(
posts: currentState.posts + posts, hasReachedMax: false);
}
} catch (_) {
yield PostError();
}
}
}
/// 是否还有更多数据
bool _hasReachMax(InfiniteState currentState) {
if (state is PostLoaded) {
return (state as PostLoaded).hasReachedMax;
} else {
return false;
}
}
/// 加载数据
Future<List<Post>> _fetchPosts(int startIndex, int limit) async {
// 获取数据
final response = await httpClient.get('https://jsonplaceholder.typicode.com/posts?_start=$startIndex&_limit=$limit');
// 检测状态码
if (response.statusCode == 200) {
final data = json.decode(response.body) as List;
return data.map((rawPost) {
return Post(
id: rawPost['id'],
title: rawPost['title'],
body: rawPost['body']
);
}).toList();
} else {
throw Exception('error fetching posts');
}
}
// 设置事件间隔事件
@override
Stream<InfiniteState> transformEvents(Stream<InfiniteEvent> events, Stream<InfiniteState> Function(InfiniteEvent event) next) {
return super.transformEvents((events as Observable<InfiniteEvent>).debounceTime(Duration(milliseconds: 500)), next);
}
}
7. UI
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:state_manage/infinite_list/bloc/bloc.dart';
import 'package:http/http.dart' as http;
import 'package:state_manage/infinite_list/post.dart';
/// 永动列表
class InfiniteListTest extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Infinite Scroll',
home: Scaffold(
appBar: AppBar(
title: Text('Posts'),
),
body: BlocProvider(
create: (ctx) =>
InfiniteBloc(httpClient: http.Client())..add(Fetch()),
child: HomePage(),
),
),
);
}
}
主页
class HomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() => _HomePageState();
}
/// 页面状态
class _HomePageState extends State<HomePage> {
// 滚动控制器
final _scrollController = ScrollController();
// 滚动域
final _scrollThreshold = 200.0;
// 文章Bloc
InfiniteBloc _bloc;
/// 初始化
@override
void initState() {
super.initState();
// 添加监听
_scrollController.addListener(_onScroll);
// 获取Bloc
_bloc = BlocProvider.of(context);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<InfiniteBloc, InfiniteState>(
builder: (ctx, state) {
// 第一批数据加载
if (state is PostUninitialized) {
return Center(
child: CircularProgressIndicator(),
);
} else if (state is PostError) {
return Center(
child: Text('failed to fetch posts.'),
);
} else if (state is PostLoaded) {
// 文件为空
if (state.posts.isEmpty) {
return Center(
child: Text('no posts.'),
);
}
return ListView.builder(
itemBuilder: (ctx, index) {
return index >= state.posts.length
? BottomLoader()
: InfiniteListItem(post: state.posts[index]);
},
itemCount: state.hasReachedMax
? state.posts.length
: state.posts.length + 1,
controller: _scrollController,
);
}
},
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
/// 滚动监听
void _onScroll() {
// 最大滚动位置
final maxScroll = _scrollController.position.maxScrollExtent;
// 获取当前位置
final currentScroll = _scrollController.position.pixels;
// 判断是否加载数据
if (maxScroll - currentScroll <= _scrollThreshold) {
_bloc.add(Fetch());
}
}
}
/// 底部加载控件
class BottomLoader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Center(
child: SizedBox(
width: 33,
height: 33,
child: CircularProgressIndicator(
strokeWidth: 1.5,
),
),
),
);
}
}
/// 列表组件
class InfiniteListItem extends StatelessWidget{
/// 文章
final Post post;
const InfiniteListItem({Key key, @required this.post}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
leading: Text(
'${post.id}',
style: TextStyle(fontSize: 10.0),
),
title: Text(post.title),
isThreeLine: true,
subtitle: Text(post.body),
dense: true,
);
}
}
效果图: