前言
本文将从前端开发者的视角出发,快速带你上手Flutter for HarmonyOS的页面开发,重点介绍:
Flutter 页面结构
布局与样式的实现方式
常见 UI 组件的使用
使用 Dio 发起 HTTP 请求
通过一个完整的登录页面示例,快速建立 Flutter 页面开发的整体认知。
准备工作
在开始之前,请确保你已经完成以下准备:
✅ Flutter for HarmonyOS 开发环境已搭建完成
✅ 已成功创建 Flutter Hello World 项目
✅ 可以正常运行并看到页面
开发目标
本示例将实现一个登录页面,页面包含以下元素:
应用 Logo
账号输入框
密码输入框
登录按钮
项目代码结构
lib/ ├── main.dart └── pages/ └── login.dart页面开发
开发一个登录页面,页面包含着应用logo,账号密码输入框,登录按钮。
代码目录
lib/main.dart 代码修改如下:
import 'package:flutter/material.dart'; import 'pages/login.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.red.shade500), appBarTheme: const AppBarTheme( // 设置背景色(可选) // backgroundColor: Colors.white, // 设置标题文本的全局样式 titleTextStyle: TextStyle( color: Colors.black, // 全局标题颜色 fontSize: 18, // 全局字体大小 fontWeight: FontWeight.bold, ), ), ), home: const LoginPage(), ); } }lib/pages/login.dart代码如下:
import 'package:flutter/material.dart'; // 1. 改为 StatefulWidget,因为我们需要持有 Input 的状态(Controller) class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override State<LoginPage> createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { // 2. 定义两个控制器 (相当于 Vue 的 data/v-model) // 它们负责监听和获取输入框的内容 final TextEditingController _accountController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); // 3. 登录逻辑方法 void _handleLogin() { // 获取输入框的文本 final String account = _accountController.text; final String password = _passwordController.text; // 判断账号是否为空 if (account.isEmpty) { // 弹出提示框 (SnackBar) ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('请输入账号'))); return; // 中断执行 } // 判断密码是否为空 if (password.isEmpty) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('请输入密码'))); return; } // 如果都通过了 print('验证通过,开始登录...'); print('账号: $account, 密码: $password'); // 这里可以写后续的 API 请求逻辑 ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('登录成功!'), backgroundColor: Colors.green), ); } // 4. 销毁控制器 (好习惯:页面关闭时释放内存) @override void dispose() { _accountController.dispose(); _passwordController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('登录'), centerTitle: true), // 标题居中 body: Container( padding: const EdgeInsets.all(12), width: double.infinity, child: Column( // mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ Container( width: 100, height: 100, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), image: const DecorationImage( image: AssetImage('assets/images/logo.png'), fit: BoxFit.cover, // 类似 CSS 的 object-fit: cover,填满容器不变形 ), boxShadow: [ BoxShadow( color: Colors.black12, blurRadius: 8, offset: Offset(0, 5), ), ], ), ), const SizedBox(height: 16), TextField( controller: _accountController, // 绑定控制器 decoration: const InputDecoration( labelText: '账号', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: _passwordController, // 绑定控制器 decoration: const InputDecoration( labelText: '密码', border: OutlineInputBorder(), ), obscureText: true, ), const SizedBox(height: 16), SizedBox( width: double.infinity, height: 45, child: ElevatedButton( onPressed: _handleLogin, child: const Text('登录'), ), ), ], ), ), ); } }为什么要用 StatefulWidget?
和 Vue / React 一样,当页面需要维护状态时,就必须使用有状态组件。
在本页面中,我们需要:
监听输入框内容
控制 loading 状态
响应按钮点击事件
因此需要使用StatefulWidget。
Flutter 与 CSS 的一些类比
颜色差异
Flutter 使用Color类,常见写法:Colors.black.withOpacity(0.1)
等价于 CSS:rgba(0, 0, 0, 0.1)
容器嵌套规则
Flutter 的布局思想与前端非常相似:
Container≈divColumn/Row≈flex-direction: column / row一切 UI 都是 Widget,可无限嵌套
盒子阴影(BoxShadow)
CSS 支持多层阴影,Flutter 同样支持:
boxShadow: [ BoxShadow(...), BoxShadow(...), ]
| CSS 属性 | Flutter 参数 | 说明 |
|---|---|---|
| rgba(0,0,0,0.1) | color | 阴影颜色 |
| 0px 4px | offset: Offset(0, 4) | 偏移量 |
| 10px | blurRadius | 模糊程度 |
| 1px | spreadRadius | 扩散大小 |
实现DIO 发起Http请求
一.安装 Dio
1.打开你的pubspec.yaml
2.在dependencies:下添加:
dependencies: flutter: sdk: flutter dio: ^5.4.03.执行命令:
flutter pub get4.或者一步执行安装(推荐)
flutter pub add dio二.编写请求代码
核心逻辑对比 (JS vs Dart):
JS (Axios): const res = await axios.post('/login', { name: 'xx' })
Dart (Dio): final res = await dio.post('/login', data: { 'name': 'xx' })
DioGET请求示例:
import 'package:dio/dio.dart'; final dio = Dio(); void request() async { Response response; response = await dio.get('/test?id=12&name=dio'); print(response.data.toString()); // The below request is the same as above. response = await dio.get( '/test', queryParameters: {'id': 12, 'name': 'dio'}, ); print(response.data.toString()); }Dio Post请求示例:
response = await dio.post('/test', data: {'id': 12, 'name': 'dio'});login.dart实现代码如下:
import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; // 1. 引入 Dio // 1. 改为 StatefulWidget,因为我们需要持有 Input 的状态(Controller) class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override State<LoginPage> createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { // 2. 定义两个控制器 (相当于 Vue 的 data/v-model) // 它们负责监听和获取输入框的内容 final TextEditingController _accountController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); // 增加一个 loading 状态,用来控制按钮转圈圈 bool _isLoading = false; // 3. 登录逻辑方法 void _handleLogin() async { // 获取输入框的文本 final String account = _accountController.text; final String password = _passwordController.text; // 判断账号是否为空 if (account.isEmpty) { // 弹出提示框 (SnackBar) ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('请输入账号'))); return; // 中断执行 } // 判断密码是否为空 if (password.isEmpty) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('请输入密码'))); return; } // --- 开始请求 --- // A. 设置 loading 为 true,让界面显示加载中 setState(() { _isLoading = true; }); // B. 创建 Dio 实例 (实际开发中通常会封装成一个单例 Global) final dio = Dio(); print("登录1. 创建 Dio 实例"); try { // C. 发送 POST 请求 (模拟请求 reqres.in 的测试接口) // 注意:await 等待结果 final response = await dio.post( 'https://reqres.in/api/login', // 这是一个免费的测试 API data: { 'email': account, // 测试账号用: eve.holt@reqres.in 'password': password, // 测试密码用: cityslicka }, // 如果需要 header 可以在这里加 options: Options(...) ); print("登录2. 发送 POST 请求"); // D. 处理结果 if (response.statusCode == 200) { // Dio 会自动把 JSON 转成 Map/List,不需要 JSON.parse final data = response.data; final token = data['token']; // 获取返回的 token print('登录成功,Token: $token'); if (!mounted) return; // ⚠️ 异步操作后检查页面是否还在,防止报错 ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('登录成功!Token: $token'), backgroundColor: Colors.green, ), ); // TODO: 这里通常会跳转页面: Navigator.push(...) } } on DioException catch (e) { print("登录3. 捕获 Dio 错误: $e"); // E. 捕获 Dio 专门的错误 (类似 Axios 的 catch) String errorMsg = '请求失败'; if (e.response != null) { // 服务器返回了错误状态码 (4xx, 5xx) errorMsg = e.response?.data['error'] ?? '服务器错误'; } else { // 网络问题 errorMsg = '网络连接异常'; } if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(errorMsg), backgroundColor: Colors.red), ); } finally { // F. 无论成功失败,都关闭 loading if (mounted) { setState(() { _isLoading = false; }); } } } // 4. 销毁控制器 (好习惯:页面关闭时释放内存) @override void dispose() { _accountController.dispose(); _passwordController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('登录'), centerTitle: true), // 标题居中 body: Container( padding: const EdgeInsets.all(12), width: double.infinity, child: Column( // mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ Container( width: 100, height: 100, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), image: const DecorationImage( image: AssetImage('assets/images/logo.png'), fit: BoxFit.cover, // 类似 CSS 的 object-fit: cover,填满容器不变形 ), boxShadow: [ BoxShadow( color: Colors.black12, blurRadius: 8, offset: Offset(0, 5), ), ], ), ), const SizedBox(height: 16), TextField( controller: _accountController, // 绑定控制器 decoration: const InputDecoration( labelText: '账号', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), TextField( controller: _passwordController, // 绑定控制器 decoration: const InputDecoration( labelText: '密码', border: OutlineInputBorder(), ), obscureText: true, ), const SizedBox(height: 16), SizedBox( width: double.infinity, height: 45, child: ElevatedButton( // loading 时禁用按钮,防止重复点击 onPressed: _isLoading ? null : _handleLogin, child: _isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Text('登录'), ), ), ], ), ), ); } }运行效果
总结
通过这个示例,你已经完成了:
Flutter 页面结构搭建
基础布局与样式开发
前端思维向 Flutter 的迁移
使用 Dio 发起 HTTP 请求
登录页面完整实现
对前端开发者来说,Flutter 的学习曲线远没有想象中陡峭。
欢迎加入开源鸿蒙跨平台社区 https://openharmonycrossplatform.csdn.net
Dio文档地址:https://pub.dev/packages/dio