
في هذا البرنامج التعليمي ، سنقوم بتطوير تطبيق يتلقى البيانات عبر الإنترنت ويعرضها في قائمة. شيء مثل هذا

حسنًا ، لنبدأ بإنشاء مشروع. اكتب ما يلي في سطر الأوامر
flutter create flutter_infinite_list
بعد ذلك ، انتقل إلى ملف التبعيات
pubspec.yaml وأضف العناصر التي نحتاج إليها
name: flutter_infinite_list description: A new Flutter project. version: 1.0.0+1 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" dependencies: flutter: sdk: flutter flutter_bloc: 0.4.11 http: 0.12.0 equatable: 0.1.1 dev_dependencies: flutter_test: sdk: flutter flutter: uses-material-design: true
بعد ذلك ، قم بتثبيت هذه التبعيات باستخدام الأمر التالي
flutter packages get
بالنسبة لهذا التطبيق ، سوف نستخدم
jsonplaceholder للحصول على بيانات المخاوي. إذا لم تكن معتادًا على هذه الخدمة ، فهذه خدمة REST API عبر الإنترنت يمكنها إرسال بيانات مزيفة. هذا مفيد جدا لبناء نماذج التطبيق.
من خلال فتح الرابط التالي
jsonplaceholder.typicode.com/posts؟_start=0&_limit=2 ، سترى استجابة JSON التي سنعمل بها.
[ { "userId": 1, "id": 1, "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" }, { "userId": 1, "id": 2, "title": "qui est esse", "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla" } ]
لاحظ أنه في طلب GET ، حددنا قيد البداية والنهاية كمعلمة.
عظيم ، الآن نحن نعرف كيف سيبدو هيكل بياناتنا! دعونا إنشاء نموذج لهم.
قم بإنشاء ملف post.dart بالمحتويات التالية
import 'package:equatable/equatable.dart'; class Post extends Equatable { final int id; final String title; final String body; Post({this.id, this.title, this.body}) : super([id, title, body]); @override String toString() => 'Post { id: $id }'; }
المشاركة هي مجرد فئة ذات معرف وعنوان وجسم. يمكننا أيضًا تجاوز وظيفة toString لعرض سلسلة ملائمة لاحقًا. بالإضافة إلى ذلك ، نقوم بتوسيع فئة Equatable حتى نتمكن من مقارنة كائنات المنشورات.
الآن لدينا نموذج استجابة من الخادم ، دعنا ننفذ منطق العمل (Business Logic Component (bloc)).
قبل أن نتعمق في تطوير التطبيق ، نحتاج إلى تحديد ما الذي ستقوم به PostBloc.
في المستوى الأعلى ، سيكون مسؤولاً عن معالجة إجراءات المستخدم (التمرير) وتلقي منشورات جديدة عندما تطلبها طبقة العرض التقديمي. دعنا نبدأ في تنفيذ هذا.
سيرد PostBloc على حدث واحد فقط. تلقي البيانات التي سيتم عرضها على الشاشة حسب الحاجة. لنقم بإنشاء post_event.dart فئة وتنفيذ حدثنا.
import 'package:equatable/equatable.dart'; abstract class PostEvent extends Equatable {} class Fetch extends PostEvent { @override String toString() => 'Fetch'; }
مرة أخرى ، أعد تعريف toString لقراءة السطر الذي يعرض حدثنا بسهولة أكبر. نحتاج أيضًا إلى توسيع فئة Equatable لمقارنة الكائنات.
باختصار ، سيتلقى PostBloc PostEvents ويقوم بتحويلها إلى PostStates. لقد قمنا بتطوير جميع أحداث PostEvents (إحضار) ، وسوف ننتقل إلى PostState.
يجب أن تحتوي طبقة العرض التقديمي على عدة حالات لعرضها بشكل صحيح.
isInitializing - يبلغ طبقة العرض التقديمي أنه من الضروري عرض مؤشر تحميل أثناء تحميل البيانات.
المشاركات - يعرض قائمة كائنات النشر
isError - تبلغ الطبقة
بحدوث أخطاء أثناء تحميل البيانات
hasReachedMax - إشارة إلى السجل الأخير المتاح
إنشاء فئة post_state.dart مع المحتوى التالي
import 'package:equatable/equatable.dart'; import 'package:flutter_infinite_list/models/models.dart'; abstract class PostState extends Equatable { PostState([Iterable props]) : super(props); } class PostUninitialized extends PostState { @override String toString() => 'PostUninitialized'; } class PostInitialized extends PostState { final List<Post> posts; final bool hasError; final bool hasReachedMax; PostInitialized({ this.hasError, this.posts, this.hasReachedMax, }) : super([posts, hasError, hasReachedMax]); factory PostInitialized.success(List<Post> posts) { return PostInitialized( posts: posts, hasError: false, hasReachedMax: false, ); } factory PostInitialized.failure() { return PostInitialized( posts: [], hasError: true, hasReachedMax: false, ); } PostInitialized copyWith({ List<Post> posts, bool hasError, bool hasReachedMax, }) { return PostInitialized( posts: posts ?? this.posts, hasError: hasError ?? this.hasError, hasReachedMax: hasReachedMax ?? this.hasReachedMax, ); } @override String toString() => 'PostInitialized { posts: ${posts.length}, hasError: $hasError, hasReachedMax: $hasReachedMax }'; }
استخدمنا نمط المصنع للراحة وسهولة القراءة. بدلاً من إنشاء كيانات PostState يدويًا ، يمكننا استخدام العديد من المصانع ، مثل PostState.initial ()
الآن لدينا أحداث وشروط ، فقد حان الوقت لإنشاء PostBloc لدينا
لتبسيط ، سيكون لدينا PostBloc تبعية مباشرة لعميل http ، ولكن في الإنتاج يجب عليك لفه في تبعية خارجية في عميل api واستخدام نمط السجل.
إنشاء post_bloc.dart
import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import 'package:http/http.dart' as http; import 'package:flutter_infinite_list/bloc/bloc.dart'; import 'package:flutter_infinite_list/models/models.dart'; class PostBloc extends Bloc<PostEvent, PostState> { final http.Client httpClient; PostBloc({@required this.httpClient}); @override // TODO: implement initialState PostState get initialState => null; @override Stream<PostState> mapEventToState( PostState currentState, PostEvent event, ) async* { // TODO: implement mapEventToState yield null; } }
لاحظ أنه فقط من الإعلان الخاص بفصلنا ، يمكننا أن نقول أنه سيقبل PostEvents ويعطي PostStates
دعنا ننتقل إلى تطوير initialState.
import 'package:bloc/bloc.dart'; import 'package:meta/meta.dart'; import 'package:http/http.dart' as http; import 'package:flutter_infinite_list/bloc/bloc.dart'; import 'package:flutter_infinite_list/models/models.dart'; class PostBloc extends Bloc<PostEvent, PostState> { final http.Client httpClient; PostBloc({@required this.httpClient}); @override PostState get initialState => PostState.initial(); @override Stream<PostState> mapEventToState( PostState currentState, PostEvent event, ) async* { // TODO: implement mapEventToState yield null; } }
بعد ذلك ، تحتاج إلى تطبيق mapEventToState ، والذي سيتم إطلاقه في كل مرة يتم فيها إرسال حدث.
import 'dart:convert'; import 'package:meta/meta.dart'; import 'package:http/http.dart' as http; import 'package:bloc/bloc.dart'; import 'package:flutter_infinite_list/bloc/bloc.dart'; import 'package:flutter_infinite_list/models/models.dart'; class PostBloc extends Bloc<PostEvent, PostState> { final http.Client httpClient; PostBloc({@required this.httpClient}); @override get initialState => PostState.initial(); @override Stream<PostState> mapEventToState(currentState, event) async* { if (event is Fetch && !currentState.hasReachedMax) { try { final posts = await _fetchPosts(currentState.posts.length, 20); if (posts.isEmpty) { yield currentState.copyWith(hasReachedMax: true); } else { yield PostState.success(currentState.posts + posts); } } catch (_) { yield PostState.failure(); } } } 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'); } } }
الآن في كل مرة يتم إرسال PostEvent ، إذا كان هذا حدثًا لأخذ العينات ولم نصل إلى نهاية القائمة ، فسيتم عرض الإدخالات العشرين التالية.
دعونا تعديل PostBloc لدينا قليلا
import 'dart:convert'; import 'package:meta/meta.dart'; import 'package:rxdart/rxdart.dart'; import 'package:http/http.dart' as http; import 'package:bloc/bloc.dart'; import 'package:flutter_infinite_list/bloc/bloc.dart'; import 'package:flutter_infinite_list/models/models.dart'; class PostBloc extends Bloc<PostEvent, PostState> { final http.Client httpClient; PostBloc({@required this.httpClient}); @override Stream<PostEvent> transform(Stream<PostEvent> events) { return (events as Observable<PostEvent>) .debounce(Duration(milliseconds: 500)); } @override get initialState => PostState.initial(); @override Stream<PostState> mapEventToState(currentState, event) async* { if (event is Fetch && !currentState.hasReachedMax) { try { final posts = await _fetchPosts(currentState.posts.length, 20); if (posts.isEmpty) { yield currentState.copyWith(hasReachedMax: true); } else { yield PostState.success(currentState.posts + posts); } } catch (_) { yield PostState.failure(); } } } 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'); } } }
عظيم ، لقد أكملنا تنفيذ منطق الأعمال!
إنشاء الفئة main.dart وتنفيذ runApp فيه لرسم واجهة المستخدم لدينا
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Infinite Scroll', home: Scaffold( appBar: AppBar( title: Text('Posts'), ), body: HomePage(), ), ); } }
بعد ذلك ، قم بإنشاء صفحة رئيسية تعرض منشوراتنا وتتصل بـ PostBloc
class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { final _scrollController = ScrollController(); final PostBloc _postBloc = PostBloc(httpClient: http.Client()); final _scrollThreshold = 200.0; _HomePageState() { _scrollController.addListener(_onScroll); _postBloc.dispatch(Fetch()); } @override Widget build(BuildContext context) { return BlocBuilder( bloc: _postBloc, builder: (BuildContext context, PostState state) { if (state.isInitializing) { return Center( child: CircularProgressIndicator(), ); } if (state.isError) { return Center( child: Text('failed to fetch posts'), ); } if (state.posts.isEmpty) { return Center( child: Text('no posts'), ); } return ListView.builder( itemBuilder: (BuildContext context, int index) { return index >= state.posts.length ? BottomLoader() : PostWidget(post: state.posts[index]); }, itemCount: state.hasReachedMax ? state.posts.length : state.posts.length + 1, controller: _scrollController, ); }, ); } @override void dispose() { _postBloc.dispose(); super.dispose(); } void _onScroll() { final maxScroll = _scrollController.position.maxScrollExtent; final currentScroll = _scrollController.position.pixels; if (maxScroll - currentScroll <= _scrollThreshold) { _postBloc.dispatch(Fetch()); } } }
بعد ذلك ، نقوم بتطبيق BottomLoader ، والتي ستظهر للمستخدم تحميل المشاركات الجديدة.
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, ), ), ), ); } }
أخيرًا ، ننفذ PostWidget التي ترسم كائنًا واحدًا من النوع Post
class PostWidget extends StatelessWidget { final Post post; const PostWidget({Key key, @required this.post}) : super(key: key); @override Widget build(BuildContext context) { return ListTile( leading: Text( post.id.toString(), style: TextStyle(fontSize: 10.0), ), title: Text('${post.title}'), isThreeLine: true, subtitle: Text(post.body), dense: true, ); } }
هذا كل شيء ، الآن يمكنك تشغيل التطبيق ورؤية النتيجة.
يمكن تنزيل مصادر المشروع على
جيثب