
In diesem Tutorial werden wir eine Anwendung entwickeln, die Daten über das Internet empfängt und in einer Liste anzeigt. So etwas in der Art

Ok, beginnen wir mit der Erstellung eines Projekts. Schreiben Sie Folgendes in die Befehlszeile
flutter create flutter_infinite_list
Gehen Sie als Nächstes zu unserer Abhängigkeitsdatei
pubspec.yaml und fügen Sie die benötigten hinzu
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
Installieren Sie danach diese Abhängigkeiten mit dem folgenden Befehl
flutter packages get
Für diese Anwendung verwenden wir
jsonplaceholder , um Mokka-Daten
abzurufen . Wenn Sie mit diesem Dienst nicht vertraut sind, handelt es sich um einen Online-REST-API-Dienst, der gefälschte Daten senden kann. Dies ist sehr nützlich, um Anwendungsprototypen zu erstellen.
Wenn Sie den folgenden Link
jsonplaceholder.typicode.com/posts?_start=0&_limit=2 öffnen , sehen Sie die JSON-Antwort, mit der wir arbeiten werden.
[ { "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" } ]
Beachten Sie, dass wir in unserer GET-Anforderung die Start- und Endbedingung als Parameter angegeben haben.
Großartig, jetzt wissen wir, wie die Struktur unserer Daten aussehen wird! Lassen Sie uns ein Modell für sie erstellen.
Erstellen Sie eine post.dart-Datei mit den folgenden Inhalten
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 }'; }
Post ist nur eine Klasse mit ID, Titel und Text. Wir können auch die toString-Funktion überschreiben, um später eine praktische Zeichenfolge anzuzeigen. Außerdem erweitern wir die Equatable-Klasse, damit wir Posts-Objekte vergleichen können.
Jetzt haben wir ein Antwortmodell vom Server. Implementieren wir die Geschäftslogik (Business Logic Component (Block)).
Bevor wir uns mit der Anwendungsentwicklung befassen, müssen wir festlegen, was unser PostBloc tun wird.
Auf der obersten Ebene ist er für die Verarbeitung von Benutzeraktionen (Scrollen) und den Empfang neuer Beiträge verantwortlich, wenn die Präsentationsschicht sie anfordert. Beginnen wir mit der Implementierung.
Unser PostBloc reagiert nur auf ein Ereignis. Empfangen von Daten, die bei Bedarf auf dem Bildschirm angezeigt werden. Lassen Sie uns eine Klasse post_event.dart erstellen und unser Ereignis implementieren.
import 'package:equatable/equatable.dart'; abstract class PostEvent extends Equatable {} class Fetch extends PostEvent { @override String toString() => 'Fetch'; }
Definieren Sie erneut toString neu, um die Zeile, in der unser Ereignis angezeigt wird, leichter lesen zu können. Wir müssen auch die Equatable-Klasse erweitern, um Objekte zu vergleichen.
Zusammenfassend erhält unser PostBloc PostEvents und konvertiert sie in PostStates. Wir haben alle PostEvents (Fetch) -Ereignisse entwickelt und werden zu PostState übergehen.
Unsere Präsentationsschicht muss mehrere Zustände haben, um korrekt angezeigt zu werden.
isInitializing -
Informiert die Präsentationsschicht darüber, dass während des Ladens der Daten eine Ladeanzeige angezeigt werden muss.
posts - Zeigt eine Liste der Post-Objekte an
isError -
Informiert die Ebene, dass beim Laden von Daten Fehler aufgetreten sind
hasReachedMax - Anzeige des letzten verfügbaren Datensatzes
Erstellen Sie eine post_state.dart-Klasse mit dem folgenden Inhalt
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 }'; }
Wir haben das Factory-Muster aus Gründen der Benutzerfreundlichkeit und Lesbarkeit verwendet. Anstatt PostState-Entitäten manuell zu erstellen, können wir verschiedene Fabriken verwenden, z. B. PostState.initial ()
Jetzt haben wir Ereignisse und Bedingungen, es ist Zeit, unseren PostBloc zu erstellen
Zur Vereinfachung hat unser PostBloc eine direkte http-Client-Abhängigkeit. In der Produktion sollten Sie es jedoch in eine externe Abhängigkeit im API-Client einbinden und das Repository-Muster verwenden.
Erstellen Sie 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; } }
Beachten Sie, dass wir nur aus der Deklaration unserer Klasse sagen können, dass sie PostEvents akzeptiert und PostStates gibt
Fahren wir mit der Entwicklung von initialState fort.
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; } }
Als Nächstes müssen Sie mapEventToState implementieren, das bei jedem Senden eines Ereignisses ausgelöst wird.
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'); } } }
Jedes Mal, wenn PostEvent versendet wird, werden die nächsten 20 Einträge angezeigt, wenn dies ein Stichprobenereignis ist und wir das Ende der Liste noch nicht erreicht haben.
Lassen Sie uns unseren PostBloc ein wenig modifizieren
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'); } } }
Großartig, wir haben die Implementierung der Geschäftslogik abgeschlossen!
Erstellen Sie die Klasse main.dart und implementieren Sie runApp darin, um unsere Benutzeroberfläche zu zeichnen
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(), ), ); } }
Erstellen Sie als Nächstes eine HomePage, auf der unsere Beiträge angezeigt und eine Verbindung zu PostBloc hergestellt werden
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()); } } }
Als nächstes implementieren wir BottomLoader, der dem Benutzer das Laden neuer Beiträge zeigt.
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, ), ), ), ); } }
Schließlich implementieren wir ein PostWidget, das ein einzelnes Objekt vom Typ Post zeichnet
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, ); } }
Das ist alles, jetzt können Sie die Anwendung ausführen und das Ergebnis sehen.
Projektquellen können auf
Github heruntergeladen werden