Top

Up

Flutter Todos Tutorials

원문: Bloc / Tutorial / Todos

advanced

다음 튜토리얼에서는 Bloc 라이브러리를 사용하여 Flutter에서 Todos App을 빌드 할 것입니다.

demo

준비 (Setup)

우리는 새로운 Flutter 프로젝트를 만드는 것으로 시작합니다.

flutter create flutter_todos

그런 다음 pubspec.yaml의 내용을 변경합니다.

name: flutter_todos
description: A new Flutter project.

environment:
  sdk: ">=2.0.0 <3.0.0"

dependencies:
  meta: ">=1.1.0 <2.0.0"
  equatable: ^0.2.0
  flutter_bloc: ^0.13.0
  flutter:
    sdk: flutter

dependency_overrides:
  todos_app_core:
    git:
      url: https://github.com/felangel/flutter_architecture_samples
      path: todos_app_core
      ref: expose-repositories
  todos_repository_core:
    git:
      url: https://github.com/felangel/flutter_architecture_samples
      path: todos_repository_core
      ref: expose-repositories
  todos_repository_simple:
    git:
      url: https://github.com/felangel/flutter_architecture_samples
      path: todos_repository_simple
      ref: expose-repositories
flutter:
  uses-material-design: true

그리고, 모든 종석성을 설치합니다.

flutter packages get

Note: 우리는 Brian Egan’s Flutter Architecture Samples에서 재사용 할 것이므로 일부 의존성을 무시하고 있습니다.

TodosRepository

이 튜토리얼에서는 Brian Egan에 의해 구현 되었고 Todo Architecture Samples에서 공유 되었기 때문에 TodosRepository의 구현 세부 사항을 다루지는 않습니다. 높은 수준에서 TodosRepository는 메소드를loadTodossaveTodos에 노출시킵니다. 우리가 알아야 할 거의 모든 것이 이 튜토리얼의 나머지 부분에서 Bloc과 프리젠테이션 레이어에 집중할 것입니다.

TodosBloc

우리의 TodosBlocTodosEventsTodosStates로 변환 할 책임이 있으며 할일 목록을 관리 할 것입니다.

모델 (Model)

우리가 해야 할 첫 번째 일은 Todo 모델을 정의하는 것입니다. 각 Todo에는 id, task, 선택적 note 및 선택적 complete 플래그가 있어야 합니다.

models 디렉토리를 만들고todo.dart를 만듭니다.

import 'package:todos_app_core/todos_app_core.dart';
import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';
import 'package:todos_repository_core/todos_repository_core.dart';

@immutable
class Todo extends Equatable {
  final bool complete;
  final String id;
  final String note;
  final String task;

  Todo(this.task, {this.complete = false, String note = '', String id})
      : this.note = note ?? '',
        this.id = id ?? Uuid().generateV4(),
        super([complete, id, note, task]);

  Todo copyWith({bool complete, String id, String note, String task}) {
    return Todo(
      task ?? this.task,
      complete: complete ?? this.complete,
      id: id ?? this.id,
      note: note ?? this.note,
    );
  }

  @override
  String toString() {
    return 'Todo { complete: $complete, task: $task, note: $note, id: $id }';
  }

  TodoEntity toEntity() {
    return TodoEntity(task, id, note, complete);
  }

  static Todo fromEntity(TodoEntity entity) {
    return Todo(
      entity.task,
      complete: entity.complete ?? false,
      note: entity.note,
      id: entity.id ?? Uuid().generateV4(),
    );
  }
}

Note: 우리는 Equatable 패키지를 사용하여 ==hashCode를 수동으로 오버라이드 할 필요 없이 Todos의 인스턴스를 비교할 수 있습니다.

다음은 프리젠테이션 레이어가 받을 TodosState를 생성해야 합니다.

상태들 (States)

blocs/todos/todos_state.dart를 만들고 취급해야 할 여러 상태를 정의 하겠습니다.

우리가 구현할 3 가지 상태는 다음과 같습니다:

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class TodosState extends Equatable {
  TodosState([List props = const []]) : super(props);
}

class TodosLoading extends TodosState {
  @override
  String toString() => 'TodosLoading';
}

class TodosLoaded extends TodosState {
  final List<Todo> todos;

  TodosLoaded([this.todos = const []]) : super([todos]);

  @override
  String toString() => 'TodosLoaded { todos: $todos }';
}

class TodosNotLoaded extends TodosState {
  @override
  String toString() => 'TodosNotLoaded';
}

Note: 우리는 TodosStateimmutable 데코레이터를 추가하여 모든 TodosState를 변경할 수 없음을 나타낼 수 있습니다.

다음으로 우리가 처리해야 할 이벤트를 구현해 보겠습니다.

Events

TodosBloc에서 처리해야 할 이벤트는 다음과 같습니다.

blocs/todos/todos_event.dart 파일을 만들고 위에서 설명한 이벤트를 구현해 보겠습니다.

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class TodosEvent extends Equatable {
  TodosEvent([List props = const []]) : super(props);
}

class LoadTodos extends TodosEvent {
  @override
  String toString() => 'LoadTodos';
}

class AddTodo extends TodosEvent {
  final Todo todo;

  AddTodo(this.todo) : super([todo]);

  @override
  String toString() => 'AddTodo { todo: $todo }';
}

class UpdateTodo extends TodosEvent {
  final Todo updatedTodo;

  UpdateTodo(this.updatedTodo) : super([updatedTodo]);

  @override
  String toString() => 'UpdateTodo { updatedTodo: $updatedTodo }';
}

class DeleteTodo extends TodosEvent {
  final Todo todo;

  DeleteTodo(this.todo) : super([todo]);

  @override
  String toString() => 'DeleteTodo { todo: $todo }';
}

class ClearCompleted extends TodosEvent {
  @override
  String toString() => 'ClearCompleted';
}

class ToggleAll extends TodosEvent {
  @override
  String toString() => 'ToggleAll';
}

이제 TodosStatesTodosEvents가 구현되었으므로 TodosBloc을 구현할 수 있습니다.

TodosBloc

blocs/todos/todos_bloc.dart 파일을 만들고 시작해 보겠습니다. 우리는 initialStatemapEventToState를 구현 하기만하면 됩니다.

import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/models/models.dart';
import 'package:todos_repository_simple/todos_repository_simple.dart';

class TodosBloc extends Bloc<TodosEvent, TodosState> {
  final TodosRepositoryFlutter todosRepository;

  TodosBloc({@required this.todosRepository});

  @override
  TodosState get initialState => TodosLoading();

  @override
  Stream<TodosState> mapEventToState(TodosEvent event) async* {
    if (event is LoadTodos) {
      yield* _mapLoadTodosToState();
    } else if (event is AddTodo) {
      yield* _mapAddTodoToState(event);
    } else if (event is UpdateTodo) {
      yield* _mapUpdateTodoToState(event);
    } else if (event is DeleteTodo) {
      yield* _mapDeleteTodoToState(event);
    } else if (event is ToggleAll) {
      yield* _mapToggleAllToState();
    } else if (event is ClearCompleted) {
      yield* _mapClearCompletedToState();
    }
  }

  Stream<TodosState> _mapLoadTodosToState() async* {
    try {
      final todos = await this.todosRepository.loadTodos();
      yield TodosLoaded(
        todos.map(Todo.fromEntity).toList(),
      );
    } catch (_) {
      yield TodosNotLoaded();
    }
  }

  Stream<TodosState> _mapAddTodoToState(AddTodo event) async* {
    if (currentState is TodosLoaded) {
      final List<Todo> updatedTodos =
          List.from((currentState as TodosLoaded).todos)..add(event.todo);
      yield TodosLoaded(updatedTodos);
      _saveTodos(updatedTodos);
    }
  }

  Stream<TodosState> _mapUpdateTodoToState(UpdateTodo event) async* {
    if (currentState is TodosLoaded) {
      final List<Todo> updatedTodos =
          (currentState as TodosLoaded).todos.map((todo) {
        return todo.id == event.updatedTodo.id ? event.updatedTodo : todo;
      }).toList();
      yield TodosLoaded(updatedTodos);
      _saveTodos(updatedTodos);
    }
  }

  Stream<TodosState> _mapDeleteTodoToState(DeleteTodo event) async* {
    if (currentState is TodosLoaded) {
      final updatedTodos = (currentState as TodosLoaded)
          .todos
          .where((todo) => todo.id != event.todo.id)
          .toList();
      yield TodosLoaded(updatedTodos);
      _saveTodos(updatedTodos);
    }
  }

  Stream<TodosState> _mapToggleAllToState() async* {
    if (currentState is TodosLoaded) {
      final allComplete =
          (currentState as TodosLoaded).todos.every((todo) => todo.complete);
      final List<Todo> updatedTodos = (currentState as TodosLoaded)
          .todos
          .map((todo) => todo.copyWith(complete: !allComplete))
          .toList();
      yield TodosLoaded(updatedTodos);
      _saveTodos(updatedTodos);
    }
  }

  Stream<TodosState> _mapClearCompletedToState() async* {
    if (currentState is TodosLoaded) {
      final List<Todo> updatedTodos = (currentState as TodosLoaded)
          .todos
          .where((todo) => !todo.complete)
          .toList();
      yield TodosLoaded(updatedTodos);
      _saveTodos(updatedTodos);
    }
  }

  Future _saveTodos(List<Todo> todos) {
    return todosRepository.saveTodos(
      todos.map((todo) => todo.toEntity()).toList(),
    );
  }
}

private mapEventToState 핸들러에서 상태를 생성 할 때, 우리는 항상 currentState를 변경하는 대신에 새로운 상태를 만듭니다. 왜냐하면 우리가 yield 할 때마다 bloc은 currentStatenextState를 비교할 것이고 두 상태가 같지 않다면 상태 변화(transition)를 일으킬 것이기 때문입니다. 우리가 mutate하여 같은 상태의 상태를 yield한다면, currentState == nextStatetrue으로 평가 될 것이고 상태 변화는 일어나지 않을 것입니다.

우리의 TodosBlocTodosRepository에 의존하여 todos를 로드하고 저장할 수 있습니다. 그것은TodosLoading의 초기 상태를 가지며 각각의 이벤트에 대한 private 핸들러를 정의합니다. TodosBloc가 todos의 목록을 변경할 때마다, 모든 것을 로컬에 유지하기 위해 TodosRepository에서 saveTodos 메소드를 호출합니다.

배럴 파일 (Barrel File)

이제 우리는 TodosBloc을 다 마쳤으므로 모든 bloc 파일을 export하여 나중에 import 할 수 있도록 배럴 파일을 만들 수 있습니다.

blocs/todos/todos.dart 파일을 만들고 bloc, 이벤트 및 상태를 export 합니다.

export './todos_bloc.dart';
export './todos_event.dart';
export './todos_state.dart';

FilteredTodosBloc

FilteredTodosBloc은 방금 생성한 TodosBloc의 상태 변화에 대응할 책임이 있으며 우리 애플리케이션에서 필터링 된 todos의 상태를 유지(maintain)할 것입니다.

Model

TodosStates를 정의하고 구현하기 전에 FilteredTodosState가 포함 할 todos를 결정하는 VisibilityFilter 모델을 구현해야합니다. 이 경우 세 가지 필터가 있습니다.

우리는 models/visibility_filter.dart를 만들고 필터를 enum으로 정의 할 수 있습니다 :

enum VisibilityFilter { all, active, completed }

States

TodosBloc을 사용한 것처럼 우리는 FilteredTodosBloc에 대해 여러 상태를 정의해야 합니다.

이 경우에는 두 가지 상태 만 있습니다.

blocs/filtered_todos/filtered_todos_state.dart 파일을 만들고 두 상태를 구현해 봅시다.

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class FilteredTodosState extends Equatable {
  FilteredTodosState([List props = const []]) : super(props);
}

class FilteredTodosLoading extends FilteredTodosState {
  @override
  String toString() => 'FilteredTodosLoading';
}

class FilteredTodosLoaded extends FilteredTodosState {
  final List<Todo> filteredTodos;
  final VisibilityFilter activeFilter;

  FilteredTodosLoaded(this.filteredTodos, this.activeFilter)
      : super([filteredTodos, activeFilter]);

  @override
  String toString() {
    return 'FilteredTodosLoaded { filteredTodos: $filteredTodos, activeFilter: $activeFilter }';
  }
}

Note: The FilteredTodosLoaded state contains the list of filtered todos as well as the active visibility filter.

FilteredTodosLoaded 상태는 List<Todo> filteredTodosVisibilityFilter activeFilter를 포함합니다.

Events

FilteredTodosBloc에 대해 두 가지 이벤트를 구현할 것입니다:

blocs/filtered_todos/filtered_todos_event.dart를 만들고 두 이벤트를 구현해 봅시다.

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class FilteredTodosEvent extends Equatable {
  FilteredTodosEvent([List props = const []]) : super(props);
}

class UpdateFilter extends FilteredTodosEvent {
  final VisibilityFilter filter;

  UpdateFilter(this.filter) : super([filter]);

  @override
  String toString() => 'UpdateFilter { filter: $filter }';
}

class UpdateTodos extends FilteredTodosEvent {
  final List<Todo> todos;

  UpdateTodos(this.todos) : super([todos]);

  @override
  String toString() => 'UpdateTodos { todos: $todos }';
}

우리는 다음에 FilteredTodosBloc을 구현할 준비가 되었습니다!

FilteredTodosBloc

우리의 FilteredTodosBloc은 우리의 TodosBloc과 유사 할 것입니다; 그러나, TodosRepository에 의존하는 대신 TodosBloc 자체에 의존 할 것입니다. 이렇게 하면 FilteredTodosBlocTodosBloc의 상태 변화에 응답하여 상태를 갱신 할 수 있습니다.

blocs/filtered_todos/filtered_todos_bloc.dart 파일을 만들고 시작해 봅시다.

import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/blocs/filtered_todos/filtered_todos.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/models/models.dart';

class FilteredTodosBloc extends Bloc<FilteredTodosEvent, FilteredTodosState> {
  final TodosBloc todosBloc;
  StreamSubscription todosSubscription;

  FilteredTodosBloc({@required this.todosBloc}) {
    todosSubscription = todosBloc.state.listen((state) {
      if (state is TodosLoaded) {
        dispatch(UpdateTodos((todosBloc.currentState as TodosLoaded).todos));
      }
    });
  }

  @override
  FilteredTodosState get initialState {
    return todosBloc.currentState is TodosLoaded
        ? FilteredTodosLoaded(
            (todosBloc.currentState as TodosLoaded).todos,
            VisibilityFilter.all,
          )
        : FilteredTodosLoading();
  }

  @override
  Stream<FilteredTodosState> mapEventToState(FilteredTodosEvent event) async* {
    if (event is UpdateFilter) {
      yield* _mapUpdateFilterToState(event);
    } else if (event is UpdateTodos) {
      yield* _mapTodosUpdatedToState(event);
    }
  }

  Stream<FilteredTodosState> _mapUpdateFilterToState(
    UpdateFilter event,
  ) async* {
    if (todosBloc.currentState is TodosLoaded) {
      yield FilteredTodosLoaded(
        _mapTodosToFilteredTodos(
          (todosBloc.currentState as TodosLoaded).todos,
          event.filter,
        ),
        event.filter,
      );
    }
  }

  Stream<FilteredTodosState> _mapTodosUpdatedToState(
    UpdateTodos event,
  ) async* {
    final visibilityFilter = currentState is FilteredTodosLoaded
        ? (currentState as FilteredTodosLoaded).activeFilter
        : VisibilityFilter.all;
    yield FilteredTodosLoaded(
      _mapTodosToFilteredTodos(
        (todosBloc.currentState as TodosLoaded).todos,
        visibilityFilter,
      ),
      visibilityFilter,
    );
  }

  List<Todo> _mapTodosToFilteredTodos(
      List<Todo> todos, VisibilityFilter filter) {
    return todos.where((todo) {
      if (filter == VisibilityFilter.all) {
        return true;
      } else if (filter == VisibilityFilter.active) {
        return !todo.complete;
      } else if (filter == VisibilityFilter.completed) {
        return todo.complete;
      }
    }).toList();
  }

  @override
  void dispose() {
    todosSubscription.cancel();
    super.dispose();
  }
}

TodosBloc에서 상태 변화를 수신할 수 있도록 TodosStates 스트림을 위한 StreamSubscription을 만듭니다. 우리는 bloc의 dispose 메소드를 재정의 하여, 구독을 취소한 후에 bloc을 폐기(dispose) 합니다.

배럴 파일 (Barrel File)

이전과 마찬가지로 다양한 filtered todos 클래스를 가져 오는 것이 편리하도록 배럴 파일을 만들 수 있습니다.

blocs/filtered_todos/filtered_todos.dart를 만들고 세 파일을 export 하세요:

export './filtered_todos_bloc.dart';
export './filtered_todos_event.dart';
export './filtered_todos_state.dart';

Next, we’re going to implement the StatsBloc.

Stats Bloc

StatsBloc은 활성 todos 수와 완성 된 todos 수에 대한 통계를 관리합니다. 비슷하게, FilteredTodosBloc에 대해서 TodosBloc 상태에 대한 변화에 반응 할 수 있도록 TodosBloc 자체에 의존성을 갖습니다.

State

우리의 StatsBloc은 다음과 같은 두 가지 상태를 가질 것입니다 :

blocs/stats/stats_state.dart 파일을 만들고 StatsState를 구현해 봅시다.

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

@immutable
abstract class StatsState extends Equatable {
  StatsState([List props = const []]) : super(props);
}

class StatsLoading extends StatsState {
  @override
  String toString() => 'StatsLoading';
}

class StatsLoaded extends StatsState {
  final int numActive;
  final int numCompleted;

  StatsLoaded(this.numActive, this.numCompleted)
      : super([numActive, numCompleted]);

  @override
  String toString() {
    return 'StatsLoaded { numActive: $numActive, numCompleted: $numCompleted }';
  }
}

다음으로 StatsEvents를 정의하고 구현해 보겠습니다.

Events

우리의 StatsBloc이 응답 할 이벤트가 하나 UpdateStats가 있습니다. 이 이벤트는 TodosBloc 상태가 바뀌어 우리의 StatsBloc이 새로운 통계를 다시 계산할 수 있을 때마다 전달됩니다.

blocs/stats/states_event.dart 파일을 만들고 구현해 봅시다.

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class StatsEvent extends Equatable {
  StatsEvent([List props = const []]) : super(props);
}

class UpdateStats extends StatsEvent {
  final List<Todo> todos;

  UpdateStats(this.todos) : super([todos]);

  @override
  String toString() => 'UpdateStats { todos: $todos }';
}

이제 FilteredTodosBloc과 매우 비슷한 StatsBloc을 구현할 준비가 되었습니다.

Bloc

우리의 StatsBlocTodosBloc에 대한 의존성을 가지게 될 것이며 TodosBloc의 상태 변화에 대응하여 상태를 갱신 할 수있게 될 것입니다.

blocs/stats/stats_bloc.dart 파일을 만들고 시작합니다.

import 'dart:async';
import 'package:meta/meta.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_todos/blocs/blocs.dart';

class StatsBloc extends Bloc<StatsEvent, StatsState> {
  final TodosBloc todosBloc;
  StreamSubscription todosSubscription;

  StatsBloc({@required this.todosBloc}) {
    todosSubscription = todosBloc.state.listen((state) {
      if (state is TodosLoaded) {
        dispatch(UpdateStats(state.todos));
      }
    });
  }

  @override
  StatsState get initialState => StatsLoading();

  @override
  Stream<StatsState> mapEventToState(StatsEvent event) async* {
    if (event is UpdateStats) {
      int numActive =
          event.todos.where((todo) => !todo.complete).toList().length;
      int numCompleted =
          event.todos.where((todo) => todo.complete).toList().length;
      yield StatsLoaded(numActive, numCompleted);
    }
  }

  @override
  void dispose() {
    todosSubscription.cancel();
    super.dispose();
  }
}

그게 전부입니다! 우리의 StatsBloc은 우리의 TodosBloc의 각 상태 변경에 대한 활성된 todos의 개수와 완료된 토드의 개수를 포함하는 상태를 다시 계산합니다.

StatsBloc으로 끝났으니 구현할 마지막 블록이 하나 TabBloc이 있습니다.

TabBloc

TabBloc은 우리 애플리케이션에서 탭의 상태를 유지할 책임이 있습니다. TabEvents를 입력 받아AppTabs를 출력합니다.

Model/State

우리는 AppStab 모델을 정의 할 필요가 있습니다. 우리는 이것을 사용하여 TabState를 표현할 것입니다. AppTab은 애플리케이션의 활성 탭을 나타내는 enum입니다. 우리가 만들고 있는 앱에는 todos와 stats라는 두 개의 탭만 있으므로 두 개의 값만 있으면 됩니다.

models/app_tab.dart를 만드세요.

enum AppTab { todos, stats }

Event

우리의 TabBloc은 하나의 TabEvent만을 처리 할 책임이 있습니다 :

blocs/tab/tab_event.dart 파일을 만듭니다.

import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import 'package:flutter_todos/models/models.dart';

@immutable
abstract class TabEvent extends Equatable {
  TabEvent([List props = const []]) : super(props);
}

class UpdateTab extends TabEvent {
  final AppTab tab;

  UpdateTab(this.tab) : super([tab]);

  @override
  String toString() => 'UpdateTab { tab: $tab }';
}

TabBloc

우리의 TabBloc 구현은 매우 간단 합니다. 항상 그렇듯이, 우리는 initialStatemapEventToState를 구현할 필요가 있습니다.

blocs/tab/tab_bloc.dart 파일을 만들고 신속하게 구현해 봅시다.

import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_todos/blocs/tab/tab.dart';
import 'package:flutter_todos/models/models.dart';

class TabBloc extends Bloc<TabEvent, AppTab> {
  @override
  AppTab get initialState => AppTab.todos;

  @override
  Stream<AppTab> mapEventToState(TabEvent event) async* {
    if (event is UpdateTab) {
      yield event.tab;
    }
  }
}

나는 그것이 간단 할 것이라고 얘기 했습니다. TabBloc이 하는 일은 todos 탭에 초기 상태를 설정하고 새로운AppTab 인스턴스를 생성하여 UpdateTab 이벤트를 처리하는 것입니다.

Barrel File

마지막으로, 우리는 TabBloc 수출을 위한 또 다른 배럴 파일을 만들 것입니다. blocs/tab/tab.dart 파일을 만들고 두 파일을 내 보냅니다.

export './tab_bloc.dart';
export './tab_event.dart';

BlocDelegate

프리젠테이션 레이어로 넘어 가기 전에, 우리는 우리 자신의 BlocDelegate를 구현할 것입니다. 이것은 우리가 모든 상태 변경과 에러를 한 곳에서 처리 할 수 있게 합니다. 개발자 로그 또는 분석과 같은 경우에 유용합니다.

blocs/simple_bloc_delegate.dart 파일을 만들고 시작하겠습니다.

import 'package:bloc/bloc.dart';

class SimpleBlocDelegate extends BlocDelegate {
  @override
  void onEvent(Bloc bloc, Object event) {
    super.onEvent(bloc, event);
    print(event);
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print(transition);
  }

  @override
  void onError(Bloc bloc, Object error, StackTrace stacktrace) {
    super.onError(bloc, error, stacktrace);
    print(error);
  }
}

이 경우 우리가 하는 모든 일은 우리가 앱을 실행할 때 어떤 일이 벌어지는지를 알 수 있도록 모든 상태 변화 (transition)와 오류를 콘솔에 출력하는 것뿐입니다. BlocDelegate를 google analytics, sentry, crashlytics 등에 연결할 수 있습니다.

Blocks Barrel

Now that we have all of our blocs implemented we can create a barrel file. Create blocs/blocs.dart and export all of our blocs so that we can conveniently import any bloc code with a single import.

이제 모든 bloc을 구현 했으므로 배럴 파일을 만들 수 있습니다. blocs/blocs.dart 파일을 만들고 모든 bloc을 export하여 하나의 bloc 코드를 편리하게 임포트 할 수 있습니다.

export './filtered_todos/filtered_todos.dart';
export './stats/stats.dart';
export './tab/tab.dart';
export './todos/todos.dart';
export './simple_bloc_delegate.dart';

다음으로, 우리는 Todos 애플리케이션의 주요 화면 구현에 중점을 둘 것입니다.

Screens

Home Screen

우리의 HomeScreen은 우리 애플리케이션의 Scaffold를 생성 할 책임이 있습니다. AppBar, BottomNavigationBar, (활성 탭에 따라) Stats /FilteredTodos 위젯 을 유지(maintain)합니다.

screens라는 새로운 디렉토리를 만들어서 새로운 스크린 위젯을 모두 넣은 다음 screens/home_screen.dart를 만듭니다.

우리의 HomeScreenTabBloc, FilteredTodosBloc, StatsBloc을 만들고 처리해야 하기 때문에StatefulWidget이 될 것입니다.

import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_todos/blocs/blocs.dart';
import 'package:flutter_todos/widgets/widgets.dart';
import 'package:flutter_todos/localization.dart';
import 'package:flutter_todos/models/models.dart';

class HomeScreen extends StatefulWidget {
  final void Function() onInit;

  HomeScreen({@required this.onInit}) : super(key: ArchSampleKeys.homeScreen);

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

class _HomeScreenState extends State<HomeScreen> {
  final TabBloc _tabBloc = TabBloc();
  FilteredTodosBloc _filteredTodosBloc;
  StatsBloc _statsBloc;

  @override
  void initState() {
    widget.onInit();
    _filteredTodosBloc = FilteredTodosBloc(
      todosBloc: BlocProvider.of<TodosBloc>(context),
    );
    _statsBloc = StatsBloc(
      todosBloc: BlocProvider.of<TodosBloc>(context),
    );
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return BlocBuilder(
      bloc: _tabBloc,
      builder: (BuildContext context, AppTab activeTab) {
        return BlocProviderTree(
          blocProviders: [
            BlocProvider<TabBloc>(bloc: _tabBloc),
            BlocProvider<FilteredTodosBloc>(bloc: _filteredTodosBloc),
            BlocProvider<StatsBloc>(bloc: _statsBloc),
          ],
          child: Scaffold(
            appBar: AppBar(
              title: Text(FlutterBlocLocalizations.of(context).appTitle),
              actions: [
                FilterButton(visible: activeTab == AppTab.todos),
                ExtraActions(),
              ],
            ),
            body: activeTab == AppTab.todos ? FilteredTodos() : Stats(),
            floatingActionButton: FloatingActionButton(
              key: ArchSampleKeys.addTodoFab,
              onPressed: () {
                Navigator.pushNamed(context, ArchSampleRoutes.addTodo);
              },
              child: Icon(Icons.add),
              tooltip: ArchSampleLocalizations.of(context).addTodo,
            ),
            bottomNavigationBar: TabSelector(
              activeTab: activeTab,
              onTabSelected: (tab) => _tabBloc.dispatch(UpdateTab(tab)),
            ),
          ),
        );
      },
    );
  }

  @override
  void dispose() {
    _statsBloc.dispose();
    _filteredTodosBloc.dispose();
    _tabBloc.dispose();
    super.dispose();
  }
}

HomeScreen은 상태의 일부로 TabBloc, FilteredTodosBlocStatsBloc을 만듭니다. 우리의 루트TodosApp 위젯에서 사용할 수 있는 TodosBloc에 접근하기 위해 BlocProvider.of<TodosBloc>(context)를 사용합니다 (이 튜토리얼의 뒷부분에서 설명 할 것입니다).

HomeScreenTodosBloc 상태의 변화에 반응 할 필요가 있기 때문에 현재의 TodosState를 기반으로 올바른 위젯을 만들기 위해BlocBuilder를 사용합니다.

HomeScreen은 [flutter_bloc](https://pub.dartlang.org/packages/flutter_bloc ')의BlocProviderTree 위젯을 사용하여 TabBloc, FilteredTodosBloc, StatsBloc`을 서브 트리의 위젯이 사용할 수 있도록 합니다

BlocProviderTree(
  blocProviders: [
    BlocProvider<TabBloc>(bloc: _tabBloc),
    BlocProvider<FilteredTodosBloc>(bloc: _filteredTodosBloc),
    BlocProvider<StatsBloc>(bloc: _statsBloc),
  ],
  child: Scaffold(...),
);

이 코드는 아래와 동일합니다.

BlocProvider<TabBloc>(
  bloc: _tabBloc,
  child: BlocProvider<FilteredTodosBloc>(
    bloc: _filteredTodosBloc,
    child: BlocProvider<StatsBloc>(
      bloc: _statsBloc,
      child: Scaffold(...),
    ),
  ),
);

BlocProviderTree를 사용하여 중첩 수준을 줄이고 코드를 보다 쉽게 읽고 유지관리하는 방법을 알았습니다.

다음으로, 우리는 DetailsScreen을 구현할 것입니다.

Detail Screen

DetailsScreen은 선택된 todo의 모든 세부 사항을 표시하고 사용자가 todo를 편집하거나 삭제할 수 있게합니다.

screens/details_screen.dart 파일을 만들고 그것을 빌드 합니다.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/screens/screens.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';

class DetailsScreen extends StatelessWidget {
  final String id;

  DetailsScreen({Key key, @required this.id})
      : super(key: key ?? ArchSampleKeys.todoDetailsScreen);

  @override
  Widget build(BuildContext context) {
    final todosBloc = BlocProvider.of<TodosBloc>(context);
    return BlocBuilder(
      bloc: todosBloc,
      builder: (BuildContext context, TodosState state) {
        final todo = (state as TodosLoaded)
            .todos
            .firstWhere((todo) => todo.id == id, orElse: () => null);
        final localizations = ArchSampleLocalizations.of(context);
        return Scaffold(
          appBar: AppBar(
            title: Text(localizations.todoDetails),
            actions: [
              IconButton(
                tooltip: localizations.deleteTodo,
                key: ArchSampleKeys.deleteTodoButton,
                icon: Icon(Icons.delete),
                onPressed: () {
                  todosBloc.dispatch(DeleteTodo(todo));
                  Navigator.pop(context, todo);
                },
              )
            ],
          ),
          body: todo == null
              ? Container(key: FlutterTodosKeys.emptyDetailsContainer)
              : Padding(
                  padding: EdgeInsets.all(16.0),
                  child: ListView(
                    children: [
                      Row(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Padding(
                            padding: EdgeInsets.only(right: 8.0),
                            child: Checkbox(
                                key: FlutterTodosKeys.detailsScreenCheckBox,
                                value: todo.complete,
                                onChanged: (_) {
                                  todosBloc.dispatch(
                                    UpdateTodo(
                                      todo.copyWith(complete: !todo.complete),
                                    ),
                                  );
                                }),
                          ),
                          Expanded(
                            child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Hero(
                                  tag: '${todo.id}__heroTag',
                                  child: Container(
                                    width: MediaQuery.of(context).size.width,
                                    padding: EdgeInsets.only(
                                      top: 8.0,
                                      bottom: 16.0,
                                    ),
                                    child: Text(
                                      todo.task,
                                      key: ArchSampleKeys.detailsTodoItemTask,
                                      style:
                                          Theme.of(context).textTheme.headline,
                                    ),
                                  ),
                                ),
                                Text(
                                  todo.note,
                                  key: ArchSampleKeys.detailsTodoItemNote,
                                  style: Theme.of(context).textTheme.subhead,
                                ),
                              ],
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
          floatingActionButton: FloatingActionButton(
            key: ArchSampleKeys.editTodoFab,
            tooltip: localizations.editTodo,
            child: Icon(Icons.edit),
            onPressed: todo == null
                ? null
                : () {
                    Navigator.of(context).push(
                      MaterialPageRoute(
                        builder: (context) {
                          return AddEditScreen(
                            key: ArchSampleKeys.editTodoScreen,
                            onSave: (task, note) {
                              todosBloc.dispatch(
                                UpdateTodo(
                                  todo.copyWith(task: task, note: note),
                                ),
                              );
                            },
                            isEditing: true,
                            todo: todo,
                          );
                        },
                      ),
                    );
                  },
          ),
        );
      },
    );
  }
}

Note: DetailsScreen은 todo id를 필요로 하기 때문에 TodosBloc에서 todo 세부 사항을 가져올 수 있으며, todo의 세부 사항이 변경 될 때마다 업데이트 할 수 있습니다 (todo의 id는 변경할 수 없습니다).

주목할 주요한 사항은 DeleteTodo 이벤트를 전달하는 IconButtonUpdateTodo 이벤트를 가져오는 체크 박스입니다.

isEditingtrue로 설정하여 AddEditScreen으로 사용자를 이동시키는 또 다른FloatingActionButton도 있습니다. 우리는 다음에 AddEditScreen을 살펴볼 것입니다.

Add/Edit Screen

AddEditScreen 위젯은 사용자가 생성자를 통해 전달 된 isEditing` 플래그에 기반하여 새로운 todo를 만들거나 기존 todo를 업데이트 할 수 있게 합니다.

screens/add_edit_screen.dart 파일을 만들고 구현을 살펴 봅시다.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';

typedef OnSaveCallback = Function(String task, String note);

class AddEditScreen extends StatefulWidget {
  final bool isEditing;
  final OnSaveCallback onSave;
  final Todo todo;

  AddEditScreen({
    Key key,
    @required this.onSave,
    @required this.isEditing,
    this.todo,
  }) : super(key: key ?? ArchSampleKeys.addTodoScreen);

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

class _AddEditScreenState extends State<AddEditScreen> {
  static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  String _task;
  String _note;

  bool get isEditing => widget.isEditing;

  @override
  Widget build(BuildContext context) {
    final localizations = ArchSampleLocalizations.of(context);
    final textTheme = Theme.of(context).textTheme;

    return Scaffold(
      appBar: AppBar(
        title: Text(
          isEditing ? localizations.editTodo : localizations.addTodo,
        ),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: ListView(
            children: [
              TextFormField(
                initialValue: isEditing ? widget.todo.task : '',
                key: ArchSampleKeys.taskField,
                autofocus: !isEditing,
                style: textTheme.headline,
                decoration: InputDecoration(
                  hintText: localizations.newTodoHint,
                ),
                validator: (val) {
                  return val.trim().isEmpty
                      ? localizations.emptyTodoError
                      : null;
                },
                onSaved: (value) => _task = value,
              ),
              TextFormField(
                initialValue: isEditing ? widget.todo.note : '',
                key: ArchSampleKeys.noteField,
                maxLines: 10,
                style: textTheme.subhead,
                decoration: InputDecoration(
                  hintText: localizations.notesHint,
                ),
                onSaved: (value) => _note = value,
              )
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        key:
            isEditing ? ArchSampleKeys.saveTodoFab : ArchSampleKeys.saveNewTodo,
        tooltip: isEditing ? localizations.saveChanges : localizations.addTodo,
        child: Icon(isEditing ? Icons.check : Icons.add),
        onPressed: () {
          if (_formKey.currentState.validate()) {
            _formKey.currentState.save();
            widget.onSave(_task, _note);
            Navigator.pop(context);
          }
        },
      ),
    );
  }
}

이 위젯에는 bloc-지정이 없습니다. 단순히 양식을 제시하고 다음을 수행합니다.

업데이트 된 또는 새로 생성 된 할 일을 부모에게 알리기 위해 onSave 콜백 함수를 사용합니다.

그것이 우리가 잊기 전에 애플리케이션의 화면을 위한 것입니다. 그래서 그들을 export할 배럴 파일을 만듭니다.

Screens Barrel

screens/screens.dart 파일을 만들고 세 파일을 모두 내 보냅니다.

export './add_edit_screen.dart';
export './details_screen.dart';
export './home_screen.dart';

다음으로 모든 “위젯”(화면이 아닌 모든 것)을 구현해 보겠습니다.

Widgets

Filter Button

FilterButton 위젯은 필터 옵션 목록을 사용자에게 제공 할 책임이 있으며, 새로운 필터가 선택되었을 때FilteredTodosBloc을 통지 할 것입니다.

widgets라는 새로운 디렉토리를 만들고 FilterButton 구현을 widgets/filter_button.dart에 넣으세요.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/filtered_todos/filtered_todos.dart';
import 'package:flutter_todos/models/models.dart';

class FilterButton extends StatelessWidget {
  final bool visible;

  FilterButton({this.visible, Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final defaultStyle = Theme.of(context).textTheme.body1;
    final activeStyle = Theme.of(context)
        .textTheme
        .body1
        .copyWith(color: Theme.of(context).accentColor);
    final FilteredTodosBloc filteredTodosBloc =
        BlocProvider.of<FilteredTodosBloc>(context);
    return BlocBuilder(
        bloc: filteredTodosBloc,
        builder: (BuildContext context, FilteredTodosState state) {
          final button = _Button(
            onSelected: (filter) {
              filteredTodosBloc.dispatch(UpdateFilter(filter));
            },
            activeFilter: state is FilteredTodosLoaded
                ? state.activeFilter
                : VisibilityFilter.all,
            activeStyle: activeStyle,
            defaultStyle: defaultStyle,
          );
          return AnimatedOpacity(
            opacity: visible ? 1.0 : 0.0,
            duration: Duration(milliseconds: 150),
            child: visible ? button : IgnorePointer(child: button),
          );
        });
  }
}

class _Button extends StatelessWidget {
  const _Button({
    Key key,
    @required this.onSelected,
    @required this.activeFilter,
    @required this.activeStyle,
    @required this.defaultStyle,
  }) : super(key: key);

  final PopupMenuItemSelected<VisibilityFilter> onSelected;
  final VisibilityFilter activeFilter;
  final TextStyle activeStyle;
  final TextStyle defaultStyle;

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<VisibilityFilter>(
      key: ArchSampleKeys.filterButton,
      tooltip: ArchSampleLocalizations.of(context).filterTodos,
      onSelected: onSelected,
      itemBuilder: (BuildContext context) => <PopupMenuItem<VisibilityFilter>>[
            PopupMenuItem<VisibilityFilter>(
              key: ArchSampleKeys.allFilter,
              value: VisibilityFilter.all,
              child: Text(
                ArchSampleLocalizations.of(context).showAll,
                style: activeFilter == VisibilityFilter.all
                    ? activeStyle
                    : defaultStyle,
              ),
            ),
            PopupMenuItem<VisibilityFilter>(
              key: ArchSampleKeys.activeFilter,
              value: VisibilityFilter.active,
              child: Text(
                ArchSampleLocalizations.of(context).showActive,
                style: activeFilter == VisibilityFilter.active
                    ? activeStyle
                    : defaultStyle,
              ),
            ),
            PopupMenuItem<VisibilityFilter>(
              key: ArchSampleKeys.completedFilter,
              value: VisibilityFilter.completed,
              child: Text(
                ArchSampleLocalizations.of(context).showCompleted,
                style: activeFilter == VisibilityFilter.completed
                    ? activeStyle
                    : defaultStyle,
              ),
            ),
          ],
      icon: Icon(Icons.filter_list),
    );
  }
}

FilterButtonFilteredTodosBloc의 상태 변화에 응답해야 하므로 BuildContext에서FilteredTodosBloc에 접근하기 위해 BlocProvider를 사용합니다. 그런 다음 FilteredTodosBloc 상태가 바뀔 때마다 BlocBuilder를 사용하여 다시 렌더링합니다.

구현의 나머지 부분은 순수한 Flutter이며 많은 일이 발생하지 않으므로 ExtraActions 위젯으로 진행 할 수 있습니다.

Extra Actions

FilterButton과 마찬가지로, ExtraActions 위젯은 사용자에게 추가 옵션 목록을 제공합니다: Todo 토글과 완료된 Todo 지우기.

이 위젯은 필터를 신경 쓰지 않기 때문에 FilteredTodosBloc 대신 TodosBloc과 상호 작용할 것입니다.

widgets/extra_actions.dart 파일을 만들고 구현해 보겠습니다.

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/todos/todos.dart';
import 'package:flutter_todos/models/models.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';

class ExtraActions extends StatelessWidget {
  ExtraActions({Key key}) : super(key: ArchSampleKeys.extraActionsButton);

  @override
  Widget build(BuildContext context) {
    final todosBloc = BlocProvider.of<TodosBloc>(context);
    return BlocBuilder(
      bloc: todosBloc,
      builder: (BuildContext context, TodosState state) {
        if (state is TodosLoaded) {
          bool allComplete = (todosBloc.currentState as TodosLoaded)
              .todos
              .every((todo) => todo.complete);
          return PopupMenuButton<ExtraAction>(
            key: FlutterTodosKeys.extraActionsPopupMenuButton,
            onSelected: (action) {
              switch (action) {
                case ExtraAction.clearCompleted:
                  todosBloc.dispatch(ClearCompleted());
                  break;
                case ExtraAction.toggleAllComplete:
                  todosBloc.dispatch(ToggleAll());
                  break;
              }
            },
            itemBuilder: (BuildContext context) => <PopupMenuItem<ExtraAction>>[
                  PopupMenuItem<ExtraAction>(
                    key: ArchSampleKeys.toggleAll,
                    value: ExtraAction.toggleAllComplete,
                    child: Text(
                      allComplete
                          ? ArchSampleLocalizations.of(context)
                              .markAllIncomplete
                          : ArchSampleLocalizations.of(context).markAllComplete,
                    ),
                  ),
                  PopupMenuItem<ExtraAction>(
                    key: ArchSampleKeys.clearCompleted,
                    value: ExtraAction.clearCompleted,
                    child: Text(
                      ArchSampleLocalizations.of(context).clearCompleted,
                    ),
                  ),
                ],
          );
        }
        return Container(key: FlutterTodosKeys.extraActionsEmptyContainer);
      },
    );
  }
}

FilterButton과 마찬가지로, 우리는 BuildContext에서 TodosBloc에 접근하기 위해 BlocProvider를 사용하고 TodosBloc에서 상태 변화에 응답하기 위해 BlocBuilder를 사용합니다.

Based on the action selected, the widget dispatches an event to the TodosBloc to either ToggleAll todos’ completion states or ClearCompleted todos.

다음으로 우리는TabSelector 위젯을 살펴볼 것입니다.

TabSelector

TabSelector 위젯은 탭을 BottomNavigationBar에 표시하고 사용자 입력을 처리합니다.

widgets/tab_selector.dart를 만들고 구현해 보겠습니다.

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';

class TabSelector extends StatelessWidget {
  final AppTab activeTab;
  final Function(AppTab) onTabSelected;

  TabSelector({
    Key key,
    @required this.activeTab,
    @required this.onTabSelected,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BottomNavigationBar(
      key: ArchSampleKeys.tabs,
      currentIndex: AppTab.values.indexOf(activeTab),
      onTap: (index) => onTabSelected(AppTab.values[index]),
      items: AppTab.values.map((tab) {
        return BottomNavigationBarItem(
          icon: Icon(
            tab == AppTab.todos ? Icons.list : Icons.show_chart,
            key: tab == AppTab.todos
                ? ArchSampleKeys.todoTab
                : ArchSampleKeys.statsTab,
          ),
          title: Text(tab == AppTab.stats
              ? ArchSampleLocalizations.of(context).stats
              : ArchSampleLocalizations.of(context).todos),
        );
      }).toList(),
    );
  }
}

이 위젯에는 블럭에 대한 의존성이 없다는 것을 알 수 있습니다. 탭이 선택되었을 때 onTabSelected를 호출 할뿐 아니라 현재 선택된 탭이 무엇인지 알 수 있도록 activeTab을 입력으로 받습니다.

다음으로 FilteredTodos 위젯을 살펴 보겠습니다.

FilteredTodos

FilteredTodos 위젯은 현재 활성화 된 필터에 기반한 todos 리스트를 보여줍니다.

widgets/filtered_todos.dart 파일을 만들고 구현해 볼까요?

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/blocs.dart';
import 'package:flutter_todos/widgets/widgets.dart';
import 'package:flutter_todos/screens/screens.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';

class FilteredTodos extends StatelessWidget {
  FilteredTodos({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final todosBloc = BlocProvider.of<TodosBloc>(context);
    final filteredTodosBloc = BlocProvider.of<FilteredTodosBloc>(context);
    final localizations = ArchSampleLocalizations.of(context);

    return BlocBuilder(
      bloc: filteredTodosBloc,
      builder: (
        BuildContext context,
        FilteredTodosState state,
      ) {
        if (state is FilteredTodosLoading) {
          return LoadingIndicator(key: ArchSampleKeys.todosLoading);
        } else if (state is FilteredTodosLoaded) {
          final todos = state.filteredTodos;
          return ListView.builder(
            key: ArchSampleKeys.todoList,
            itemCount: todos.length,
            itemBuilder: (BuildContext context, int index) {
              final todo = todos[index];
              return TodoItem(
                todo: todo,
                onDismissed: (direction) {
                  todosBloc.dispatch(DeleteTodo(todo));
                  Scaffold.of(context).showSnackBar(DeleteTodoSnackBar(
                    key: ArchSampleKeys.snackbar,
                    todo: todo,
                    onUndo: () => todosBloc.dispatch(AddTodo(todo)),
                    localizations: localizations,
                  ));
                },
                onTap: () async {
                  final removedTodo = await Navigator.of(context).push(
                    MaterialPageRoute(builder: (_) {
                      return DetailsScreen(id: todo.id);
                    }),
                  );
                  if (removedTodo != null) {
                    Scaffold.of(context).showSnackBar(DeleteTodoSnackBar(
                      key: ArchSampleKeys.snackbar,
                      todo: todo,
                      onUndo: () => todosBloc.dispatch(AddTodo(todo)),
                      localizations: localizations,
                    ));
                  }
                },
                onCheckboxChanged: (_) {
                  todosBloc.dispatch(
                    UpdateTodo(todo.copyWith(complete: !todo.complete)),
                  );
                },
              );
            },
          );
        } else {
          return Container(key: FlutterTodosKeys.filteredTodosEmptyContainer);
        }
      },
    );
  }
}

앞서 작성한 위젯과 마찬가지로 FilteredTodos 위젯은 블럭에 접근하기 위해 BlocProvider를 사용합니다 (이 경우FilteredTodosBlocTodosBloc 둘 다 필요합니다).

FilteredTodosBloc은 현재 필터를 기반으로 올바른 할일을 렌더링하는 데 필요합니다.

TodosBloc은 각각의 todos 위에서 스와이프 같은 사용자 상호 작용에 대한 응답으로 todos를 추가 / 삭제할 수있게하는 데 필요합니다.

FilteredTodos 위젯에서, 사용자는 선택된 todos를 편집하거나 삭제할 수 있는 DetailsScreen으로 이동할 수 있습니다. 우리의 FilteredTodos 위젯은 TodoItem 위젯들의 목록을 렌더링하기 때문에, 다음에 그것들을 살펴 보겠습니다.

TodoItem

TodoItem은 하나의 todo를 렌더링하고 사용자 상호 작용 (탭 / 스 와이프)을 처리하는 책임이 있는 StatelessWidget 입니다.

widgets/todo_item.dart를 만들고 그것을 빌드 하겠습니다.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';

class TodoItem extends StatelessWidget {
  final DismissDirectionCallback onDismissed;
  final GestureTapCallback onTap;
  final ValueChanged<bool> onCheckboxChanged;
  final Todo todo;

  TodoItem({
    Key key,
    @required this.onDismissed,
    @required this.onTap,
    @required this.onCheckboxChanged,
    @required this.todo,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Dismissible(
      key: ArchSampleKeys.todoItem(todo.id),
      onDismissed: onDismissed,
      child: ListTile(
        onTap: onTap,
        leading: Checkbox(
          key: ArchSampleKeys.todoItemCheckbox(todo.id),
          value: todo.complete,
          onChanged: onCheckboxChanged,
        ),
        title: Hero(
          tag: '${todo.id}__heroTag',
          child: Container(
            width: MediaQuery.of(context).size.width,
            child: Text(
              todo.task,
              key: ArchSampleKeys.todoItemTask(todo.id),
              style: Theme.of(context).textTheme.title,
            ),
          ),
        ),
        subtitle: todo.note.isNotEmpty
            ? Text(
                todo.note,
                key: ArchSampleKeys.todoItemNote(todo.id),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                style: Theme.of(context).textTheme.subhead,
              )
            : null,
      ),
    );
  }
}

다시 말하지만, TodoItem에는 Bloc 전용 코드가 없습니다. 단순히 생성자를 통해 전달되는 todo을 기반으로 렌더링하고 사용자가 todo을 수행 할 때마다 삽입 된 콜백 함수를 호출합니다.

다음은 DeleteTodoSnackBar입니다.

DeleteTodoSnackBar

DeleteTodoSnackBar는 todo가 삭제되었다는 것을 사용자에게 알려주고 사용자가 그/그녀의 행동을 취소(undo) 할 수 있게 합니다.

widgets/delete_todo_snack_bar.dart를 만들고 그것을 구현 합니다.

import 'package:flutter/material.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/models/models.dart';

class DeleteTodoSnackBar extends SnackBar {
  final ArchSampleLocalizations localizations;

  DeleteTodoSnackBar({
    Key key,
    @required Todo todo,
    @required VoidCallback onUndo,
    @required this.localizations,
  }) : super(
          key: key,
          content: Text(
            localizations.todoDeleted(todo.task),
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
          ),
          duration: Duration(seconds: 2),
          action: SnackBarAction(
            label: localizations.undo,
            onPressed: onUndo,
          ),
        );
}

지금까지 패턴을 알아 차렸을 것입니다. 이 위젯에는 bloc 전용 코드가 없습니다. 사용자가 작업 취소 버튼을 누르면 작업을 렌더링하고 수행 할 콜백 함수 인 onUndo를 호출하기만 하면 됩니다.

거의 완료되었습니다. 이제 두가지 위젯이 남았습니다!

LoadingIndicator

LoadingIndicator 위젯은 무언가 진행 중임을 사용자에게 알리는 책임이 있는 StatelessWidgets입니다.

widgets/loading_indicator.dart를 만들고 그것을 작성하겠습니다.

import 'package:flutter/material.dart';

class LoadingIndicator extends StatelessWidget {
  LoadingIndicator({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: CircularProgressIndicator(),
    );
  }
}

여기서 다룰 부분이 많지 않습니다. 우리는 단지 Center 위젯에 싸여 있는 CircularProgressIndicator를 사용하고 있습니다 (역시 Bloc 전용 코드가 없습니다).

마지막으로Stats 위젯을 만들어야 합니다.

Stats

Stats 위젯은 얼마나 많은 todos가 활성화 또는 완료되었는지를 사용자에게 보여줍니다.

widgets/stats.dart 파일을 생성하고 다음을 구현하겠습니다.

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/blocs/stats/stats.dart';
import 'package:flutter_todos/widgets/widgets.dart';
import 'package:flutter_todos/flutter_todos_keys.dart';

class Stats extends StatelessWidget {
  Stats({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final StatsBloc statsBloc = BlocProvider.of<StatsBloc>(context);
    return BlocBuilder(
      bloc: statsBloc,
      builder: (BuildContext context, StatsState state) {
        if (state is StatsLoading) {
          return LoadingIndicator(key: FlutterTodosKeys.statsLoadingIndicator);
        } else if (state is StatsLoaded) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Padding(
                  padding: EdgeInsets.only(bottom: 8.0),
                  child: Text(
                    ArchSampleLocalizations.of(context).completedTodos,
                    style: Theme.of(context).textTheme.title,
                  ),
                ),
                Padding(
                  padding: EdgeInsets.only(bottom: 24.0),
                  child: Text(
                    '${state.numCompleted}',
                    key: ArchSampleKeys.statsNumCompleted,
                    style: Theme.of(context).textTheme.subhead,
                  ),
                ),
                Padding(
                  padding: EdgeInsets.only(bottom: 8.0),
                  child: Text(
                    ArchSampleLocalizations.of(context).activeTodos,
                    style: Theme.of(context).textTheme.title,
                  ),
                ),
                Padding(
                  padding: EdgeInsets.only(bottom: 24.0),
                  child: Text(
                    "${state.numActive}",
                    key: ArchSampleKeys.statsNumActive,
                    style: Theme.of(context).textTheme.subhead,
                  ),
                )
              ],
            ),
          );
        } else {
          return Container(key: FlutterTodosKeys.emptyStatsContainer);
        }
      },
    );
  }
}

우리는 BlocProvider을 사용하여 StatsBloc을 액세스하고, StatsBloc 상태에 대한 응답으로 BlocBuilder를 사용하여 다시 빌드합니다.

Putting it all together

main.dartTodosApp 위젯을 만들어 봅시다. 우리는 main 함수를 만들고 TodosApp를 실행할 필요가 있습니다.

void main() {
  BlocSupervisor().delegate = SimpleBlocDelegate();
  runApp(TodosApp());
}

Note: BlocSupervisor의 델리게이트를 이전에 작성한 SimpleBlocDelegate에 설정하여 모든 전환 및 오류를 연결할 수 있도록 했습니다.

다음으로TodosApp 위젯을 구현해 보겠습니다.

class TodosApp extends StatelessWidget {
  final todosBloc = TodosBloc(
    todosRepository: const TodosRepositoryFlutter(
      fileStorage: const FileStorage(
        '__flutter_bloc_app__',
        getApplicationDocumentsDirectory,
      ),
    ),
  );

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      bloc: todosBloc,
      child: MaterialApp(
        title: FlutterBlocLocalizations().appTitle,
        theme: ArchSampleTheme.theme,
        localizationsDelegates: [
          ArchSampleLocalizationsDelegate(),
          FlutterBlocLocalizationsDelegate(),
        ],
        routes: {
          ArchSampleRoutes.home: (context) {
            return HomeScreen(
              onInit: () => todosBloc.dispatch(LoadTodos()),
            );
          },
          ArchSampleRoutes.addTodo: (context) {
            return AddEditScreen(
              key: ArchSampleKeys.addTodoScreen,
              onSave: (task, note) {
                todosBloc.dispatch(
                  AddTodo(Todo(task, note: note)),
                );
              },
              isEditing: false,
            );
          },
        },
      ),
    );
  }
}

우리의 TodosAppTodosBloc을 생성하고 flutter_blocBlocProvider 위젯을 사용하여 전체 애플리케이션을 통해 사용할 수 있게 해주는 StatelessWidget입니다.

TodosApp 은 두개의 루트를 가지고 있습니다.

main.dart 아래와 같아야 합니다:

import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:todos_repository_simple/todos_repository_simple.dart';
import 'package:todos_app_core/todos_app_core.dart';
import 'package:flutter_todos/localization.dart';
import 'package:flutter_todos/blocs/blocs.dart';
import 'package:flutter_todos/models/models.dart';
import 'package:flutter_todos/screens/screens.dart';

void main() {
  BlocSupervisor().delegate = SimpleBlocDelegate();
  runApp(TodosApp());
}

class TodosApp extends StatelessWidget {
  final todosBloc = TodosBloc(
    todosRepository: const TodosRepositoryFlutter(
      fileStorage: const FileStorage(
        '__flutter_bloc_app__',
        getApplicationDocumentsDirectory,
      ),
    ),
  );

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      bloc: todosBloc,
      child: MaterialApp(
        title: FlutterBlocLocalizations().appTitle,
        theme: ArchSampleTheme.theme,
        localizationsDelegates: [
          ArchSampleLocalizationsDelegate(),
          FlutterBlocLocalizationsDelegate(),
        ],
        routes: {
          ArchSampleRoutes.home: (context) {
            return HomeScreen(
              onInit: () => todosBloc.dispatch(LoadTodos()),
            );
          },
          ArchSampleRoutes.addTodo: (context) {
            return AddEditScreen(
              key: ArchSampleKeys.addTodoScreen,
              onSave: (task, note) {
                todosBloc.dispatch(
                  AddTodo(Todo(task, note: note)),
                );
              },
              isEditing: false,
            );
          },
        },
      ),
    );
  }
}

그게 전부입니다! 우리는 이제 blocflutter_bloc을 사용하여 Flutter 로 todos 앱을 성공적으로 구현했습니다. 우리는 프레젠테이션 레이어와 비지니스 로직 레이어를 성공적으로 분리했습니다.

이 예제의 전체 소스는 여기에 있습니다.


이전: 날씨 (Weather)

다음: 파이어 베이스 로그인 (Firebase Login)