Bloc Pattern (with flutter_bloc Plugin)

Updated:

1.왜? Bloc Pattern 써야 하는가?

  • 모든 flutter app 은 Stateful Widget 을 사용해서 동적인 화면을 변경을 setState() 를 사용해서 쉽게 구현 할 수 있습니다.

  • 하지만, 이런한 구조는 단순한 위젯의 구조 일 경우에 해당되는 부분이고, 보통 App 을 만들고 사용자들에게 배포를 할 경우 서버로 부터 data 를 받아오거나, local 에 있는 data 를 business logic 를 통해 만들경우 복잡한 Widget tree 가 되게 됩니다. 이러한 경우에 setState() 로만 사용해서 data 를 변경 할 경우 더 복잡하게 됩니다

  • 이러한 불편하고, 복잡한 부분을 해결하기 위해 Bloc (Business Logic Component) pattern 이라고 합니다. 말 그대로 Business logic 을 따로 때내어 분리한 일종의 component 입니다.

  • UI 부분에서는 business 부분을 관여할 필요가 없게 되며, data 의 값을 가공할 필요가 없게 되고 bloc pattern 로 정리된 data 를 그대로 받아서 보여주기만 하면 되고, 모든 business logic 은 bloc pattern 에서 다루게 됩니다.

image

  • 위의 그림에서는 UI widget 에서는 events 만 bloc 에 넘기고 bloc 안에 transition 이라던지 모든 state 는 bloc 안에서 Streams 로 UI widget 에 넘기게 됩니다

  • bloc pattern 의 가장 큰 장점은 state 를 widget tree 상에서 관리하지 않기 때문에 예전에는 만약 scaffold 상의 setState() 를 변경할때는 전체 widget tree 에서 build context 에서 reload 가 되지 않기 때문에 tree 구조가 복잡할 경우, app 구동 속도가 빨라 집니다

image

image

Bloc pattern 의 단점

  • 관리되는 파일이 많이 집니다. (state 를 다루는게 하나의 Bloc 만 있는것이 아니라, 다수의 Bloc 이 있을 것인데, 관리하기가 복잡하고, setState() 와 비교해도 복잡한 구조를 가지고 있게 됩니다)

  • 그것을 보안하기 위해서 나온것이 Provider 가 있습니다

Bloc pattern code 예시로

  • flutter 설치시 demo 로 보이는 숫자 count 되는 화면을 bloc pattern 으로 만든 예시 입니다

image

구조는 ui 부분 ui 부분의 body 부분은 component 로 분리해서 count_view.dart 파일로 이루어 져있고 모든 state 값은 bloc 폴더에 count_bloc.dart 에 위치하게 됩니다

// in main.dart

class _HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Center(
              child: ElevatedButton(
                child: Text("bloc 패턴"),
                onPressed: () {
                  Navigator.push(context, MaterialPageRoute(builder: (_) {
                    return BlocDisplayWidget();
                  }));
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// in bloc_display_widget.dart

// Global 변수생성
CountBloc countBloc;

class BlocDisplayWidget extends StatefulWidget {
  BlocDisplayWidget({Key key}) : super(key: key);

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

class _BlocDisplayWidgetState extends State<BlocDisplayWidget> {
  // initState() : Bloc 생성
  @override
  void initState() {
    super.initState();
    countBloc = CountBloc();
  }

  // dispose() : 페이지가 닫히면 app 이 dispose 되게 함
  @override
  void dispose() {
    super.dispose();
    countBloc.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("bloc 패턴"),
      ),
      body: CountView(countBloc: countBloc),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () {
          countBloc.add();
        },
      ),
    );
  }
}

// in count_view.dart

class CountView extends StatelessWidget {
  CountBloc countBloc;
  CountView({Key key, this.countBloc}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print("CountView Build!!");
    return Center(
      child: StreamBuilder(
        stream: countBloc.count,
        initialData: 0,
        // AsyncSnapshot 으로 값이 들어오게 됨
        builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
          if (snapshot.hasData) {
            return Text(
              snapshot.data.toString(),
              style: TextStyle(fontSize: 70.0),
            );
          }
          return CircularProgressIndicator();
        },
      ),
    );
  }
}

// in count_bloc.dart

class CountBloc {
  int _count = 0;
  final StreamController<int> _countSubject = StreamController<int>();
  // 변경된 widget 값을 전달 하게 됨
  Stream<int> get count => _countSubject.stream;

  add() {
    _count++;
    _countSubject.sink.add(_count);
  }

  dispose() {
    _countSubject.close();
  }
}

2.flutter_bloc package

flutter_bloc_package

  • bloc pattern 을 flutter 에 적용할경우, package 없이 그냥 custom 하게 될 경우 작성해야될 경우가 너무 많게 되고 관리도 복잡하기 때문에 flutter 에서 bloc 패턴 사용시 주로 flutter_bloc_package 를 설치해서 사용하게 됩니다

flutter_bloc package 사용 예제(todo List) 환경 설정

설치 dependency

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.0
  flutter_bloc: ^7.3.1
  freezed_annotation:
  json_annotation: ^4.3.0
  equatable:

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner:
  freezed:
  json_serializable:

model 생성

  • freezed 를 사용해서 Todo class model 생성

참조 Flutter Freezed

 // in model/todo.dart

import 'package:freezed_annotation/freezed_annotation.dart';

part 'todo.freezed.dart';
part 'todo.g.dart';

@freezed
class Todo with _$Todo {
  factory Todo({
    required int id,
    required String title,
    required String createdAt,
  }) = _Todo;

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

repository 생성

// in repository/todo_repository.dart

// todo list app 은 실제로 만들면 server 와 연동을 하게 되는데 RestAPI 연동하는것을 시뮬레이션 하기 위한 repository 생성

// 3개의 method 생성 (이벤트 처리를 위한)
// GET = listTodo
// POST = createTodo
// DELETE = deleteTodo

import 'package:flutter_bloc_sample/model/todo.dart';

class TodoRepository {
  Future<List<Map<String, dynamic>>> listTodo() async {
    await Future.delayed(Duration(seconds: 1));
    return [
      {
        'id': 1,
        'title': 'Flutter Study',
        'createdAt': DateTime.now().toString(),
      },
      {
        'id': 2,
        'title': 'Dart Study',
        'createdAt': DateTime.now().toString(),
      },
    ];
  }

  Future<Map<String, dynamic>> createTodo(Todo todo) async {
    // 원래는 이런 방식으로 작성해야 하는데 body - request - response - return 과정을 생략한것임
    await Future.delayed(Duration(seconds: 1));

    return todo.toJson();
  }

  Future<Map<String, dynamic>> deleteTodo(Todo todo) async {
    await Future.delayed(Duration(seconds: 1));

    return todo.toJson();
  }
}

bloc state

  • bloc state 은 가장 핵심 적인 부분입니다. 어떠한 상태를 가지고 있는가를 결정하게 됩니다. 나중에 bloc 을 생성할때, 어떠한 type 이 상태가 될지를 입력을 해줘야 되기 때문에 하나의 base class 를 만들어서 다 extend 한 다음에 상태들을 생성해야 합니다

  • Equatable 플러그인을 사용해서 TodoState 에 적용해줍니다

왜 Equatable 플러그인은 사용하는지에 대한 글


// in bloc/todo_state.dart

import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc_sample/model/todo.dart';

// immutable TodoState extends Equatable
@immutable
abstract class TodoState extends Equatable {}

// Empty TodoState : 맨처음에 아무것도 state 가 없을때 사용
class Empty extends TodoState {
  @override
  // TODO: implement props
  List<Object?> get props => [];
}

// Loading TodoState :RestAPI 에 요청을 했을 때 사용(repository 실행 했을때)
class Loading extends TodoState {
  @override
  // TODO: implement props
  List<Object?> get props => [];
}

// Error TodoState : server 에서 error message 가 나왔을때
class Error extends TodoState {
  final String message;

  Error({
    required this.message,
  });

  @override
// TODO: implement props
  List<Object?> get props => [this.message];
}

// Loaded TodoState : Loaded 가 완료 되는 시점에 todos list 에 값을 넘겨 주는 state
class Loaded extends TodoState {

  final List<Todo> todos;

  Loaded({
    required this.todos
  })

  @override
// TODO: implement props
  List<Object?> get props => [this.todos];
}

bloc event

  • bloc_flutter 를 쓸때 2가지 개념이 있는데 cubic 을 사용할때는 event 가 필요 없지만, bloc 을 사용할때는 event 가 필요해서 만든 파일 임

  • event 는 거의 restAPI 의 endpoint 와 갯수가 같아야 한다 (Get, Create, Delete 해오는거와 같이. )

// in bloc/todo_state.dart

import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc_sample/model/todo.dart';

// 기본 class 생성

@immutable
abstract class TodoEvent extends Equatable {}

class ListTodosEvent extends TodoEvent {
  @override
  // TODO: implement props
  List<Object?> get props => [];
}

// todo 를 입력 받게 되어 있는데, 그렇게 하려면 todo 안에서도 todo object 를 가지고 있어야 함
class CreateTodoEvent extends TodoEvent {
  final Todo todo;

  CreateTodoEvent({
    required this.todo,
  });

  @override
  // TODO: implement props
  List<Object?> get props => [this.todo];
}

// DeleteTodo 에서도 Todo object 가 들어가 잇으니까 같이 받아 줘야 함
class DeleteTodoEvent extends TodoEvent {
  final Todo todo;

  DeleteTodoEvent({
    required this.todo,
  });

  @override
  // TODO: implement props
  List<Object?> get props => [this.todo];
}

bloc logic 생성

  • flutter_bloc library 에서 제공을 해주는 bloc base class 로는 2가지가 있습니다. cubic 과 bloc 이 있는데 먼저 bloc 에 대해서 로직을 구성해 봅니다
// in bloc/todo_bloc.dart

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_bloc_sample/bloc/todo_event.dart';
import 'package:flutter_bloc_sample/bloc/todo_state.dart';
import 'package:flutter_bloc_sample/model/todo.dart';
import 'package:flutter_bloc_sample/repository/todo_repository.dart';

// Bloc logic 생성
// <> generic 으로 첫번째는 event 를 받고, 그 다음에는 state를 받습니다

class TodoBloc extends Bloc<TodoEvent, TodoState> {
  // dependancy injection 하기 위한 변수 선언
  final TodoRepository repository;

  // constructor 생성 : super 에는 가장 기본이 되는 state 인 Empty() 를 넣어 줌(처음 실행할때 아무것도 없는 상태이기 때문임) / TodoBloc 안에서 repository 의 로직도 안에서 실행하기 위해서 dependency injection 을 해줌
  TodoBloc({
    required this.repository,
  }) : super(Empty());

  // 모든 event 들이 이 함수를 통해서 실행이 됨
  // Stream 은 async* 해줘야함 Future 는 그냥 async
  @override
  Stream<TodoState> mapEventToState(TodoEvent event) async* {
    // 먼저 어떠한 event 인지 check 하는 것 (ListTodosEvent, CreateTodoEvent, DeleteTodoEvent)
    if (event is ListTodosEvent) {
      yield* _mapListTodoEvent(event);
    } else if (event is CreateTodoEvent) {
      yield* _mapCreateTodoEvent(event);
    } else if (event is DeleteTodoEvent) {
      yield* _mapDeleteTodoEvent(event);
    }
  }

  // 아래의 로직이 가장 처음으로 UI 와 연결되는 부분이기 때문에 Stream builder 형태로 Stream 으로 들어가게 되는데 UI 에서 error 가 나는것을 최소화 시켜 줘야 함. 그래서 모든 error 를 이 단계에서 설정을 함(try , catch 로)
  // _mapListTodoEvent Stream logic 생성
  Stream<TodoState> _mapListTodoEvent(ListTodosEvent event) async* {
    try {
      // circular indicator 를 보여주기 위해 Loading을 호출
      yield Loading();

      // repository 에서 가져온 정보 변수 선언
      final resp = await this.repository.listTodo();

      // listTodo 는 Map<String, dynamic> 을 return 해주기 때문에 따로 class 화를 해줘야 함
      final todos = resp.map<Todo>((e) => Todo.fromJson(e)).toList();

      // 값을 가져왔으니 loading 이 끝난것을 호출하고 todos 를 넘긴다
      yield Loaded(todos: todos);
    } catch (e) {
      yield Error(message: e.toString());
    }
  }

  // _mapCreateTodoEvent Stream logic 생성
  Stream<TodoState> _mapCreateTodoEvent(CreateTodoEvent event) async* {
    try {
      // 아래의 state 가 loaded state 인지 확인 (아직 load 가 안됬는데 data 를 가져 오면 안되기때문에)
      if (state is Loaded) {
        // todo 를 만들기 전에 기존 데이터를 가져와야 함
        // 모두 yield 된것들은 state 안에서 가져올수 있음 state 인데 사실 이건 Loaded state 라는것
        final parseState = (state as Loaded);

        // todo 생성
        final newTodo = Todo(
          //  ID: todos 의 길이 -1 의 index 의 id 값이 + 1 해서 추가 ID 번호 생성
          id: parseState.todos[parseState.todos.length - 1].id + 1,
          // Title : event 에서 title 을 불러옴
          title: event.title,
          // CreatedAt : 지금 시간 호출해서 String 으로 반환
          createdAt: DateTime.now().toString(),
        );

        // repository 전송전에 UI 화면에 변경된 내용을 표시해주는부분 todos 호출
        // prevTodos 에 기존에 있는것을 복사
        final prevTodos = [
          ...parseState.todos,
        ];

        // newTodos 에 기존거 + 생성한거 새로 생성
        final newTodos = [
          ...prevTodos,
          newTodo,
        ];

        // 요청을 하기전에 가상으로 yield 하기 (load 가 다 됬다고 임의로 선언 UI 에 표시하기 위해서)
        yield Loaded(todos: newTodos);

        // repository 로 데이터 전송
        final resp = await this.repository.createTodo(newTodo);

        // id 값과 createdAt 의 값을 서버에 있는 쪽과 UI 쪽의 id 와 createdAt 의 값을 맞춰야 되기때문에 repository 전송후에 다시 Loaded 호출해서 업데이트 하는것
        yield Loaded(todos: [
          ...prevTodos,
          Todo.fromJson(resp),
        ]);
      }
    } catch (e) {
      yield Error(message: e.toString());
    }
  }

  // _mapDeleteTodoEvent Stream logic 생성
  Stream<TodoState> _mapDeleteTodoEvent(DeleteTodoEvent event) async* {
    try {
      if (state is Loaded) {
        final newTodos = (state as Loaded)
            .todos
            .where((todo) => todo.id != event.todo.id)
            .toList();

        yield Loaded(todos: newTodos);

        await repository.deleteTodo(event.todo);
      }
    } catch (e) {
      yield Error(message: e.toString());
    }
  }
}

bloc logic 을 UI 에 나타내기

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_bloc_sample/bloc/todo_bloc.dart';
import 'package:flutter_bloc_sample/bloc/todo_event.dart';
import 'package:flutter_bloc_sample/bloc/todo_state.dart';
import 'package:flutter_bloc_sample/repository/todo_repository.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({Key? key}) : super(key: key);

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

class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    // BlocProvider 호출 사용 :  Provider 를 초기화 하는 작업
    return BlocProvider(
      // 함수를 만들어서 bloc 을 return 해줌 : BlocProvider 가 생성해준 bloc 을 child 에 있는 HomeWidget() 에 TodoBloc 이 사용가능하도록 해줌
      create: (_) => TodoBloc(repository: TodoRepository()),
      child: HomeWidget(),
    );
  }
}

class HomeWidget extends StatefulWidget {
  const HomeWidget({Key? key}) : super(key: key);

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

class _HomeWidgetState extends State<HomeWidget> {
  String title = '';

  // initState 생성
  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    // ListTodosEvent 를 BlocProvider 에서 TodoBloc 을 부를 수 있는것
    BlocProvider.of<TodoBloc>(context).add(ListTodosEvent());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Bloc'),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // bloc 을 부르는 2번째 방법 BlocProvider.of.. 위의 것과 동일한것
          context.read<TodoBloc>().add(
                CreateTodoEvent(
                  title: this.title,
                ),
              );
        },
        child: Icon(Icons.edit),
      ),
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 20.0),
        child: Column(
          children: [
            TextField(
              onChanged: (val) {
                this.title = val;
              },
            ),
            SizedBox(height: 16.0),

            // 만들어진 bloc 을 불러들이기 위해선 BlocBuilder 를 사용해야 함
            // 2개의 generic 을 불러와야 되는데 처음에는 실제 가져올 bloc 을 다음에는 그것의 상태를 넣어주면 됨
            Expanded(
              child: BlocBuilder<TodoBloc, TodoState>(builder: (_, state) {
                // state 가 Empty 이면 그냥 Container() return
                if (state is Empty) {
                  return Container();
                  // state 가 Error  일 경우
                } else if (state is Error) {
                  return Container(
                    child: Text(state.message),
                  );
                  // state 가 Loading 중일때는 circularProgressindecator()
                } else if (state is Loading) {
                  return Center(
                    child: CircularProgressIndicator(),
                  );
                  // state 가 Loaded 중일때 기존의 todos 를 불러 온다음에 item 별로 화면에 나타 내기
                } else if (state is Loaded) {
                  final items = state.todos;

                  return ListView.separated(
                    itemBuilder: (_, index) {
                      final item = items[index];
                      return Row(
                        children: [
                          Expanded(
                            child: Text(
                              item.title,
                            ),
                          ),
                          GestureDetector(
                            onTap: () {
                              BlocProvider.of<TodoBloc>(context).add(
                                DeleteTodoEvent(todo: item),
                              );
                            },
                            child: Icon(Icons.delete),
                          )
                        ],
                      );
                    },
                    separatorBuilder: (_, index) => Divider(),
                    itemCount: items.length,
                  );
                }
                return Container();
              }),
            ),
          ],
        ),
      ),
    );
  }
}

cubit 으로 logic 만들기

  • flutter_bloc 의 2번째 방법으로 cubit 을 사용하는 방법인데, Provider 나 GetX 와 비슷한 방법입니다.

  • 위의 todo_bloc.dart 에서 _mapListTodoEvent, _mapCreateTodoEvent , _mapDeleteTodoEvent 과 마찬가지로 똑같은 logic 인데 다른점은.

    • cubit 은 generic 으로 하나만 state 로 가짐

    • yield 를 emit() 으로 바꿔 주면 됨

// in bloc/todo_bloc.dart

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_bloc_sample/bloc/todo_event.dart';
import 'package:flutter_bloc_sample/bloc/todo_state.dart';
import 'package:flutter_bloc_sample/model/todo.dart';
import 'package:flutter_bloc_sample/repository/todo_repository.dart';

// Bloc logic 생성
// <> generic 으로 첫번째는 event 를 받고, 그 다음에는 state를 받습니다

class TodoBloc extends Bloc<TodoEvent, TodoState> {
  // dependency injection 하기 위한 변수 선언
  final TodoRepository repository;

  // constructor 생성 : super 에는 가장 기본이 되는 state 인 Empty() 를 넣어 줌(처음 실행할때 아무것도 없는 상태이기 때문임) / TodoBloc 안에서 repository 의 로직도 안에서 실행하기 위해서 dependency injection 을 해줌
  TodoBloc({
    required this.repository,
  }) : super(Empty());

  // 모든 event 들이 이 함수를 통해서 실행이 됨
  // Stream 은 async* 해줘야함 Future 는 그냥 async
  @override
  Stream<TodoState> mapEventToState(TodoEvent event) async* {
    // 먼저 어떠한 event 인지 check 하는 것 (ListTodosEvent, CreateTodoEvent, DeleteTodoEvent)
    if (event is ListTodosEvent) {
      yield* _mapListTodoEvent(event);
    } else if (event is CreateTodoEvent) {
      yield* _mapCreateTodoEvent(event);
    } else if (event is DeleteTodoEvent) {
      yield* _mapDeleteTodoEvent(event);
    }
  }

  // 아래의 로직이 가장 처음으로 UI 와 연결되는 부분이기 때문에 Stream builder 형태로 Stream 으로 들어가게 되는데 UI 에서 error 가 나는것을 최소화 시켜 줘야 함. 그래서 모든 error 를 이 단계에서 설정을 함(try , catch 로)
  // _mapListTodoEvent Stream logic 생성
  Stream<TodoState> _mapListTodoEvent(ListTodosEvent event) async* {
    try {
      // circular indicator 를 보여주기 위해 Loading을 호출
      yield Loading();

      // repository 에서 가져온 정보 변수 선언
      final resp = await this.repository.listTodo();

      // listTodo 는 Map<String, dynamic> 을 return 해주기 때문에 따로 class 화를 해줘야 함
      final todos = resp.map<Todo>((e) => Todo.fromJson(e)).toList();

      // 값을 가져왔으니 loading 이 끝난것을 호출하고 todos 를 넘긴다
      yield Loaded(todos: todos);
    } catch (e) {
      yield Error(message: e.toString());
    }
  }

  // _mapCreateTodoEvent Stream logic 생성
  Stream<TodoState> _mapCreateTodoEvent(CreateTodoEvent event) async* {
    try {
      // 아래의 state 가 loaded state 인지 확인 (아직 load 가 안됬는데 data 를 가져 오면 안되기때문에)
      if (state is Loaded) {
        // todo 를 만들기 전에 기존 데이터를 가져와야 함
        // 모두 yield 된것들은 state 안에서 가져올수 있음 state 인데 사실 이건 Loaded state 라는것
        final parseState = (state as Loaded);

        // todo 생성
        final newTodo = Todo(
          //  ID: todos 의 길이 -1 의 index 의 id 값이 + 1 해서 추가 ID 번호 생성
          id: parseState.todos[parseState.todos.length - 1].id + 1,
          // Title : event 에서 title 을 불러옴
          title: event.title,
          // CreatedAt : 지금 시간 호출해서 String 으로 반환
          createdAt: DateTime.now().toString(),
        );

        // repository 전송전에 UI 화면에 변경된 내용을 표시해주는부분 todos 호출
        // prevTodos 에 기존에 있는것을 복사
        final prevTodos = [
          ...parseState.todos,
        ];

        // newTodos 에 기존거 + 생성한거 새로 생성
        final newTodos = [
          ...prevTodos,
          newTodo,
        ];

        // 요청을 하기전에 가상으로 yield 하기 (load 가 다 됬다고 임의로 선언 UI 에 표시하기 위해서)
        yield Loaded(todos: newTodos);

        // repository 로 데이터 전송
        final resp = await this.repository.createTodo(newTodo);

        // id 값과 createdAt 의 값을 서버에 있는 쪽과 UI 쪽의 id 와 createdAt 의 값을 맞춰야 되기때문에 repository 전송후에 다시 Loaded 호출해서 업데이트 하는것
        yield Loaded(todos: [
          ...prevTodos,
          Todo.fromJson(resp),
        ]);
      }
    } catch (e) {
      yield Error(message: e.toString());
    }
  }

  // _mapDeleteTodoEvent Stream logic 생성
  Stream<TodoState> _mapDeleteTodoEvent(DeleteTodoEvent event) async* {
    try {
      if (state is Loaded) {
        final newTodos = (state as Loaded)
            .todos
            .where((todo) => todo.id != event.todo.id)
            .toList();

        yield Loaded(todos: newTodos);

        await repository.deleteTodo(event.todo);
      }
    } catch (e) {
      yield Error(message: e.toString());
    }
  }
}

cubit 적용하기

기존 UI page 에서 변경사항은..

  • BlocProvider 의 TodoBloc 대신 TodoCubit 으로 바꿔 줌
  // ListTodosEvent 를 BlocProvider 에서 TodoBloc 을 부를 수 있는것
  BlocProvider.of<TodoBloc>(context).add(ListTodosEvent());

  // 위에것을 TodoCubit 으로 만드는것
  BlocProvider.of<TodoCubit>(context).listTodo();

Kapture 2021-11-03 at 16 20 43

Source Code

github source


🔶 🔷 📌 🔑

Reference

Getting Started with the BLoC Pattern - https://www.raywenderlich.com/4074597-getting-started-with-the-bloc-pattern

BLoC in Flutter: Implement Clean, Flux-like Architecture - https://everyday.codes/mobile/bloc-in-flutter-implement-clean-flux-like-architecture/

개발하는남자 - https://youtu.be/AY6i0a4BM7o

코드팩토리 - https://youtu.be/xlmkMF5kVvA

Categories:

Updated:

Leave a comment