From a2a6f8e5baaa2bc44b7bea6761b70a654b147114 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 31 Jul 2022 13:03:55 +0200 Subject: [PATCH] Upgrade dependencies and split home.dart --- examples/stackoverflow/lib/common.dart | 52 +---- examples/stackoverflow/lib/home.dart | 181 +--------------- examples/stackoverflow/lib/question.dart | 193 ++++++++++++++++++ examples/stackoverflow/lib/tag.dart | 56 +++++ examples/stackoverflow/pubspec.yaml | 22 +- examples/stackoverflow/pubspec_overrides.yaml | 7 + .../riverpod/test/framework/listen_test.dart | 2 - 7 files changed, 267 insertions(+), 246 deletions(-) create mode 100644 examples/stackoverflow/lib/question.dart create mode 100644 examples/stackoverflow/lib/tag.dart create mode 100644 examples/stackoverflow/pubspec_overrides.yaml diff --git a/examples/stackoverflow/lib/common.dart b/examples/stackoverflow/lib/common.dart index 67ed368db..91c43e8a0 100644 --- a/examples/stackoverflow/lib/common.dart +++ b/examples/stackoverflow/lib/common.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -8,6 +9,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; part 'common.freezed.dart'; part 'common.g.dart'; +final client = Provider((ref) => Dio()); + /// A Provider that exposes the current theme. /// /// This is unimplemented by default, and will be overriden inside [MaterialApp] @@ -278,52 +281,3 @@ class PostInfo extends HookConsumerWidget { ); } } - -@freezed -abstract class TagTheme with _$TagTheme { - const factory TagTheme({ - required TextStyle style, - required EdgeInsets padding, - required Color backgroundColor, - required BorderRadius borderRadius, - }) = _TagTheme; -} - -final tagThemeProvider = Provider((ref) { - final theme = ref.watch(themeProvider); - - return TagTheme( - padding: EdgeInsets.symmetric( - horizontal: theme.textTheme.bodyText1!.fontSize! * 0.5, - vertical: theme.textTheme.bodyText1!.fontSize! * 0.4, - ), - style: theme.textTheme.bodyText2!.copyWith( - color: const Color(0xff9cc3db), - ), - borderRadius: BorderRadius.circular(3), - backgroundColor: const Color(0xFF3e4a52), - ); -}, dependencies: [themeProvider]); - -class Tag extends HookConsumerWidget { - const Tag({ - Key? key, - required this.tag, - }) : super(key: key); - - final String tag; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final tagTheme = ref.watch(tagThemeProvider); - - return Container( - decoration: BoxDecoration( - borderRadius: tagTheme.borderRadius, - color: tagTheme.backgroundColor, - ), - padding: tagTheme.padding, - child: Text(tag, style: tagTheme.style), - ); - } -} diff --git a/examples/stackoverflow/lib/home.dart b/examples/stackoverflow/lib/home.dart index a72b92932..0c7a90a1d 100644 --- a/examples/stackoverflow/lib/home.dart +++ b/examples/stackoverflow/lib/home.dart @@ -1,120 +1,9 @@ // ignore: import_of_legacy_library_into_null_safe import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -// ignore: import_of_legacy_library_into_null_safe -import 'package:html/parser.dart'; -import 'common.dart'; - -part 'home.freezed.dart'; -part 'home.g.dart'; - -@freezed -abstract class QuestionsResponse with _$QuestionsResponse { - factory QuestionsResponse({ - required List items, - required int total, - }) = _QuestionsResponse; - - factory QuestionsResponse.fromJson(Map json) => - _$QuestionsResponseFromJson(json); -} - -@freezed -abstract class Question with _$Question { - @JsonSerializable(fieldRename: FieldRename.snake) - factory Question({ - required List tags, - required int viewCount, - required int score, - int? bountyAmount, - int? acceptedAnswerId, - required Owner owner, - required int answerCount, - @TimestampParser() required DateTime creationDate, - required int questionId, - required String link, - required String title, - required String body, - }) = _Question; - - factory Question.fromJson(Map json) => - _$QuestionFromJson(json); -} - -final client = Provider((ref) => Dio()); - -final _fetchedPages = StateProvider((ref) => []); - -final paginatedQuestionsProvider = FutureProvider.autoDispose - .family((ref, pageIndex) async { - final fetchedPages = ref.watch(_fetchedPages.state).state; - fetchedPages.add(pageIndex); - ref.onDispose(() => fetchedPages.remove(pageIndex)); - - final cancelToken = CancelToken(); - ref.onDispose(cancelToken.cancel); - - final uri = Uri( - scheme: 'https', - host: 'api.stackexchange.com', - path: '/2.2/questions', - queryParameters: { - 'order': 'desc', - 'sort': 'creation', - 'site': 'stackoverflow', - 'filter': '!17vW1m9jnXcpKOO(p4a5Jj.QeqRQmvxcbquXIXJ1fJcKq4', - 'tagged': 'flutter', - 'pagesize': '50', - 'page': '${pageIndex + 1}', - }, - ); - - final response = await ref - .watch(client) - .getUri>(uri, cancelToken: cancelToken); - - final parsed = QuestionsResponse.fromJson(response.data!); - final page = parsed.copyWith( - items: parsed.items.map((e) { - final document = parse(e.body); - return e.copyWith(body: document.body!.text.replaceAll('\n', ' ')); - }).toList(), - ); - - ref.keepAlive(); - - return page; -}); - -final questionsCountProvider = Provider.autoDispose((ref) { - return ref - .watch(paginatedQuestionsProvider(0)) - .whenData((page) => page.total); -}); - -@freezed -abstract class QuestionTheme with _$QuestionTheme { - const factory QuestionTheme({ - required TextStyle titleStyle, - required TextStyle descriptionStyle, - }) = _QuestionTheme; -} - -final questionThemeProvider = Provider((ref) { - return const QuestionTheme( - titleStyle: TextStyle( - color: Color(0xFF3ca4ff), - fontSize: 16, - ), - descriptionStyle: TextStyle( - color: Color(0xFFe7e8eb), - fontSize: 13, - ), - ); -}); +import 'question.dart'; class MyHomePage extends HookConsumerWidget { const MyHomePage({Key? key}) : super(key: key); @@ -174,71 +63,3 @@ class MyHomePage extends HookConsumerWidget { ); } } - -final currentQuestion = Provider>((ref) { - throw UnimplementedError(); -}); - -class QuestionItem extends HookConsumerWidget { - const QuestionItem({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final question = ref.watch(currentQuestion); - final questionTheme = ref.watch(questionThemeProvider); - - if (question is AsyncLoading) { - return const Center(child: Text('loading')); - } - - final data = question.value!; - - return ListTile( - title: Text( - data.title, - style: questionTheme.titleStyle, - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 5), - Text( - data.body, - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 5, - runSpacing: 5, - children: [ - for (final tag in data.tags) Tag(tag: tag), - ], - ), - const SizedBox(height: 6), - Row( - children: [ - Expanded( - child: PostInfo( - owner: data.owner, - creationDate: data.creationDate, - ), - ), - UpvoteCount(data.score), - const SizedBox(width: 10), - AnswersCount( - data.answerCount, - accepted: data.acceptedAnswerId != null, - ), - ], - ), - ], - ), - ], - ), - ); - } -} diff --git a/examples/stackoverflow/lib/question.dart b/examples/stackoverflow/lib/question.dart new file mode 100644 index 000000000..4b1307906 --- /dev/null +++ b/examples/stackoverflow/lib/question.dart @@ -0,0 +1,193 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:html/parser.dart'; + +import 'common.dart'; +import 'tag.dart'; + +part 'question.g.dart'; +part 'question.freezed.dart'; + +@freezed +class QuestionsResponse with _$QuestionsResponse { + factory QuestionsResponse({ + required List items, + required int total, + }) = _QuestionsResponse; + + factory QuestionsResponse.fromJson(Map json) => + _$QuestionsResponseFromJson(json); +} + +@freezed +class Question with _$Question { + @JsonSerializable(fieldRename: FieldRename.snake) + factory Question({ + required List tags, + required int viewCount, + required int score, + int? bountyAmount, + int? acceptedAnswerId, + required Owner owner, + required int answerCount, + @TimestampParser() required DateTime creationDate, + required int questionId, + required String link, + required String title, + required String body, + }) = _Question; + + factory Question.fromJson(Map json) => + _$QuestionFromJson(json); +} + +final paginatedQuestionsProvider = FutureProvider.autoDispose + .family((ref, pageIndex) async { + final cancelToken = CancelToken(); + ref.onDispose(cancelToken.cancel); + + final uri = Uri( + scheme: 'https', + host: 'api.stackexchange.com', + path: '/2.2/questions', + queryParameters: { + 'order': 'desc', + 'sort': 'creation', + 'site': 'stackoverflow', + 'filter': '!17vW1m9jnXcpKOO(p4a5Jj.QeqRQmvxcbquXIXJ1fJcKq4', + 'tagged': 'flutter', + 'pagesize': '50', + 'page': '${pageIndex + 1}', + }, + ); + + final response = await ref + .watch(client) + .getUri>(uri, cancelToken: cancelToken); + + final parsed = QuestionsResponse.fromJson(response.data!); + final page = parsed.copyWith( + items: parsed.items.map((e) { + final document = parse(e.body); + return e.copyWith(body: document.body!.text.replaceAll('\n', ' ')); + }).toList(), + ); + + ref.keepAlive(); + + return page; +}); + +final questionsCountProvider = Provider.autoDispose((ref) { + return ref + .watch(paginatedQuestionsProvider(0)) + .whenData((page) => page.total); +}); + +@freezed +class QuestionTheme with _$QuestionTheme { + const factory QuestionTheme({ + required TextStyle titleStyle, + required TextStyle descriptionStyle, + }) = _QuestionTheme; +} + +final questionThemeProvider = Provider((ref) { + return const QuestionTheme( + titleStyle: TextStyle( + color: Color(0xFF3ca4ff), + fontSize: 16, + ), + descriptionStyle: TextStyle( + color: Color(0xFFe7e8eb), + fontSize: 13, + ), + ); +}); + +/// A scoped provider, exposing the current question used by [QuestionItem]. +/// +/// This is used as a performance optimization to pass a [Question] to +/// [QuestionItem], while still instantiating [QuestionItem] using the `const` +/// keyword. +/// +/// This allows [QuestionItem] to rebuild less often. +/// By doing so, even when using [QuestionItem] in a [ListView], even if new +/// questions are obtained, previously rendered [QuestionItem]s won't rebuild. +/// +/// This is an optional step. Since scoping is a fairly advanced mechanism, +/// it's entirely fine to simply pass the [Question] to [QuestionItem] directly. +final currentQuestion = Provider>((ref) { + throw UnimplementedError(); +}); + +/// A UI widget rendering a [Question]. +/// +/// That question will be obtained through [currentQuestion]. As such, it is +/// necessary to override that provider before using [QuestionItem]. +class QuestionItem extends HookConsumerWidget { + const QuestionItem({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final question = ref.watch(currentQuestion); + final questionTheme = ref.watch(questionThemeProvider); + + return question.when( + // TODO(rrousselGit): improve error rendering + error: (error, stack) => const Center(child: Text('Error')), + loading: () => const Center(child: Text('loading')), + data: (question) { + return ListTile( + title: Text( + question.title, + style: questionTheme.titleStyle, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 5), + Text( + question.body, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 5, + runSpacing: 5, + children: [ + for (final tag in question.tags) Tag(tag: tag), + ], + ), + const SizedBox(height: 6), + Row( + children: [ + Expanded( + child: PostInfo( + owner: question.owner, + creationDate: question.creationDate, + ), + ), + UpvoteCount(question.score), + const SizedBox(width: 10), + AnswersCount( + question.answerCount, + accepted: question.acceptedAnswerId != null, + ), + ], + ), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/examples/stackoverflow/lib/tag.dart b/examples/stackoverflow/lib/tag.dart new file mode 100644 index 000000000..92e6d1c0e --- /dev/null +++ b/examples/stackoverflow/lib/tag.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import 'common.dart'; + +part 'tag.freezed.dart'; + +@freezed +class TagTheme with _$TagTheme { + const factory TagTheme({ + required TextStyle style, + required EdgeInsets padding, + required Color backgroundColor, + required BorderRadius borderRadius, + }) = _TagTheme; +} + +final tagThemeProvider = Provider((ref) { + final theme = ref.watch(themeProvider); + + return TagTheme( + padding: EdgeInsets.symmetric( + horizontal: theme.textTheme.bodyText1!.fontSize! * 0.5, + vertical: theme.textTheme.bodyText1!.fontSize! * 0.4, + ), + style: theme.textTheme.bodyText2!.copyWith( + color: const Color(0xff9cc3db), + ), + borderRadius: BorderRadius.circular(3), + backgroundColor: const Color(0xFF3e4a52), + ); +}, dependencies: [themeProvider]); + +class Tag extends HookConsumerWidget { + const Tag({ + Key? key, + required this.tag, + }) : super(key: key); + + final String tag; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tagTheme = ref.watch(tagThemeProvider); + + return Container( + decoration: BoxDecoration( + borderRadius: tagTheme.borderRadius, + color: tagTheme.backgroundColor, + ), + padding: tagTheme.padding, + child: Text(tag, style: tagTheme.style), + ); + } +} diff --git a/examples/stackoverflow/pubspec.yaml b/examples/stackoverflow/pubspec.yaml index 856dc8c83..743d24093 100644 --- a/examples/stackoverflow/pubspec.yaml +++ b/examples/stackoverflow/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: "none" version: 1.0.0+1 environment: - sdk: ">=2.14.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=1.17.0" dependencies: @@ -12,26 +12,18 @@ dependencies: flutter: sdk: flutter flutter_hooks: ^0.18.0 - freezed_annotation: ^0.14.0 - hooks_riverpod: - path: ../../packages/hooks_riverpod + freezed_annotation: ^2.1.0 + hooks_riverpod: any html: ^0.15.0 + json_annotation: ^4.6.0 dev_dependencies: - build_runner: ^1.11.0 + build_runner: ^2.0.0 flutter_test: sdk: flutter - freezed: ^0.14.0 - json_serializable: ^4.0.0 + freezed: ^2.1.0 + json_serializable: ^6.3.0 mockito: ^5.0.0 -dependency_overrides: - flutter_riverpod: - path: ../../packages/flutter_riverpod - hooks_riverpod: - path: ../../packages/hooks_riverpod - riverpod: - path: ../../packages/riverpod - flutter: uses-material-design: true diff --git a/examples/stackoverflow/pubspec_overrides.yaml b/examples/stackoverflow/pubspec_overrides.yaml new file mode 100644 index 000000000..4d269761a --- /dev/null +++ b/examples/stackoverflow/pubspec_overrides.yaml @@ -0,0 +1,7 @@ +dependency_overrides: + flutter_riverpod: + path: ../../packages/flutter_riverpod + hooks_riverpod: + path: ../../packages/hooks_riverpod + riverpod: + path: ../../packages/riverpod diff --git a/packages/riverpod/test/framework/listen_test.dart b/packages/riverpod/test/framework/listen_test.dart index 9dca3b354..a8ae7c3c7 100644 --- a/packages/riverpod/test/framework/listen_test.dart +++ b/packages/riverpod/test/framework/listen_test.dart @@ -1177,7 +1177,6 @@ void main() { container.listen>( provider.state, (prev, notifier) => listener(prev?.state, notifier.state), - fireImmediately: false, ); verifyZeroInteractions(listener); @@ -1524,7 +1523,6 @@ void main() { container.listen>( count.state, (prev, value) => listener(prev?.state, value.state), - fireImmediately: false, ); container.read(count.state).state++;