doc(arch): add fully featured example
This commit is contained in:
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright (C) 2022 WYATT GROUP
|
||||
// Please see the AUTHORS file for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:architecture_example/domain/usecases/photos/check_if_photo_is_in_favorites.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
part 'favorite_checker_state.dart';
|
||||
|
||||
class FavoriteCheckerCubit extends Cubit<FavoriteCheckerState> {
|
||||
final CheckIfPhotoIsInFavorites _checkIfPhotoIsInFavorites;
|
||||
|
||||
FavoriteCheckerCubit(this._checkIfPhotoIsInFavorites)
|
||||
: super(FavoriteCheckerInitial());
|
||||
|
||||
FutureOr<void> checkIfPhotoIsInFavorites(int photoId) async {
|
||||
final response = await _checkIfPhotoIsInFavorites.call(photoId);
|
||||
emit(
|
||||
response.fold(
|
||||
(value) => FavoriteCheckerSuccess(photoId, isFavorite: value),
|
||||
(error) => FavoriteCheckerFailure(error.toString()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
part of 'favorite_checker_cubit.dart';
|
||||
|
||||
abstract class FavoriteCheckerState extends Equatable {
|
||||
const FavoriteCheckerState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class FavoriteCheckerInitial extends FavoriteCheckerState {}
|
||||
|
||||
class FavoriteCheckerSuccess extends FavoriteCheckerState {
|
||||
final int photoId;
|
||||
final bool isFavorite;
|
||||
|
||||
const FavoriteCheckerSuccess(this.photoId, {required this.isFavorite});
|
||||
|
||||
@override
|
||||
List<Object> get props => [photoId, isFavorite];
|
||||
}
|
||||
|
||||
class FavoriteCheckerFailure extends FavoriteCheckerState {
|
||||
final String error;
|
||||
|
||||
const FavoriteCheckerFailure(this.error);
|
||||
|
||||
@override
|
||||
List<Object> get props => [error];
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
// Copyright (C) 2022 WYATT GROUP
|
||||
// Please see the AUTHORS file for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import 'package:architecture_example/core/enums/fetch_status.dart';
|
||||
import 'package:architecture_example/domain/entities/photo.dart';
|
||||
import 'package:architecture_example/domain/usecases/photos/open_album.dart';
|
||||
import 'package:architecture_example/domain/usecases/photos/params/query_parameters.dart';
|
||||
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:stream_transform/stream_transform.dart';
|
||||
|
||||
part 'photo_event.dart';
|
||||
part 'photo_state.dart';
|
||||
|
||||
const _photoLimit = 40;
|
||||
const throttleDuration = Duration(milliseconds: 100);
|
||||
|
||||
EventTransformer<E> throttleDroppable<E>(Duration duration) =>
|
||||
(events, mapper) => droppable<E>().call(events.throttle(duration), mapper);
|
||||
|
||||
class PhotoBloc extends Bloc<PhotoEvent, PhotoState> {
|
||||
final OpenAlbum _openAlbum;
|
||||
|
||||
PhotoBloc(this._openAlbum) : super(const PhotoState()) {
|
||||
on<PhotoFetched>(
|
||||
_onPhotoFetched,
|
||||
transformer: throttleDroppable(throttleDuration),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onPhotoFetched(
|
||||
PhotoFetched event,
|
||||
Emitter<PhotoState> emit,
|
||||
) async {
|
||||
if (state.hasReachedMax) {
|
||||
return;
|
||||
}
|
||||
if (state.status == FetchStatus.initial) {
|
||||
final photos = await _openAlbum.call(
|
||||
QueryParameters(
|
||||
0,
|
||||
_photoLimit,
|
||||
albumId: event.albumId,
|
||||
),
|
||||
);
|
||||
return emit(
|
||||
photos.fold(
|
||||
(value) => state.copyWith(
|
||||
status: FetchStatus.success,
|
||||
photos: value,
|
||||
hasReachedMax: false,
|
||||
),
|
||||
(error) => state.copyWith(
|
||||
status: FetchStatus.failure,
|
||||
photos: [],
|
||||
hasReachedMax: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final photos = await _openAlbum.call(
|
||||
QueryParameters(
|
||||
state.photos.length,
|
||||
_photoLimit,
|
||||
albumId: event.albumId,
|
||||
),
|
||||
);
|
||||
return emit(
|
||||
photos.fold(
|
||||
(value) {
|
||||
if (value.isEmpty) {
|
||||
return state.copyWith(
|
||||
hasReachedMax: true,
|
||||
);
|
||||
}
|
||||
return state.copyWith(
|
||||
status: FetchStatus.success,
|
||||
photos: List.of(state.photos)..addAll(value),
|
||||
hasReachedMax: false,
|
||||
);
|
||||
},
|
||||
(error) => state.copyWith(
|
||||
status: FetchStatus.failure,
|
||||
photos: List.of(state.photos),
|
||||
hasReachedMax: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright (C) 2022 WYATT GROUP
|
||||
// Please see the AUTHORS file for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
part of 'photo_bloc.dart';
|
||||
|
||||
abstract class PhotoEvent extends Equatable {
|
||||
const PhotoEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class PhotoFetched extends PhotoEvent {
|
||||
final int albumId;
|
||||
|
||||
const PhotoFetched(this.albumId);
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// Copyright (C) 2022 WYATT GROUP
|
||||
// Please see the AUTHORS file for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
part of 'photo_bloc.dart';
|
||||
|
||||
class PhotoState extends Equatable {
|
||||
const PhotoState({
|
||||
this.status = FetchStatus.initial,
|
||||
this.photos = const <Photo>[],
|
||||
this.hasReachedMax = false,
|
||||
});
|
||||
|
||||
final FetchStatus status;
|
||||
final List<Photo> photos;
|
||||
final bool hasReachedMax;
|
||||
|
||||
PhotoState copyWith({
|
||||
FetchStatus? status,
|
||||
List<Photo>? photos,
|
||||
bool? hasReachedMax,
|
||||
}) =>
|
||||
PhotoState(
|
||||
status: status ?? this.status,
|
||||
photos: photos ?? this.photos,
|
||||
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object> get props => [status, photos, hasReachedMax];
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (C) 2022 WYATT GROUP
|
||||
// Please see the AUTHORS file for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import 'package:architecture_example/presentation/features/photos/state_management/photos_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Photos extends StatelessWidget {
|
||||
const Photos({required this.albumId, super.key});
|
||||
|
||||
final int albumId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => PhotosScreen(albumId: albumId);
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// Copyright (C) 2022 WYATT GROUP
|
||||
// Please see the AUTHORS file for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import 'package:architecture_example/core/enums/fetch_status.dart';
|
||||
import 'package:architecture_example/domain/repositories/photo_repository.dart';
|
||||
import 'package:architecture_example/domain/usecases/photos/open_album.dart';
|
||||
import 'package:architecture_example/presentation/features/photos/blocs/photo/photo_bloc.dart';
|
||||
import 'package:architecture_example/presentation/features/photos/state_management/photos_wrapper_widget.dart';
|
||||
import 'package:architecture_example/presentation/features/photos/state_management/widgets/photos_grid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wyatt_bloc_helper/wyatt_bloc_helper.dart';
|
||||
|
||||
class PhotosScreen extends BlocScreen<PhotoBloc, PhotoEvent, PhotoState> {
|
||||
const PhotosScreen({required this.albumId, super.key});
|
||||
|
||||
final int albumId;
|
||||
|
||||
@override
|
||||
PhotoBloc create(BuildContext context) =>
|
||||
PhotoBloc(OpenAlbum(repo<PhotoRepository>(context)));
|
||||
|
||||
@override
|
||||
PhotoBloc init(BuildContext context, PhotoBloc bloc) =>
|
||||
bloc..add(PhotoFetched(albumId));
|
||||
|
||||
@override
|
||||
Widget onWrap(BuildContext context, Widget child) => PhotosWrapperWidget(
|
||||
child: child,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget onBuild(BuildContext context, PhotoState state) {
|
||||
switch (state.status) {
|
||||
case FetchStatus.initial:
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
case FetchStatus.failure:
|
||||
return const Center(
|
||||
child: Text('failed to fetch photos'),
|
||||
);
|
||||
case FetchStatus.success:
|
||||
return PhotosGrid(
|
||||
photos: state.photos,
|
||||
albumId: albumId,
|
||||
hasReachedMax: state.hasReachedMax,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Copyright (C) 2022 WYATT GROUP
|
||||
// Please see the AUTHORS file for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PhotosWrapperWidget extends StatelessWidget {
|
||||
const PhotosWrapperWidget({required this.child, super.key});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Photos'),
|
||||
),
|
||||
body: child,
|
||||
);
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
// Copyright (C) 2022 WYATT GROUP
|
||||
// Please see the AUTHORS file for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import 'package:architecture_example/domain/entities/photo.dart';
|
||||
import 'package:architecture_example/presentation/features/photos/blocs/photo/photo_bloc.dart';
|
||||
import 'package:architecture_example/presentation/features/photos/state_management/widgets/photos_grid_thumbnail.dart';
|
||||
import 'package:architecture_example/presentation/shared/widgets/bottom_loader.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class PhotosGrid extends StatefulWidget {
|
||||
const PhotosGrid({
|
||||
required this.photos,
|
||||
required this.albumId,
|
||||
required this.hasReachedMax,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<Photo> photos;
|
||||
final int albumId;
|
||||
final bool hasReachedMax;
|
||||
|
||||
@override
|
||||
State<PhotosGrid> createState() => _PhotosGridState();
|
||||
}
|
||||
|
||||
class _PhotosGridState extends State<PhotosGrid> {
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController
|
||||
..removeListener(_onScroll)
|
||||
..dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_isBottom) {
|
||||
context.read<PhotoBloc>().add(PhotoFetched(widget.albumId));
|
||||
}
|
||||
}
|
||||
|
||||
bool get _isBottom {
|
||||
if (!_scrollController.hasClients) {
|
||||
return false;
|
||||
}
|
||||
final maxScroll = _scrollController.position.maxScrollExtent;
|
||||
final currentScroll = _scrollController.offset;
|
||||
return currentScroll >= (maxScroll * 0.9);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisSpacing: 4,
|
||||
mainAxisSpacing: 4,
|
||||
crossAxisCount: 4,
|
||||
),
|
||||
itemCount: widget.hasReachedMax
|
||||
? widget.photos.length
|
||||
: widget.photos.length + 1,
|
||||
controller: _scrollController,
|
||||
itemBuilder: (context, index) => index >= widget.photos.length
|
||||
? const BottomLoader()
|
||||
: PhotosGridThumbnail(photo: widget.photos[index]),
|
||||
);
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
// Copyright (C) 2022 WYATT GROUP
|
||||
// Please see the AUTHORS file for details.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import 'package:architecture_example/domain/entities/photo.dart';
|
||||
import 'package:architecture_example/domain/repositories/photo_repository.dart';
|
||||
import 'package:architecture_example/domain/usecases/photos/check_if_photo_is_in_favorites.dart';
|
||||
import 'package:architecture_example/presentation/features/photo_details/photo_details.dart';
|
||||
import 'package:architecture_example/presentation/features/photos/blocs/favorite_checker/favorite_checker_cubit.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wyatt_bloc_helper/wyatt_bloc_helper.dart';
|
||||
|
||||
class PhotosGridThumbnail
|
||||
extends CubitScreen<FavoriteCheckerCubit, FavoriteCheckerState> {
|
||||
const PhotosGridThumbnail({required this.photo, super.key});
|
||||
|
||||
final Photo photo;
|
||||
|
||||
@override
|
||||
FavoriteCheckerCubit create(BuildContext context) => FavoriteCheckerCubit(
|
||||
CheckIfPhotoIsInFavorites(repo<PhotoRepository>(context)),
|
||||
);
|
||||
|
||||
@override
|
||||
FavoriteCheckerCubit init(BuildContext context, FavoriteCheckerCubit bloc) =>
|
||||
bloc..checkIfPhotoIsInFavorites(photo.id);
|
||||
|
||||
@override
|
||||
Widget onBuild(BuildContext context, FavoriteCheckerState state) =>
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => PhotoDetails(photoId: photo.id),
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.network(
|
||||
photo.thumbnailUrl,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
if (state is FavoriteCheckerSuccess && state.isFavorite) ...[
|
||||
const Positioned(
|
||||
bottom: 10, right: 10, child: Icon(Icons.favorite),)
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user