diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..90c7212 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,47 @@ +name: Release + +on: + workflow_dispatch: + release: + types: [published] + +jobs: + test: + name: Test and Analyze + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup Flutter + uses: subosito/flutter-action@v1 + with: + channel: 'stable' + + - name: Download pub dependencies + run: flutter pub get + + - name: Check formatting + run: flutter format --set-exit-if-changed --dry-run . + + - name: Run analyzer + run: flutter analyze + + - name: Run tests + run: flutter test + publish: + name: Publish to pub.dev + if: success() + needs: test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: flutter_bloc_patterns + uses: k-paxian/dart-package-publisher@master + with: + accessToken: ${{ secrets.OAUTH_ACCESS_TOKEN }} + refreshToken: ${{ secrets.OAUTH_REFRESH_TOKEN }} + format: true + skipTests: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b3e6c9d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Flutter analyze and tests + +on: + pull_request: + branches: + - 'master' + tags-ignore: + - 'v*' + +jobs: + on-pull-request: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: subosito/flutter-action@v1 + with: + channel: 'stable' + + - name: Download pub dependencies + run: flutter pub get + + - name: Check formatting + run: flutter format --set-exit-if-changed --dry-run . + + - name: Run analyzer + run: flutter analyze + + - name: Run tests + run: flutter test diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce2aea..e38ae4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# Changelog + +## [0.9.0] * Breaking Changes * + +* Migrating to `bloc` 7.0.0 and `flutter_bloc` 7.0.1, +* Migrating to `null-safety`. + ## [0.8.0] * Breaking Changes * * Migrating to `bloc` 6.1.1 and `flutter_bloc` 6.1.1. diff --git a/README.md b/README.md index 9a5f6d7..c647aa5 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Where: ## Dart version -- Dart 2: >= 2.6.0 +- Dart 2: >= 2.12.0 ## Author - [Karol Lisiewicz](https://github.com/klisiewicz) diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 9a7b2ed..4fda472 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -32,9 +32,6 @@ android { main.java.srcDirs += 'src/main/kotlin' } - lintOptions { - disable 'InvalidPackage' - } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). @@ -43,7 +40,7 @@ android { targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } buildTypes { @@ -53,6 +50,9 @@ android { signingConfig signingConfigs.debug } } + lint { + disable 'InvalidPackage' + } } flutter { @@ -62,6 +62,6 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' } diff --git a/example/android/build.gradle b/example/android/build.gradle index b7faad8..d7006c2 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.2.71' + ext.kotlin_version = '1.3.40' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' + classpath 'com.android.tools.build:gradle:7.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 1441b1d..2324ab5 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,3 +1,5 @@ org.gradle.jvmargs=-Xmx1536M android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 2819f02..595fb86 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip diff --git a/example/lib/src/album/model/album.dart b/example/lib/src/album/model/album.dart index 5e9d9db..f836f95 100644 --- a/example/lib/src/album/model/album.dart +++ b/example/lib/src/album/model/album.dart @@ -3,17 +3,17 @@ import 'package:flutter/foundation.dart'; @immutable class Album { final int id; - final String title; + final String? title; const Album({ - @required this.id, + required this.id, this.title, - }) : assert(id != null); + }); factory Album.fromJson(Map json) { return Album( id: json['id'] as int, - title: json['title'] as String, + title: json['title'] as String?, ); } } diff --git a/example/lib/src/album/model/photo.dart b/example/lib/src/album/model/photo.dart index 4e359e6..0c8a7f5 100644 --- a/example/lib/src/album/model/photo.dart +++ b/example/lib/src/album/model/photo.dart @@ -5,13 +5,13 @@ class Photo { final String thumbnailUrl; Photo({ - this.id, - this.title, - this.url, - this.thumbnailUrl, + required this.id, + required this.title, + required this.url, + required this.thumbnailUrl, }); - factory Photo.fromJson(dynamic json) { + factory Photo.fromJson(Map json) { return Photo( id: json['id'] as int, title: json['title'] as String, diff --git a/example/lib/src/album/model/photo_repository.dart b/example/lib/src/album/model/photo_repository.dart index 6199bc5..e1cac66 100644 --- a/example/lib/src/album/model/photo_repository.dart +++ b/example/lib/src/album/model/photo_repository.dart @@ -3,36 +3,45 @@ import 'dart:io'; import 'package:example/src/album/model/album.dart'; import 'package:example/src/album/model/photo.dart'; -import 'package:example/src/common/url.dart'; import 'package:flutter_bloc_patterns/page.dart'; import 'package:flutter_bloc_patterns/paged_filter_list.dart'; import 'package:http/http.dart' as http; class PagedFilterPhotoRepository implements PagedListFilterRepository { - static const _photosUrl = '$baseUrl/photos'; - @override - Future> getAll(Page page) => getBy(page, null); + Future> getAll(Page page) async { + final uri = _buildUri(page, null); + return _getPhotosFrom(uri); + } @override Future> getBy(Page page, Album album) async { - final response = await http.get(_buildUrl(page, album)); + final uri = _buildUri(page, album); + return _getPhotosFrom(uri); + } + Future> _getPhotosFrom(Uri uri) async { + final response = await http.get(uri); if (response.statusCode != HttpStatus.ok) { throw Exception('Failed to load photos'); } - final dynamic postsJson = json.decode(response.body); return (postsJson is List) - ? postsJson.map((photo) => Photo.fromJson(photo)).toList() + ? postsJson.map((photo) => Photo.fromJson(photo as Map)).toList() : []; } - String _buildUrl(Page page, Album album) { - final pageQuery = '_start=${page.offset}&_limit=${page.size}'; - final userQuery = (album != null) ? 'albumId=${album.id}' : null; - final query = pageQuery + (userQuery != null ? '&$userQuery' : ''); - return '$_photosUrl/?$query'; + static Uri _buildUri(Page page, Album? album) { + return Uri( + scheme: 'http', + host: 'jsonplaceholder.typicode.com', + path: 'photos', + queryParameters: { + '_start': '${page.offset}', + '_limit': '${page.size}', + if (album != null) 'albumId': '${album.id}' + }, + ); } } diff --git a/example/lib/src/album/ui/photos_list_empty.dart b/example/lib/src/album/ui/photos_list_empty.dart index b4a6d07..3293361 100644 --- a/example/lib/src/album/ui/photos_list_empty.dart +++ b/example/lib/src/album/ui/photos_list_empty.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; class PhotosListEmpty extends StatelessWidget { - const PhotosListEmpty({Key key}) : super(key: key); + const PhotosListEmpty({Key? key}) : super(key: key); @override - Widget build(BuildContext context) => const Center( - child: Text('No photos found'), - ); + Widget build(BuildContext context) { + return const Center( + child: Text('No photos found'), + ); + } } diff --git a/example/lib/src/album/ui/photos_list_paged.dart b/example/lib/src/album/ui/photos_list_paged.dart index f4c8102..156e405 100644 --- a/example/lib/src/album/ui/photos_list_paged.dart +++ b/example/lib/src/album/ui/photos_list_paged.dart @@ -10,8 +10,8 @@ class PhotosListPaged extends StatelessWidget { const PhotosListPaged( this.page, { - Key key, - @required this.onLoadNextPage, + Key? key, + required this.onLoadNextPage, }) : super(key: key); @override @@ -30,7 +30,10 @@ class PhotosListPaged extends StatelessWidget { class _PhotoGridItem extends StatelessWidget { final Photo photo; - const _PhotoGridItem(this.photo, {Key key}) : super(key: key); + const _PhotoGridItem( + this.photo, { + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { diff --git a/example/lib/src/common/error_message.dart b/example/lib/src/common/error_message.dart index 6d27a5a..3e8a0c3 100644 --- a/example/lib/src/common/error_message.dart +++ b/example/lib/src/common/error_message.dart @@ -1,13 +1,17 @@ import 'package:flutter/widgets.dart'; class ErrorMessage extends StatelessWidget { - final dynamic error; + final Object error; const ErrorMessage({ - Key key, - this.error, + required this.error, + Key? key, }) : super(key: key); @override - Widget build(BuildContext context) => Center(child: Text(error.toString())); + Widget build(BuildContext context) { + return Center( + child: Text(error.toString()), + ); + } } diff --git a/example/lib/src/common/loading_indicator.dart b/example/lib/src/common/loading_indicator.dart index c4b6f94..6368882 100644 --- a/example/lib/src/common/loading_indicator.dart +++ b/example/lib/src/common/loading_indicator.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; class LoadingIndicator extends StatelessWidget { - const LoadingIndicator({Key key}) : super(key: key); + const LoadingIndicator({Key? key}) : super(key: key); @override - Widget build(BuildContext context) => const Center( - child: CircularProgressIndicator(), - ); + Widget build(BuildContext context) { + return const Center( + child: CircularProgressIndicator(), + ); + } } diff --git a/example/lib/src/common/loading_page_indicator.dart b/example/lib/src/common/loading_page_indicator.dart index 0f8c996..40655d0 100644 --- a/example/lib/src/common/loading_page_indicator.dart +++ b/example/lib/src/common/loading_page_indicator.dart @@ -1,17 +1,19 @@ import 'package:flutter/material.dart'; class LoadingPageIndicator extends StatelessWidget { - const LoadingPageIndicator({Key key}) : super(key: key); + const LoadingPageIndicator({Key? key}) : super(key: key); @override - Widget build(BuildContext context) => Container( - padding: const EdgeInsets.symmetric(vertical: 8), - child: const Center( - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 1.5), - ), + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 1.5), ), - ); + ), + ); + } } diff --git a/example/lib/src/common/url.dart b/example/lib/src/common/url.dart deleted file mode 100644 index 2f26906..0000000 --- a/example/lib/src/common/url.dart +++ /dev/null @@ -1 +0,0 @@ -const baseUrl = 'https://jsonplaceholder.typicode.com'; \ No newline at end of file diff --git a/example/lib/src/list_app.dart b/example/lib/src/list_app.dart index 34222ad..9ecd4da 100644 --- a/example/lib/src/list_app.dart +++ b/example/lib/src/list_app.dart @@ -5,7 +5,6 @@ import 'package:example/src/post/model/post_repository.dart'; import 'package:example/src/post/ui/posts_list.dart'; import 'package:example/src/post/ui/posts_list_empty.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc_patterns/base_list.dart'; import 'package:flutter_bloc_patterns/view.dart'; @@ -32,7 +31,7 @@ class PostsPage extends StatefulWidget { } class _PostsPageState extends State { - ListBloc listBloc; + late ListBloc listBloc; @override void initState() { @@ -45,11 +44,11 @@ class _PostsPageState extends State { return Scaffold( appBar: AppBar(title: const Text('Posts')), body: ViewStateBuilder, ListBloc>( - cubit: listBloc, + bloc: listBloc, onLoading: (context) => const LoadingIndicator(), - onSuccess: (context, List posts) => + onSuccess: (context, posts) => PostsList(posts, onRefresh: _refreshPosts), - onRefreshing: (context, List posts) => + onRefreshing: (context, posts) => PostsList(posts, onRefresh: _refreshPosts), onEmpty: (context) => const PostsListEmpty(), onError: (context, error) => ErrorMessage(error: error), diff --git a/example/lib/src/list_details_app.dart b/example/lib/src/list_details_app.dart index 65e46dd..a5032e7 100644 --- a/example/lib/src/list_details_app.dart +++ b/example/lib/src/list_details_app.dart @@ -31,7 +31,7 @@ class _PostsPage extends StatefulWidget { } class _PostsPageState extends State<_PostsPage> { - ListBloc listBloc; + late ListBloc listBloc; @override void initState() { @@ -44,7 +44,7 @@ class _PostsPageState extends State<_PostsPage> { return Scaffold( appBar: AppBar(title: const Text('Posts')), body: ViewStateBuilder, ListBloc>( - cubit: listBloc, + bloc: listBloc, onLoading: (context) => const LoadingIndicator(), onSuccess: (context, posts) => PostsList( posts, @@ -70,14 +70,17 @@ class _PostsPageState extends State<_PostsPage> { class _PostDetailPage extends StatefulWidget { final int postId; - const _PostDetailPage(this.postId, {Key key}) : super(key: key); + const _PostDetailPage( + this.postId, { + Key? key, + }) : super(key: key); @override _PostDetailPageState createState() => _PostDetailPageState(); } class _PostDetailPageState extends State<_PostDetailPage> { - DetailsBloc detailsBloc; + late DetailsBloc detailsBloc; @override void initState() { @@ -90,19 +93,22 @@ class _PostDetailPageState extends State<_PostDetailPage> { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Post')), - body: ViewStateBuilder>( - cubit: detailsBloc, - onLoading: (context) => const LoadingIndicator(), - onSuccess: (context, post) => _PostDetailsContent(post), + body: ViewStateListener( + bloc: detailsBloc, onEmpty: _showSnackbarAndPopPage, - onError: (context, error) => ErrorMessage(error: error), + child: ViewStateBuilder>( + bloc: detailsBloc, + onLoading: (context) => const LoadingIndicator(), + onSuccess: (context, post) => _PostDetailsContent(post), + onError: (context, error) => ErrorMessage(error: error), + ), ), ); } - Widget _showSnackbarAndPopPage(BuildContext context) { - WidgetsBinding.instance.addPostFrameCallback((_) { - Scaffold.of(context) + void _showSnackbarAndPopPage(BuildContext context) { + WidgetsBinding.instance?.addPostFrameCallback((_) { + ScaffoldMessenger.of(context) .showSnackBar( const SnackBar( content: Text('Post not found!'), @@ -110,11 +116,8 @@ class _PostDetailPageState extends State<_PostDetailPage> { ), ) .closed - .then((reason) { - Navigator.pop(context); - }); + .then((reason) => Navigator.pop(context)); }); - return Container(); } @override @@ -127,7 +130,10 @@ class _PostDetailPageState extends State<_PostDetailPage> { class _PostDetailsContent extends StatelessWidget { final PostDetails post; - const _PostDetailsContent(this.post, {Key key}) : super(key: key); + const _PostDetailsContent( + this.post, { + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -156,6 +162,7 @@ class _Route { static const String post = '/post'; } +// ignore: avoid_classes_with_only_static_members class _Router { static Route generateRoute(RouteSettings settings) { switch (settings.name) { @@ -172,7 +179,7 @@ class _Router { create: (_) => DetailsBloc( PostDetailsRepository(), ), - child: _PostDetailPage(settings.arguments as int), + child: _PostDetailPage(settings.arguments! as int), ), ); default: diff --git a/example/lib/src/list_filter_app.dart b/example/lib/src/list_filter_app.dart index d0fe4db..b3dd6ea 100644 --- a/example/lib/src/list_filter_app.dart +++ b/example/lib/src/list_filter_app.dart @@ -36,7 +36,7 @@ class _PostsPage extends StatefulWidget { } class _PostsPageState extends State<_PostsPage> { - FilterListBloc listBloc; + late FilterListBloc listBloc; _Posts selectedPosts = _Posts.all; @override @@ -59,7 +59,7 @@ class _PostsPageState extends State<_PostsPage> { Widget _buildBody() { return ViewStateBuilder, FilterListBloc>( - cubit: listBloc, + bloc: listBloc, onLoading: (context) => const LoadingIndicator(), onSuccess: (context, posts) => PostsList(posts, onRefresh: _refreshPosts), onRefreshing: (context, posts) => diff --git a/example/lib/src/list_paged_app.dart b/example/lib/src/list_paged_app.dart index 4e5c0fb..7008a47 100644 --- a/example/lib/src/list_paged_app.dart +++ b/example/lib/src/list_paged_app.dart @@ -31,7 +31,7 @@ class _PostsPage extends StatefulWidget { } class _PostsPageState extends State<_PostsPage> { - PagedListBloc _listBloc; + late PagedListBloc _listBloc; @override void initState() { @@ -45,7 +45,7 @@ class _PostsPageState extends State<_PostsPage> { return Scaffold( appBar: AppBar(title: const Text('Posts')), body: ViewStateBuilder, PagedListBloc>( - cubit: _listBloc, + bloc: _listBloc, onLoading: (context) => const LoadingIndicator(), onSuccess: (context, page) => PostsListPaged( page, diff --git a/example/lib/src/list_paged_filter_app.dart b/example/lib/src/list_paged_filter_app.dart index ef7ee72..4b3e186 100644 --- a/example/lib/src/list_paged_filter_app.dart +++ b/example/lib/src/list_paged_filter_app.dart @@ -35,16 +35,13 @@ class _PhotosPage extends StatefulWidget { class _PhotosPageState extends State<_PhotosPage> { final _myAlbum = const Album(id: 1); - PagedListFilterBloc _photosBloc; + late PagedListFilterBloc _photosBloc; @override void initState() { super.initState(); _photosBloc = BlocProvider.of>(context) - ..loadFirstPage( - pageSize: 12, - filter: _myAlbum, - ); + ..loadFirstPage(pageSize: 12, filter: _myAlbum); } @override @@ -52,7 +49,7 @@ class _PhotosPageState extends State<_PhotosPage> { return Scaffold( appBar: AppBar(title: const Text('Photos')), body: ViewStateBuilder, PagedListFilterBloc>( - cubit: _photosBloc, + bloc: _photosBloc, onLoading: (context) => const LoadingIndicator(), onSuccess: (context, page) => PhotosListPaged( page, diff --git a/example/lib/src/post/model/post.dart b/example/lib/src/post/model/post.dart index a5e1465..c281aa9 100644 --- a/example/lib/src/post/model/post.dart +++ b/example/lib/src/post/model/post.dart @@ -3,11 +3,11 @@ class Post { final String title; Post({ - this.id, - this.title, + required this.id, + required this.title, }); - factory Post.fromJson(dynamic json) { + factory Post.fromJson(Map json) { return Post( id: json['id'] as int, title: json['title'] as String, diff --git a/example/lib/src/post/model/post_details.dart b/example/lib/src/post/model/post_details.dart index 62d368e..7df4421 100644 --- a/example/lib/src/post/model/post_details.dart +++ b/example/lib/src/post/model/post_details.dart @@ -4,15 +4,12 @@ class PostDetails extends Post { final String body; PostDetails({ - int id, - String title, - this.body, - }) : super( - id: id, - title: title, - ); + required int id, + required String title, + required this.body, + }) : super(id: id, title: title); - factory PostDetails.fromJson(dynamic json) { + factory PostDetails.fromJson(Map json) { return PostDetails( id: json['id'] as int, title: json['title'] as String, diff --git a/example/lib/src/post/model/post_repository.dart b/example/lib/src/post/model/post_repository.dart index 639f74c..b623666 100644 --- a/example/lib/src/post/model/post_repository.dart +++ b/example/lib/src/post/model/post_repository.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:io'; -import 'package:example/src/common/url.dart'; import 'package:example/src/post/model/post.dart'; import 'package:example/src/post/model/post_details.dart'; import 'package:example/src/user/model/user.dart'; @@ -12,59 +11,68 @@ import 'package:flutter_bloc_patterns/page.dart'; import 'package:flutter_bloc_patterns/paged_list.dart'; import 'package:http/http.dart' as http; -const _postsUrl = '$baseUrl/posts'; - class PostListRepository implements ListRepository { @override - Future> getAll() => _getPostsFromUrl(_postsUrl); + Future> getAll() => _getPostsFromUrl(); } class FilterPostRepository implements FilterListRepository { @override - Future> getAll() => _getPostsFromUrl(_postsUrl); + Future> getAll() => _getPostsFromUrl(); @override Future> getBy(User user) { - if (user == null || user.id == null) { - return _getPostsFromUrl(_postsUrl); - } else { - return _getPostsFromUrl('$_postsUrl?userId=${user.id}'); - } + return _getPostsFromUrl(query: {'userId': user.id}); } } class PagedPostRepository implements PagedListRepository { @override - Future> getAll(Page page) => - _getPostsFromUrl('$_postsUrl?_start=${page.offset}&_limit=${page.size}'); + Future> getAll(Page page) { + return _getPostsFromUrl( + query: { + '_start': '${page.offset}', + '_limit': '${page.size}', + }, + ); + } } class PostDetailsRepository implements DetailsRepository { @override - Future getById(int id) async { - final response = await http.get('$_postsUrl/$id'); - + Future getById(int id) async { + final uri = Uri( + scheme: 'http', + host: 'jsonplaceholder.typicode.com', + path: 'posts/$id', + ); + final response = await http.get(uri); if (response.statusCode == HttpStatus.notFound) { return null; } else if (response.statusCode != HttpStatus.ok) { throw Exception('Failed to load post with id $id'); } - final dynamic postJson = json.decode(response.body); + final Map postJson = json.decode(response.body) as Map; return PostDetails.fromJson(postJson); } } -Future> _getPostsFromUrl(String url) async { - final response = await http.get(url); - +Future> _getPostsFromUrl({ + Map? query, +}) async { + final uri = Uri( + scheme: 'http', + host: 'jsonplaceholder.typicode.com', + path: 'posts', + queryParameters: query, + ); + final response = await http.get(uri); if (response.statusCode != HttpStatus.ok) { throw Exception('Failed to load post'); } - final dynamic postsJson = json.decode(response.body); if (postsJson is List) { - final List posts = - postsJson.map((post) => Post.fromJson(post)).toList(); + final posts = postsJson.map((post) => Post.fromJson(post as Map)).toList(); // Shuffle the list to achieve refresh impression return posts..shuffle(); } else { diff --git a/example/lib/src/post/ui/posts_list.dart b/example/lib/src/post/ui/posts_list.dart index 4f1d61d..afd2b34 100644 --- a/example/lib/src/post/ui/posts_list.dart +++ b/example/lib/src/post/ui/posts_list.dart @@ -4,12 +4,12 @@ import 'package:flutter_bloc_patterns/view.dart'; class PostsList extends StatelessWidget { final List posts; - final VoidCallback onRefresh; - final ValueSetter onPostSelected; + final VoidCallback? onRefresh; + final ValueSetter? onPostSelected; const PostsList( this.posts, { - Key key, + Key? key, this.onRefresh, this.onPostSelected, }) : super(key: key); @@ -32,12 +32,12 @@ class PostsList extends StatelessWidget { class PostListItem extends StatelessWidget { final Post post; - final ValueSetter onPostSelected; + final ValueSetter? onPostSelected; const PostListItem( this.post, { this.onPostSelected, - Key key, + Key? key, }) : super(key: key); @override diff --git a/example/lib/src/post/ui/posts_list_empty.dart b/example/lib/src/post/ui/posts_list_empty.dart index 7a1f282..50dc1cf 100644 --- a/example/lib/src/post/ui/posts_list_empty.dart +++ b/example/lib/src/post/ui/posts_list_empty.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; class PostsListEmpty extends StatelessWidget { - const PostsListEmpty({Key key}) : super(key: key); + const PostsListEmpty({Key? key}) : super(key: key); @override - Widget build(BuildContext context) => - const Center(child: Text('No posts found')); + Widget build(BuildContext context) { + return const Center( + child: Text('No posts found'), + ); + } } diff --git a/example/lib/src/post/ui/posts_list_paged.dart b/example/lib/src/post/ui/posts_list_paged.dart index 1934d0f..5c47d2e 100644 --- a/example/lib/src/post/ui/posts_list_paged.dart +++ b/example/lib/src/post/ui/posts_list_paged.dart @@ -11,8 +11,8 @@ class PostsListPaged extends StatelessWidget { const PostsListPaged( this.page, { - Key key, - @required this.onLoadNextPage, + Key? key, + required this.onLoadNextPage, }) : super(key: key); @override diff --git a/example/lib/src/user/model/user.dart b/example/lib/src/user/model/user.dart index 3c079b5..b40905d 100644 --- a/example/lib/src/user/model/user.dart +++ b/example/lib/src/user/model/user.dart @@ -1,9 +1,9 @@ import 'package:equatable/equatable.dart'; -class User extends Equatable { +class User with EquatableMixin { final String id; - const User(this.id) : assert(id != null); + const User(this.id); @override List get props => [id]; diff --git a/example/pubspec.lock b/example/pubspec.lock index b270197..b9db62c 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,89 +7,89 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0-nullsafety.1" + version: "2.8.2" bloc: dependency: transitive description: name: bloc url: "https://pub.dartlang.org" source: hosted - version: "6.1.1" + version: "7.0.0" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.3" + version: "1.2.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.3.1" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0-nullsafety.3" + version: "1.15.0" cupertino_icons: dependency: "direct main" description: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "1.0.4" equatable: - dependency: transitive + dependency: "direct main" description: name: equatable url: "https://pub.dartlang.org" source: hosted - version: "1.2.5" + version: "2.0.3" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" flutter_bloc: - dependency: transitive + dependency: "direct main" description: name: flutter_bloc url: "https://pub.dartlang.org" source: hosted - version: "6.1.1" + version: "7.2.0" flutter_bloc_patterns: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.8.0" + version: "0.9.0" flutter_test: dependency: "direct dev" description: flutter @@ -101,70 +101,63 @@ packages: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.0+4" + version: "0.13.4" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.3" + version: "4.0.0" infinite_widgets: dependency: "direct main" description: name: infinite_widgets url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "2.0.0" lint: dependency: "direct main" description: name: lint url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.8.2" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10-nullsafety.1" + version: "0.12.11" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.7.0" nested: dependency: transitive description: name: nested url: "https://pub.dartlang.org" source: hosted - version: "0.0.4" + version: "1.0.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.1" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0+1" + version: "1.8.0" provider: dependency: transitive description: name: provider url: "https://pub.dartlang.org" source: hosted - version: "4.3.2+2" + version: "6.0.2" sky_engine: dependency: transitive description: flutter @@ -176,56 +169,56 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.2" + version: "1.8.1" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety.1" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19-nullsafety.2" + version: "0.4.3" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.3.0" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.3" + version: "2.1.1" sdks: - dart: ">=2.10.0-110 <2.11.0" + dart: ">=2.15.0-7.0.dev <3.0.0" flutter: ">=1.16.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 4b4d8a4..9003a8d 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,19 +1,22 @@ name: example description: BLoC pattern samples. +publish_to: 'none' version: 1.0.0+1 environment: - sdk: ">=2.6.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: + cupertino_icons: ^1.0.4 + equatable: ^2.0.3 flutter: sdk: flutter - http: ^0.12.0+4 + flutter_bloc: ^7.0.1 flutter_bloc_patterns: path: ../ - cupertino_icons: ^0.1.3 - infinite_widgets: ^1.0.1 - lint: ^1.1.1 + http: ^0.13.4 + infinite_widgets: ^2.0.0 + lint: ^1.8.2 dev_dependencies: flutter_test: diff --git a/lib/src/details/details_bloc.dart b/lib/src/details/details_bloc.dart index f7e9432..a4044ea 100644 --- a/lib/src/details/details_bloc.dart +++ b/lib/src/details/details_bloc.dart @@ -1,7 +1,5 @@ import 'package:bloc/bloc.dart'; import 'package:flutter_bloc_patterns/details.dart'; -import 'package:flutter_bloc_patterns/src/details/details_events.dart'; -import 'package:flutter_bloc_patterns/src/details/details_repository.dart'; import 'package:flutter_bloc_patterns/src/view/view_state.dart'; import 'package:flutter_bloc_patterns/src/view/view_state_builder.dart'; @@ -16,12 +14,10 @@ import 'package:flutter_bloc_patterns/src/view/view_state_builder.dart'; class DetailsBloc extends Bloc { final DetailsRepository _repository; - DetailsBloc(this._repository) - : assert(_repository != null), - super(const Initial()); + DetailsBloc(this._repository) : super(const Initial()); /// Loads an element with given [id]. - void loadElement([I id]) => add(LoadDetails(id)); + void loadElement(I id) => add(LoadDetails(id)); @override Stream mapEventToState(DetailsEvent event) async* { @@ -35,8 +31,6 @@ class DetailsBloc extends Bloc { yield const Loading(); final element = await _repository.getById(id); yield element != null ? Success(element) : const Empty(); - } on ElementNotFoundException { - yield const Empty(); } catch (e) { yield Failure(e); } diff --git a/lib/src/details/details_events.dart b/lib/src/details/details_events.dart index 85e45fc..5b371ca 100644 --- a/lib/src/details/details_events.dart +++ b/lib/src/details/details_events.dart @@ -13,10 +13,10 @@ abstract class DetailsEvent extends Equatable { class LoadDetails extends DetailsEvent { final I id; - const LoadDetails([this.id]); + const LoadDetails(this.id); @override - List get props => [id]; + List get props => [id]; @override String toString() => 'LoadDetails: $id'; diff --git a/lib/src/details/details_repository.dart b/lib/src/details/details_repository.dart index 2865914..977fc09 100644 --- a/lib/src/details/details_repository.dart +++ b/lib/src/details/details_repository.dart @@ -4,19 +4,6 @@ /// [I] - the element's id type. abstract class DetailsRepository { /// Retrieves an element with given id. When there's no element matching the - /// given [id] null should be returned or [ElementNotFoundException] should - /// be thrown. - Future getById(I id); -} - -/// Exception indicating that element with [id] was not found in the -/// repository. -class ElementNotFoundException implements Exception { - final I id; - - ElementNotFoundException(this.id); - - @override - String toString() => - 'ElementNotFoundException: Unable to find element with id $id.'; + /// given [id] `null` should be returned. + Future getById(I id); } diff --git a/lib/src/list/base/list_bloc.dart b/lib/src/list/base/list_bloc.dart index 0ed643f..06e91c3 100644 --- a/lib/src/list/base/list_bloc.dart +++ b/lib/src/list/base/list_bloc.dart @@ -15,8 +15,7 @@ import 'package:flutter_bloc_patterns/src/view/view_state_builder.dart'; /// [T] - type of list items. class ListBloc extends FilterListBloc { ListBloc(ListRepository repository) - : assert(repository != null), - super(_FilterRepositoryAdapter(repository)); + : super(_FilterRepositoryAdapter(repository)); } class _FilterRepositoryAdapter extends FilterListRepository { diff --git a/lib/src/list/base/list_events.dart b/lib/src/list/base/list_events.dart index 4469c8f..c1d830d 100644 --- a/lib/src/list/base/list_events.dart +++ b/lib/src/list/base/list_events.dart @@ -11,12 +11,12 @@ abstract class ListEvent extends Equatable { /// /// [F] - the filter type. class LoadList extends ListEvent { - final F filter; + final F? filter; const LoadList([this.filter]); @override - List get props => [filter]; + List get props => [filter]; @override String toString() => 'LoadList: $filter'; @@ -26,12 +26,12 @@ class LoadList extends ListEvent { /// /// [F] - the filter type. class RefreshList extends ListEvent { - final F filter; + final F? filter; const RefreshList([this.filter]); @override - List get props => [filter]; + List get props => [filter]; @override String toString() => 'RefreshList: $filter'; diff --git a/lib/src/list/filter/filter_list_bloc.dart b/lib/src/list/filter/filter_list_bloc.dart index 63e9121..2ee60e0 100644 --- a/lib/src/list/filter/filter_list_bloc.dart +++ b/lib/src/list/filter/filter_list_bloc.dart @@ -18,21 +18,20 @@ import 'package:flutter_bloc_patterns/src/view/view_state_builder.dart'; /// [F] - the type of filter. class FilterListBloc extends Bloc { final FilterListRepository _repository; - F _filter; + F? _filter; FilterListBloc(FilterListRepository repository) - : assert(repository != null), - _repository = repository, + : _repository = repository, super(const Initial()); - F get filter => _filter; + F? get filter => _filter; /// Loads elements using the given [filter]. /// /// It's most suitable for initial data fetch or for retry action when /// the first fetch fails. It can also be used when [filter] changes when a /// full reload is required. - void loadElements({F filter}) => add(LoadList(filter)); + void loadElements({F? filter}) => add(LoadList(filter)); /// Refreshes elements using the given [filter]. /// @@ -41,7 +40,7 @@ class FilterListBloc extends Bloc { /// /// It can be used when [filter] changes and there's no need for displaying a /// loading indicator. - void refreshElements({F filter}) => add(RefreshList(filter)); + void refreshElements({F? filter}) => add(RefreshList(filter)); @override Stream mapEventToState(ListEvent event) async* { @@ -55,12 +54,12 @@ class FilterListBloc extends Bloc { bool _isRefreshPossible(ListEvent event) => state is Success || state is Empty; - Stream _mapLoadList(F filter) async* { + Stream _mapLoadList(F? filter) async* { yield const Loading(); yield* _getListState(filter); } - Stream _mapRefreshList(F filter) async* { + Stream _mapRefreshList(F? filter) async* { final elements = _getCurrentStateElements(); yield Refreshing(elements); yield* _getListState(filter); @@ -69,7 +68,7 @@ class FilterListBloc extends Bloc { List _getCurrentStateElements() => (state is Success>) ? (state as Success>).data : []; - Stream _getListState(F filter) async* { + Stream _getListState(F? filter) async* { try { final List elements = await _getElementsFromRepository(filter); yield elements.isNotEmpty @@ -82,7 +81,7 @@ class FilterListBloc extends Bloc { } } - Future> _getElementsFromRepository(F filter) { + Future> _getElementsFromRepository(F? filter) { if (filter != null) { return _repository.getBy(filter); } else { diff --git a/lib/src/list/paged/page.dart b/lib/src/list/paged/page.dart index 7b9fb1b..ecc11d9 100644 --- a/lib/src/list/paged/page.dart +++ b/lib/src/list/paged/page.dart @@ -12,13 +12,13 @@ class Page extends Equatable { /// [size] - the size of the page to be returned. const Page({ this.number = 0, - this.size, + required this.size, }) : assert(number >= 0, 'Page index must not be less than zero'), assert(size >= 1, 'Page size must not be less than one'); /// Creates first page. /// [size] - the size of the page to be returned. - const Page.first({int size}) : this(number: 0, size: size); + const Page.first({required int size}) : this(number: 0, size: size); /// Returns the offset to be taken according to page and page size. int get offset => size * number; diff --git a/lib/src/list/paged/paged_list.dart b/lib/src/list/paged/paged_list.dart index d5b2b40..0daa9ec 100644 --- a/lib/src/list/paged/paged_list.dart +++ b/lib/src/list/paged/paged_list.dart @@ -16,11 +16,10 @@ class PagedList extends Equatable { /// [elements] - list of elements, cannot be null or empty, /// [hasReachedMax] - flag informing if all elements has already been fetched. /// True if there are more pages, false otherwise. - PagedList(List elements, {this.hasReachedMax = false}) - : assert( - elements != null && elements.isNotEmpty, - 'Elements cannot be empty', - ), + PagedList( + List elements, { + this.hasReachedMax = false, + }) : assert(elements.isNotEmpty, 'Elements cannot be empty'), elements = UnmodifiableListView(elements); bool get hasMoreElements => !hasReachedMax; diff --git a/lib/src/list/paged/paged_list_bloc.dart b/lib/src/list/paged/paged_list_bloc.dart index d7b1776..9e99e23 100644 --- a/lib/src/list/paged/paged_list_bloc.dart +++ b/lib/src/list/paged/paged_list_bloc.dart @@ -1,6 +1,5 @@ import 'package:flutter_bloc_patterns/paged_filter_list.dart'; import 'package:flutter_bloc_patterns/src/list/paged/page.dart'; -import 'package:flutter_bloc_patterns/src/list/paged/paged_list_filter_bloc.dart'; import 'package:flutter_bloc_patterns/src/list/paged/paged_list_repository.dart'; import 'package:flutter_bloc_patterns/src/view/view_state_builder.dart'; @@ -14,8 +13,7 @@ import 'package:flutter_bloc_patterns/src/view/view_state_builder.dart'; /// [T] - the type of list elements. class PagedListBloc extends PagedListFilterBloc { PagedListBloc(PagedListRepository repository) - : assert(repository != null), - super(_PagedListRepositoryAdapter(repository)); + : super(_PagedListRepositoryAdapter(repository)); } class _PagedListRepositoryAdapter diff --git a/lib/src/list/paged/paged_list_events.dart b/lib/src/list/paged/paged_list_events.dart index fc6e6e7..9110a76 100644 --- a/lib/src/list/paged/paged_list_events.dart +++ b/lib/src/list/paged/paged_list_events.dart @@ -13,12 +13,15 @@ abstract class PagedListEvent extends Equatable { /// [F] - the filter type. class LoadPage extends PagedListEvent { final Page page; - final F filter; + final F? filter; - const LoadPage(this.page, {this.filter}) : assert(page != null); + const LoadPage( + this.page, { + this.filter, + }); @override - List get props => [page, filter]; + List get props => [page, filter]; @override String toString() => 'LoadPage: $page ${filter ?? ''}'; diff --git a/lib/src/list/paged/paged_list_filter_bloc.dart b/lib/src/list/paged/paged_list_filter_bloc.dart index 90a14e9..161a171 100644 --- a/lib/src/list/paged/paged_list_filter_bloc.dart +++ b/lib/src/list/paged/paged_list_filter_bloc.dart @@ -1,7 +1,6 @@ import 'package:bloc/bloc.dart'; import 'package:flutter_bloc_patterns/paged_filter_list.dart'; import 'package:flutter_bloc_patterns/src/list/paged/page.dart'; -import 'package:flutter_bloc_patterns/src/list/paged/paged_list.dart'; import 'package:flutter_bloc_patterns/src/list/paged/paged_list_events.dart'; import 'package:flutter_bloc_patterns/src/list/paged/paged_list_repository.dart'; import 'package:flutter_bloc_patterns/src/view/view_state.dart'; @@ -22,37 +21,39 @@ class PagedListFilterBloc extends Bloc { static const defaultPageSize = 10; final PagedListFilterRepository _repository; - F _filter; + F? _filter; PagedListFilterBloc(PagedListFilterRepository repository) - : assert(repository != null), - _repository = repository, + : _repository = repository, super(const Initial()); List get _currentElements => (state is Success>) ? (state as Success>).data.elements : []; - Page _page; + Page? _page; - F get filter => _filter; + F? get filter => _filter; /// Loads elements using the given [filter] and [pageSize]. When no size /// is given [_defaultPageSize] is used. /// /// It's most suitable for initial data fetch or for retry action when /// the first fetch fails. - void loadFirstPage({int pageSize = defaultPageSize, F filter}) { + void loadFirstPage({ + int pageSize = defaultPageSize, + F? filter, + }) { _page = Page.first(size: pageSize); _filter = filter; - add(LoadPage(_page, filter: _filter)); + add(LoadPage(_page!, filter: _filter)); } /// Loads next page. When no page has been loaded before the first one is /// loaded with the default page size [_defaultPageSize]. void loadNextPage() { _page = _page?.next() ?? const Page.first(size: defaultPageSize); - add(LoadPage(_page, filter: _filter)); + add(LoadPage(_page!, filter: _filter)); } @override @@ -62,10 +63,10 @@ class PagedListFilterBloc extends Bloc { } } - Stream _mapLoadPage(Page page, F filter) async* { + Stream _mapLoadPage(Page page, F? filter) async* { try { yield* _emitLoadingWhenFirstPage(page); - final List pageElements = await _repository.getBy(page, filter); + final pageElements = await _getElements(page, filter); yield* (pageElements.isEmpty) ? _emitEmptyPageLoaded(page) : _emitNextPageLoaded(page, pageElements); @@ -76,6 +77,12 @@ class PagedListFilterBloc extends Bloc { } } + Future> _getElements(Page page, F? filter) async { + return filter != null + ? _repository.getBy(page, filter) + : _repository.getAll(page); + } + Stream _emitLoadingWhenFirstPage(Page page) async* { if (page.isFirst) { yield const Loading(); diff --git a/lib/src/view/refresh_view.dart b/lib/src/view/refresh_view.dart index 12733f8..03351bf 100644 --- a/lib/src/view/refresh_view.dart +++ b/lib/src/view/refresh_view.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; /// A widget that wraps a [widget] with a [RefreshIndicator] designed to /// be used with BLoC pattern. @@ -10,17 +9,17 @@ import 'package:flutter/widgets.dart'; /// executed. The indicator remains visible until the widget is rebuilt. class RefreshView extends StatefulWidget { final Widget child; - final VoidCallback onRefresh; + final VoidCallback? onRefresh; final double displacement; - final Color color; - final Color backgroundColor; + final Color? color; + final Color? backgroundColor; final ScrollNotificationPredicate notificationPredicate; - final String semanticsLabel; - final String semanticsValue; + final String? semanticsLabel; + final String? semanticsValue; const RefreshView({ - Key key, - @required this.child, + Key? key, + required this.child, this.onRefresh, this.backgroundColor, this.color, @@ -28,17 +27,14 @@ class RefreshView extends StatefulWidget { this.notificationPredicate = defaultScrollNotificationPredicate, this.semanticsLabel, this.semanticsValue, - }) : assert(child != null), - assert(displacement != null), - assert(notificationPredicate != null), - super(key: key); + }) : super(key: key); @override _RefreshViewState createState() => _RefreshViewState(); } class _RefreshViewState extends State { - Completer _refreshCompleter = Completer(); + Completer? _refreshCompleter; @override Widget build(BuildContext context) { @@ -57,8 +53,8 @@ class _RefreshViewState extends State { ); } - Future _refresh() { + Future _refresh() async { widget.onRefresh?.call(); - return _refreshCompleter.future; + return _refreshCompleter?.future; } } diff --git a/lib/src/view/view_state.dart b/lib/src/view/view_state.dart index 984e674..023cd99 100644 --- a/lib/src/view/view_state.dart +++ b/lib/src/view/view_state.dart @@ -7,7 +7,7 @@ abstract class ViewState extends Equatable { const ViewState(); @override - List get props => []; + List get props => []; } /// The initial view state. @@ -35,7 +35,7 @@ class Refreshing extends ViewState { const Refreshing(this.data); @override - List get props => [data]; + List get props => [data]; @override String toString() => 'Refreshing: $data'; @@ -57,7 +57,7 @@ class Success extends ViewState { const Success(this.data) : assert(data != null); @override - List get props => [data]; + List get props => [data]; @override String toString() => 'Success: $data'; @@ -66,12 +66,12 @@ class Success extends ViewState { /// State indicating that loading or refreshing has failed. It contains an /// exact [error] that has occurred. class Failure extends ViewState { - final dynamic error; + final Object error; - const Failure(this.error) : assert(error != null); + const Failure(this.error); @override - List get props => [error]; + List get props => [error]; @override String toString() => 'Failure: $error'; diff --git a/lib/src/view/view_state_builder.dart b/lib/src/view/view_state_builder.dart index ee3a495..c8515c4 100644 --- a/lib/src/view/view_state_builder.dart +++ b/lib/src/view/view_state_builder.dart @@ -1,4 +1,3 @@ -import 'package:bloc/bloc.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc_patterns/src/view/view_state.dart'; @@ -25,7 +24,7 @@ typedef EmptyBuilder = Widget Function(BuildContext context); /// which may allow a view to react differently on different errors. typedef ErrorBuilder = Widget Function( BuildContext context, - dynamic error, + Object error, ); /// Signature for the [buildWhen] function which takes the previous [ViewState] @@ -47,22 +46,22 @@ typedef ViewStateBuilderCondition = bool Function( /// [onError] builder function for an error state. /// /// [T] - the type of elements, -/// [C] - the type of cubit. -class ViewStateBuilder> - extends BlocBuilder { +/// [B] - the type of bloc. +class ViewStateBuilder> + extends BlocBuilder { ViewStateBuilder({ - Key key, - C cubit, - InitialBuilder onReady, - LoadingBuilder onLoading, - RefreshingBuilder onRefreshing, - SuccessBuilder onSuccess, - EmptyBuilder onEmpty, - ErrorBuilder onError, - ViewStateBuilderCondition buildWhen, + Key? key, + B? bloc, + InitialBuilder? onReady, + LoadingBuilder? onLoading, + RefreshingBuilder? onRefreshing, + SuccessBuilder? onSuccess, + EmptyBuilder? onEmpty, + ErrorBuilder? onError, + ViewStateBuilderCondition? buildWhen, }) : super( key: key, - cubit: cubit, + bloc: bloc, buildWhen: buildWhen, builder: (BuildContext context, ViewState state) { if (state is Initial) { diff --git a/lib/src/view/view_state_listener.dart b/lib/src/view/view_state_listener.dart index 8229879..4b1d79a 100644 --- a/lib/src/view/view_state_listener.dart +++ b/lib/src/view/view_state_listener.dart @@ -18,10 +18,7 @@ typedef EmptyCallback = void Function(BuildContext context); /// Callback function for an error. It contains an [error] that has caused /// which may allow a view to react differently on different errors. -typedef ErrorCallback = void Function( - BuildContext context, - dynamic error, -); +typedef ErrorCallback = void Function(BuildContext context, Object error); /// Signature for the [listenWhen] function which takes the previous [ViewState] /// and the current [ViewState] and is responsible for returning a [bool] which @@ -46,22 +43,22 @@ typedef ViewStateListenerCondition = bool Function( /// [onError] callback function for an error state. /// /// [T] - the type of elements, -/// [C] - the type of cubit. -class ViewStateListener> - extends BlocListener { +/// [B] - the type of bloc. +class ViewStateListener> + extends BlocListener { ViewStateListener({ - Key key, - C cubit, - ViewStateListenerCondition listenWhen, - LoadingCallback onLoading, - RefreshingCallback onRefreshing, - SuccessCallback onSuccess, - EmptyCallback onEmpty, - ErrorCallback onError, - Widget child, + Key? key, + B? bloc, + ViewStateListenerCondition? listenWhen, + LoadingCallback? onLoading, + RefreshingCallback? onRefreshing, + SuccessCallback? onSuccess, + EmptyCallback? onEmpty, + ErrorCallback? onError, + Widget? child, }) : super( key: key, - cubit: cubit, + bloc: bloc, listenWhen: listenWhen, child: child, listener: (BuildContext context, ViewState state) { diff --git a/pubspec.yaml b/pubspec.yaml index 2066192..d1b9a6e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,21 +1,22 @@ name: flutter_bloc_patterns description: A set of most common BLoC use cases build on top of flutter_bloc library. -version: 0.8.0 +version: 0.9.0 homepage: https://github.com/klisiewicz/flutter-bloc-patterns environment: - sdk: ">=2.6.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' dependencies: + bloc: ^7.0.0 + collection: ^1.15.0 + equatable: ^2.0.3 flutter: sdk: flutter - equatable: ^1.2.5 - bloc: ^6.1.1 - flutter_bloc: ^6.1.1 + flutter_bloc: ^7.0.1 dev_dependencies: + bloc_test: ^8.0.0 flutter_test: sdk: flutter - bloc_test: ^7.1.0 - lint: ^1.3.0 - mockito: ^4.1.4 + lint: ^1.8.2 + mocktail: ^0.2.0 diff --git a/test/src/details/details_bloc_test.dart b/test/src/details/details_bloc_test.dart index 032c157..e2ccd70 100644 --- a/test/src/details/details_bloc_test.dart +++ b/test/src/details/details_bloc_test.dart @@ -6,12 +6,14 @@ import 'package:flutter_test/flutter_test.dart'; import 'details_repository_mock.dart'; void main() { - DetailsBloc detailsBloc; + late DetailsBloc detailsBloc; - Future thenExpectStates(Iterable states) async => expect( - detailsBloc, - emitsInOrder(states), - ); + Future thenExpectStates(Iterable states) async { + expect( + detailsBloc.stream, + emitsInOrder(states), + ); + } group('repository with elements', () { const _existingId = 1; @@ -64,7 +66,7 @@ void main() { final _exception = Exception('Oh no!'); final _error = Error(); - void givenFailingRepository(error) => + void givenFailingRepository(Object error) => detailsBloc = DetailsBloc(FailingDetailsRepository(error)); void whenLoadingElement() => detailsBloc.loadElement(0); @@ -78,17 +80,6 @@ void main() { ]); }); - test( - 'should emit [$Loading, $Empty] when $ElementNotFoundException is thrown', - () { - givenFailingRepository(ElementNotFoundException(0)); - whenLoadingElement(); - thenExpectStates(const [ - Loading(), - Empty(), - ]); - }); - test('should emit [$Loading, $Failure] when an error is thrown', () { givenFailingRepository(_error); whenLoadingElement(); diff --git a/test/src/details/details_bloc_view_state_test.dart b/test/src/details/details_bloc_view_state_test.dart index 413934e..05f57c7 100644 --- a/test/src/details/details_bloc_view_state_test.dart +++ b/test/src/details/details_bloc_view_state_test.dart @@ -8,7 +8,7 @@ import 'details_repository_mock.dart'; void main() { const someData = 0; - DetailsBloc bloc; + late DetailsBloc bloc; setUp(() { bloc = DetailsBloc(InMemoryDetailsRepository()); diff --git a/test/src/details/details_repository_mock.dart b/test/src/details/details_repository_mock.dart index 4ddcd29..a386402 100644 --- a/test/src/details/details_repository_mock.dart +++ b/test/src/details/details_repository_mock.dart @@ -8,14 +8,14 @@ class InMemoryDetailsRepository extends DetailsRepository { InMemoryDetailsRepository([this.elements = const {}]); @override - Future getById(I id) => Future.delayed(_delay, () => elements[id]); + Future getById(I id) => Future.delayed(_delay, () => elements[id]); } class FailingDetailsRepository extends DetailsRepository { - final dynamic error; + final Object error; FailingDetailsRepository(this.error); @override - Future getById(I id) async => Future.delayed(_delay, () => throw error); + Future getById(I id) async => Future.delayed(_delay, () => throw error); } diff --git a/test/src/list/base/list_bloc_test.dart b/test/src/list/base/list_bloc_test.dart index dc5eda8..1dcca0e 100644 --- a/test/src/list/base/list_bloc_test.dart +++ b/test/src/list/base/list_bloc_test.dart @@ -2,15 +2,15 @@ import 'package:flutter_bloc_patterns/src/list/base/list_bloc.dart'; import 'package:flutter_bloc_patterns/src/list/base/list_repository.dart'; import 'package:flutter_bloc_patterns/src/view/view_state.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart' as mock; +import 'package:mocktail/mocktail.dart' as m; import '../../util/bdd.dart'; import '../../util/bloc_state_assertion.dart'; import 'list_repository_mock.dart'; void main() { - ListBloc bloc; - ListRepository repository; + late ListBloc bloc; + late ListRepository repository; setUp(() { repository = ListRepositoryMock(); @@ -18,13 +18,12 @@ void main() { }); void emptyRepository() => - mock.when(repository.getAll()).thenAnswer((_) async => []); + m.when(repository.getAll).thenAnswer((_) async => []); void repositoryWithElements() => - mock.when(repository.getAll()).thenAnswer((_) async => _someData); + m.when(repository.getAll).thenAnswer((_) async => _someData); - void failingRepository() => - mock.when(repository.getAll()).thenThrow(_exception); + void failingRepository() => m.when(repository.getAll).thenThrow(_exception); void loadingElements() => bloc.loadElements(); @@ -45,15 +44,19 @@ void main() { () { given(repositoryWithElements); when(loadingElements); - then(() => - withBloc(bloc).expectStates(const [Loading(), Success(_someData)])); + then( + () => + withBloc(bloc).expectStates(const [Loading(), Success(_someData)]), + ); }); test('should emit [$Loading, $Failure(error)] when loading data fails', () { given(failingRepository); when(loadingElements); - then(() => - withBloc(bloc).expectStates([const Loading(), Failure(_exception)])); + then( + () => + withBloc(bloc).expectStates([const Loading(), Failure(_exception)]), + ); }); }); diff --git a/test/src/list/base/list_bloc_view_state_test.dart b/test/src/list/base/list_bloc_view_state_test.dart index 3eedb33..8830153 100644 --- a/test/src/list/base/list_bloc_view_state_test.dart +++ b/test/src/list/base/list_bloc_view_state_test.dart @@ -7,7 +7,7 @@ import '../../view/view_state_matchers.dart'; import '../filter/filter_list_repository_mock.dart'; void main() { - ListBloc bloc; + late ListBloc bloc; setUp(() { bloc = ListBloc(InMemoryFilterRepository()); diff --git a/test/src/list/base/list_repository_mock.dart b/test/src/list/base/list_repository_mock.dart index 2a7e885..bf3ba1c 100644 --- a/test/src/list/base/list_repository_mock.dart +++ b/test/src/list/base/list_repository_mock.dart @@ -1,4 +1,4 @@ import 'package:flutter_bloc_patterns/src/list/base/list_repository.dart'; -import 'package:mockito/mockito.dart'; +import 'package:mocktail/mocktail.dart'; class ListRepositoryMock extends Mock implements ListRepository {} diff --git a/test/src/list/filter/filter_list_bloc_test.dart b/test/src/list/filter/filter_list_bloc_test.dart index 47ee048..02e9d70 100644 --- a/test/src/list/filter/filter_list_bloc_test.dart +++ b/test/src/list/filter/filter_list_bloc_test.dart @@ -12,11 +12,12 @@ void main() { const _matchingFilter = 1; const _matchingElements = [1]; - FilterListBloc bloc; + late FilterListBloc bloc; - void loadingElements({int filter}) => bloc.loadElements(filter: filter); + void loadingElements({int? filter}) => bloc.loadElements(filter: filter); - void refreshingElements({int filter}) => bloc.refreshElements(filter: filter); + void refreshingElements({int? filter}) => + bloc.refreshElements(filter: filter); group('empty repository', () { setUp(() { @@ -24,7 +25,7 @@ void main() { }); test('should emit [$Loading, $Empty] state when no filter is set', () { - when(() => loadingElements()); + when(loadingElements); then(() => withBloc(bloc).expectStates(const [Loading(), Empty()])); }); @@ -50,7 +51,7 @@ void main() { test( 'should emit [$Loading, $Success] with all elements when no filter is set', () { - when(() => loadingElements()); + when(loadingElements); then( () => withBloc(bloc).expectStates(const [Loading(), Success(_someData)]), diff --git a/test/src/list/filter/filter_list_repository_mock.dart b/test/src/list/filter/filter_list_repository_mock.dart index 27ec029..9765e45 100644 --- a/test/src/list/filter/filter_list_repository_mock.dart +++ b/test/src/list/filter/filter_list_repository_mock.dart @@ -11,7 +11,7 @@ class InMemoryFilterRepository extends FilterListRepository { Future> getAll() async => Future.delayed(_delay, () => elements); @override - Future> getBy(F filter) { + Future> getBy(F? filter) { return Future.delayed( _delay, () => elements.where((item) => item == filter).toList(), @@ -20,7 +20,7 @@ class InMemoryFilterRepository extends FilterListRepository { } class FailingFilterRepository extends FilterListRepository { - final dynamic error; + final Object error; FailingFilterRepository(this.error); @@ -28,5 +28,5 @@ class FailingFilterRepository extends FilterListRepository { Future> getAll() => Future.delayed(_delay, () => throw error); @override - Future> getBy(F filter) => Future.delayed(_delay, () => throw error); + Future> getBy(F? filter) => Future.delayed(_delay, () => throw error); } diff --git a/test/src/list/paged/paged_list_bloc_test.dart b/test/src/list/paged/paged_list_bloc_test.dart index 4a1b18b..37cd4ea 100644 --- a/test/src/list/paged/paged_list_bloc_test.dart +++ b/test/src/list/paged/paged_list_bloc_test.dart @@ -1,5 +1,4 @@ import 'package:flutter_bloc_patterns/paged_list.dart'; -import 'package:flutter_bloc_patterns/src/list/paged/paged_list.dart'; import 'package:flutter_bloc_patterns/src/view/view_state.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -8,8 +7,8 @@ import '../../util/bloc_state_assertion.dart'; import 'paged_list_repository_mock.dart'; void main() { - PagedListBloc bloc; - PagedListRepository repository; + late PagedListBloc bloc; + late PagedListRepository repository; void loadingFirstPage() => bloc.loadFirstPage(pageSize: 3); @@ -92,10 +91,12 @@ void main() { const Loading(), Success(PagedList(firstPage, hasReachedMax: false)), Success(PagedList(firstPage + secondPage, hasReachedMax: false)), - Success(PagedList( - firstPage + secondPage + thirdPage, - hasReachedMax: true, - )), + Success( + PagedList( + firstPage + secondPage + thirdPage, + hasReachedMax: true, + ), + ), ]); }); }); diff --git a/test/src/list/paged/paged_list_filter_bloc_test.dart b/test/src/list/paged/paged_list_filter_bloc_test.dart index 9dab759..74e569e 100644 --- a/test/src/list/paged/paged_list_filter_bloc_test.dart +++ b/test/src/list/paged/paged_list_filter_bloc_test.dart @@ -9,13 +9,17 @@ import '../../util/bloc_state_assertion.dart'; import 'paged_list_filter_repository_mock.dart'; void main() { + const filter = 1; const pageSize = 3; - PagedListFilterBloc bloc; - PagedListFilterRepository repository; + late PagedListFilterBloc bloc; + late PagedListFilterRepository repository; - void loadingFirstPage({int filter}) => - bloc.loadFirstPage(pageSize: pageSize, filter: filter); + void loadingFirstPage({ + required int filter, + }) { + bloc.loadFirstPage(pageSize: pageSize, filter: filter); + } void loadingNextPage() => bloc.loadNextPage(); @@ -27,7 +31,7 @@ void main() { test('should emit [$Loading, $Empty] when first page contains no elements', () { - when(loadingFirstPage); + when(() => loadingFirstPage(filter: filter)); then( () => withBloc(bloc).expectStates(const [ Loading(), @@ -42,7 +46,6 @@ void main() { }); group('repository with elements', () { - const filter = 1; const elements = [1, 1, 0, 1, 2, 3, 1, 1, 0]; final firstPage = List.generate(3, (i) => filter); final secondPage = List.generate(2, (i) => filter); @@ -55,9 +58,7 @@ void main() { test( 'should emit [$Loading, $Success] with elements matching the filter when loading first page', () { - when(() { - loadingFirstPage(filter: filter); - }); + when(() => loadingFirstPage(filter: filter)); then(() { withBloc(bloc).expectStates([ @@ -99,7 +100,7 @@ void main() { }); test('should emit [$Loading, $Failure] when error occurs', () { - when(loadingFirstPage); + when(() => loadingFirstPage(filter: 1)); then( () => withBloc(bloc).expectStates([ @@ -123,7 +124,7 @@ void main() { }); test('should emit [$Loading, $Empty] when first page was not found', () { - when(loadingFirstPage); + when(() => loadingFirstPage(filter: filter)); then( () => withBloc(bloc).expectStates(const [ Loading(), diff --git a/test/src/list/paged/paged_list_filter_repository_mock.dart b/test/src/list/paged/paged_list_filter_repository_mock.dart index 481b4bb..03a0014 100644 --- a/test/src/list/paged/paged_list_filter_repository_mock.dart +++ b/test/src/list/paged/paged_list_filter_repository_mock.dart @@ -11,7 +11,9 @@ class InMemoryPagedListFilterRepository InMemoryPagedListFilterRepository(this.elements); @override - Future> getAll(Page page) => getBy(page, null); + Future> getAll(Page page) async { + return InMemoryPagedListRepository(elements).getAll(page); + } @override Future> getBy(Page page, F filter) { @@ -23,13 +25,13 @@ class InMemoryPagedListFilterRepository class FailingPagedListFilterRepository implements PagedListFilterRepository { - final dynamic error; + final Object error; FailingPagedListFilterRepository(this.error); @override - Future> getAll(Page page) => throw error; + Future> getAll(Page page) => Future.error(error); @override - Future> getBy(Page page, F filter) => throw error; + Future> getBy(Page page, F filter) => Future.error(error); } diff --git a/test/src/list/paged/paged_list_repository_mock.dart b/test/src/list/paged/paged_list_repository_mock.dart index 528d077..087de73 100644 --- a/test/src/list/paged/paged_list_repository_mock.dart +++ b/test/src/list/paged/paged_list_repository_mock.dart @@ -22,7 +22,7 @@ class InMemoryPagedListRepository implements PagedListRepository { } class FailingPagedRepository implements PagedListRepository { - final dynamic error; + final Object error; FailingPagedRepository(this.error); diff --git a/test/src/util/bloc_state_assertion.dart b/test/src/util/bloc_state_assertion.dart index b793733..060ff11 100644 --- a/test/src/util/bloc_state_assertion.dart +++ b/test/src/util/bloc_state_assertion.dart @@ -12,7 +12,7 @@ class BlocStateAssertion { Future expectStates(Iterable states) { return expectLater( - _bloc, + _bloc.stream, emitsInOrder(states), ); } diff --git a/test/src/util/view_test_util.dart b/test/src/util/view_test_util.dart index 807ccdc..ec2d77b 100644 --- a/test/src/util/view_test_util.dart +++ b/test/src/util/view_test_util.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -Widget makeTestableWidget({Widget child}) => MaterialApp( - home: Material(child: child), -); \ No newline at end of file +Widget makeTestableWidget({Widget? child}) => MaterialApp( + home: Material(child: child), + ); diff --git a/test/src/view/view_state_builder_test.dart b/test/src/view/view_state_builder_test.dart index 85d16bf..013a373 100644 --- a/test/src/view/view_state_builder_test.dart +++ b/test/src/view/view_state_builder_test.dart @@ -2,26 +2,31 @@ import 'package:bloc/bloc.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_bloc_patterns/src/view/view_state.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; +import 'package:mocktail/mocktail.dart'; import 'view_state_builder_util.dart'; +import 'view_state_fakes.dart'; import 'view_state_keys.dart'; -class MockTestBloc extends MockBloc implements Bloc { -} +class MockTestBloc extends MockBloc {} void main() { - Bloc bloc; + late Bloc bloc; const someData = 0; final someError = Exception(); + setUpAll(() { + registerVieStateFallbackValue(); + registerBuildContextFallbackValue(); + }); + setUp(() { bloc = MockTestBloc(); }); testWidgets('should display onReady widget when bloc is in initial state', (WidgetTester tester) async { - when(bloc.state).thenReturn(const Initial()); + when(() => bloc.state).thenReturn(const Initial()); await tester.pumpWidget(makeTestableViewStateBuilder(bloc)); @@ -30,7 +35,7 @@ void main() { testWidgets('should display onLoading widget when bloc is in loading state', (WidgetTester tester) async { - when(bloc.state).thenReturn(const Loading()); + when(() => bloc.state).thenReturn(const Loading()); await tester.pumpWidget(makeTestableViewStateBuilder(bloc)); @@ -40,7 +45,7 @@ void main() { testWidgets( 'should display onRefreshing widget when bloc is in refreshing state', (WidgetTester tester) async { - when(bloc.state).thenReturn(const Refreshing(someData)); + when(() => bloc.state).thenReturn(const Refreshing(someData)); await tester.pumpWidget(makeTestableViewStateBuilder(bloc)); @@ -49,7 +54,7 @@ void main() { testWidgets('should display onEmpty widget when bloc is in empty state', (WidgetTester tester) async { - when(bloc.state).thenReturn(const Empty()); + when(() => bloc.state).thenReturn(const Empty()); await tester.pumpWidget(makeTestableViewStateBuilder(bloc)); @@ -58,7 +63,7 @@ void main() { testWidgets('should display onSuccess widget when bloc is in success state', (WidgetTester tester) async { - when(bloc.state).thenReturn(const Success(someData)); + when(() => bloc.state).thenReturn(const Success(someData)); await tester.pumpWidget(makeTestableViewStateBuilder(bloc)); @@ -67,7 +72,7 @@ void main() { testWidgets('should display onError widget when bloc is in failure state', (WidgetTester tester) async { - when(bloc.state).thenReturn(Failure(someError)); + when(() => bloc.state).thenReturn(Failure(someError)); await tester.pumpWidget(makeTestableViewStateBuilder(bloc)); diff --git a/test/src/view/view_state_builder_util.dart b/test/src/view/view_state_builder_util.dart index 4b8edeb..f26e59a 100644 --- a/test/src/view/view_state_builder_util.dart +++ b/test/src/view/view_state_builder_util.dart @@ -6,10 +6,10 @@ import 'package:flutter_bloc_patterns/src/view/view_state_builder.dart'; import '../util/view_test_util.dart'; import 'view_state_keys.dart'; -Widget makeTestableViewStateBuilder>(C cubit) { +Widget makeTestableViewStateBuilder>(B bloc) { return makeTestableWidget( - child: ViewStateBuilder( - cubit: cubit, + child: ViewStateBuilder( + bloc: bloc, onReady: (context) => const SizedBox.shrink(key: readyKey), onLoading: (context) => const SizedBox.shrink(key: loadKey), onRefreshing: (context, data) => const SizedBox.shrink(key: refreshKey), diff --git a/test/src/view/view_state_fakes.dart b/test/src/view/view_state_fakes.dart new file mode 100644 index 0000000..04e6fd4 --- /dev/null +++ b/test/src/view/view_state_fakes.dart @@ -0,0 +1,16 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc_patterns/src/view/view_state.dart'; +import 'package:mocktail/mocktail.dart'; + +// ignore: avoid_implementing_value_types +class ViewStateFake extends Fake implements ViewState {} + +class BuildContextFake extends Fake implements BuildContext {} + +void registerVieStateFallbackValue() { + registerFallbackValue(ViewStateFake()); +} + +void registerBuildContextFallbackValue() { + registerFallbackValue(BuildContextFake()); +} diff --git a/test/src/view/view_state_listener_test.dart b/test/src/view/view_state_listener_test.dart index fb631de..e8b7f26 100644 --- a/test/src/view/view_state_listener_test.dart +++ b/test/src/view/view_state_listener_test.dart @@ -1,15 +1,14 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_bloc_patterns/src/view/view_state.dart'; import 'package:flutter_bloc_patterns/view.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; +import 'package:mocktail/mocktail.dart'; import '../util/view_test_util.dart'; +import 'view_state_fakes.dart'; -class MockTestBloc extends MockBloc implements Bloc { -} +class MockTestBloc extends MockBloc {} class LoadingMock extends Mock { void call(BuildContext context); @@ -35,12 +34,17 @@ const _someData = 1; final _someException = Exception('Damn, I have failed...'); void main() { - Bloc bloc; - LoadingCallback loadingCallback; - SuccessCallback successCallback; - RefreshingCallback refreshCallback; - EmptyCallback emptyCallback; - ErrorCallback errorCallback; + late Bloc bloc; + late LoadingCallback loadingCallback; + late SuccessCallback successCallback; + late RefreshingCallback refreshCallback; + late EmptyCallback emptyCallback; + late ErrorCallback errorCallback; + + setUpAll(() { + registerVieStateFallbackValue(); + registerBuildContextFallbackValue(); + }); setUp(() { bloc = MockTestBloc(); @@ -54,7 +58,7 @@ void main() { Widget makeTestableViewStateListener() { return makeTestableWidget( child: ViewStateListener>( - cubit: bloc, + bloc: bloc, onLoading: loadingCallback, onRefreshing: refreshCallback, onSuccess: successCallback, @@ -69,79 +73,84 @@ void main() { (WidgetTester tester) async { whenListen( bloc, - Stream.fromIterable(const [Initial(), Loading()]), + Stream.value(const Loading()), + initialState: const Initial(), ); await tester.pumpWidget(makeTestableViewStateListener()); - verify(loadingCallback.call(any)); - verifyNever(successCallback.call(any, any)); - verifyNever(refreshCallback.call(any, any)); - verifyNever(emptyCallback.call(any)); - verifyNever(errorCallback.call(any, any)); + verify(() => loadingCallback.call(any())); + verifyNever(() => successCallback.call(any(), any())); + verifyNever(() => refreshCallback.call(any(), any())); + verifyNever(() => emptyCallback.call(any())); + verifyNever(() => errorCallback.call(any(), any())); }); testWidgets('should invoke success callback when loaded', (WidgetTester tester) async { whenListen( bloc, - Stream.fromIterable(const [Initial(), Success(_someData)]), + Stream.value(const Success(_someData)), + initialState: const Initial(), ); await tester.pumpWidget(makeTestableViewStateListener()); - verifyNever(loadingCallback.call(any)); - verify(successCallback.call(any, _someData)); - verifyNever(refreshCallback.call(any, any)); - verifyNever(emptyCallback.call(any)); - verifyNever(errorCallback.call(any, any)); + verifyNever(() => loadingCallback.call(any())); + verify(() => successCallback.call(any(), _someData)); + verifyNever(() => refreshCallback.call(any(), any())); + verifyNever(() => emptyCallback.call(any())); + verifyNever(() => errorCallback.call(any(), any())); }); testWidgets('should invoke refresh callback when refreshing', (WidgetTester tester) async { whenListen( bloc, - Stream.fromIterable(const [Initial(), Refreshing(_someData)]), + Stream.value(const Refreshing(_someData)), + initialState: const Initial(), ); await tester.pumpWidget(makeTestableViewStateListener()); - verifyNever(loadingCallback.call(any)); - verifyNever(successCallback.call(any, any)); - verify(refreshCallback.call(any, _someData)); - verifyNever(emptyCallback.call(any)); - verifyNever(errorCallback.call(any, any)); + verifyNever(() => loadingCallback.call(any())); + verifyNever(() => successCallback.call(any(), any())); + verify(() => refreshCallback.call(any(), _someData)); + verifyNever(() => emptyCallback.call(any())); + verifyNever(() => errorCallback.call(any(), any())); }); testWidgets('should invoke empty callback when empty', (WidgetTester tester) async { whenListen( bloc, - Stream.fromIterable(const [Initial(), Empty()]), + Stream.value(const Empty()), + initialState: const Initial(), ); await tester.pumpWidget(makeTestableViewStateListener()); - verifyNever(loadingCallback.call(any)); - verifyNever(successCallback.call(any, any)); - verifyNever(refreshCallback.call(any, any)); - verify(emptyCallback.call(any)); - verifyNever(errorCallback.call(any, any)); + verifyNever(() => loadingCallback.call(any())); + verifyNever(() => successCallback.call(any(), any())); + verifyNever(() => refreshCallback.call(any(), any())); + verify(() => emptyCallback.call(any())); + verifyNever(() => errorCallback.call(any(), any())); }); testWidgets('should invoke error callback when error', (WidgetTester tester) async { whenListen( bloc, - Stream.fromIterable([const Initial(), Failure(_someException)]), + Stream.value(Failure(_someException)), + initialState: const Initial(), ); await tester.pumpWidget(makeTestableViewStateListener()); - verifyNever(loadingCallback.call(any)); - verifyNever(successCallback.call(any, any)); - verifyNever(refreshCallback.call(any, any)); - verifyNever(emptyCallback.call(any)); - verify(errorCallback.call(any, _someException)); + verifyNever(() => loadingCallback.call(any())); + verifyNever(() => successCallback.call(any(), any())); + verifyNever(() => refreshCallback.call(any(), any())); + verifyNever(() => emptyCallback.call(any())); + verify(() => errorCallback.call(any(), _someException)); }); }