下面介绍 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: flutterflutter_screenutil: ^5.9.3get: ^4.6.5flutter:assets:- assets/images/
因为用到了 Get 插件与 ScreenUtilInit,所以需要加上这两种的依赖;另外定义了图片资源文件的路径;
2.2 Translation.dart
实现了中文简体、中文繁体和英文的语言切换,翻译文件如下所示:
import 'package:get/get.dart';// (2)自定义自己的国际化字符串
class Translation extends Translations {@overrideMap<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>();@overrideWidget 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>(); // 登录界面控制器 @overrideWidget 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>();@overrideWidget 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();@overrideWidget build(BuildContext context) {return ScreenUtilInit(designSize: Size(1920, 1080),builder: (context, child) {return GetMaterialApp(// 配置GetMaterialApptranslations: 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