iT邦幫忙

2021 iThome 鐵人賽

DAY 28
0
Modern Web

Flutter web 的奇妙冒險系列 第 28

Day 28 | 狀態管理-從官方範例來看如何使用BLoC

那今天我們就來使用blocflutter_bloc 這兩個來實作範例,基本上我們在實作BLoC pattern時我們都會切分成三層分別是:資料層、BLoC層、UI層。

那這次我們直接來看官方提供其中一個範例:無限滾動列表

這次會用到的套件

dependencies:
  freezed_annotation: ^0.14.3
  dio: ^4.0.0
  bloc: ^7.2.1
  flutter_bloc: ^7.3.0
  equatable: ^2.0.3
  bloc_concurrency: ^0.1.0
  stream_transform: ^2.0.0

dev_dependencies:
  build_runner: ^2.1.4
  freezed: ^0.14.5
  json_serializable: ^5.0.2

資料層

我們一樣使用 jsonPlaceholder 加上quicktype 的資料產出這個 model

// To parse this JSON data, do
//
//     final post = postFromJson(jsonString);

import 'package:equatable/equatable.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:convert';

part 'post.freezed.dart';
part 'post.g.dart';

List<Post> postFromJson(String str) =>
    List<Post>.from(json.decode(str).map((x) => Post.fromJson(x)));

String postToJson(List<Post> data) =>
    json.encode(List<dynamic>.from(data.map((x) => x.toJson())));

@freezed
abstract class Post with _$Post {
  const factory Post({
    int? userId,
    int? id,
    String? title,
    String? body,
  }) = _Post;

  factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
}

BLoC 層

首先我們需要新增三個檔案分別代表「事件」、「狀態」、「BLoC」

// post_event.dart
part of 'post_bloc.dart';

abstract class PostEvent extends Equatable {
  @override
  List<Object> get props => [];
}

class PostFetched extends PostEvent {}
// post_state.dart

part of 'post_bloc.dart';

enum PostStatus { initial, success, failure }

class PostState extends Equatable {
  const PostState({
    this.status = PostStatus.initial,
    this.posts = const <Post>[],
    this.hasReachedMax = false,
  });

  final PostStatus status;
  final List<Post> posts;
  final bool hasReachedMax;

  PostState copyWith({
    PostStatus? status,
    List<Post>? posts,
    bool? hasReachedMax,
  }) {
    return PostState(
      status: status ?? this.status,
      posts: posts ?? this.posts,
      hasReachedMax: hasReachedMax ?? this.hasReachedMax,
    );
  }

  @override
  String toString() {
    return '''PostState { status: $status, hasReachedMax: $hasReachedMax, posts: ${posts.length} }''';
  }

  @override
  List<Object> get props => [status, posts, hasReachedMax];
}

首先來看「事件」及「狀態」

PostEvent 就是我們這個BLoC會接收到的所以事件的父類,而這裡繼承了 Equatable 是為了能讓我們的在比較兩個 instance時可以正確的比對,因為就算我們傳入一模一樣的值進入同一個constructor 還是會產生兩個不一樣的實例,Equatable override了 ==hashcode 讓我們可以能夠變成「值一樣就代表時同一個instance」。

那我們這裡就只要有一個事件: PostFetched

接下來看到狀態,我們一樣繼承了 Equatable ,然後我們的狀態有三個值: statuspostshasReachedMax 來表示fetch的狀態、存放Post的值以及是否讀取到最後了。

這邊最主要是實作了 copyWith 這個方法,因為每次我們要從BLoC的送出資料時都是送出「完整一份狀態」,也就是利用immutable的概念。

所以為了減少麻煩如果我只要更改其中一種field我只要 copyWith 後然後傳入我們要變更的field及數值就好。

最後就來看看我們的BLoC

import 'dart:convert';

import 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_rest_api_playground/model/post/post.dart';
import 'package:flutter_rest_api_playground/service/http.dart';
import 'package:stream_transform/stream_transform.dart';

part 'post_event.dart';
part 'post_state.dart';

const _postLimit = 20;
const throttleDuration = Duration(milliseconds: 100);
EventTransformer<E> throttleDroppable<E>(Duration duration) {
  return (events, mapper) {
    return droppable<E>().call(events.throttle(duration), mapper);
  };
}

class PostBloc extends Bloc<PostEvent, PostState> {
  PostBloc() : super(const PostState()) {
    on<PostFetched>(
      _onPostFetched,
      transformer: throttleDroppable(throttleDuration),
    );
  }

  final HttpService _httpService = HttpService();

  Future<void> _onPostFetched(
      PostFetched event, Emitter<PostState> emit) async {
    if (state.hasReachedMax) return;
    try {
      if (state.status == PostStatus.initial) {
     
        final posts = await _fetchPosts();
        return emit(state.copyWith(
          status: PostStatus.success,
          posts: posts,
          hasReachedMax: false,
        ));
      }

     
      final posts = await _fetchPosts(state.posts.length);
      emit(posts.isEmpty
          ? state.copyWith(hasReachedMax: true)
          : state.copyWith(
              status: PostStatus.success,
              posts: List.of(state.posts)..addAll(posts),
              hasReachedMax: false,
            ));
    } catch (_) {
      emit(state.copyWith(status: PostStatus.failure));
    }
  }

  Future<List<Post>> _fetchPosts([int startIndex = 0]) async {
    final response = await _httpService.get(
      '/posts',
      queryParameters: {'_start': '$startIndex', '_limit': '$_postLimit'},
    );

    final jsonStr = json.encode(response.data);
    final result = postFromJson(jsonStr);
    return result;
  }
}

首先我們先實例化一個這個BLoC私有的 _httpService 做為我們call api 的 client。

首先先來實作 _fetchPosts 這個call api 的method , 主要就是封裝了資料轉換及傳入 queryParameters

然後就是要來實作event handler: _onPostFetched

首先會看到如果我們讀到最後了就會直接return 不 emit 也就代表 UI層那邊不會收到這件事情,接下來就是做初次的fetch,這裡會看到我們用 emit 包裹我們要送出的狀態,這裡就用 copyWith 來讓我們創造一份新的狀態。

接下來就是實作接下來正常的每次fetch,其實也只是繼續用emit 將狀態送出。這裡會用到 .. casecade 運算子,因為有些method不會回傳值就只是單純的mutate操作,但使用.. 就能直接回傳那個instance。


今天的程式碼:
https://github.com/zxc469469/flutter_rest_api_playground/tree/Day28

今天就是直接從官方範例來做這個Demo,看看做完後還能不能再額外加什麼功能之類的。

明天我們就繼續來說明 UI層的實作


參考資料:

https://bloclibrary.dev/#/flutterinfinitelisttutorial


上一篇
Day 27 | 狀態管理 - BLoC基本介紹
下一篇
Day 29 | 狀態管理-從官方範例來看如何使用BLoC (2)
系列文
Flutter web 的奇妙冒險30

尚未有邦友留言

立即登入留言