Skip to content

Commit

Permalink
Merge branch 'master' into firebase-apple
Browse files Browse the repository at this point in the history
  • Loading branch information
marcossevilla authored Oct 6, 2021
2 parents f3718c7 + 1448f9a commit e333a27
Show file tree
Hide file tree
Showing 24 changed files with 484 additions and 173 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
```dart
class BookBloc extends Bloc<BookEvent, BookState> {
BookBloc() : super(BookState()) {
on<BookSelected>((event, emit) {
emit(state.copyWith(selectedBook: () => event.book));
});
on<BookDeselected>((event, emit) {
emit(state.copyWith(selectedBook: () => null));
});
}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
```dart
abstract class BookEvent extends Equatable {
const BookEvent();
@override
List<Object> get props => [];
}
class BookSelected extends BookEvent {
const BookSelected({required this.book});
final Book book;
@override
List<Object> get props => [book];
}
class BookDeselected extends BookEvent {
const BookDeselected();
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
```dart
class Book extends Equatable {
const Book(this.title, this.author);
final String title;
final String author;
@override
List<Object> get props => [title, author];
}
const defaultBooks = <Book>[
Book('Left Hand of Darkness', 'Ursula K. Le Guin'),
Book('Too Like the Lightning', 'Ada Palmer'),
Book('Kindred', 'Octavia E. Butler'),
];
class BookState extends Equatable {
const BookState({this.selectedBook, this.books = defaultBooks});
final Book? selectedBook;
final List<Book> books;
@override
List<Object?> get props => [selectedBook, books];
BookState copyWith({
ValueGetter<Book?>? selectedBook,
ValueGetter<List<Book>>? books,
}) {
return BookState(
selectedBook: selectedBook != null ? selectedBook() : this.selectedBook,
books: books != null ? books() : this.books,
);
}
}
```
100 changes: 100 additions & 0 deletions docs/_snippets/recipes_flutter_navigation/navigation2/main.dart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
```dart
void main() {
runApp(
BlocProvider(
create: (_) => BookBloc(),
child: BooksApp(),
),
);
}
class BooksApp extends StatelessWidget {
const BooksApp({Key? key}) : super(key: key);
List<Page> onGeneratePages(BookState state, List<Page> pages) {
final selectedBook = state.selectedBook;
return [
BooksListPage.page(books: state.books),
if (selectedBook != null) BookDetailsPage.page(book: selectedBook)
];
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Books App',
home: FlowBuilder(
state: context.watch<BookBloc>().state,
onGeneratePages: onGeneratePages,
),
);
}
}
class BooksListPage extends StatelessWidget {
const BooksListPage({Key? key, required this.books}) : super(key: key);
static Page page({required List<Book> books}) {
return MaterialPage<void>(
child: BooksListPage(books: books),
);
}
final List<Book> books;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Books')),
body: ListView(
children: [
for (final book in books)
ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () {
context.read<BookBloc>().add(BookSelected(book: book));
},
)
],
),
);
}
}
class BookDetailsPage extends StatelessWidget {
const BookDetailsPage({Key? key, required this.book});
static Page page({required Book book}) {
return MaterialPage<void>(
child: BookDetailsPage(book: book),
);
}
final Book book;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return WillPopScope(
onWillPop: () async {
context.read<BookBloc>().add(BookDeselected());
return true;
},
child: Scaffold(
appBar: AppBar(title: const Text('Details')),
body: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(book.title, style: theme.textTheme.headline6),
Text(book.author, style: theme.textTheme.subtitle1),
],
),
),
),
);
}
}
```
2 changes: 1 addition & 1 deletion docs/flutterweathertutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ In a later step, we will also use the [http](https://pub.dev/packages/http) pack

Let's add these dependencies to the `pubspec.yaml`.

[pubspec.yaml](https://raw.githubusercontent.com/felangel/bloc/master/examples/flutter_weather/packages/weather_repository/pubspec.yaml ':include')
[pubspec.yaml](https://raw.githubusercontent.com/felangel/bloc/master/examples/flutter_weather/packages/meta_weather_api/pubspec.yaml ':include')

?> **Note**: Remember to run `flutter pub get` after adding the dependencies.

Expand Down
39 changes: 38 additions & 1 deletion docs/recipesflutternavigation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Recipes: Navigation

> In this recipe, we're going to take a look at how to use `BlocBuilder` and/or `BlocListener` to do navigation. We're going to explore two approaches: Direct Navigation and Route Navigation.
> In this recipe, we're going to take a look at how to use `BlocBuilder` and/or `BlocListener` to do navigation. We're going to explore three approaches: Direct Navigation, Route Navigation, and Declarative Navigation using Navigator 2.0.
## Direct Navigation

Expand Down Expand Up @@ -66,3 +66,40 @@ Let's take a look at how to route to a different page based on the state of `MyB
!> For the sake of this example, we are adding an event just for navigation. In a real application, you should not create explicit navigation events. If there is no "business logic" necessary in order to trigger navigation, you should always directly navigate in response to user input (in the `onPressed` callback, etc...). Only navigate in response to state changes if some "business logic" is required in order to determine where to navigate.

The full source for this recipe can be found [here](https://gist.github.com/felangel/6bcd4be10c046ceb33eecfeb380135dd).

## Navigation 2.0

> In this example, we're going to take a look at how to use the Navigator 2.0 pages API to handle routing in response to state changes in a bloc.
?> Note: We're going to use [package:flow_builder](https://pub.dev/packages/flow_builder) to make working with the Navigator 2.0 API simpler.

### Bloc

For the sake of showcasing Navigator's advantages, we will build a slightly more complex example.
Let's build `BookBloc` which will take `BookEvents` and convert them into `BookStates`.

#### BookEvent

`BookEvent` will respond to two events: selecting a book, and deselecting a book.

[book_event.dart](_snippets/recipes_flutter_navigation/navigation2/book_event.dart.md ':include')

#### BookState

`BookState` will contain the list of books and an optional selected book if the user taps a book.

[book_state.dart](_snippets/recipes_flutter_navigation/navigation2/book_state.dart.md ':include')

#### BookBloc

`BookBloc` will handle responding to each `BookEvent` and will emit the appropriate `BookState` in response:

[book_bloc.dart](_snippets/recipes_flutter_navigation/navigation2/book_bloc.dart.md ':include')

### UI Layer

Now let's hook up the bloc to our UI using `FlowBuilder`!

[main.dart](_snippets/recipes_flutter_navigation/navigation2/main.dart.md ':include')

The full source for this recipe can be found [here](https://gist.github.com/felangel/bd3cf504a10c0763a32f7a94e2649369).
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ void main() {
group('fetchList', () {
blocTest<ComplexListCubit, ComplexListState>(
'emits ComplexListState.success after fetching list',
build: () {
setUp: () {
when(repository.fetchItems).thenAnswer((_) async => mockItems);
return ComplexListCubit(repository: repository);
},
build: () => ComplexListCubit(repository: repository),
act: (cubit) => cubit.fetchList(),
expect: () => [
const ComplexListState.success(mockItems),
Expand All @@ -43,10 +43,10 @@ void main() {

blocTest<ComplexListCubit, ComplexListState>(
'emits ComplexListState.failure after failing to fetch list',
build: () {
setUp: () {
when(repository.fetchItems).thenThrow(Exception('Error'));
return ComplexListCubit(repository: repository);
},
build: () => ComplexListCubit(repository: repository),
act: (cubit) => cubit.fetchList(),
expect: () => [
const ComplexListState.failure(),
Expand All @@ -58,10 +58,10 @@ void main() {
group('deleteItem', () {
blocTest<ComplexListCubit, ComplexListState>(
'emits corrects states when deleting an item',
build: () {
setUp: () {
when(() => repository.deleteItem('2')).thenAnswer((_) async => null);
return ComplexListCubit(repository: repository);
},
build: () => ComplexListCubit(repository: repository),
seed: () => const ComplexListState.success(mockItems),
act: (cubit) => cubit.deleteItem('2'),
expect: () => [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ void main() {

blocTest<NewCarBloc, NewCarState>(
'emits brands loading in progress and brands load success',
build: () {
setUp: () {
when(newCarRepository.fetchBrands).thenAnswer((_) async => mockBrands);
return NewCarBloc(newCarRepository: newCarRepository);
},
build: () => NewCarBloc(newCarRepository: newCarRepository),
act: (bloc) => bloc.add(const NewCarFormLoaded()),
expect: () => [
const NewCarState.brandsLoadInProgress(),
Expand All @@ -44,12 +44,12 @@ void main() {

blocTest<NewCarBloc, NewCarState>(
'emits models loading in progress and models load success',
build: () {
when(() => newCarRepository.fetchModels(brand: mockBrand)).thenAnswer(
(_) async => mockModels,
);
return NewCarBloc(newCarRepository: newCarRepository);
setUp: () {
when(
() => newCarRepository.fetchModels(brand: mockBrand),
).thenAnswer((_) async => mockModels);
},
build: () => NewCarBloc(newCarRepository: newCarRepository),
act: (bloc) => bloc.add(NewCarBrandChanged(brand: mockBrand)),
expect: () => [
NewCarState.modelsLoadInProgress(brands: [], brand: mockBrand),
Expand All @@ -66,12 +66,12 @@ void main() {

blocTest<NewCarBloc, NewCarState>(
'emits years loading in progress and year load success',
build: () {
setUp: () {
when(
() => newCarRepository.fetchYears(brand: null, model: mockModel),
).thenAnswer((_) async => mockYears);
return NewCarBloc(newCarRepository: newCarRepository);
},
build: () => NewCarBloc(newCarRepository: newCarRepository),
act: (bloc) => bloc.add(NewCarModelChanged(model: mockModel)),
expect: () => [
NewCarState.yearsLoadInProgress(
Expand Down Expand Up @@ -104,18 +104,18 @@ void main() {

blocTest<NewCarBloc, NewCarState>(
'emits correct states when complete flow is executed',
build: () {
when(newCarRepository.fetchBrands).thenAnswer(
(_) => Future.value(mockBrands),
);
setUp: () {
when(
newCarRepository.fetchBrands,
).thenAnswer((_) => Future.value(mockBrands));
when(
() => newCarRepository.fetchModels(brand: mockBrand),
).thenAnswer((_) => Future.value(mockModels));
when(
() => newCarRepository.fetchYears(brand: mockBrand, model: mockModel),
).thenAnswer((_) => Future.value(mockYears));
return NewCarBloc(newCarRepository: newCarRepository);
},
build: () => NewCarBloc(newCarRepository: newCarRepository),
act: (bloc) => bloc
..add(const NewCarFormLoaded())
..add(NewCarBrandChanged(brand: mockBrand))
Expand Down
18 changes: 11 additions & 7 deletions examples/flutter_firebase_login/test/app/bloc/app_bloc_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,35 +35,39 @@ void main() {
group('UserChanged', () {
blocTest<AppBloc, AppState>(
'emits authenticated when user is not empty',
build: () {
setUp: () {
when(() => user.isNotEmpty).thenReturn(true);
when(() => authenticationRepository.user).thenAnswer(
(_) => Stream.value(user),
);
return AppBloc(authenticationRepository: authenticationRepository);
},
build: () => AppBloc(
authenticationRepository: authenticationRepository,
),
seed: () => AppState.unauthenticated(),
expect: () => [AppState.authenticated(user)],
);

blocTest<AppBloc, AppState>(
'emits unauthenticated when user is empty',
build: () {
setUp: () {
when(() => authenticationRepository.user).thenAnswer(
(_) => Stream.value(User.empty),
);
return AppBloc(authenticationRepository: authenticationRepository);
},
build: () => AppBloc(
authenticationRepository: authenticationRepository,
),
expect: () => [AppState.unauthenticated()],
);
});

group('LogoutRequested', () {
blocTest<AppBloc, AppState>(
'invokes logOut',
build: () {
return AppBloc(authenticationRepository: authenticationRepository);
},
build: () => AppBloc(
authenticationRepository: authenticationRepository,
),
act: (bloc) => bloc.add(AppLogoutRequested()),
verify: (_) {
verify(() => authenticationRepository.logOut()).called(1);
Expand Down
Loading

0 comments on commit e333a27

Please sign in to comment.