doc(arch): add fully featured example

This commit is contained in:
2022-11-08 20:09:18 -05:00
parent db67491876
commit 6602db215f
109 changed files with 4499 additions and 29 deletions
@@ -0,0 +1,25 @@
// 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/albums/state_management/albums_screen.dart';
import 'package:flutter/material.dart';
class Albums extends StatelessWidget {
const Albums({super.key});
@override
Widget build(BuildContext context) => const AlbumsScreen();
}
@@ -0,0 +1,94 @@
// 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/album.dart';
import 'package:architecture_example/domain/usecases/photos/params/query_parameters.dart';
import 'package:architecture_example/domain/usecases/photos/retrieve_all_albums.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 'album_event.dart';
part 'album_state.dart';
const _albumLimit = 20;
const throttleDuration = Duration(milliseconds: 100);
EventTransformer<E> throttleDroppable<E>(Duration duration) =>
(events, mapper) => droppable<E>().call(events.throttle(duration), mapper);
class AlbumBloc extends Bloc<AlbumEvent, AlbumState> {
final RetrieveAllAlbums _retrieveAllAlbums;
AlbumBloc(this._retrieveAllAlbums) : super(const AlbumState()) {
on<AlbumFetched>(
_onAlbumFetched,
transformer: throttleDroppable(throttleDuration),
);
}
Future<void> _onAlbumFetched(
AlbumFetched event,
Emitter<AlbumState> emit,
) async {
if (state.hasReachedMax) {
return;
}
if (state.status == FetchStatus.initial) {
final albums =
await _retrieveAllAlbums.call(QueryParameters(0, _albumLimit));
return emit(
albums.fold(
(value) => state.copyWith(
status: FetchStatus.success,
albums: value,
hasReachedMax: false,
),
(error) => state.copyWith(
status: FetchStatus.failure,
albums: [],
hasReachedMax: false,
),
),
);
}
final albums = await _retrieveAllAlbums
.call(QueryParameters(state.albums.length, _albumLimit));
return emit(
albums.fold(
(value) {
if (value.isEmpty) {
return state.copyWith(
hasReachedMax: true,
);
}
return state.copyWith(
status: FetchStatus.success,
albums: List.of(state.albums)..addAll(value),
hasReachedMax: false,
);
},
(error) => state.copyWith(
status: FetchStatus.failure,
albums: List.of(state.albums),
hasReachedMax: false,
),
),
);
}
}
@@ -0,0 +1,26 @@
// 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 'album_bloc.dart';
abstract class AlbumEvent extends Equatable {
const AlbumEvent();
@override
List<Object> get props => [];
}
class AlbumFetched extends AlbumEvent {}
@@ -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 'album_bloc.dart';
class AlbumState extends Equatable {
const AlbumState({
this.status = FetchStatus.initial,
this.albums = const <Album>[],
this.hasReachedMax = false,
});
final FetchStatus status;
final List<Album> albums;
final bool hasReachedMax;
AlbumState copyWith({
FetchStatus? status,
List<Album>? albums,
bool? hasReachedMax,
}) =>
AlbumState(
status: status ?? this.status,
albums: albums ?? this.albums,
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
);
@override
List<Object> get props => [status, albums, hasReachedMax];
}
@@ -0,0 +1,58 @@
// 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/retrieve_all_albums.dart';
import 'package:architecture_example/presentation/features/albums/blocs/album/album_bloc.dart';
import 'package:architecture_example/presentation/features/albums/state_management/albums_wrapper_widget.dart';
import 'package:architecture_example/presentation/features/albums/state_management/widgets/albums_list.dart';
import 'package:flutter/material.dart';
import 'package:wyatt_bloc_helper/wyatt_bloc_helper.dart';
class AlbumsScreen extends BlocScreen<AlbumBloc, AlbumEvent, AlbumState> {
const AlbumsScreen({super.key});
@override
AlbumBloc create(BuildContext context) =>
AlbumBloc(RetrieveAllAlbums(repo<PhotoRepository>(context)));
@override
AlbumBloc init(BuildContext context, AlbumBloc bloc) =>
bloc..add(AlbumFetched());
@override
Widget onWrap(BuildContext context, Widget child) => AlbumsWrapperWidget(
child: child,
);
@override
Widget onBuild(BuildContext context, AlbumState state) {
switch (state.status) {
case FetchStatus.initial:
return const Center(child: CircularProgressIndicator());
case FetchStatus.failure:
return const Center(
child: Text('failed to fetch albums'),
);
case FetchStatus.success:
return AlbumsList(
albums: state.albums,
hasReachedMax: state.hasReachedMax,
);
}
}
}
@@ -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 AlbumsWrapperWidget extends StatelessWidget {
const AlbumsWrapperWidget({required this.child, super.key});
final Widget child;
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Albums'),
),
body: child,
);
}
@@ -0,0 +1,77 @@
// 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/album.dart';
import 'package:architecture_example/presentation/features/albums/blocs/album/album_bloc.dart';
import 'package:architecture_example/presentation/features/albums/state_management/widgets/albums_list_item.dart';
import 'package:architecture_example/presentation/shared/widgets/bottom_loader.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AlbumsList extends StatefulWidget {
const AlbumsList(
{required this.albums, required this.hasReachedMax, super.key,});
final List<Album> albums;
final bool hasReachedMax;
@override
State<AlbumsList> createState() => _AlbumsListState();
}
class _AlbumsListState extends State<AlbumsList> {
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<AlbumBloc>().add(AlbumFetched());
}
}
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) => ListView.builder(
itemBuilder: (context, index) => index >= widget.albums.length
? const BottomLoader()
: AlbumsListItem(album: widget.albums[index]),
itemCount: widget.hasReachedMax
? widget.albums.length
: widget.albums.length + 1,
controller: _scrollController,
);
}
@@ -0,0 +1,37 @@
// 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/album.dart';
import 'package:architecture_example/presentation/features/photos/photos.dart';
import 'package:flutter/material.dart';
class AlbumsListItem extends StatelessWidget {
const AlbumsListItem({required this.album, super.key});
final Album album;
@override
Widget build(BuildContext context) => ListTile(
leading: Text('${album.id}'),
title: Text(album.title),
dense: true,
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => Photos(albumId: album.id),
),
),
);
}