下面介紹 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