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


<<:  30-26 之 DDD 战略设计 2 - 实作方法之 Event Storm

>>:  【Day 25】React 与 Immutible

[Day14] 测试与迭代

现在,基於我们现有的初始对话流与打造完成的语音应用程序。 来试着让它变得更好! 现在我们进入设计对...

[Java Day03] 1.1. 变数

教材网址 https://coding104.blogspot.com/2021/06/java-v...

[Lesson14] Retrofit

在 gradle (Module) 层级的 dependencies 中内加入: implement...

食谱搜寻系统後端语法简介

学习原因 JS和html 、css一直是网页前端设计的三巨头,与html、css不同的是,JS同时也...

Day10 iPhone捷径-位置Part2

Hello 大家, 废话不多说赶紧赶紧进入主题吧! 今天从叫车开始说起~ 这个动作要设定的参数有三个...