D1APP-47 empty states configuration

This commit is contained in:
MohammadTaha Basiri 2022-01-24 02:23:32 +03:30
parent e642d58e46
commit 17287f4b5f
27 changed files with 1063 additions and 83 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 59 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 59 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 43 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 39 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 42 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 46 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -5,6 +5,7 @@ class Assets {
static const String _basePath = 'lib/assets';
static const String _baseImagesPath = _basePath + '/images';
static const String _baseThemesPath = _basePath + '/images/themes';
static const String _baseEmptyStatesPath = _basePath + '/images/empty_states';
static const String _baseAnimationsPath = _basePath + '/animations';
static const String _baseRecordsPath = _basePath + '/images/records';
@ -29,6 +30,19 @@ class Assets {
static String get techCategoryIcon =>
_baseImagesPath + '/categories/tech-$_themeSuffix.svg';
static String get emptyBookmark =>
_baseEmptyStatesPath + '/bookmark-$_themeSuffix.svg';
static String get emptyChart =>
_baseEmptyStatesPath + '/chart-$_themeSuffix.svg';
static String get emptyChat =>
_baseEmptyStatesPath + '/chat-$_themeSuffix.svg';
static String get emptyConnection =>
_baseEmptyStatesPath + '/connection-$_themeSuffix.svg';
static String get emptyResult =>
_baseEmptyStatesPath + '/result-$_themeSuffix.svg';
static String get emptyStudio =>
_baseEmptyStatesPath + '/studio-$_themeSuffix.svg';
static const String lightTheme = _baseThemesPath + '/theme-light.svg';
static const String darkTheme = _baseThemesPath + '/theme-dark.svg';

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/pages/home/news/news_state.dart';
import 'package:didvan/pages/home/news/widgets/news_item.dart';
@ -12,7 +13,8 @@ import 'package:didvan/pages/home/widgets/logo_app_bar.dart';
import 'package:didvan/widgets/didvan/card.dart';
import 'package:didvan/widgets/didvan/divider.dart';
import 'package:didvan/widgets/shimmer_placeholder.dart';
import 'package:didvan/widgets/sliver_state_handler.dart';
import 'package:didvan/widgets/state_handlers/empty_result.dart';
import 'package:didvan/widgets/state_handlers/sliver_state_handler.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -25,25 +27,25 @@ class News extends StatefulWidget {
class _NewsState extends State<News> {
Timer? _timer;
final _focusNode = FocusNode();
@override
void initState() {
Future.delayed(Duration.zero, () {
context.read<NewsState>().getNews(page: 1);
});
context.read<NewsState>().init();
super.initState();
}
@override
Widget build(BuildContext context) {
final state = context.watch<NewsState>();
return Scaffold(
body: CustomScrollView(
return CustomScrollView(
slivers: [
const SliverToBoxAdapter(child: LogoAppBar()),
if (state.appState != AppState.failed)
SliverPadding(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
sliver: SliverToBoxAdapter(
child: SearchField(
focusNode: _focusNode,
title: 'اخبار',
onChanged: _onChanged,
onFilterButtonPressed: _showFilterBottomSheet,
@ -52,16 +54,20 @@ class _NewsState extends State<News> {
),
),
SliverStateHandler<NewsState>(
onRetry: () => state.getNews(page: state.page),
state: state,
builder: (context, state, index) => NewsItem(
news: state.news[index],
),
enableEmptyState: state.news.isEmpty,
emptyState: EmptyResult(
onNewSearch: () => _focusNode.requestFocus(),
),
childCount: state.news.length,
itemPadding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
placeholder: const _NewsItemPlaceholder(),
),
],
),
);
}

View File

@ -21,7 +21,8 @@ import 'package:didvan/widgets/didvan/divider.dart';
import 'package:didvan/widgets/didvan/text.dart';
import 'package:didvan/widgets/item_title.dart';
import 'package:didvan/widgets/shimmer_placeholder.dart';
import 'package:didvan/widgets/sliver_state_handler.dart';
import 'package:didvan/widgets/state_handlers/empty_result.dart';
import 'package:didvan/widgets/state_handlers/sliver_state_handler.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -34,7 +35,7 @@ class Radar extends StatefulWidget {
class _RadarState extends State<Radar> {
final ScrollController _scrollController = ScrollController();
// final ScrollController _categoriesScrollController = ScrollController();
final _focusNode = FocusNode();
bool _isAnimating = false;
@ -58,8 +59,7 @@ class _RadarState extends State<Radar> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Consumer<RadarState>(
return Consumer<RadarState>(
builder: (context, state, child) => Stack(
children: [
CustomScrollView(
@ -70,6 +70,7 @@ class _RadarState extends State<Radar> {
controller: _scrollController,
slivers: [
const SliverToBoxAdapter(child: LogoAppBar()),
if (state.appState != AppState.failed)
SliverPadding(
padding: const EdgeInsets.only(
left: 16,
@ -78,6 +79,7 @@ class _RadarState extends State<Radar> {
),
sliver: SliverToBoxAdapter(
child: SearchField(
focusNode: _focusNode,
isFiltered: state.filtering,
title: 'رادار',
onChanged: _onChanged,
@ -85,10 +87,13 @@ class _RadarState extends State<Radar> {
),
),
),
if (!state.filtering && !state.searching)
if (!state.filtering &&
!state.searching &&
state.appState != AppState.failed)
const SliverToBoxAdapter(
child: SizedBox(height: 276),
),
if (state.appState != AppState.failed)
SliverPadding(
padding: const EdgeInsets.only(right: 16, bottom: 20),
sliver: SliverToBoxAdapter(
@ -109,12 +114,16 @@ class _RadarState extends State<Radar> {
),
),
SliverStateHandler<RadarState>(
onRetry: () => state.getRadarOverviews(page: state.page),
state: state,
itemPadding: const EdgeInsets.only(
bottom: 20,
left: 16,
right: 16,
),
enableEmptyState: state.radars.isEmpty,
emptyState:
EmptyResult(onNewSearch: () => _focusNode.requestFocus()),
placeholder: const _RadarItemPlaceholder(),
builder: (context, state, index) => RadarItem(
radar: state.radars[index],
@ -127,20 +136,22 @@ class _RadarState extends State<Radar> {
),
],
),
if (state.appState != AppState.failed)
CategoriesRow1(
isColapsed:
state.isColapsed || state.searching || state.filtering,
),
if (state.appState != AppState.failed)
CategoriesRow2(
isColapsed:
state.isColapsed || state.searching || state.filtering,
),
if (state.appState != AppState.failed)
CategoriesList(
isColapsed: state.isColapsed && !state.searching,
),
],
),
),
);
}

View File

@ -1,6 +1,7 @@
import 'package:collection/collection.dart';
import 'package:didvan/constants/assets.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/requests/radar.dart';
import 'package:didvan/models/view/radar_category.dart';
import 'package:didvan/models/radar_overview.dart';
import 'package:didvan/providers/core_provider.dart';
@ -12,6 +13,7 @@ class RadarState extends CoreProvier {
String lastSearch = '';
String? startDate;
String? endDate;
int page = 1;
bool isScrolled = false;
bool shouldColapse = false;
final List<MapEntry> _markQueue = [];
@ -44,17 +46,22 @@ class RadarState extends CoreProvier {
Future<void> getRadarOverviews({
required int page,
}) async {
if (this.page == page) {
radars.clear();
}
this.page = page;
lastSearch = search;
appState = AppState.busy;
final RequestService service = RequestService(
RequestHelper.radarOverviews(
args: RadarRequestArgs(
page: page,
startDate: startDate?.split(' ').first,
endDate: endDate?.split(' ').first,
search: search == '' ? null : search,
categories: selectedCats.map((e) => e.id).toList(),
),
),
);
await service.httpGet();
if (service.isSuccess) {

View File

@ -15,7 +15,7 @@ import 'package:didvan/widgets/didvan/divider.dart';
import 'package:didvan/widgets/didvan/scaffold.dart';
import 'package:didvan/widgets/didvan/text.dart';
import 'package:didvan/widgets/item_title.dart';
import 'package:didvan/widgets/state_handler.dart';
import 'package:didvan/widgets/state_handlers/state_handler.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
@ -27,6 +27,7 @@ class GeneralSettings extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer<GeneralSettingsState>(
builder: (context, state, child) => StateHandler<GeneralSettingsState>(
onRetry: () {},
state: context.read<GeneralSettingsState>(),
builder: (context, state) => DidvanScaffold(
appBarData: AppBarData(hasBack: true, title: 'تنظیمات'),

View File

@ -1,3 +1,7 @@
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/assets.dart';
import 'package:didvan/pages/home/widgets/logo_app_bar.dart';
import 'package:didvan/widgets/state_handlers/empty_state.dart';
import 'package:flutter/material.dart';
class Statictics extends StatelessWidget {
@ -5,6 +9,18 @@ class Statictics extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
return Column(
children: [
const LogoAppBar(),
Expanded(
child: EmptyState(
asset: Assets.emptyChart,
title: 'قیمت‌ها و شاخص‌های اقتصادی',
subtitle: 'به زودی...',
titleColor: Theme.of(context).colorScheme.title,
),
),
],
);
}
}

View File

@ -1,3 +1,7 @@
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/assets.dart';
import 'package:didvan/pages/home/widgets/logo_app_bar.dart';
import 'package:didvan/widgets/state_handlers/empty_state.dart';
import 'package:flutter/material.dart';
class Studio extends StatelessWidget {
@ -5,6 +9,18 @@ class Studio extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
return Column(
children: [
const LogoAppBar(),
Expanded(
child: EmptyState(
asset: Assets.emptyStudio,
title: 'استودیو آینده',
subtitle: 'به زودی...',
titleColor: Theme.of(context).colorScheme.title,
),
),
],
);
}
}

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
class SearchField extends StatefulWidget {
final String title;
final FocusNode focusNode;
final bool? isFiltered;
final void Function(String value) onChanged;
final VoidCallback? onFilterButtonPressed;
@ -13,6 +14,7 @@ class SearchField extends StatefulWidget {
Key? key,
required this.title,
required this.onChanged,
required this.focusNode,
this.onFilterButtonPressed,
this.isFiltered,
}) : super(key: key);
@ -22,11 +24,9 @@ class SearchField extends StatefulWidget {
}
class _SearchFieldState extends State<SearchField> {
final FocusNode _focusNode = FocusNode();
@override
void initState() {
_focusNode.addListener(() {
widget.focusNode.addListener(() {
setState(() {});
});
super.initState();
@ -44,8 +44,8 @@ class _SearchFieldState extends State<SearchField> {
color: _fillColor(),
),
child: TextFormField(
focusNode: _focusNode,
style: Theme.of(context).textTheme.bodyText1,
focusNode: widget.focusNode,
style: Theme.of(context).textTheme.bodyText2,
textAlignVertical: TextAlignVertical.center,
onChanged: widget.onChanged,
keyboardType: TextInputType.text,
@ -117,7 +117,7 @@ class _SearchFieldState extends State<SearchField> {
}
Color _fillColor() {
if (_focusNode.hasFocus) {
if (widget.focusNode.hasFocus) {
return Theme.of(context).colorScheme.surface;
}
return Theme.of(context).colorScheme.surface;
@ -125,7 +125,7 @@ class _SearchFieldState extends State<SearchField> {
@override
void dispose() {
_focusNode.dispose();
widget.focusNode.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,18 @@
import 'package:didvan/constants/assets.dart';
import 'package:didvan/widgets/state_handlers/empty_state.dart';
import 'package:flutter/material.dart';
class EmptyConnection extends StatelessWidget {
final VoidCallback onRetry;
const EmptyConnection({Key? key, required this.onRetry}) : super(key: key);
@override
Widget build(BuildContext context) {
return EmptyState(
asset: Assets.emptyConnection,
title: 'ارتباط با اینترنت قطع شد...',
action: onRetry,
buttonTitle: 'تلاش دوباره',
);
}
}

View File

@ -0,0 +1,18 @@
import 'package:didvan/constants/assets.dart';
import 'package:didvan/widgets/state_handlers/empty_state.dart';
import 'package:flutter/material.dart';
class EmptyResult extends StatelessWidget {
final VoidCallback onNewSearch;
const EmptyResult({Key? key, required this.onNewSearch}) : super(key: key);
@override
Widget build(BuildContext context) {
return EmptyState(
asset: Assets.emptyResult,
title: 'نتیجه‌ای پیدا نشد',
buttonTitle: 'تغییر جستجو',
action: onNewSearch,
);
}
}

View File

@ -0,0 +1,54 @@
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/widgets/didvan/button.dart';
import 'package:didvan/widgets/didvan/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class EmptyState extends StatelessWidget {
final String asset;
final String title;
final String? subtitle;
final String? buttonTitle;
final VoidCallback? action;
final Color? titleColor;
const EmptyState({
Key? key,
required this.asset,
required this.title,
this.action,
this.buttonTitle,
this.subtitle,
this.titleColor,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: 210, child: SvgPicture.asset(asset)),
const SizedBox(height: 16),
DidvanText(
title,
style: Theme.of(context).textTheme.headline3,
color: titleColor ?? Theme.of(context).colorScheme.caption,
),
if (subtitle != null) const SizedBox(height: 8),
if (subtitle != null)
DidvanText(
subtitle!,
color: Theme.of(context).colorScheme.caption,
),
if (action != null) const SizedBox(height: 16),
if (action != null)
DidvanButton(
height: 40,
onPressed: action,
title: buttonTitle,
width: 112,
),
],
);
}
}

View File

@ -1,13 +1,13 @@
import 'package:didvan/models/enums.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/widgets/didvan/text.dart';
import 'package:didvan/widgets/state_handlers/empty_connection.dart';
import 'package:flutter/material.dart';
class SliverStateHandler<T extends CoreProvier> extends SliverList {
final T state;
final Widget Function(BuildContext context, T state, int index) builder;
final int childCount;
final VoidCallback? onRefresh;
final VoidCallback onRetry;
final bool enableEmptyState;
final Widget? emptyState;
final Widget? placeholder;
@ -17,20 +17,28 @@ class SliverStateHandler<T extends CoreProvier> extends SliverList {
required this.state,
required this.builder,
required this.childCount,
required this.onRetry,
this.itemPadding,
this.placeholder,
this.emptyState,
this.enableEmptyState = false,
this.onRefresh,
}) : super(
key: key,
delegate: SliverChildBuilderDelegate(
(context, index) {
if (state.appState == AppState.failed) {
return const DidvanText('مشکل اتصال');
return SizedBox(
height: MediaQuery.of(context).size.height - 240,
child: EmptyConnection(
onRetry: onRetry,
),
);
}
if (enableEmptyState) {
return emptyState;
if (enableEmptyState && state.appState == AppState.idle) {
return SizedBox(
height: MediaQuery.of(context).size.height - 240,
child: emptyState,
);
}
if (state.appState == AppState.busy) {
return Padding(
@ -44,7 +52,9 @@ class SliverStateHandler<T extends CoreProvier> extends SliverList {
);
},
childCount: state.appState == AppState.idle
? childCount
? enableEmptyState
? 1
: childCount
: state.appState == AppState.busy
? 3
: 1,

View File

@ -1,12 +1,13 @@
import 'package:didvan/models/enums.dart';
import 'package:didvan/providers/core_provider.dart';
import 'package:didvan/widgets/state_handlers/empty_connection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
class StateHandler<T extends CoreProvier> extends StatelessWidget {
final T state;
final Widget Function(BuildContext context, T state) builder;
final VoidCallback? onRefresh;
final VoidCallback onRetry;
final bool enableEmptyState;
final Widget? placeholder;
final Widget? emptyState;
@ -14,11 +15,11 @@ class StateHandler<T extends CoreProvier> extends StatelessWidget {
const StateHandler({
Key? key,
required this.builder,
required this.onRetry,
required this.state,
this.emptyState,
this.enableEmptyState = false,
this.onRefresh,
this.topPadding = 0,
required this.state,
this.placeholder,
}) : super(key: key);
@ -42,7 +43,7 @@ class StateHandler<T extends CoreProvier> extends StatelessWidget {
color: Theme.of(context).colorScheme.primary,
);
case AppState.failed:
return Container();
return EmptyConnection(onRetry: onRetry);
default:
return Container();
}

View File

@ -160,6 +160,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.0"
flutter_html:
dependency: "direct main"
description:
name: flutter_html
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0-alpha.2"
flutter_lints:
dependency: "direct dev"
description:
@ -378,6 +385,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
numerus:
dependency: transitive
description:
name: numerus
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
octo_image:
dependency: transitive
description:

View File

@ -58,6 +58,7 @@ dependencies:
image_cropper: ^1.4.1
bot_toast: ^4.0.1
flutter_secure_storage: ^5.0.2
flutter_html: ^3.0.0-alpha.2
dev_dependencies:
flutter_test:
@ -103,6 +104,18 @@ flutter:
- lib/assets/images/themes/theme-dark.svg
- lib/assets/images/records/record-dark.svg
- lib/assets/images/records/record-light.svg
- lib/assets/images/empty_states/bookmark-light.svg
- lib/assets/images/empty_states/chart-light.svg
- lib/assets/images/empty_states/chat-light.svg
- lib/assets/images/empty_states/connection-light.svg
- lib/assets/images/empty_states/result-light.svg
- lib/assets/images/empty_states/studio-light.svg
- lib/assets/images/empty_states/bookmark-dark.svg
- lib/assets/images/empty_states/chart-dark.svg
- lib/assets/images/empty_states/chat-dark.svg
- lib/assets/images/empty_states/connection-dark.svg
- lib/assets/images/empty_states/result-dark.svg
- lib/assets/images/empty_states/studio-dark.svg
- lib/assets/animations/indicator-light.riv
- lib/assets/animations/indicator-dark.riv