Skip to content

Commit 3997ccd

Browse files
authored
Dart 3.0 (#2)
* flutter 3.10.0, deps * either, dart 3.0 * cancel op * cancel op * better * better
1 parent de4d982 commit 3997ccd

File tree

11 files changed

+392
-195
lines changed

11 files changed

+392
-195
lines changed

lib/main.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import 'package:batch_api_demo/main/bloc.dart';
21
import 'package:batch_api_demo/main/home_page.dart';
2+
import 'package:batch_api_demo/main/main_bloc.dart';
33
import 'package:batch_api_demo/users_repo.dart';
44
import 'package:flutter/material.dart';
55
import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart';
@@ -25,6 +25,7 @@ class MyApp extends StatelessWidget {
2525
useMaterial3: true,
2626
colorSchemeSeed: Colors.purple,
2727
),
28+
debugShowCheckedModeBanner: false,
2829
home: BlocProvider(
2930
initBloc: (context) => MainBloc(usersRepo: context.get()),
3031
child: const MyHomePage(),

lib/main/bloc.dart

Lines changed: 0 additions & 76 deletions
This file was deleted.

lib/main/home_page.dart

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import 'package:batch_api_demo/main/bloc.dart';
2-
import 'package:batch_api_demo/main/state.dart';
1+
import 'package:batch_api_demo/main/main_bloc.dart';
2+
import 'package:batch_api_demo/main/main_state.dart';
33
import 'package:batch_api_demo/optional.dart';
4+
import 'package:batch_api_demo/users_repo.dart';
5+
import 'package:built_collection/built_collection.dart';
46
import 'package:flutter/material.dart';
57
import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart';
68

@@ -23,14 +25,40 @@ class _MyHomePageState extends State<MyHomePage> {
2325
return Scaffold(
2426
appBar: AppBar(
2527
title: const Text('Batch API demo'),
28+
actions: [
29+
IconButton(
30+
onPressed: () => context.bloc<MainBloc>().fetch(),
31+
icon: const Icon(Icons.refresh),
32+
),
33+
IconButton(
34+
onPressed: () => context.bloc<MainBloc>().cancel(),
35+
icon: const Icon(Icons.cancel),
36+
),
37+
RxStreamBuilder<bool>(
38+
stream: UsersRepo.failed$,
39+
builder: (context, state) => TextButton(
40+
onPressed: UsersRepo.toggleFailed,
41+
child: Text(
42+
state ? 'failed' : 'succeed',
43+
style: TextStyle(color: state ? Colors.red : Colors.green),
44+
),
45+
),
46+
),
47+
],
2648
),
2749
body: SizedBox.expand(
2850
child: RxStreamBuilder<MainState>(
2951
stream: context.bloc<MainBloc>().state$,
3052
builder: (context, state) {
53+
if (state.cancelled) {
54+
return const Center(
55+
child: Text('Cancelled'),
56+
);
57+
}
58+
3159
if (state.error.isNotEmpty) {
3260
return Center(
33-
child: Text('Error: ${state.error.valueOrNull()}'),
61+
child: Text('Error: ${state.error.valueOrNull()!.message}'),
3462
);
3563
}
3664

@@ -49,7 +77,7 @@ class _MyHomePageState extends State<MyHomePage> {
4977
}
5078

5179
class UsersListView extends StatelessWidget {
52-
final List<UserItem> items;
80+
final BuiltList<UserItem> items;
5381

5482
const UsersListView({Key? key, required this.items}) : super(key: key);
5583

@@ -59,7 +87,10 @@ class UsersListView extends StatelessWidget {
5987
itemCount: items.length,
6088
itemBuilder: (context, index) {
6189
final item = items[index];
62-
return UserItemRow(item: item);
90+
return UserItemRow(
91+
key: ValueKey(item.user.id),
92+
item: item,
93+
);
6394
},
6495
);
6596
}
@@ -74,17 +105,19 @@ class UserItemRow extends StatelessWidget {
74105
Widget build(BuildContext context) {
75106
return ListTile(
76107
title: Text(item.user.name),
77-
subtitle: item.isLoading
78-
? Text(
79-
'Loading...',
80-
style: Theme.of(context)
81-
.textTheme
82-
.bodyMedium!
83-
.copyWith(color: Colors.red),
84-
)
85-
: item.user.avatarUrl != null
86-
? Text('Avatar: ${item.user.avatarUrl}')
87-
: const Text('No avatar'),
108+
subtitle: switch ((item.isLoading, item.error)) {
109+
(true, _) => Text(
110+
'Loading...',
111+
style: Theme.of(context)
112+
.textTheme
113+
.bodyMedium!
114+
.copyWith(color: Colors.red),
115+
),
116+
(_, Some(value: final error)) => Text('Error: ${error.message}'),
117+
_ => item.user.avatarUrl != null
118+
? Text('Avatar: ${item.user.avatarUrl}')
119+
: const Text('No avatar'),
120+
},
88121
leading: const CircleAvatar(
89122
child: Icon(Icons.person),
90123
),

lib/main/main_bloc.dart

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import 'dart:async';
2+
3+
import 'package:batch_api_demo/main/main_state.dart';
4+
import 'package:batch_api_demo/main/partial_state_change.dart';
5+
import 'package:batch_api_demo/users_repo.dart';
6+
import 'package:batch_api_demo/utils.dart';
7+
import 'package:built_collection/built_collection.dart';
8+
import 'package:disposebag/disposebag.dart';
9+
import 'package:flutter/foundation.dart';
10+
import 'package:flutter_bloc_pattern/flutter_bloc_pattern.dart';
11+
import 'package:rxdart_ext/rxdart_ext.dart';
12+
13+
const _batchSize = 4;
14+
const _maxRetries = 3;
15+
16+
class MainBloc extends DisposeCallbackBaseBloc {
17+
final Func0<void> fetch;
18+
final Func0<void> cancel;
19+
20+
final StateStream<MainState> state$;
21+
22+
MainBloc._({
23+
required Func0<void> dispose,
24+
required this.fetch,
25+
required this.cancel,
26+
required this.state$,
27+
}) : super(dispose);
28+
29+
factory MainBloc({
30+
required UsersRepo usersRepo,
31+
}) {
32+
final fetchS = StreamController<void>();
33+
final cancelS = PublishSubject<void>(sync: true);
34+
35+
final state$ = Rx.merge([
36+
fetchS.stream.switchMap(
37+
(_) => _fetchUsersAndAvatars(usersRepo)
38+
.doOnCancel(
39+
() => debugPrint('MainBloc._fetchUsersAndAvatars() cancelled!'))
40+
.takeUntil(cancelS.stream),
41+
),
42+
cancelS.stream.asyncMap((event) => Future.value(UsersCancelledChange())),
43+
])
44+
.scan((state, change, _) => change.reduce(state), MainState.initial)
45+
.publishState(MainState.initial);
46+
47+
return MainBloc._(
48+
dispose: DisposeBag([
49+
fetchS,
50+
cancelS,
51+
state$.connect(),
52+
]).dispose,
53+
fetch: fetchS.addNull,
54+
cancel: cancelS.addNull,
55+
state$: state$,
56+
);
57+
}
58+
}
59+
60+
Stream<MainPartialStateChange> _fetchUsersAndAvatars(UsersRepo usersRepo) =>
61+
usersRepo
62+
.fetchUsers()
63+
.exhaustMap(
64+
(either) => either.fold(
65+
ifLeft: (e) => Stream.value(UsersErrorChange(e)),
66+
ifRight: (users) {
67+
final items = users.map(UserItem.loading).toBuiltList();
68+
69+
return Rx.concat<MainPartialStateChange>([
70+
Stream.value(UsersListChange(items)),
71+
Stream.fromIterable(items)
72+
.flatMapBatches(
73+
(e) => _fetchAvatars(usersRepo, e.user), _batchSize)
74+
.expand(identity),
75+
]);
76+
},
77+
),
78+
)
79+
.startWith(UsersLoadingChange());
80+
81+
Stream<MainPartialStateChange> _fetchAvatars(
82+
UsersRepo usersRepo,
83+
User user,
84+
) =>
85+
retryEitherSingle<UserError, String>(
86+
() => usersRepo.fetchUserAvatarUrl(user),
87+
_maxRetries,
88+
)
89+
.map(
90+
(either) => either.fold(
91+
ifLeft: (e) => UserItem.failed(user, e),
92+
ifRight: (avatarUrl) =>
93+
UserItem.loaded(user.copyWithNewAvatarUrl(avatarUrl)),
94+
),
95+
)
96+
.map(UserItemUpdatedChange.new);

0 commit comments

Comments
 (0)