下面介绍 Flutter 最基本的通用项目框架搭建,同时实现了一个登录界面图标和登录界面。
先看下效果图:
- 使用
ScreenUtilInit
自适应界面大小; - 使用
Stack
支持多个子界面在同一个全屏主界面上选择显示; - 使用 Get 插件实现界面之间的跳转和国际化翻译;
- 界面都通过
Transform
实现了鼠标移动界面; - 使用
Controller.dart
管理所有全局变量和界面控制器;
一、项目目录结构
asserts\images
:存放图片资源文件的目录;Translation.dart
:翻译文件;Controller.dart
:全局变量和对应控制器的定义;LogoWidget.dart
:入口图标界面;LoginWidget.dart
:登录界面;main.dart
:主界面;
二、代码实现与分析
2.1 pubspec.yaml
pubspec.yaml
的内容如下:
dependencies:
flutter:
sdk: flutter
flutter_screenutil: ^5.9.3
get: ^4.6.5
flutter:
assets:
- assets/images/
因为用到了 Get 插件与 ScreenUtilInit,所以需要加上这两种的依赖;另外定义了图片资源文件的路径;
2.2 Translation.dart
实现了中文简体、中文繁体和英文的语言切换,翻译文件如下所示:
import 'package:get/get.dart';
// (2)自定义自己的国际化字符串
class Translation extends Translations {
@override
Map<String, Map<String, String>> get keys => {
// 1-配置中文简体
'zh_CN': {
'登录': '登录',
'用户协议未选中': '用户协议未选中',
'请勾选用户协议': '请勾选用户协议',
'用户名异常': '用户名异常',
'用户名为空': '用户名为空',
'密码异常': '密码异常',
'密码为空': '密码为空',
'用户名、密码正确': '用户名、密码正确',
'去登陆': '去登陆',
'用户': '用户',
'密码': '密码',
'同意': '同意',
'<服务协议>': '<服务协议>',
'<隐私政策>': '<隐私政策>',
},
// 2-配置中文繁体
'zh_HK': {
'登录': '登錄',
'用户协议未选中': '用戶協議未選中',
'请勾选用户协议': '請勾選用戶協議',
'用户名异常': '用戶名異常',
'用户名为空': '用戶名爲空',
'密码异常': '密碼異常',
'密码为空': '密碼爲空',
'用户名、密码正确': '用戶名、密碼正確',
'去登陆': '去登陸',
'用户': '用戶',
'密码': '密碼',
'同意': '同意',
'<服务协议>': '<服務協議>',
'<隐私政策>': '<隱私政策>',
},
// 3-配置英文
'en_US': {
'登录': 'Login',
'用户协议未选中': 'User agreement not selected',
'请勾选用户协议': 'Please check the user agreement',
'用户名异常': 'Abnormal username',
'用户名为空': 'The username is empty',
'密码异常': 'Password exception',
'密码为空': 'Password is empty',
'用户名、密码正确': 'The username and password are correct',
'去登陆': 'Go log in',
'用户': 'user',
'密码': 'password',
'同意': 'agree with',
'<服务协议>': '<Service Agreement>',
'<隐私政策>': '<Privacy Policy>',
}
};
}
这里使用了 Get 插件方式实现国际化翻译,具体可参考:Flutter插件Get(7):实现语言的国际化 - fengMisaka - 博客园
2.3 Controller.dart
全局变量和对应控制器的定义:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'LogoWidget.dart';
import 'LoginWidget.dart';
// state只专注数据,需要使用数据,直接通过state获取
// logic只专注于触发事件交互,操作或更新数据
// view只专注UI显示
// 全局状态
class GlobalState {
final screenSize = const Size(1920,1080).obs; // 屏幕尺寸
var language = const Locale('zh', 'CN').obs; //语言参数
}
// 全局变量控制器
class GlobalController extends GetxController {
// 全局变量, 内部调用
final GlobalState _globalState = GlobalState();
// 获取屏幕尺寸与设置屏幕尺寸的函数
Size get screenSize => _globalState.screenSize.value;
set screenSize(Size value) => _globalState.screenSize.value = value;
// 获取当前语言与设置当前语言的函数
Locale get language => _globalState.language.value;
set language(Locale language) => () {
_globalState.language.value = language;
Get.updateLocale(language);
}();
}
// 定义全局变量控制器
final GlobalController globalCtrl = Get.put(GlobalController());
// 初始化通用配置
void initCommomCfg() {
Get.lazyPut<LoginController>(() => LoginController());
Get.lazyPut<LogoControl>(() => LogoControl());
}
2.4 LogoWidget.dart
入口图标界面:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'LoginWidget.dart';
// 状态类
class LogoState {
final _offset = Offset.zero.obs; // 平移距离
final _isVisable = true.obs; // 是否显示的变量
final _x = 50.0.obs; // 水平方向的边距
final _y = 20.0.obs; // 垂直方向的边距
}
// 控制器类
class LogoControl extends LogoState {
final _state = LogoState();
// 控制函数实现,以下类似
Offset get offset => _state._offset.value;
set offset(Offset value) => _state._offset.value = value;
bool get isVisable => _state._isVisable.value;
set setVisable(bool val) => _state._isVisable.value = val;
double get x => _state._x.value;
set x(double value) => _state._x.value = value;
double get y => _state._y.value;
set y(double value) => _state._y.value = value;
}
class LogoWidget extends StatelessWidget {
LogoWidget({super.key});
// 实现控制器
final LogoControl logoControl = Get.find<LogoControl>();
final LoginController loginControl = Get.find<LoginController>();
@override
Widget build(BuildContext context) {
return Positioned (
right: logoControl.x.w,
top: logoControl.y.h,
child: Obx(() => Transform.translate ( // 让部件在 x、y 轴上平移指定的距离
offset: logoControl.offset, // 平移距离
child: GestureDetector( // 手势识别组件
behavior: HitTestBehavior.opaque,
child: Visibility( // 是一个用于根据布尔值条件显示或隐藏小部件的控件
visible: loginControl.isHidden(),
maintainState: true,
child: MouseRegion( // 以让鼠标移动到该组件上时光标为"选中样式"
cursor: SystemMouseCursors.click, // 光标为"选中样式"
child: IconButton(
mouseCursor: SystemMouseCursors.click,
onPressed: null,
iconSize: 45.w,
icon: Image.asset("assets/images/btn_logo.png"), // 显示图标
),
),
),
// 按压拖动回调,以支持鼠标移动界面
onPanUpdate: (details) {
// 通过修改平移距离变量来移动界面
logoControl.offset += details.delta;
},
// 点击事件回调
onTap: () {
// 显示登录界面
loginControl.show();
},
),
))
);
}
}
2.5 LoginWidget.dart
登录界面:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'Controller.dart';
// 状态类
class LoginState {
final _isHidden = true.obs; // 是否隐藏
final _width = 400.0.obs; // 宽度
final _height = 280.0.obs; // 高度
final _offset = const Offset(0, 0).obs; // 位置
final _isLogined = false.obs; // 是否登陆完成
final _x = 0.0.obs; // 水平方向的边距
final _y = 0.0.obs; // 垂直方向的边距
}
// 控制器类
class LoginController extends GetxController {
final LoginState state = LoginState();
double get width => state._width.value;
set width(double value) => state._width.value = value;
double get height => state._height.value;
set height(double value) => state._height.value = value;
Offset get offset => state._offset.value;
set offset(Offset value) => state._offset.value = value;
bool get isLogined => state._isLogined.value;
set isLogined(bool flag) => state._isLogined.value = flag;
double get x => state._x.value;
set x(double value) => state._x.value = value;
double get y => state._y.value;
set y(double value) => state._y.value = value;
// 是否隐藏
bool isHidden() {
return state._isHidden.value;
}
// 显示
void show() {
state._isHidden.value = false;
}
// 隐藏
void hide() {
state._isHidden.value = true;
}
// 设置窗口显示/隐藏状态
void setVisable(bool isVisable)
{
state._isHidden.value = !isVisable;
}
// 移动
void move(double x, double y) {
state._offset.value = Offset(x, y);
}
// 登陆按钮点击事件
login(TextEditingController userNameController,
TextEditingController passWordController) {
var userName = userNameController.text;
var passWord = passWordController.text;
// 1-用户协议是否勾选
if (!isChecked.value) {
Get.snackbar('用户协议未选中'.tr, '请勾选用户协议'.tr, snackPosition: SnackPosition.BOTTOM);
return;
}
// 2-用户名判断
if (userName.isEmpty) {
Get.snackbar('用户名异常'.tr, '用户名为空'.tr, snackPosition: SnackPosition.BOTTOM);
return;
}
// 3-密码判断
if (passWord.isEmpty) {
Get.snackbar('密码异常'.tr, '密码为空'.tr, snackPosition: SnackPosition.BOTTOM);
return;
}
Get.snackbar('用户名、密码正确'.tr, '去登陆'.tr, snackPosition: SnackPosition.BOTTOM);
}
// 用户协议勾选事件
var isChecked = false.obs;
void changeChecked(bool value) {
isChecked.value = value;
}
}
// 登陆界面
class LoginWidget extends StatelessWidget {
LoginWidget({super.key});
final userNameController = TextEditingController();
final passWordController = TextEditingController();
final LoginController controller = Get.find<LoginController>(); // 登录界面控制器
@override
Widget build(BuildContext context) {
return Positioned (
left: (globalCtrl.screenSize.width - controller.width)/2,
top: (globalCtrl.screenSize.height - controller.height)/2,
child: Obx(() => Transform.translate ( // 让部件在 x、y 轴上平移指定的距离
offset: controller.offset, // 平移距离
child: GestureDetector( // 手势识别组件,以让鼠标移动到该组件上时光标为"选中样式"
behavior: HitTestBehavior.opaque,
child: Visibility( // 是一个用于根据布尔值条件显示或隐藏小部件的控件
visible: !controller.isHidden(), // 控制是否显示
maintainState: true,
child:Container(
width: controller.width,
height: controller.height,
padding: const EdgeInsets.symmetric(horizontal: 0.0),
decoration: const BoxDecoration(
color: Colors.grey,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 登录界面标题栏
LoginTabBar(),
// 距离上一个View距离
const SizedBox(height: 12),
// 下方编辑界面
buildInputWidget(),
],
),
)
),
// 按压拖动回调,以支持鼠标移动界面
onPanUpdate: (details) {
controller.offset += details.delta;
}
)
)
)
);
}
// 下方编辑界面
Widget buildInputWidget() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0), // 两侧边距
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextField(
controller: userNameController,
decoration: InputDecoration(labelText: '用户'.tr),
style: const TextStyle(fontSize: 16),
keyboardType: TextInputType.phone,
),
const SizedBox(height: 12), //距离上一个View距离
TextField(
controller: passWordController,
obscureText: true,
decoration: InputDecoration(labelText: "密码".tr),
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 12), //距离上一个View距离
buildPrivacyWidget(), //隐私政策
const SizedBox(height: 12), //距离上一个View距离
SizedBox(
width: controller.width-32,
child: ElevatedButton(
child: Text('登录'.tr),
onPressed: () {
debugPrint("ElevatedButton Click");
controller.login(userNameController, passWordController);
},
)
),
const SizedBox(height: 12), //距离上一个View距离
],
)
);
}
// 隐私协议勾选框
Widget buildPrivacyWidget() {
return Row(
children: [
Obx(() => Checkbox(
value: controller.isChecked.value,
onChanged: (value) => controller.changeChecked(value!))),
Text('同意'.tr, style: const TextStyle(fontSize: 14)),
Text('<服务协议>'.tr,
style: const TextStyle(fontSize: 14, color: Colors.blue)),
Text('<隐私政策>'.tr, style: const TextStyle(fontSize: 14, color: Colors.blue))
],
);
}
}
// 登录界面标题栏
class LoginTabBar extends StatelessWidget {
LoginTabBar({super.key});
final LoginController loginCtrl = Get.find<LoginController>();
@override
Widget build(BuildContext context) {
return Container(
height: 44.h,
color: const Color.fromARGB(128, 20, 45, 86),
child: Flex(
direction: Axis.horizontal,
children: <Widget>[
const SizedBox(width: 6),
SizedBox(
width: 40.h,
height: 40.h,
child: Obx(() => IconButton(
onPressed: () {
if ( globalCtrl.language == const Locale('zh', 'CN') )
{
globalCtrl.language = const Locale('zh', 'HK');
}
else if ( globalCtrl.language == const Locale('zh', 'HK') )
{
globalCtrl.language = const Locale('en', 'US');
}
else if ( globalCtrl.language == const Locale('en', 'US') )
{
globalCtrl.language = const Locale('zh', 'CN');
}
},
icon: () {
if(globalCtrl.language == const Locale('zh', 'CN'))
{
return Image.asset("assets/images/btn_Chinese_jianti.png", width: 40.w, height: 40.h);
}
else if(globalCtrl.language == const Locale('zh', 'HK'))
{
return Image.asset("assets/images/btn_Chinese_fanti.png", width: 40.w, height: 40.h);
}
else if(globalCtrl.language == const Locale('en', 'US'))
{
return Image.asset("assets/images/btn_English.png", width: 40.w, height: 40.h);
}
else
{
return const Icon(null);
}
}(),
padding: EdgeInsets.zero,
)),
),
const SizedBox(width: 6),
Expanded(
flex: 15,
child: Text(
"登录".tr,
style: const TextStyle(
color: Colors.white,
fontSize: 16.0,)
)
),
SizedBox(
width: 30.h,
height: 30.h,
child: IconButton(
onPressed: () {
loginCtrl.hide(); // 隐藏登录界面
},
icon: Icon(
Icons.close,
size: 30.w,
),
padding: EdgeInsets.zero,
),
),
const SizedBox(width: 6),
],
),
);
}
}
2.6 main.dart
主界面:
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'Controller.dart';
import 'Translation.dart';
import 'LogoWidget.dart';
import 'LoginWidget.dart';
// 主函数
main(List<String> args) {
// 初始化通用配置
initCommomCfg();
runApp(MainApp());
}
// 主界面
class MainApp extends StatelessWidget {
MainApp({super.key});
// 各界面的实例
final LoginWidget loginWidget = LoginWidget();
final LogoWidget logoWidget = LogoWidget();
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: Size(1920, 1080),
builder: (context, child) {
return GetMaterialApp(
// 配置GetMaterialApp
translations: Translation(), // 你的翻译
locale: const Locale('zh', 'CN'), // 将会按照此处指定的语言翻译
fallbackLocale: const Locale('en', 'US'), // 添加一个回调语言选项,以备上面指定的语言翻译不存在
debugShowCheckedModeBanner: false,
theme: ThemeData(fontFamily: "Ali"),
home: Scaffold(
backgroundColor: Colors.white,
body: Stack( // 使用Stack以同时选择显示多个子界面在同一个主界面中
alignment: Alignment.center,
children: <Widget>[
logoWidget,
loginWidget,
],
),
),
);
},
);
}
}
三、程序下载
程序下载:Flutter_Demo/CommonFrame-1 at main · confidentFeng/Flutter_Demo
标签:const,final,项目,value,dart,state,._,Flutter,搭建 From: https://www.cnblogs.com/linuxAndMcu/p/18552904