Flutter Client Serverアプリケーションの例





このチュートリアルでは、インターネット経由でデータを受信し、リストに表示するアプリケーションを開発します。 このようなもの







では、プロジェクトを作成することから始めましょう。 コマンドラインで以下を書いてください



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を使用してmochaデータを取得します。 このサービスに慣れていない場合、これは偽のデータを送信できるオンライン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 }'; }
      
      





Postは、id、title、およびbodyを持つ単なるクラスです。 toString関数をオーバーライドして、後で便利な文字列を表示することもできます。 さらに、Equatableクラスを拡張して、Postsオブジェクトを比較できるようにします。



サーバーからの応答モデルができたので、ビジネスロジック(ビジネスロジックコンポーネント(ブロック))を実装しましょう。



アプリケーション開発に飛び込む前に、PostBlocの機能を決定する必要があります。



最上位では、ユーザーアクション(スクロール)の処理と、プレゼンテーションレイヤーが要求した場合の新しい投稿の受信を担当します。 これの実装を始めましょう。



PostBlocは1つのイベントにのみ応答します。 必要に応じて画面に表示されるデータを受信します。 クラスpost_event.dartを作成し、イベントを実装します



 import 'package:equatable/equatable.dart'; abstract class PostEvent extends Equatable {} class Fetch extends PostEvent { @override String toString() => 'Fetch'; }
      
      





繰り返しますが、toStringをオーバーライドして、イベントを表示する行を読みやすくします。 Equatableクラスを拡張してオブジェクトを比較する必要もあります。



要約すると、PostBlocはPostEventを受け取り、PostStateに変換します。 すべてのPostEvents(フェッチ)イベントを開発しました。PostStateに進みます。



プレゼンテーション層には、正しく表示するためにいくつかの状態が必要です。



isInitializing-プレゼンテーション層に、データの読み込み中に読み込みインジケーターを表示する必要があることを通知します。



posts -Postオブジェクトのリストを表示します



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 }'; }
      
      





利便性と読みやすさのために、Factoryパターンを使用しました。 PostStateエンティティを手動で作成する代わりに、PostState.initial()などのさまざまなファクトリを使用できます



イベントと条件ができたので、PostBlocを作成します

単純化するために、PostBlocには直接httpクライアントの依存関係がありますが、実稼働環境ではそれをAPIクライアントの外部依存関係にラップし、Repositoryパターンを使用する必要があります。



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がディスパッチされるたびに、これがサンプリングイベントであり、リストの最後に到達していない場合、次の20エントリが表示されます。



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を実装してUIを描画します



 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に接続するHomePageを作成します



 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, ); } }
      
      





以上で、アプリケーションを実行して結果を確認できます。



プロジェクトソースはGithubからダウンロードできます



All Articles