Skip to content

Commit 48a3a39

Browse files
feat(ui_storage): add StorageGridView widget (#11206)
Co-authored-by: Russell Wheatley <[email protected]>
1 parent fd832fb commit 48a3a39

File tree

8 files changed

+201
-24
lines changed

8 files changed

+201
-24
lines changed

packages/firebase_ui_storage/example/lib/src/apps.dart

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:firebase_ui_storage_example/main.dart';
66
import 'package:firebase_ui_storage_example/src/storage_image_app.dart';
77
import 'package:flutter/material.dart';
88

9+
import 'grid_view_app.dart';
910
import 'progress_bar_app.dart';
1011
import 'upload_button_app.dart';
1112
import 'list_view_app.dart';
@@ -19,6 +20,7 @@ const apps = <App>[
1920
ProgressBarApp(),
2021
StorageImageApp(),
2122
StorageListViewApp(),
23+
StorageGridViewApp(),
2224
];
2325

2426
class AppList extends StatelessWidget {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2023, the Chromium project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
7+
import 'package:firebase_storage/firebase_storage.dart';
8+
import 'package:firebase_ui_storage/firebase_ui_storage.dart';
9+
import 'package:flutter/material.dart';
10+
11+
import 'apps.dart';
12+
13+
class StorageGridViewApp extends StatelessWidget implements App {
14+
const StorageGridViewApp({super.key});
15+
16+
@override
17+
String get name => 'StorageGridView';
18+
19+
@override
20+
Widget build(BuildContext context) {
21+
return StorageGridView(
22+
ref: FirebaseStorage.instance.ref('list'),
23+
itemBuilder: (context, ref) {
24+
return Card(
25+
child: Center(
26+
child: FutureBuilder(
27+
future: ref.getData(),
28+
builder: (context, snapshot) {
29+
if (snapshot.hasError) {
30+
return Text(snapshot.error.toString());
31+
}
32+
if (snapshot.hasData) {
33+
return Text(utf8.decode(snapshot.data!));
34+
}
35+
36+
return const CircularProgressIndicator();
37+
},
38+
),
39+
),
40+
);
41+
},
42+
);
43+
}
44+
}

packages/firebase_ui_storage/example/lib/src/list_view_app.dart

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:convert';
6+
57
import 'package:firebase_storage/firebase_storage.dart';
68
import 'package:firebase_ui_storage/firebase_ui_storage.dart';
79
import 'package:flutter/material.dart';
@@ -27,7 +29,7 @@ class StorageListViewApp extends StatelessWidget implements App {
2729
return Text(snapshot.error.toString());
2830
}
2931
if (snapshot.hasData) {
30-
return Text(snapshot.data.toString());
32+
return Text(utf8.decode(snapshot.data!));
3133
}
3234

3335
return const Text('Loading...');

packages/firebase_ui_storage/lib/firebase_ui_storage.dart

+1
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ export 'src/widgets/progress_indicator.dart'
2222
export 'src/widgets/image.dart' show StorageImage, LoadingStateVariant;
2323
export 'src/paginated_loading_controller.dart';
2424
export 'src/widgets/list_view.dart';
25+
export 'src/widgets/grid_view.dart';

packages/firebase_ui_storage/lib/src/paginated_loading_controller.dart

+26-16
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:async';
6+
57
import 'package:firebase_storage/firebase_storage.dart';
68
import 'package:flutter/foundation.dart';
9+
import 'package:flutter/scheduler.dart';
710

811
/// A base class for loading states.
912
sealed class PaginatedLoadingState {
@@ -70,26 +73,33 @@ class PaginatedLoadingController extends ChangeNotifier {
7073
? const InitialPageLoading()
7174
: PageLoading(items: _items!);
7275

73-
notifyListeners();
74-
75-
return ref.list(_listOptions).then((value) {
76-
_cursor = value;
77-
(_items ??= []).addAll(value.items);
78-
79-
_state = PageLoadComplete(
80-
pageItems: value.items,
81-
items: _items!,
82-
);
76+
final completer = Completer();
8377

78+
SchedulerBinding.instance.addPostFrameCallback((_) async {
8479
notifyListeners();
85-
}).catchError((e) {
86-
_state = PageLoadError(
87-
error: e,
88-
items: _items,
89-
);
9080

91-
notifyListeners();
81+
try {
82+
final value = await ref.list(_listOptions);
83+
84+
_cursor = value;
85+
(_items ??= []).addAll(value.items);
86+
87+
_state = PageLoadComplete(
88+
pageItems: value.items,
89+
items: _items!,
90+
);
91+
} catch (err) {
92+
_state = PageLoadError(
93+
error: err,
94+
items: _items,
95+
);
96+
} finally {
97+
completer.complete();
98+
notifyListeners();
99+
}
92100
});
101+
102+
return completer.future;
93103
}
94104

95105
bool shouldLoadNextPage(int itemIndex) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2023, the Chromium project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:firebase_ui_shared/firebase_ui_shared.dart';
6+
import 'package:flutter/widgets.dart';
7+
8+
class DefaultLoadingIndicator extends StatelessWidget {
9+
const DefaultLoadingIndicator({super.key});
10+
11+
@override
12+
Widget build(BuildContext context) {
13+
return const Center(
14+
child: LoadingIndicator(
15+
size: 32,
16+
borderWidth: 2,
17+
),
18+
);
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2023, the Chromium project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:firebase_storage/firebase_storage.dart';
6+
import 'package:firebase_ui_storage/firebase_ui_storage.dart';
7+
import 'package:flutter/widgets.dart';
8+
9+
import 'default_loading_indicator.dart';
10+
11+
Widget _defaultLoadingBuilder(BuildContext context) {
12+
return const DefaultLoadingIndicator();
13+
}
14+
15+
class StorageGridView extends StatefulWidget {
16+
/// The [Reference] to list items from.
17+
/// If not provided, a [loadingController] must be created and passed.
18+
final Reference? ref;
19+
20+
final PaginatedLoadingController? loadingController;
21+
22+
/// The number of items to load per page.
23+
/// Defaults to 50.
24+
final int pageSize;
25+
26+
/// A builder that is called for the first page load.
27+
final Widget Function(BuildContext context) loadingBuilder;
28+
29+
/// A builder that is called when an error occurs during page loading.
30+
final Widget Function(
31+
BuildContext context,
32+
Object? error,
33+
PaginatedLoadingController controller,
34+
)? errorBuilder;
35+
36+
/// A builder that is called for each item in the list.
37+
final Widget Function(BuildContext context, Reference ref) itemBuilder;
38+
39+
/// See [SliverGridDelegate].
40+
final SliverGridDelegate gridDelegate;
41+
42+
const StorageGridView({
43+
super.key,
44+
this.ref,
45+
this.loadingController,
46+
this.pageSize = 50,
47+
this.loadingBuilder = _defaultLoadingBuilder,
48+
this.errorBuilder,
49+
this.gridDelegate = const SliverGridDelegateWithFixedCrossAxisCount(
50+
crossAxisCount: 3,
51+
),
52+
required this.itemBuilder,
53+
}) : assert(
54+
ref != null || loadingController != null,
55+
'ref or loadingController must be provided',
56+
);
57+
58+
@override
59+
State<StorageGridView> createState() => _StorageGridViewState();
60+
}
61+
62+
class _StorageGridViewState extends State<StorageGridView> {
63+
late PaginatedLoadingController ctrl = widget.loadingController ??
64+
PaginatedLoadingController(
65+
ref: widget.ref!,
66+
pageSize: widget.pageSize,
67+
);
68+
69+
Widget gridBuilder(BuildContext context, List<Reference> items) {
70+
return GridView.builder(
71+
gridDelegate: widget.gridDelegate,
72+
itemCount: items.length,
73+
itemBuilder: (context, index) {
74+
if (ctrl.shouldLoadNextPage(index)) {
75+
ctrl.load();
76+
}
77+
78+
return widget.itemBuilder(context, items[index]);
79+
},
80+
);
81+
}
82+
83+
@override
84+
Widget build(BuildContext context) {
85+
return AnimatedBuilder(
86+
animation: ctrl,
87+
builder: (context, _) {
88+
return switch (ctrl.state) {
89+
InitialPageLoading() => widget.loadingBuilder(context),
90+
PageLoadError(
91+
error: final error,
92+
items: final items,
93+
) =>
94+
widget.errorBuilder != null
95+
? widget.errorBuilder!(context, error, ctrl)
96+
: gridBuilder(context, items ?? []),
97+
PageLoading(items: final items) => gridBuilder(context, items),
98+
PageLoadComplete(items: final items) => gridBuilder(context, items),
99+
};
100+
},
101+
);
102+
}
103+
}

packages/firebase_ui_storage/lib/src/widgets/list_view.dart

+2-7
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,14 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'package:firebase_storage/firebase_storage.dart';
6-
import 'package:firebase_ui_shared/firebase_ui_shared.dart';
76
import 'package:flutter/gestures.dart';
87
import 'package:flutter/widgets.dart';
98

109
import '../paginated_loading_controller.dart';
10+
import 'default_loading_indicator.dart';
1111

1212
Widget _defaultLoadingBuilder(BuildContext context) {
13-
return const Center(
14-
child: LoadingIndicator(
15-
size: 32,
16-
borderWidth: 2,
17-
),
18-
);
13+
return const DefaultLoadingIndicator();
1914
}
2015

2116
/// A [ListView.builder] that automatically handles paginated loading from

0 commit comments

Comments
 (0)