firset commit

This commit is contained in:
GYJ
2024-11-22 16:48:12 +08:00
commit 8d57c7f406
105 changed files with 3615 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
import '../utils/func_tools.dart';
class BaseModel {
/// 发生错误时错误信息, 有可能会被后面的请求覆盖掉
String? msg;
String? getMsg() {
return isEmptyString(msg) ? '' : msg;
}
}

View File

@@ -0,0 +1,15 @@
import 'package:cashier_reserve/common/base/ui_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
export 'package:provider/provider.dart';
class MyProvider<T> {
static T of<T>(BuildContext context, {bool listen = true}) {
T provider = Provider.of<T>(context, listen: listen);
if (provider is BaseUIModel) {
BaseUIModel viewModel = provider;
viewModel.context = context;
}
return provider;
}
}

129
lib/common/base/ui.dart Normal file
View File

@@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../push/push.dart';
import './ui_model.dart';
import './widgets.dart';
export 'package:flutter/material.dart';
class BaseUIController extends StatefulWidget {
final BaseUI stateWidget;
const BaseUIController({super.key, required this.stateWidget});
@override
State<BaseUIController> createState() {
// ignore: no_logic_in_create_state
return stateWidget;
}
}
abstract class BaseUI extends State<BaseUIController>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
final provider = getProvider(context, listen: true);
return Stack(
children: [
Scaffold(
backgroundColor: getBackgroundColor(),
appBar: getAppBar(context),
body: _buildBody(context),
bottomNavigationBar: getBottomNavigationBar(context),
floatingActionButton: getFloatActionButton(context),
),
if (provider.showProgressHUD)
Material(
color: Colors.black.withAlpha(50),
child: Center(
child: Card(
child: Container(
width: 100,
height: 100,
color: const Color(0xaa000000),
child: const Center(
child: SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(),
),
),
),
),
),
),
],
);
}
dismissKeyboard(BuildContext context) {
FocusScope.of(context).requestFocus(FocusNode());
}
Widget? getFloatActionButton(BuildContext context) {
return null;
}
Widget? getBottomNavigationBar(BuildContext context) {
return null;
}
@protected
BaseUIModel getProvider(BuildContext context, {bool listen = true});
@protected
String? getTitleStr(BuildContext context);
Future pushToPage<T extends BaseUIModel>(
BuildContext context, BaseUIModel model) {
return YJPush.pushWidget(
context,
getBuild<T>(model),
);
}
Widget getBuild<T extends BaseUIModel>(BaseUIModel model) {
return ChangeNotifierProvider(
create: (BuildContext context) {
return model as T;
},
child: BaseUIController(
stateWidget: this,
),
);
}
AppBar? getAppBar(BuildContext context) {
return makeAppbar(context, getTitleStr(context));
}
Color getBackgroundColor() {
return appBackGroundColor;
}
bool hiddenBackBtn() {
return false;
}
Widget _buildBody(BuildContext context) {
return GestureDetector(
child: Container(
color: getBackgroundColor(),
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: buildBody(context),
),
onTap: () {
dismissKeyboard(context);
},
);
}
@protected
Widget buildBody(BuildContext context);
@override
bool get wantKeepAlive => false;
}

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
class BaseUIModel with ChangeNotifier {
static Function? staticNotifyListeners;
BuildContext? context;
bool showProgressHUD = false;
bool initialPage = true;
/// 判断页面是否被销毁
bool disposed = false;
dismissKeyboard(BuildContext? context) {
if (context == null) {
return;
}
FocusScope.of(context).requestFocus(FocusNode());
}
showLoadingHud() {
showProgressHUD = true;
notifyListeners();
}
dismissLoadingHud() {
showProgressHUD = false;
notifyListeners();
}
@override
void notifyListeners() {
if (!hasListeners) {
return;
}
if (disposed) {
return;
}
super.notifyListeners();
}
@override
void dispose() {
disposed = true;
super.dispose();
}
}

View File

@@ -0,0 +1,483 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cashier_reserve/common/base/ui.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
/// 应用主体背景颜色
const Color appBackGroundColor = Color.fromARGB(255, 247, 250, 255);
/// 主色调
const Color mdMainColor = Color(0xff2854BC);
/// fix for Flutter 2.5
/// https://github.com/flutter/flutter/issues/77904
Brightness? getAppBrightness(bool isLight) {
if (kIsWeb) {
return null;
}
if (Platform.isIOS) {
return isLight ? Brightness.dark : Brightness.light;
}
return isLight ? Brightness.light : Brightness.dark;
}
/// 微信风格Appbar
AppBar makeWechatStyleAppbar(BuildContext context, String title,
{List<Widget>? actions,
final Color backGroundColor = Colors.white,
final Color textColor = Colors.black,
final Brightness brightness = Brightness.light,
bool centerTitle = false,
PreferredSizeWidget? bottom}) {
return AppBar(
backgroundColor: backGroundColor,
elevation: 0,
centerTitle: centerTitle,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: getAppBrightness(false),
statusBarBrightness: getAppBrightness(false)),
actions: actions,
bottom: bottom,
title: Text(
title,
style: TextStyle(
fontSize: 17, color: textColor, fontWeight: FontWeight.bold),
),
leading: IconButton(
iconSize: 18,
tooltip: "返回",
color: Colors.black,
icon: Icon(
Icons.arrow_back_ios,
color: textColor,
),
onPressed: () {
Navigator.pop(context);
},
),
);
}
/// 通用Appbar
AppBar makeAppbar(BuildContext context, String? title,
{List<Widget>? actions,
Color? backgroundColor,
PreferredSizeWidget? bottom,
final bool showBack = true,
Widget? titleWidget,
Widget? backWidget,
VoidCallback? onBack,
Key? key}) {
backgroundColor ??= mdMainColor;
return AppBar(
key: key,
backgroundColor: backgroundColor,
elevation: 0,
centerTitle: true,
actions: actions,
bottom: bottom,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: getAppBrightness(false),
statusBarBrightness: getAppBrightness(false)),
title: titleWidget ??
Text(
title!,
style: const TextStyle(
fontSize: 17, color: Colors.white, fontWeight: FontWeight.bold),
),
leading: showBack
? (backWidget ??
IconButton(
iconSize: 18,
tooltip: "返回",
color: Colors.white,
splashColor: Colors.black,
hoverColor: Colors.black,
icon: const Icon(Icons.arrow_back_ios),
onPressed: onBack ??
() {
Navigator.pop(context);
},
))
: null,
);
}
/// ListView 无感上拉加载
class ListViewPullUpLoadMore {
final ScrollController scrollController;
final Future<void> Function() onLoadMore;
bool _isLoading = false;
ListViewPullUpLoadMore(this.scrollController, {required this.onLoadMore}) {
scrollController.addListener(() {
if (_isLoading) {
return;
}
if (scrollController.offset ==
scrollController.position.maxScrollExtent) {
/// 到底部,添加加载中标记,触发回调
_isLoading = true;
onLoadMore().then((_) {
/// 加载完毕,标记复位
_isLoading = false;
});
}
});
}
}
/// 绘制通用下划线
Widget makeDivider(BuildContext? context,
{Color? color, EdgeInsetsGeometry? padding, double? height}) {
color ??= Colors.black.withAlpha(50);
return Padding(
padding: padding ??
const EdgeInsets.only(left: 18.5, right: 18.5, top: 5, bottom: 5),
child: Divider(
color: color,
height: height ?? 0.5,
),
);
}
/// 卡片容器
Widget makeCardContainer(
{required BuildContext context,
required Widget child,
Key? key,
EdgeInsetsGeometry? padding,
EdgeInsetsGeometry? paddingIn,
BorderRadius? borderRadius,
Color? color,
double? elevation,
double? width,
bool nullWidth = false}) {
return SizedBox(
key: key,
width: nullWidth ? null : (width ?? MediaQuery.of(context).size.width),
child: Padding(
padding: padding ?? const EdgeInsets.only(left: 8, right: 8),
child: Card(
color: color,
elevation: elevation ?? 0,
shape: RoundedRectangleBorder(
borderRadius: borderRadius ?? BorderRadius.circular(0),
),
child: Padding(
padding:
paddingIn ?? const EdgeInsets.only(top: 10, bottom: 10),
child: child,
))));
}
/// 通用页面加载动画
Widget makeLoadBody(BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
}
/// 通用网络图片
Widget makeNetImage(BuildContext context, String? imageUrl,
{BoxFit? fit, double? height, double? width}) {
imageUrl ??= "https://image.xiaomfzs.com/xmf-app/user_icon.png";
if (kIsWeb) {
return Image.network(imageUrl, fit: fit, height: height, width: width);
}
return CachedNetworkImage(
height: height,
width: width,
imageUrl: imageUrl,
fit: fit,
placeholder: (context, url) {
return Container(
color: Colors.black.withAlpha(50),
child: Center(
child: Image.asset("images/loading.png"),
));
},
errorWidget: (BuildContext context, String url, dynamic error) {
return const Icon(Icons.error);
},
);
}
/// 通用头像
Widget makeUserAvatar(BuildContext context, String? imgUrl,
{final double size = 46}) {
return SizedBox(
width: size,
height: size,
child: ClipRRect(
borderRadius: BorderRadius.circular(10000.0),
child: makeNetImage(context, imgUrl, fit: BoxFit.fill),
),
);
}
/// 底部按钮边距容器
Widget makeNavButtonPadding(Widget? widget) {
return Padding(
padding: const EdgeInsets.only(bottom: 14),
child: widget,
);
}
/// 底部按钮
Widget makeBottomNavButton(String text,
{required VoidCallback? onTap,
Color color = mdMainColor,
EdgeInsetsGeometry? padding,
TextStyle? textStyle,
double? elevation}) {
return Padding(
padding: padding ?? const EdgeInsets.only(left: 20, right: 20),
child: ElevatedButton(
style: TextButton.styleFrom(
backgroundColor: color, elevation: elevation ?? 1),
onPressed: onTap,
child: Text(
text,
style: textStyle ??
const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
);
}
/// 收起键盘容器
Widget makeDismissKeyboardContainer(BuildContext context, Widget child,
{VoidCallback? onDismissed}) {
return GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
if (onDismissed != null) {
onDismissed();
}
},
child: child,
);
}
/// 底部弹出按钮
Future<String?> showBottomMenu(
BuildContext context, Map<String, String> items) async {
return await showCupertinoModalPopup<String?>(
context: context,
builder: (ctx) {
return CupertinoActionSheet(
cancelButton: CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(context);
},
child: const Text(
'取消',
style: TextStyle(fontSize: 14, color: Colors.red),
),
),
actions: <Widget>[
for (var kv in (items.entries))
CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(context, kv.key);
},
child: Text(
kv.value,
style: const TextStyle(fontSize: 14, color: Colors.black),
),
),
],
);
});
}
/// 日期选择器
Future<DateTime?> showMyDatePicker(
BuildContext context, CupertinoDatePickerMode mode,
{DateTime? defaultTime, DateTime? minTime, DateTime? maxTime}) async {
final DateTime? date = await showModalBottomSheet<DateTime>(
context: context,
builder: (context) {
DateTime pickedDate = DateTime.now();
return SizedBox(
height: 250,
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
CupertinoButton(
child: const Text('取消'),
onPressed: () {
Navigator.of(context).pop();
},
),
CupertinoButton(
child: const Text('确认'),
onPressed: () {
Navigator.pop(context, pickedDate);
},
),
],
),
const Divider(
height: 0,
thickness: 1,
),
Expanded(
child: CupertinoApp(
debugShowCheckedModeBanner: false,
locale: const Locale('zh', 'CN'),
localizationsDelegates: const [
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('zh', 'CN'),
Locale('en', 'US'),
],
home: CupertinoDatePicker(
mode: mode,
initialDateTime: defaultTime ?? DateTime.now(),
minimumDate: minTime,
maximumDate: maxTime,
onDateTimeChanged: (DateTime dateTime) {
pickedDate = dateTime;
},
),
),
),
],
),
);
},
);
return date;
}
/// appBar Search Widget
PreferredSizeWidget makeAppBarSearchWidget(
BuildContext context,
String tipText,
ValueChanged<String> onSubmitted, {
double height = 56,
double? width,
TextInputType? textInputType,
List<TextInputFormatter>? inputFormatters,
ValueChanged<String>? onChanged,
TextEditingController? controller,
}) {
return PreferredSize(
child: SizedBox(
height: height,
child: Padding(
padding:
const EdgeInsets.only(top: 5, left: 10, right: 10, bottom: 10),
child: Container(
width: width ?? MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: Colors.white.withAlpha(100),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
margin: const EdgeInsets.only(left: 15),
child: const Icon(
Icons.search,
color: Colors.black45,
size: 20,
),
),
Container(
margin: const EdgeInsets.only(left: 5),
width: MediaQuery.of(context).size.width -
15 * 2 -
15 -
20 -
5 -
(width ?? 0),
child: TextField(
controller: controller,
textInputAction: TextInputAction.search,
onSubmitted: onSubmitted,
keyboardType: textInputType,
inputFormatters: inputFormatters,
onChanged: onChanged,
decoration: InputDecoration(
hintText: tipText,
hintStyle: const TextStyle(
fontSize: 15, color: Colors.black45),
border: InputBorder.none,
),
),
),
],
)),
),
),
preferredSize: const Size(0, 42));
}
Widget makeSearchWidget(
BuildContext context, String hintText, ValueChanged<String> onSubmitted) {
return Padding(
padding: const EdgeInsets.only(top: 5, left: 0, right: 0, bottom: 14),
child: Container(
height: 42,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: const Color.fromARGB(255, 247, 250, 255),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
margin: const EdgeInsets.only(left: 15),
child: const Icon(
Icons.search,
color: Colors.black45,
size: 20,
),
),
Container(
margin: const EdgeInsets.only(left: 5),
width: MediaQuery.of(context).size.width - 15 * 2 - 82,
child: TextField(
textInputAction: TextInputAction.search,
onSubmitted: onSubmitted,
onChanged: (str) {
if (str == "") {
onSubmitted("");
}
},
decoration: InputDecoration(
hintText: hintText,
hintStyle:
const TextStyle(fontSize: 15, color: Colors.black45),
border: InputBorder.none,
),
),
),
],
)),
);
}
class AlwaysDisabledFocusNode extends FocusNode {
@override
bool get hasFocus => false;
}

View File

@@ -0,0 +1,182 @@
import 'package:flutter/foundation.dart' show SynchronousFuture;
import 'package:flutter/cupertino.dart';
class CupertinoLocalizationsDelegate
extends LocalizationsDelegate<CupertinoLocalizations> {
const CupertinoLocalizationsDelegate();
@override
bool isSupported(Locale locale) =>
<String>['en', 'zh'].contains(locale.languageCode);
@override
SynchronousFuture<_DefaultCupertinoLocalizations> load(Locale locale) {
return SynchronousFuture<_DefaultCupertinoLocalizations>(
_DefaultCupertinoLocalizations(locale.languageCode));
}
@override
bool shouldReload(CupertinoLocalizationsDelegate old) => false;
}
class _DefaultCupertinoLocalizations extends CupertinoLocalizations {
_DefaultCupertinoLocalizations(this._languageCode);
final DefaultCupertinoLocalizations _en =
const DefaultCupertinoLocalizations();
final String _languageCode;
final Map<String, Map<String, String>> _dict = <String, Map<String, String>>{
'en': <String, String>{
'alert': 'Alert',
'copy': 'Copy',
'paste': 'Paste',
'cut': 'Cut',
'selectAll': 'Select all',
'today': 'today'
},
'zh': <String, String>{
'alert': '警告',
'copy': '复制',
'paste': '粘贴',
'cut': '剪切',
'selectAll': '选择全部',
'today': '今天'
}
};
@override
String get alertDialogLabel => _get('alert')!;
@override
String get anteMeridiemAbbreviation => _en.anteMeridiemAbbreviation;
@override
String get postMeridiemAbbreviation => _en.postMeridiemAbbreviation;
@override
String get copyButtonLabel => _get('copy')!;
@override
String get cutButtonLabel => _get('cut')!;
@override
String get pasteButtonLabel => _get('paste')!;
@override
String get selectAllButtonLabel => _get('selectAll')!;
@override
DatePickerDateOrder get datePickerDateOrder => _en.datePickerDateOrder;
@override
DatePickerDateTimeOrder get datePickerDateTimeOrder =>
_en.datePickerDateTimeOrder;
@override
String datePickerHour(int hour) => _en.datePickerHour(hour);
@override
String datePickerHourSemanticsLabel(int hour) =>
_en.datePickerHourSemanticsLabel(hour);
@override
String datePickerMediumDate(DateTime date) => _en.datePickerMediumDate(date);
@override
String datePickerMinute(int minute) => _en.datePickerMinute(minute);
@override
String datePickerMinuteSemanticsLabel(int minute) =>
_en.datePickerMinuteSemanticsLabel(minute);
@override
String datePickerMonth(int monthIndex) => _en.datePickerMonth(monthIndex);
@override
String datePickerYear(int yearIndex) => _en.datePickerYear(yearIndex);
@override
String timerPickerHour(int hour) => _en.timerPickerHour(hour);
@override
String timerPickerHourLabel(int hour) => _en.timerPickerHourLabel(hour);
@override
String timerPickerMinute(int minute) => _en.timerPickerMinute(minute);
@override
String timerPickerMinuteLabel(int minute) =>
_en.timerPickerMinuteLabel(minute);
@override
String timerPickerSecond(int second) => _en.timerPickerSecond(second);
@override
String timerPickerSecondLabel(int second) =>
_en.timerPickerSecondLabel(second);
String? _get(String key) {
return _dict[_languageCode]![key];
}
@override
String get todayLabel => _get("today")!;
@override
String get modalBarrierDismissLabel => _en.modalBarrierDismissLabel;
@override
String tabSemanticsLabel({required int tabIndex, required int tabCount}) {
return _en.tabSemanticsLabel(tabIndex: tabIndex, tabCount: tabCount);
}
@override
List<String> get timerPickerHourLabels => _en.timerPickerHourLabels;
@override
List<String> get timerPickerMinuteLabels => _en.timerPickerMinuteLabels;
@override
List<String> get timerPickerSecondLabels => _en.timerPickerSecondLabels;
@override
String get searchTextFieldPlaceholderLabel => throw UnimplementedError();
@override
String datePickerDayOfMonth(int dayIndex, [int? weekDay]) {
return _en.datePickerDayOfMonth(dayIndex, weekDay);
}
@override
// TODO: implement noSpellCheckReplacementsLabel
String get noSpellCheckReplacementsLabel => throw UnimplementedError();
@override
String datePickerStandaloneMonth(int monthIndex) {
// TODO: implement datePickerStandaloneMonth
throw UnimplementedError();
}
@override
// TODO: implement lookUpButtonLabel
String get lookUpButtonLabel => throw UnimplementedError();
@override
// TODO: implement menuDismissLabel
String get menuDismissLabel => throw UnimplementedError();
@override
// TODO: implement searchWebButtonLabel
String get searchWebButtonLabel => throw UnimplementedError();
@override
// TODO: implement shareButtonLabel
String get shareButtonLabel => throw UnimplementedError();
@override
// TODO: implement clearButtonLabel
String get clearButtonLabel => throw UnimplementedError();
}

View File

@@ -0,0 +1,52 @@
import 'package:timeago/timeago.dart';
/// Chinese-China messages
class ZhCnMessages implements LookupMessages {
@override
String prefixAgo() => '';
@override
String prefixFromNow() => '';
@override
String suffixAgo() => '';
@override
String suffixFromNow() => '';
@override
String lessThanOneMinute(int seconds) => '不到一分钟';
@override
String aboutAMinute(int minutes) => '约 1 分钟';
@override
String minutes(int minutes) => '$minutes 分钟';
@override
String aboutAnHour(int minutes) => '约 1 小时';
@override
String hours(int hours) => '$hours 小时';
@override
String aDay(int hours) => '约 1 天';
@override
String days(int days) => '$days';
@override
String aboutAMonth(int days) => '约 1 个月';
@override
String months(int months) => '$months';
@override
String aboutAYear(int year) => '约 1 年';
@override
String years(int years) => '$years';
@override
String wordSeparator() => '';
}

87
lib/common/push/push.dart Normal file
View File

@@ -0,0 +1,87 @@
import 'package:flutter/cupertino.dart';
class YJPush {
static Future presentWidget(BuildContext context, Widget widget) {
return Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return widget;
},
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
// 添加一个平移动画
return YJPush.createTransition(animation, child);
},
opaque: false,
barrierDismissible: true));
}
static void presentWidgetNoAnimation(BuildContext context, Widget widget) {
Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return widget;
},
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
// 添加一个平移动画
return YJPush.createNoAnimationTransition(animation, child);
},
opaque: false,
barrierDismissible: true));
}
/// 创建一个平移变换
/// 跳转过去查看源代码,可以看到有各种各样定义好的变换
static SlideTransition createTransition(
Animation<double> animation, Widget child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: const Offset(0.0, 0.0),
).animate(animation),
child: child, // child is the value returned by pageBuilder
);
}
/// 创建一个平移变换 没有动画
static SlideTransition createNoAnimationTransition(
Animation<double> animation, Widget child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 0.0),
end: const Offset(0.0, 0.0),
).animate(animation),
child: child, // child is the value returned by pageBuilder
);
}
static Future pushWidget(BuildContext context, Widget widget) async {
Route route = CupertinoPageRoute(builder: (context) => widget);
return await Navigator.push(context, route);
}
static Future pushAndRemoveWidget(BuildContext context, Widget widget) async {
Route route = CupertinoPageRoute(builder: (context) => widget);
return await Navigator.pushAndRemoveUntil(context, route, (route) => false);
}
static SlideTransition createHTransition(
Animation<double> animation, Widget child) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: const Offset(0.0, 0.0),
).animate(animation),
child: child, // child is the value returned by pageBuilder
);
}
}

View File

@@ -0,0 +1,8 @@
bool isEmptyString(String? str) {
if (str == null || str.isEmpty) {
return true;
}
return false;
}