
Dalam tutorial ini, kita akan mengembangkan aplikasi yang menerima data melalui Internet dan menampilkannya dalam daftar. Sesuatu seperti ini

Ok, mari kita mulai dengan membuat proyek. Tuliskan yang berikut di baris perintah
flutter create flutter_infinite_list
Selanjutnya, buka file dependensi kami
pubspec.yaml dan tambahkan yang kami butuhkan
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
Setelah itu, instal dependensi ini dengan perintah berikut
flutter packages get
Untuk aplikasi ini, kami akan menggunakan
jsonplaceholder untuk mendapatkan data moka. Jika Anda tidak terbiasa dengan layanan ini, ini adalah layanan REST API online yang dapat mengirim data palsu. Ini sangat berguna untuk membangun prototipe aplikasi.
Dengan membuka tautan berikut
jsonplaceholder.typicode.com/posts?_start=0&_limit=2 Anda akan melihat respons JSON yang dengannya kami akan bekerja.
[ { "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" } ]
Perhatikan bahwa dalam permintaan GET kami, kami menentukan batasan awal dan akhir sebagai parameter.
Hebat, sekarang kita tahu bagaimana struktur data kita akan terlihat! Mari kita buat model untuk mereka.
Buat file post.dart dengan konten berikut
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 }'; }
Pos hanya kelas dengan id, judul, dan isi. Kita juga bisa mengganti fungsi toString untuk menampilkan string yang nyaman nanti. Selain itu, kami memperluas kelas Equatable sehingga kami dapat membandingkan objek Posting.
Sekarang kita memiliki model respons dari server, mari kita terapkan logika bisnis (Business Logic Component (blok)).
Sebelum kita menyelami pengembangan aplikasi, kita perlu menentukan apa yang akan dilakukan PostBloc kita.
Di tingkat atas, ia akan bertanggung jawab untuk memproses tindakan pengguna (menggulir) dan menerima posting baru ketika lapisan presentasi memintanya. Mari kita mulai menerapkan ini.
PostBloc kami hanya akan merespons satu peristiwa. Menerima data yang akan ditampilkan di layar sesuai kebutuhan. Mari kita membuat post_event.dart kelas dan menerapkan acara kami.
import 'package:equatable/equatable.dart'; abstract class PostEvent extends Equatable {} class Fetch extends PostEvent { @override String toString() => 'Fetch'; }
Sekali lagi, definisikan ulang toString untuk lebih mudah membaca baris yang menampilkan acara kami. Kita juga perlu memperluas kelas Equatable untuk membandingkan objek.
Singkatnya, PostBloc kami akan menerima PostEvents dan mengubahnya menjadi PostStates. Kami telah mengembangkan semua acara PostEvents (Ambil), kami akan beralih ke PostState.
Lapisan presentasi kami harus memiliki beberapa status untuk ditampilkan dengan benar.
isInitializing - memberi tahu lapisan presentasi bahwa perlunya menampilkan indikator pemuatan saat data dimuat.
posting - menampilkan daftar objek Posting
isError - menginformasikan lapisan bahwa kesalahan terjadi saat memuat data
hasReachedMax - indikasi catatan terakhir yang tersedia
Buat kelas post_state.dart dengan konten berikut
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 }'; }
Kami menggunakan pola Pabrik untuk kenyamanan dan keterbacaan. Alih-alih secara manual membuat entitas PostState, kita dapat menggunakan berbagai pabrik, seperti PostState.initial ()
Sekarang kami memiliki acara dan ketentuan, saatnya untuk membuat PostBloc kami
Untuk mempermudah, PostBloc kami akan memiliki ketergantungan klien http langsung, namun dalam produksi Anda harus membungkusnya dalam ketergantungan eksternal pada klien api dan menggunakan pola Repositori.
Buat 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; } }
Perhatikan bahwa hanya dari deklarasi kelas kami yang dapat kami katakan bahwa ia akan menerima PostEvents dan memberikan PostStates
Mari kita beralih ke mengembangkan status awal.
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; } }
Selanjutnya, Anda perlu mengimplementasikan mapEventToState, yang akan diaktifkan setiap kali sebuah acara dikirimkan.
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'); } } }
Sekarang setiap kali PostEvent dikirim, jika ini adalah acara pengambilan sampel dan kami belum mencapai akhir daftar, 20 entri berikutnya akan ditampilkan.
Mari kita sedikit memodifikasi PostBloc kita
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'); } } }
Hebat, kami telah menyelesaikan implementasi logika bisnis!
Buat kelas main.dart dan terapkan runApp di dalamnya untuk menggambar UI kami
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(), ), ); } }
Selanjutnya, buat HomePage yang menampilkan posting kami dan menghubungkan ke 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()); } } }
Selanjutnya, kami menerapkan BottomLoader, yang akan menunjukkan kepada pengguna pemuatan posting baru.
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, ), ), ), ); } }
Akhirnya, kami menerapkan PostWidget yang akan menggambar objek tunggal bertipe 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, ); } }
Itu saja, sekarang Anda bisa menjalankan aplikasi dan melihat hasilnya.
Sumber proyek dapat diunduh di
Github