Upgrade dependencies and split home.dart

This commit is contained in:
Remi Rousselet
2022-07-31 13:03:55 +02:00
parent 0359ecf090
commit a2a6f8e5ba
7 changed files with 267 additions and 246 deletions

View File

@ -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<TagTheme>((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),
);
}
}

View File

@ -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<Question> items,
required int total,
}) = _QuestionsResponse;
factory QuestionsResponse.fromJson(Map<String, Object?> json) =>
_$QuestionsResponseFromJson(json);
}
@freezed
abstract class Question with _$Question {
@JsonSerializable(fieldRename: FieldRename.snake)
factory Question({
required List<String> 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<String, Object?> json) =>
_$QuestionFromJson(json);
}
final client = Provider((ref) => Dio());
final _fetchedPages = StateProvider((ref) => <int>[]);
final paginatedQuestionsProvider = FutureProvider.autoDispose
.family<QuestionsResponse, int>((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: <String, Object>{
'order': 'desc',
'sort': 'creation',
'site': 'stackoverflow',
'filter': '!17vW1m9jnXcpKOO(p4a5Jj.QeqRQmvxcbquXIXJ1fJcKq4',
'tagged': 'flutter',
'pagesize': '50',
'page': '${pageIndex + 1}',
},
);
final response = await ref
.watch(client)
.getUri<Map<String, Object?>>(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<QuestionTheme>((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<AsyncValue<Question>>((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,
),
],
),
],
),
],
),
);
}
}

View File

@ -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<Question> items,
required int total,
}) = _QuestionsResponse;
factory QuestionsResponse.fromJson(Map<String, Object?> json) =>
_$QuestionsResponseFromJson(json);
}
@freezed
class Question with _$Question {
@JsonSerializable(fieldRename: FieldRename.snake)
factory Question({
required List<String> 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<String, Object?> json) =>
_$QuestionFromJson(json);
}
final paginatedQuestionsProvider = FutureProvider.autoDispose
.family<QuestionsResponse, int>((ref, pageIndex) async {
final cancelToken = CancelToken();
ref.onDispose(cancelToken.cancel);
final uri = Uri(
scheme: 'https',
host: 'api.stackexchange.com',
path: '/2.2/questions',
queryParameters: <String, Object>{
'order': 'desc',
'sort': 'creation',
'site': 'stackoverflow',
'filter': '!17vW1m9jnXcpKOO(p4a5Jj.QeqRQmvxcbquXIXJ1fJcKq4',
'tagged': 'flutter',
'pagesize': '50',
'page': '${pageIndex + 1}',
},
);
final response = await ref
.watch(client)
.getUri<Map<String, Object?>>(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<QuestionTheme>((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<AsyncValue<Question>>((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,
),
],
),
],
),
],
),
);
},
);
}
}

View File

@ -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<TagTheme>((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),
);
}
}

View File

@ -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

View File

@ -0,0 +1,7 @@
dependency_overrides:
flutter_riverpod:
path: ../../packages/flutter_riverpod
hooks_riverpod:
path: ../../packages/hooks_riverpod
riverpod:
path: ../../packages/riverpod