iT邦幫忙

2021 iThome 鐵人賽

DAY 9
0
Mobile Development

Flutter - 複製貼上到開發套件之旅系列 第 9

【第九天 - Flutter Bloc+Cubit 架構教學】

今日的程式碼

Bloc 範例 => GITHUB Bloc
Cubit 範例 => GITHUB Cubit

什麼事 Bloc ㄋ?




flutter_bloc 文件

我們來看這張圖,簡單來說,他就是 UI 觸發一個事件,傳到了 bloc 後,處理邏輯,處理好後,丟出一個狀態。當 UI 接收到對應的狀態,給予對應的 UI。

Bloc 的種類

Bloc 這個套件有兩種寫法。

  • Bloc
  • Cubit(flutter_bloc version 5.0以上才有的)。

差異

  • Bloc 當觸發新的狀態, blocBuilder 會重新建立。
    • Bloc 需要有 event、bloc、state 三種組成,yield 是作為輸出狀態的指令。
  • Cubit 當觸發新的狀態, blocBuilder 會重新建立。
    • cubit 則是需要 cuibt 裡面的 function、state 兩種組成,emit 作為輸出狀態

Cubit

Cubit 的 State(狀態)(Cubit、Bloc 程式碼一樣)

這邊一般會先用一個 IPostEvent 然後後面的繼承他這樣子。前面加上一個 I 是因為這樣我比較好知道他是 abstract class。

  • PostLoading 的狀態
  • PostSuccess 的狀態。(裡面的參數是用來顯示在畫面上的,因此我需要 postList 這筆資料。)

Equatable 在前幾 Day 7 有講過。

abstract class IPostState {}

class PostLoading extends IPostState {}

class PostSuccess extends IPostState {
  final List<PostModel> postList;
  PostSuccess( this.postList);
}

直接來看 Cubit 邏輯範例

  • 這邊定義了排序的狀態(SortState)。
  • PostCubit 丟了一個建構子,是一個 IPostRepository,用來請求資料的。super 是用來設定初始值的。(這邊預設的狀態是在 PostLoading)。
  • emit 代表,輸出狀態。預設一剛開始就是 PostLoading 因此,在 fetchDatasort 結束後,狀態將會是 PostSuccess
enum SortState { userId, id, title, body }

class PostCubit extends Cubit<IPostState> {
  IPostRepository _repository;
  List<PostModel> postList = [];

  PostCubit(IPostRepository repository)
      : _repository = repository,
        super(PostLoading());

  Future<void> fetchData() async {
    postList = await _repository.fetchData();
    sort(SortState.id);
  }

  void sort(SortState sortBy) {
    switch (sortBy) {
      case SortState.userId:
        postList.sort((a, b) => a.userId.compareTo(b.userId));
        break;
      case SortState.id:
        postList.sort((a, b) => a.id.compareTo(b.id));

        break;
      case SortState.title:
        postList.sort((a, b) => a.title.compareTo(b.title));
        break;
      case SortState.body:
        postList.sort((a, b) => a.body.compareTo(b.body));
        break;
    }
    emit(PostSuccess([...postList]));
  }
}

Cubit 初始化(Cubit、Bloc 使用方式一樣)

Cubit 的初始化和後面會講到的 Bloc 一樣,和 Provider 類似。
MultiBlocProvider 元件是一個初始化的共享裝置元件。
這邊我們在 MaterialApp 外面建立了一個 MultiBlocProvider,代表只要在 MaterialApp 裡面都可以取到 PostCubit 的資料。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider<PostCubit>(
          create: (BuildContext context) => PostCubit(
            PostRepository(PostService()),
          ),
        ),
      ],
      child: MaterialApp(
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: MyHomePage(title: 'Cubit Sample'),
      ),
    );
  }
}

Cubit 專用的觸發事件(Event)

Cubit 的觸發事件不會向 bloc 有一個專門的 Event,而是直接呼叫 PostCubit 裡面的 sort() 方法。

context.read<PostCubit>().sort(value);

BlocBuilder(Cubit、Bloc 使用方式一樣)

CubitBlocBuilderBloc 一樣

BlocBuilder<PostCubit, IPostState>(
    builder: (_, state) {
      // 如果種態勢 PostSuccess 的話
      if (state is PostSuccess) {
        return Widget;
      }
      // else{} //預設值。
      return Center(child: CircularProgressIndicator());
    },
  );

BlocListener(Cubit、Bloc 使用方式一樣)

這邊引用 flutter_bloc 文件範例

BlocBuilder<BlocA, BlocAState>(
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)
// or
// 你不想要共享資料的話,可以放在這。
BlocBuilder<BlocA, BlocAState>(
  bloc: blocA, // provide the local bloc instance
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)

BlocBuilder 和 BlocListener 的差異

我會這樣說,BlocBuilder 只是用來回應不同狀態下對應的 widgets,BlocListener 則是用來在不同狀態 "do things "。比方說跳頁showSnackBar 等等...,你可以考範例 showsnackbarNavigator

BlocBuilder (_MyListView)範例

如果狀態是 PostSuccess,那麼我就回傳一個 ListView.builder,其他的則是回傳一個 CircularProgressIndicator

class _MyListView extends StatelessWidget {
  const _MyListView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<PostCubit, IPostState>(
      builder: (_, state) {
        if (state is PostSuccess) {
          return ListView.builder(
            itemCount: state.postList.length,
            itemBuilder: (context, index) {
              PostModel item = state.postList[index];
              return Container(
                  decoration: BoxDecoration(
                      borderRadius: BorderRadius.all(Radius.circular(16)),
                      color: Colors.white,
                      border: Border.all(color: Colors.blueAccent, width: 2.0)),
                  margin: EdgeInsets.all(8),
                  padding: EdgeInsets.all(8),
                  child: RichText(
                    text: TextSpan(
                      style: DefaultTextStyle.of(context).style,
                      children: <TextSpan>[
                        TextSpan(
                          text: item.id.toString() + ". " + item.title,
                          style: TextStyle(fontSize: 18, color: Colors.red),
                        ),
                        TextSpan(
                          text: '\n' + item.body,
                          style: TextStyle(fontWeight: FontWeight.bold),
                        ),
                        TextSpan(
                          text: "\nUser ID:" + item.userId.toString(),
                          style: TextStyle(fontSize: 18),
                        ),
                      ],
                    ),
                  ));
            },
          );
        }
        return Center(child: CircularProgressIndicator());
      },
    );
  }
}

MyHomePage 的程式碼

class MyHomePage extends StatefulWidget {
  final String title;

  const MyHomePage({Key? key, required this.title}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  void initState() {
    super.initState();
    context.read<PostCubit>().fetchData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
          actions: <Widget>[
            PopupMenuButton<SortState>(
              icon: Icon(Icons.more_vert),
              itemBuilder: (context) => [
                PopupMenuItem(
                  child: Text('使用 userId 排序'),
                  value: SortState.userId,
                ),
                PopupMenuItem(
                  child: Text('使用 id 排序'),
                  value: SortState.id,
                ),
                PopupMenuItem(
                  child: Text('使用 title 排序'),
                  value: SortState.title,
                ),
                PopupMenuItem(
                  child: Text('使用 body 排序'),
                  value: SortState.body,
                )
              ],
              onSelected: (SortState value) {
                context.read<PostCubit>().sort(value);
              },
            )
          ],
        ),
        body: _MyListView());
  }
}

Bloc

Bloc 則是把事件分開需要有 EventBlocState 作為事件。
這邊順便介紹一個 Android Studio Bloc 套件

post_bloc_event(Evnet,觸發事件,Bloc 專用的)

part of 'post_bloc.dart';
abstract class IPostEvent {}
/// cal Api 的事件
class FetchPostData extends IPostEvent {}
/// 排序的事件
class SortPostEvent extends IPostEvent {
  final SortState sortState;
  SortPostEvent({required this.sortState});
}

post_bloc(Bloc,處理邏輯的部份)

enum SortState { userID, id, title, body }

class PostBloc extends Bloc<IPostEvent, IPostState> {
  final IPostRepository _repository;

  List<PostModel> postList = [];
  /// 設定初始狀態 super(這裡要放初始狀態)
  PostBloc({required IPostRepository repository}) : _repository = repository,super(PostLoading());

  @override
  Stream<IPostState> mapEventToState(IPostEvent event) async* {
    // 如果是件事 FetchPostData
    if (event is FetchPostData) {yield* _fetchData(event);}
    // 如果是件事 SortPostEvent
    else if(event is SortPostEvent){yield* _sortState(event);}
  }

  Stream<IPostState> _fetchData(FetchPostData event) async* {
    // 請求 API
    postList = await _repository.fetchData();
    // 觸發另一個排序事件 SortPostEvent
    add(SortPostEvent(sortState: SortState.id));
  }
  // 自定義的 method
  Stream<IPostState> _sortState(SortPostEvent event) async* {
    _sort(event.sortState);
    yield PostSuccess(postList);
  }
  // sort method
  Future<void> _sort(SortState sortState)async {
    switch (sortState) {
      case SortState.title:
        postList.sort((a, b) => a.title.compareTo(b.title));
        break;
      case SortState.id:
        postList.sort((a, b) => a.id.compareTo(b.id));
        break;
      case SortState.userID:
        postList.sort((a, b) => a.userId.compareTo(b.userId));
        break;
      case SortState.body:
        postList.sort((a, b) => a.body.compareTo(b.body));
        break;
    }
  }
}

State

這邊的 StateCubit 一樣。
State 的程式碼 => GITHUB

初始化設定

這邊初始化設定和 Cubit 一樣。
初始化的程式碼 => GITHUB

觸發事件 Event

bloc 是使用 add() 裡面放 event 的 class

/// cubit
 context.read<PostCubit>().sort(value);
/// bloc
 context.read<PostBloc>().add(SortPostEvent(sortState: value));

上一篇
【第八天 - Flutter Provider 架構教學】
下一篇
【第十天 - Flutter Bloc Unit Test+Mocktail 範例】
系列文
Flutter - 複製貼上到開發套件之旅30

尚未有邦友留言

立即登入留言