Implemented the upload status snackbar on the home screen, created the [DistinctStreamTransformer] and refactored how screens decide when to show a snackbar
This commit is contained in:
parent
28a112779a
commit
10aabed07e
8 changed files with 186 additions and 68 deletions
|
|
@ -4,18 +4,18 @@ import 'dart:io';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
|
import '../mixins/upload_status_mixin.dart';
|
||||||
import '../models/event_model.dart';
|
import '../models/event_model.dart';
|
||||||
import '../models/task_model.dart';
|
import '../models/task_model.dart';
|
||||||
import '../models/user_model.dart';
|
import '../models/user_model.dart';
|
||||||
import '../resources/firestore_provider.dart';
|
import '../resources/firestore_provider.dart';
|
||||||
import '../resources/firebase_storage_provider.dart';
|
import '../resources/firebase_storage_provider.dart';
|
||||||
import '../services/auth_service.dart';
|
import '../services/auth_service.dart';
|
||||||
import '../services/upload_status_service.dart';
|
|
||||||
import '../utils.dart'
|
import '../utils.dart'
|
||||||
show kTaskListPriorityTransforemer, getImageThumbnailPath;
|
show kTaskListPriorityTransforemer, getImageThumbnailPath;
|
||||||
|
|
||||||
/// A business logic component that manages the state for an event screen.
|
/// A business logic component that manages the state for an event screen.
|
||||||
class EventBloc {
|
class EventBloc extends Object with UploadStatusMixin {
|
||||||
/// The name of the event being shown.
|
/// The name of the event being shown.
|
||||||
final String eventName;
|
final String eventName;
|
||||||
|
|
||||||
|
|
@ -28,9 +28,6 @@ class EventBloc {
|
||||||
/// An instace of the auth service.
|
/// An instace of the auth service.
|
||||||
final AuthService _auth = authService;
|
final AuthService _auth = authService;
|
||||||
|
|
||||||
/// An instance of the upload status service.
|
|
||||||
final UploadStatusService _uploadStatus = uploadStatusService;
|
|
||||||
|
|
||||||
/// A subject of list of task model.
|
/// A subject of list of task model.
|
||||||
final _tasks = BehaviorSubject<List<TaskModel>>();
|
final _tasks = BehaviorSubject<List<TaskModel>>();
|
||||||
|
|
||||||
|
|
@ -49,9 +46,6 @@ class EventBloc {
|
||||||
/// A subject of a cache that contains the image files.
|
/// A subject of a cache that contains the image files.
|
||||||
final _images = BehaviorSubject<Map<String, Future<File>>>();
|
final _images = BehaviorSubject<Map<String, Future<File>>>();
|
||||||
|
|
||||||
/// A subject of a flag that indicates if there is a snack bar showing.
|
|
||||||
final _snackBarStatus = BehaviorSubject<bool>(seedValue: false);
|
|
||||||
|
|
||||||
/// The event being managed by this bloc.
|
/// The event being managed by this bloc.
|
||||||
EventModel _event;
|
EventModel _event;
|
||||||
|
|
||||||
|
|
@ -74,13 +68,6 @@ class EventBloc {
|
||||||
/// An observable of a cache of the images files.
|
/// An observable of a cache of the images files.
|
||||||
Observable<Map<String, Future<File>>> get images => _images.stream;
|
Observable<Map<String, Future<File>>> get images => _images.stream;
|
||||||
|
|
||||||
/// An observable of a flag that indicates whether or not a snackBar is
|
|
||||||
/// currently showing.
|
|
||||||
ValueObservable<bool> get snackBarStatus => _snackBarStatus.stream;
|
|
||||||
|
|
||||||
/// An observable of the status of files being uploaded.
|
|
||||||
Observable<UploadStatus> get uploadStatus => _uploadStatus.status;
|
|
||||||
|
|
||||||
// Sinks getters.
|
// Sinks getters.
|
||||||
/// Starts the fetching process for an image given its path.
|
/// Starts the fetching process for an image given its path.
|
||||||
Function(String) get fetchImage => _imagesFetcher.sink.add;
|
Function(String) get fetchImage => _imagesFetcher.sink.add;
|
||||||
|
|
@ -88,12 +75,10 @@ class EventBloc {
|
||||||
/// Starts the fetching process for an image thumbail given its path.
|
/// Starts the fetching process for an image thumbail given its path.
|
||||||
Function(String) get fetchThumbnail => _imagesThumbnailsFetcher.sink.add;
|
Function(String) get fetchThumbnail => _imagesThumbnailsFetcher.sink.add;
|
||||||
|
|
||||||
/// Updates the snack bar status.
|
|
||||||
Function(bool) get updateSnackBarStatus => _snackBarStatus.sink.add;
|
|
||||||
|
|
||||||
EventBloc({
|
EventBloc({
|
||||||
@required this.eventName,
|
@required this.eventName,
|
||||||
}) {
|
}) {
|
||||||
|
initializeSnackBarStatus();
|
||||||
_ready = _initUserAndEvent();
|
_ready = _initUserAndEvent();
|
||||||
_imagesFetcher.transform(_imagesTransformer()).pipe(_images);
|
_imagesFetcher.transform(_imagesTransformer()).pipe(_images);
|
||||||
_imagesThumbnailsFetcher
|
_imagesThumbnailsFetcher
|
||||||
|
|
@ -159,8 +144,7 @@ class EventBloc {
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() async {
|
void dispose() async {
|
||||||
await _snackBarStatus.drain();
|
await disposeUploadStatusMixin();
|
||||||
_snackBarStatus.close();
|
|
||||||
await _imagesThumbnailsFetcher.drain();
|
await _imagesThumbnailsFetcher.drain();
|
||||||
_imagesThumbnailsFetcher.close();
|
_imagesThumbnailsFetcher.close();
|
||||||
await _imagesFetcher.drain();
|
await _imagesFetcher.drain();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
import '../utils.dart' show kTaskListPriorityTransforemer;
|
import '../utils.dart' show kTaskListPriorityTransforemer;
|
||||||
|
import '../mixins/upload_status_mixin.dart';
|
||||||
import '../models/task_model.dart';
|
import '../models/task_model.dart';
|
||||||
import '../resources/firestore_provider.dart';
|
import '../resources/firestore_provider.dart';
|
||||||
import '../services/auth_service.dart';
|
import '../services/auth_service.dart';
|
||||||
|
|
@ -10,7 +11,7 @@ import '../services/auth_service.dart';
|
||||||
export '../services/auth_service.dart' show FirebaseUser;
|
export '../services/auth_service.dart' show FirebaseUser;
|
||||||
|
|
||||||
/// A business logic component that manages the state of the home screen.
|
/// A business logic component that manages the state of the home screen.
|
||||||
class HomeBloc {
|
class HomeBloc extends Object with UploadStatusMixin {
|
||||||
/// An instance of the auth service.
|
/// An instance of the auth service.
|
||||||
final AuthService _auth = authService;
|
final AuthService _auth = authService;
|
||||||
|
|
||||||
|
|
@ -47,6 +48,10 @@ class HomeBloc {
|
||||||
/// An observable of the current logged in user.
|
/// An observable of the current logged in user.
|
||||||
Observable<FirebaseUser> get userStream => _auth.userStream;
|
Observable<FirebaseUser> get userStream => _auth.userStream;
|
||||||
|
|
||||||
|
HomeBloc() {
|
||||||
|
initializeSnackBarStatus();
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Include the priority in the filtering.
|
// TODO: Include the priority in the filtering.
|
||||||
/// Returns a stream transformer that filters the task with the text from
|
/// Returns a stream transformer that filters the task with the text from
|
||||||
/// the search box.
|
/// the search box.
|
||||||
|
|
@ -106,6 +111,7 @@ class HomeBloc {
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() async {
|
void dispose() async {
|
||||||
|
await disposeUploadStatusMixin();
|
||||||
await _searchBoxText.drain();
|
await _searchBoxText.drain();
|
||||||
_searchBoxText.close();
|
_searchBoxText.close();
|
||||||
await _tasks.drain();
|
await _tasks.drain();
|
||||||
|
|
|
||||||
40
lib/src/mixins/upload_status_mixin.dart
Normal file
40
lib/src/mixins/upload_status_mixin.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
|
import '../utils.dart';
|
||||||
|
import '../services/upload_status_service.dart';
|
||||||
|
|
||||||
|
mixin UploadStatusMixin {
|
||||||
|
/// An instance of the upload status service.
|
||||||
|
final UploadStatusService uploadStatusSer = uploadStatusService;
|
||||||
|
|
||||||
|
/// A subject of a flag that indicates if there is a snack bar showing.
|
||||||
|
final snackBarStatusSubject = BehaviorSubject<bool>(seedValue: false);
|
||||||
|
|
||||||
|
/// An observable of a flag that indicates whether or not a snackBar is
|
||||||
|
/// currently showing.
|
||||||
|
ValueObservable<bool> get snackBarStatus => snackBarStatusSubject.stream;
|
||||||
|
|
||||||
|
/// Updates the snack bar status.
|
||||||
|
Function(bool) get updateSnackBarStatus => snackBarStatusSubject.sink.add;
|
||||||
|
|
||||||
|
/// An observable of the status of files being uploaded.
|
||||||
|
Observable<UploadStatus> get uploadStatus => uploadStatusSer.status;
|
||||||
|
|
||||||
|
Future<void> disposeUploadStatusMixin() async {
|
||||||
|
await snackBarStatusSubject.drain();
|
||||||
|
snackBarStatusSubject.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void initializeSnackBarStatus() {
|
||||||
|
uploadStatusService.status
|
||||||
|
.transform(StreamTransformer<UploadStatus, bool>.fromHandlers(
|
||||||
|
handleData: (value, sink) {
|
||||||
|
sink.add(value.numberOfFiles != 0);
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.transform(DistinctStreamTransformer())
|
||||||
|
.pipe(snackBarStatusSubject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -41,12 +41,25 @@ class _EventScreenState extends State<EventScreen>
|
||||||
/// Needed for showing snackbars.
|
/// Needed for showing snackbars.
|
||||||
BuildContext _scaffoldContext;
|
BuildContext _scaffoldContext;
|
||||||
|
|
||||||
|
StreamSubscription _snackBarStatusSubscription;
|
||||||
|
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
bloc = EventBloc(eventName: widget.eventName);
|
bloc = EventBloc(eventName: widget.eventName);
|
||||||
bloc.fetchTasks();
|
bloc.fetchTasks();
|
||||||
bloc.fetchImagesPaths();
|
bloc.fetchImagesPaths();
|
||||||
_tabController = TabController(vsync: this, length: 2);
|
_tabController = TabController(vsync: this, length: 2);
|
||||||
|
_snackBarStatusSubscription = bloc.snackBarStatus.listen((bool visible) {
|
||||||
|
if (visible) {
|
||||||
|
showUploadStatusSnackBar(
|
||||||
|
_scaffoldContext,
|
||||||
|
bloc.uploadStatus,
|
||||||
|
bloc.updateSnackBarStatus,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Scaffold.of(_scaffoldContext).hideCurrentSnackBar();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -164,7 +177,8 @@ class _EventScreenState extends State<EventScreen>
|
||||||
Widget buildAddPictureButton() {
|
Widget buildAddPictureButton() {
|
||||||
return GradientTouchableContainer(
|
return GradientTouchableContainer(
|
||||||
radius: 8,
|
radius: 8,
|
||||||
onTap: onAddPicturePressed,
|
onTap: () =>
|
||||||
|
Navigator.of(context).pushNamed('newImage/${bloc.eventName}'),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.camera_alt,
|
Icons.camera_alt,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
|
@ -190,19 +204,8 @@ class _EventScreenState extends State<EventScreen>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use a block provider instead of passing callbacks
|
|
||||||
Future<void> onAddPicturePressed() async {
|
|
||||||
await Navigator.of(context).pushNamed('newImage/${bloc.eventName}');
|
|
||||||
if (bloc.snackBarStatus.value == false) {
|
|
||||||
showUploadStatusSnackBar(
|
|
||||||
_scaffoldContext,
|
|
||||||
bloc.uploadStatus,
|
|
||||||
bloc.updateSnackBarStatus,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_snackBarStatusSubscription.cancel();
|
||||||
bloc.dispose();
|
bloc.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
|
||||||
|
import '../utils.dart' show showUploadStatusSnackBar;
|
||||||
import '../models/task_model.dart';
|
import '../models/task_model.dart';
|
||||||
import '../blocs/home_bloc.dart';
|
import '../blocs/home_bloc.dart';
|
||||||
import '../widgets/home_app_bar.dart';
|
import '../widgets/home_app_bar.dart';
|
||||||
|
|
@ -20,10 +23,28 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
/// An instance of the bloc for this screen.
|
/// An instance of the bloc for this screen.
|
||||||
final HomeBloc bloc = HomeBloc();
|
final HomeBloc bloc = HomeBloc();
|
||||||
|
|
||||||
|
/// The context of the scaffold being shown.
|
||||||
|
///
|
||||||
|
/// Needed for showing snackbars.
|
||||||
|
BuildContext _scaffoldContext;
|
||||||
|
|
||||||
|
StreamSubscription _snackBarStatusSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
bloc.fetchTasks();
|
bloc.fetchTasks();
|
||||||
|
_snackBarStatusSubscription = bloc.snackBarStatus.listen((bool visible) {
|
||||||
|
if (visible) {
|
||||||
|
showUploadStatusSnackBar(
|
||||||
|
_scaffoldContext,
|
||||||
|
bloc.uploadStatus,
|
||||||
|
bloc.updateSnackBarStatus,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Scaffold.of(_scaffoldContext).hideCurrentSnackBar();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -48,38 +69,43 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
child: Icon(FontAwesomeIcons.plus),
|
child: Icon(FontAwesomeIcons.plus),
|
||||||
backgroundColor: Color(0xFF707070),
|
backgroundColor: Color(0xFF707070),
|
||||||
onPressed: () => _showDialog(context),
|
onPressed: () => Navigator.of(context).push(NewItemDialogRoute()),
|
||||||
),
|
),
|
||||||
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
||||||
appBar: HomeAppBar(
|
appBar: HomeAppBar(
|
||||||
avatarUrl: userAvatarUrl,
|
avatarUrl: userAvatarUrl,
|
||||||
subtitle: 'Hello $userDisplayName!',
|
subtitle: 'Hello $userDisplayName!',
|
||||||
),
|
),
|
||||||
body: StreamBuilder(
|
body: Builder(
|
||||||
stream: bloc.userTasks,
|
builder: (BuildContext context) {
|
||||||
builder:
|
_scaffoldContext = context;
|
||||||
(BuildContext context, AsyncSnapshot<List<TaskModel>> snap) {
|
return StreamBuilder(
|
||||||
if (!snap.hasData) {
|
stream: bloc.userTasks,
|
||||||
return Center(
|
builder: (BuildContext context,
|
||||||
child: LoadingIndicator(),
|
AsyncSnapshot<List<TaskModel>> snap) {
|
||||||
);
|
if (!snap.hasData) {
|
||||||
}
|
return Center(
|
||||||
return Stack(
|
child: LoadingIndicator(),
|
||||||
overflow: Overflow.visible,
|
);
|
||||||
children: <Widget>[
|
}
|
||||||
_buildTasksList(snap.data),
|
return Stack(
|
||||||
// This container is needed to make it seem like the search box is
|
overflow: Overflow.visible,
|
||||||
// part of the app bar.
|
children: <Widget>[
|
||||||
Container(
|
_buildTasksList(snap.data),
|
||||||
height: _searchBoxHeight / 2,
|
// This container is needed to make it seem like the search box is
|
||||||
width: double.infinity,
|
// part of the app bar.
|
||||||
color: Theme.of(context).cardColor,
|
Container(
|
||||||
),
|
height: _searchBoxHeight / 2,
|
||||||
SearchBox(
|
width: double.infinity,
|
||||||
onChanged: bloc.updateSearchBoxText,
|
color: Theme.of(context).cardColor,
|
||||||
height: 50.0,
|
),
|
||||||
),
|
SearchBox(
|
||||||
],
|
onChanged: bloc.updateSearchBoxText,
|
||||||
|
height: 50.0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -88,10 +114,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showDialog(BuildContext context) {
|
|
||||||
Navigator.of(context).push(NewItemDialogRoute());
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTasksList(List<TaskModel> tasks) {
|
Widget _buildTasksList(List<TaskModel> tasks) {
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: EdgeInsets.only(top: _searchBoxHeight + 15),
|
padding: EdgeInsets.only(top: _searchBoxHeight + 15),
|
||||||
|
|
@ -118,6 +140,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_snackBarStatusSubscription.cancel();
|
||||||
bloc.dispose();
|
bloc.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
@ -97,14 +98,12 @@ void showUploadStatusSnackBar(
|
||||||
}
|
}
|
||||||
print('Number of files: ${snap.data.numberOfFiles}');
|
print('Number of files: ${snap.data.numberOfFiles}');
|
||||||
if (snap.data.numberOfFiles == 0) {
|
if (snap.data.numberOfFiles == 0) {
|
||||||
onSuccessfullyClosed(false);
|
return Text('Done!');
|
||||||
scaffoldState.hideCurrentSnackBar();
|
|
||||||
return Text('');
|
|
||||||
}
|
}
|
||||||
return Row(
|
return Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Spacer(),
|
Spacer(),
|
||||||
Text('${snap.data.numberOfFiles} pending files'),
|
Text('${snap.data.numberOfFiles} pending'),
|
||||||
Spacer(flex: 5),
|
Spacer(flex: 5),
|
||||||
Text(snap.data.percentage + '%'),
|
Text(snap.data.percentage + '%'),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
|
|
@ -114,3 +113,49 @@ void showUploadStatusSnackBar(
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A transformer that only emits a value if it is different than the last one
|
||||||
|
/// emitted.
|
||||||
|
///
|
||||||
|
/// The [==] operator is to check for inequality, be sure that the objects
|
||||||
|
/// passing through the stream behave correctly with theses operator.
|
||||||
|
class DistinctStreamTransformer<T> extends StreamTransformerBase<T, T> {
|
||||||
|
final StreamTransformer<T, T> transformer;
|
||||||
|
|
||||||
|
DistinctStreamTransformer() : transformer = _buildTransformer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<T> bind(Stream<T> stream) => transformer.bind(stream);
|
||||||
|
|
||||||
|
static StreamTransformer<T, T> _buildTransformer<T>() {
|
||||||
|
return StreamTransformer<T, T>((Stream<T> input, bool cancelOnError) {
|
||||||
|
T last;
|
||||||
|
StreamController<T> controller;
|
||||||
|
StreamSubscription<T> subscription;
|
||||||
|
|
||||||
|
controller = StreamController<T>(
|
||||||
|
sync: true,
|
||||||
|
onListen: () {
|
||||||
|
subscription = input.listen(
|
||||||
|
(T value) {
|
||||||
|
if (!(last == value)) {
|
||||||
|
last = value;
|
||||||
|
controller.add(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: controller.addError,
|
||||||
|
onDone: controller.close,
|
||||||
|
cancelOnError: cancelOnError,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onPause: ([Future<dynamic> resumeSignal]) =>
|
||||||
|
subscription.pause(resumeSignal),
|
||||||
|
onResume: () => subscription.resume(),
|
||||||
|
onCancel: () {
|
||||||
|
return subscription.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
return controller.stream.listen(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
|
|
||||||
15
test/src/utils_test.dart
Normal file
15
test/src/utils_test.dart
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:do_more/src/utils.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
main() {
|
||||||
|
group('DistinctStreamTransformer', () {
|
||||||
|
test('should only emit non repeated elements', () {
|
||||||
|
final stream = Stream.fromIterable([1, 1, 1, 2, 2, 2, 1, 1, 1, 1]);
|
||||||
|
final transformedStream = stream.transform(DistinctStreamTransformer());
|
||||||
|
|
||||||
|
expect(transformedStream.toList(), completion(equals([1, 2, 1])));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue