flutter 数据加载

    科技2026-09-28  13

    flutter 数据加载

    Fetching data from a database and displaying it on a list is a very common task in app development. One crucial task that’s often neglected when doing this, especially by new developers, is lazy loading the data to avoid fetching a whole collection, making your app slower and racking up your bill.

    从数据库中获取数据并将其显示在列表上是应用程序开发中非常常见的任务。 执行此操作时经常被忽略的一项关键任务,尤其是对于新开发人员而言,是懒惰地加载数据以避免获取整个集合,这会使您的应用程序变慢并增加费用。

    Paginating or lazy loading your data using Firestore is relatively simple — that is if you’re getting the documents once. When you attempt to lazy load your data using streams to keep your data updated in real time, it’s a whole other story.

    使用Firestore分页或延迟加载数据相对简单-也就是说,一次获取文档即可。 当您尝试使用流来延迟加载数据以保持数据实时更新时,这完全是另一回事了。

    In this article, I’m going to teach you a technique to lazy load your data from Firestore in real time using the Bloc pattern. If you don’t know anything about the Bloc pattern, I suggest you read their documentation to get an idea of how it works.

    在本文中,我将教您一种使用Bloc模式从Firestore实时延迟加载数据的技术。 如果您对Bloc模式一无所知,建议您阅读他们的文档,以了解其工作原理。

    我们正在建设 (What We’re Building)

    We’ll build a simple app that lazy loads posts from a Firestore collection and displays them on a ListView.

    我们将构建一个简单的应用程序,以延迟加载Firestore集合中的帖子并将其显示在ListView 。

    项目创建 (Project Creation)

    Start by creating a new Flutter project:

    首先创建一个新的Flutter项目:

    $ flutter create lazy_loading_demo$ cd lazy_loading_demo

    Add the following dependencies to your pubspec.yaml:

    将以下依赖项添加到您的pubspec.yaml :

    firebase_core: ^0.5.0cloud_firestore: ^0.14.0+2flutter_bloc: ^6.0.5equatable: ^1.2.5

    Next, go to the Firebase console, and create a new project. Once the project is created, enable Firestore, and follow the instructions to add it to your app.

    接下来,转到Firebase控制台,然后创建一个新项目。 创建项目后,启用Firestore,然后按照说明将其添加到您的应用中。

    帖子库 (Post Repository)

    We’re going to need to create a repository that our Bloc can use to fetch data from Firestore. Create a new folder named supplemental, and then create post_repository.dart inside of it. Add the following code:

    我们将需要创建一个存储库,Bloc可以使用该存储库从Firestore提取数据。 创建一个名为supplemental的新文件夹,然后在其中创建post_repository.dart 。 添加以下代码:

    import 'package:cloud_firestore/cloud_firestore.dart'; class PostRepository { // Singleton boilerplate PostRepository._(); static PostRepository _instance = PostRepository._(); static PostRepository get instance => _instance; // Instance final CollectionReference _postCollection = FirebaseFirestore.instance.collection('posts'); Stream<QuerySnapshot> getPosts() { return _postCollection .limit(15) .snapshots(); } Stream<QuerySnapshot> getPostsPage(DocumentSnapshot lastDoc) { return _postCollection .startAfterDocument(lastDoc) .limit(15) .snapshots(); } }

    Now we have a simple repository we can use to get our data. The code is relatively easy to understand: getPosts returns a stream containing the first 15 documents of the posts collection. getPostsPage returns a stream containing 15 documents from the collection, starting after the document we pass as an argument.

    现在我们有了一个简单的存储库,可以用来获取数据。 该代码相对容易理解: getPosts返回一个流,其中包含posts集合的前15个文档。 getPostsPage从集合中返回一个包含15个文档的流,从我们作为参数传递的文档开始。

    We created this repository as a singleton; however, you may want to use dependency injection or a lazy singleton in your app, depending on your use case.

    我们将此存储库创建为单例。 但是,根据您的用例,您可能需要在应用程序中使用依赖项注入或惰性单例。

    邮政模型 (Post Model)

    We’re going to need a model to represent each post retrieved from the database. For this example, the following class will do:

    我们将需要一个模型来表示从数据库检索到的每个帖子。 对于此示例,下面的类将起作用:

    import 'package:equatable/equatable.dart'; class Post extends Equatable { final String title; final String author; const Post(this.title, this.author); factory Post.fromSnapshot(Map data) { return Post(data['title'], data['author']); } @override List<Object> get props => [title, author]; }

    Create a file named models/post.dart and paste it into the previous code. Note that we’re extending Equatable so we (and our Bloc) can compare instances of Post.

    创建一个名为models/post.dart的文件,并将其粘贴到之前的代码中。 请注意,我们正在扩展Equatable以便我们(和我们的Bloc)可以比较Post实例。

    集团实施 (Bloc Implementation)

    Create a new directory called data_bloc. There, create the following files:

    创建一个名为data_bloc的新目录。 在那里,创建以下文件:

    data_bloc.dart

    data_bloc.dart

    data_events.dart

    data_events.dart

    data_state.dart

    data_state.dart

    Lets start by creating the events for our Bloc. In the file data_events.dart, add the following lines:

    让我们从为我们的Bloc创建事件开始。 在文件data_events.dart ,添加以下行:

    part of 'data_bloc.dart'; abstract class DataEvent extends Equatable { const DataEvent(); @override List<Object> get props => []; }

    Here, we’re creating a base class extending Equatable so we can compare our events. Now that we have the class we’ll base our events on, let’s define which events our Bloc should have:

    在这里,我们正在创建扩展Equatable的基类,以便我们可以比较事件。 现在我们有了将事件作为基础的类,让我们定义Bloc应该具有哪些事件:

    DataEventStart: Used to configure our Bloc

    DataEventStart :用于配置我们的Bloc

    DataEventLoad: Used to notify the Bloc new data is available

    DataEventLoad :用于通知Bloc新数据可用

    DataEventFetchMore: Used to get more data once we run out

    DataEventFetchMore :一旦耗尽,用于获取更多数据

    Add the following lines to data_events.dart:

    data_events.dart添加到data_events.dart :

    class DataEventStart extends DataEvent {} class DataEventLoad extends DataEvent { final List<List<Post>> data; const DataEventLoad(this.data); @override List<Object> get props => [data]; } class DataEventFetchMore extends DataEvent {}

    Now let’s define the states our Bloc will emit:

    现在,让我们定义我们的Bloc将发出的状态:

    DataStateLoading: Used when the data isn’t loaded yet

    DataStateLoading :尚未加载数据时使用

    DataStateEmpty: Used when there are no documents in the collection we pulled

    DataStateEmpty :当我们提取的集合中没有文档时使用

    DataStateLoadSuccess: Used when the data was pulled successfully and contains a list of posts

    DataStateLoadSuccess :在成功提取数据并包含帖子列表时使用

    It may be a good idea to add a DataLoadFailure state to emit in case something goes wrong. Try to implement it yourself.

    最好添加一个DataLoadFailure状态以防万一出问题。 尝试自己实施。

    Add the following code to data_state.dart:

    将以下代码添加到data_state.dart :

    part of 'data_bloc.dart'; abstract class DataState extends Equatable { const DataState(); @override List<Object> get props => []; } class DataStateLoading extends DataState {} class DataStateEmpty extends DataState {} class DataStateLoadSuccess extends DataState { final List<Post> posts; final bool hasMoreData; const DataStateLoadSuccess(this.posts, this.hasMoreData); @override List<Object> get props => [posts]; }

    Here, we implemented the base class and the states we defined earlier.

    在这里,我们实现了基类和之前定义的状态。

    Now it’s time to pull our Bloc together. Add the following to data_bloc.dart:

    现在是时候将我们的集团团结起来了。 将以下内容添加到data_bloc.dart :

    import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../models/post.dart'; import '../supplemental/post_repository.dart'; part 'data_events.dart'; part 'data_state.dart'; class DataBloc extends Bloc<DataEvent, DataState> { DataBloc() : super(DataStateLoading()); List<StreamSubscription> subscriptions = []; List<List<Post>> posts = []; bool hasMoreData = true; DocumentSnapshot lastDoc; @override Stream<DataState> mapEventToState(DataEvent event) async* { if (event is DataEventStart) { // Clean up our variables hasMoreData = true; lastDoc = null; subscriptions.forEach((sub) { sub.cancel(); }); posts.clear(); subscriptions.clear(); subscriptions.add( PostRepository.instance.getPosts().listen((event) { handleStreamEvent(0, event); }) ); } if (event is DataEventLoad) { // Flatten the posts list final elements = posts.expand((i) => i).toList(); if (elements.isEmpty) { yield DataStateEmpty(); } else { yield DataStateLoadSuccess(elements, hasMoreData); } } if (event is DataEventFetchMore) { if (lastDoc == null) { throw Exception("Last doc is not set"); } final index = posts.length; subscriptions.add( PostRepository.instance.getPostsPage(lastDoc).listen((event) { handleStreamEvent(index, event); }) ); } } @override onChange(change) { print(change); super.onChange(change); } @override Future<void> close() async { subscriptions.forEach((s) => s.cancel()); super.close(); } }

    Most of this code is very simple. When the Bloc receives DataEventStart, it resets the class variables and cancels the stream subscriptions. It then creates the first subscription by requesting it from our PostRepository and letting handleStreamEvent (we’ll implement this method next) take care of the events received from the stream.

    大部分代码非常简单。 当Bloc收到DataEventStart ,它将重置类变量并取消流订阅。 然后,它通过从我们的PostRepository请求它来创建第一个订阅,并让handleStreamEvent (接下来将实现此方法)处理从流中接收到的事件。

    When our Bloc receives a DataEventLoad event, it first flattens the list of posts and then checks if it’s empty and returns either DataStateLoadSuccess or DataStateEmpty.

    当我们的Bloc收到DataEventLoad事件时,它首先将帖子列表放平,然后检查它是否为空,并返回DataStateLoadSuccess或DataStateEmpty 。

    Our last event, DataEventFetchMore, requests a new page of data from our PostRepository and adds the stream subscription to our subscriptions list.

    我们的最后一个事件DataEventFetchMore ,从PostRepository请求新的数据页面,并将流订阅添加到我们的订阅列表中。

    Now that we’ve finished most of our Bloc, let’s implement the handleStreamEvent method. Copy the following into your Bloc:

    现在,我们已经完成了大部分Bloc,让我们实现handleStreamEvent方法。 将以下内容复制到您的Bloc中:

    class DataBloc extends Bloc<DataEvent, DataState> { // ... snip // We use this function to handle events from our streams handleStreamEvent(int index, QuerySnapshot snap) { // We request 15 docs at a time if (snap.docs.length < 15) { hasMoreData = false; } // If the snapshot is empty, there's nothing for us to do if (snap.docs.isEmpty) return; if (index == posts.length) { // Set the last document we pulled to use as a cursor lastDoc = snap.docs[snap.docs.length - 1]; } // Turn the QuerySnapshot into a List of posts List<Post> newList = []; snap.docs.forEach((doc) { // This is a good spot to filter your data if you're not able // to compose the query you want. newList.add(Post.fromSnapshot(doc.data())); }); // Update the posts list if (posts.length <= index) { posts.add(newList); } else { posts[index].clear(); posts[index] = newList; } add(DataEventLoad(posts)); } // snip ... }

    The handleStreamEvent method is simpler than it looks. It takes two arguments, whichever stream called it (passed as its index in the posts list), and the event received from the stream as a QuerySnapshot.

    handleStreamEvent方法比看起来简单。 它带有两个参数,无论哪个流调用它(作为其在posts列表中的index传递),以及从流接收的事件作为QuerySnapshot 。

    When called, the method first checks the length of the QuerySnapshot received. We request 15 docs at a time, so if we get less than that it means we’ve reached the end of the collection.

    调用时,该方法首先检查接收到的QuerySnapshot的长度。 我们一次需要15个文档,因此,如果收到的文档少于该文档,则意味着我们已经到了收藏集的结尾。

    It then checks if it’s handling events from the last subscription in the list, and if that’s the case, it sets the lastDoc variable to the last document in the query snapshot.

    然后,它检查是否正在处理列表中最后一个订阅的事件,如果是这样,它lastDoc变量设置为查询快照中的最后一个文档。

    Finally, it updates the posts list and adds DataEventLoad to our Bloc.

    最后,它更新posts列表并将DataEventLoad添加到我们的Bloc中。

    用户界面 (The UI)

    Now that we have a mechanism to lazy load our data, it’s time to create a UI to test it. Create a new directory for your UI with a file named lazy_list.dart. There, add the following:

    现在我们有了延迟加载数据的机制,现在该创建一个UI来测试它了。 使用名为lazy_list.dart的文件为您的UI创建一个新目录。 在那里,添加以下内容:

    import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../data_bloc/data_bloc.dart'; class LazyListScreen extends StatefulWidget { @override createState() => _LazyListScreenState(); } class _LazyListScreenState extends State<LazyListScreen> { DataBloc _dataBloc = DataBloc(); @override initState() { super.initState(); _dataBloc.add(DataEventStart()); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Posts'), ), body: BlocBuilder<DataBloc, DataState>( cubit: _dataBloc, builder: (BuildContext context, DataState state) { if (state is DataStateLoading) { return Center( child: CircularProgressIndicator(), ); } else if (state is DataStateEmpty) { return Center( child: Text('No Posts', style: Theme.of(context).textTheme.bodyText1,), ); } else if (state is DataStateLoadSuccess) { return ListView.separated( padding: EdgeInsets.fromLTRB(15, 10, 15, 0), itemCount: state.hasMoreData ? state.posts.length + 1 : state.posts.length, itemBuilder: (context, i) { if (i >= state.posts.length) { _dataBloc.add(DataEventFetchMore()); return Container( margin: EdgeInsets.only(top: 15), height: 30, width: 30, child: Center(child: CircularProgressIndicator()), ); } return ListTile( title: Text(state.posts[i].title), subtitle: Text(state.posts[i].author), ); }, separatorBuilder: (context, i) { return Divider(); } ); } } ), ); } @override void dispose() { _dataBloc.close(); super.dispose(); } }

    The implementation of the UI is very straightforward. We return a Scaffold with a BlocBuilder, which handles all the states the Bloc can emit.

    UI的实现非常简单。 我们用BlocBuilder返回一个Scaffold ,它处理Bloc可以发出的所有状态。

    If the posts were loaded successfully, it returns a ListView, which has an extra item if hasMoreItems is true. In the list builder, if the current index is greater than or equal to the post-list length, it adds DataEventFetchMore to the Bloc and displays a CircularProgressIndicator in the meantime. Finally, we close the Bloc on the dispose() method.

    如果帖子成功加载,则返回一个ListView ,如果hasMoreItems为true ,则该列表有一个额外的项目。 在列表构建器中,如果当前索引大于或等于列表后长度,则它将DataEventFetchMore添加到Bloc并同时显示CircularProgressIndicator 。 最后,我们在dispose()方法上关闭Bloc。

    One last thing, change your main.dart to contain the code below:

    最后一件事,将main.dart更改为包含以下代码:

    import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:lazy_loading_demo/ui/lazy_list.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Lazy Loading Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: LazyListScreen(), ); } }

    Here, we just initialize Firebase and set LazyListScreen as the home screen. We call WidgetsFlutterBinding.ensureInitialized() so we can make a call to the Firebase Core plugin in our main function.

    在这里,我们只是初始化Firebase并将LazyListScreen设置LazyListScreen屏幕。 我们调用WidgetsFlutterBinding.ensureInitialized()以便可以在主函数中调用Firebase Core插件。

    结论 (Conclusion)

    We’ve successfully implemented a lazy-loading list. This is a very simple implementation you can build on; in a real app, you may want to inject the Bloc to the UI using BlocProvider or modify this example to suit your requirements. There are also different approaches you can take to implement this behavior — the one used here is just the simplest I could think of.

    我们已经成功实现了延迟加载列表。 这是您可以构建的非常简单的实现; 在实际应用中,您可能想使用BlocProvider将Bloc注入UI或修改此示例以适合您的要求。 您还可以采用多种方法来实现此行为-此处使用的方法只是我能想到的最简单的方法。

    资源资源 (Resources)

    FlutterFire documentation

    FlutterFire文档

    Bloc library documentation

    Bloc库文档

    Firebase documentation

    Firebase文档

    Source code for this project

    该项目的源代码

    翻译自: https://medium.com/better-programming/lazy-loading-data-from-firestore-in-real-time-using-flutter-baa4157462fe

    flutter 数据加载

    相关资源:flutter_medical:使用Dart与Flutter创建的功能DoctorHealthcare Catalog App。 从Firebase Firestore DB存储和加载数据-源码
    Processed: 0.016, SQL: 9