Skip to content

Commit

Permalink
feat(ui_storage): add StorageGridView widget (#11206)
Browse files Browse the repository at this point in the history
Co-authored-by: Russell Wheatley <russellwheatley85@gmail.com>
  • Loading branch information
lesnitsky and russellwheatley committed Jul 13, 2023
1 parent fd832fb commit 48a3a39
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 24 deletions.
2 changes: 2 additions & 0 deletions packages/firebase_ui_storage/example/lib/src/apps.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:firebase_ui_storage_example/main.dart';
import 'package:firebase_ui_storage_example/src/storage_image_app.dart';
import 'package:flutter/material.dart';

import 'grid_view_app.dart';
import 'progress_bar_app.dart';
import 'upload_button_app.dart';
import 'list_view_app.dart';
Expand All @@ -19,6 +20,7 @@ const apps = <App>[
ProgressBarApp(),
StorageImageApp(),
StorageListViewApp(),
StorageGridViewApp(),
];

class AppList extends StatelessWidget {
Expand Down
44 changes: 44 additions & 0 deletions packages/firebase_ui_storage/example/lib/src/grid_view_app.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2023, the Chromium project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';

import 'package:firebase_storage/firebase_storage.dart';
import 'package:firebase_ui_storage/firebase_ui_storage.dart';
import 'package:flutter/material.dart';

import 'apps.dart';

class StorageGridViewApp extends StatelessWidget implements App {
const StorageGridViewApp({super.key});

@override
String get name => 'StorageGridView';

@override
Widget build(BuildContext context) {
return StorageGridView(
ref: FirebaseStorage.instance.ref('list'),
itemBuilder: (context, ref) {
return Card(
child: Center(
child: FutureBuilder(
future: ref.getData(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
if (snapshot.hasData) {
return Text(utf8.decode(snapshot.data!));
}

return const CircularProgressIndicator();
},
),
),
);
},
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';

import 'package:firebase_storage/firebase_storage.dart';
import 'package:firebase_ui_storage/firebase_ui_storage.dart';
import 'package:flutter/material.dart';
Expand All @@ -27,7 +29,7 @@ class StorageListViewApp extends StatelessWidget implements App {
return Text(snapshot.error.toString());
}
if (snapshot.hasData) {
return Text(snapshot.data.toString());
return Text(utf8.decode(snapshot.data!));
}

return const Text('Loading...');
Expand Down
1 change: 1 addition & 0 deletions packages/firebase_ui_storage/lib/firebase_ui_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export 'src/widgets/progress_indicator.dart'
export 'src/widgets/image.dart' show StorageImage, LoadingStateVariant;
export 'src/paginated_loading_controller.dart';
export 'src/widgets/list_view.dart';
export 'src/widgets/grid_view.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';

import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';

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

notifyListeners();

return ref.list(_listOptions).then((value) {
_cursor = value;
(_items ??= []).addAll(value.items);

_state = PageLoadComplete(
pageItems: value.items,
items: _items!,
);
final completer = Completer();

SchedulerBinding.instance.addPostFrameCallback((_) async {
notifyListeners();
}).catchError((e) {
_state = PageLoadError(
error: e,
items: _items,
);

notifyListeners();
try {
final value = await ref.list(_listOptions);

_cursor = value;
(_items ??= []).addAll(value.items);

_state = PageLoadComplete(
pageItems: value.items,
items: _items!,
);
} catch (err) {
_state = PageLoadError(
error: err,
items: _items,
);
} finally {
completer.complete();
notifyListeners();
}
});

return completer.future;
}

bool shouldLoadNextPage(int itemIndex) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2023, the Chromium project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:firebase_ui_shared/firebase_ui_shared.dart';
import 'package:flutter/widgets.dart';

class DefaultLoadingIndicator extends StatelessWidget {
const DefaultLoadingIndicator({super.key});

@override
Widget build(BuildContext context) {
return const Center(
child: LoadingIndicator(
size: 32,
borderWidth: 2,
),
);
}
}
103 changes: 103 additions & 0 deletions packages/firebase_ui_storage/lib/src/widgets/grid_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2023, the Chromium project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:firebase_storage/firebase_storage.dart';
import 'package:firebase_ui_storage/firebase_ui_storage.dart';
import 'package:flutter/widgets.dart';

import 'default_loading_indicator.dart';

Widget _defaultLoadingBuilder(BuildContext context) {
return const DefaultLoadingIndicator();
}

class StorageGridView extends StatefulWidget {
/// The [Reference] to list items from.
/// If not provided, a [loadingController] must be created and passed.
final Reference? ref;

final PaginatedLoadingController? loadingController;

/// The number of items to load per page.
/// Defaults to 50.
final int pageSize;

/// A builder that is called for the first page load.
final Widget Function(BuildContext context) loadingBuilder;

/// A builder that is called when an error occurs during page loading.
final Widget Function(
BuildContext context,
Object? error,
PaginatedLoadingController controller,
)? errorBuilder;

/// A builder that is called for each item in the list.
final Widget Function(BuildContext context, Reference ref) itemBuilder;

/// See [SliverGridDelegate].
final SliverGridDelegate gridDelegate;

const StorageGridView({
super.key,
this.ref,
this.loadingController,
this.pageSize = 50,
this.loadingBuilder = _defaultLoadingBuilder,
this.errorBuilder,
this.gridDelegate = const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
required this.itemBuilder,
}) : assert(
ref != null || loadingController != null,
'ref or loadingController must be provided',
);

@override
State<StorageGridView> createState() => _StorageGridViewState();
}

class _StorageGridViewState extends State<StorageGridView> {
late PaginatedLoadingController ctrl = widget.loadingController ??
PaginatedLoadingController(
ref: widget.ref!,
pageSize: widget.pageSize,
);

Widget gridBuilder(BuildContext context, List<Reference> items) {
return GridView.builder(
gridDelegate: widget.gridDelegate,
itemCount: items.length,
itemBuilder: (context, index) {
if (ctrl.shouldLoadNextPage(index)) {
ctrl.load();
}

return widget.itemBuilder(context, items[index]);
},
);
}

@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: ctrl,
builder: (context, _) {
return switch (ctrl.state) {
InitialPageLoading() => widget.loadingBuilder(context),
PageLoadError(
error: final error,
items: final items,
) =>
widget.errorBuilder != null
? widget.errorBuilder!(context, error, ctrl)
: gridBuilder(context, items ?? []),
PageLoading(items: final items) => gridBuilder(context, items),
PageLoadComplete(items: final items) => gridBuilder(context, items),
};
},
);
}
}
9 changes: 2 additions & 7 deletions packages/firebase_ui_storage/lib/src/widgets/list_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,14 @@
// BSD-style license that can be found in the LICENSE file.

import 'package:firebase_storage/firebase_storage.dart';
import 'package:firebase_ui_shared/firebase_ui_shared.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';

import '../paginated_loading_controller.dart';
import 'default_loading_indicator.dart';

Widget _defaultLoadingBuilder(BuildContext context) {
return const Center(
child: LoadingIndicator(
size: 32,
borderWidth: 2,
),
);
return const DefaultLoadingIndicator();
}

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

0 comments on commit 48a3a39

Please sign in to comment.