news 2026/4/23 17:07:58

Flutter for HarmonyOS 开发指南(三):登录页面开发,使用DIO发起http请求

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter for HarmonyOS 开发指南(三):登录页面开发,使用DIO发起http请求

前言

本文将从前端开发者的视角出发,快速带你上手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 的布局思想与前端非常相似:

  • Containerdiv

  • Column/Rowflex-direction: column / row

  • 一切 UI 都是 Widget,可无限嵌套


盒子阴影(BoxShadow)

CSS 支持多层阴影,Flutter 同样支持:

boxShadow: [ BoxShadow(...), BoxShadow(...), ]

CSS 属性Flutter 参数说明
rgba(0,0,0,0.1)color阴影颜色
0px 4pxoffset: Offset(0, 4)偏移量
10pxblurRadius模糊程度
1pxspreadRadius扩散大小

实现DIO 发起Http请求

一.安装 Dio

1.打开你的pubspec.yaml

2.在dependencies:下添加:

dependencies: flutter: sdk: flutter dio: ^5.4.0

3.执行命令:

flutter pub get

4.或者一步执行安装(推荐)

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 10:41:34

如何用一张3060跑通Llama3?低成本GPU部署教程入门必看

如何用一张3060跑通Llama3&#xff1f;低成本GPU部署教程入门必看 你是不是也遇到过这些情况&#xff1a;想本地跑个大模型&#xff0c;但显卡只有RTX 3060&#xff08;12GB显存&#xff09;&#xff0c;查了一圈发现主流教程动辄推荐A100、4090&#xff0c;甚至要求双卡&…

作者头像 李华
网站建设 2026/4/23 12:14:28

多主模式下I2C总线仲裁过程全面讲解

以下是对您提供的博文《多主模式下IC总线仲裁过程全面讲解》的 深度润色与专业重构版本 。本次优化严格遵循您的全部要求: ✅ 彻底去除AI痕迹,全文以资深嵌入式系统工程师第一人称视角口吻撰写,语言自然、有节奏、带思考痕迹; ✅ 所有模块化标题(如“引言”“总结与展…

作者头像 李华
网站建设 2026/4/23 12:11:32

PyTorch预装库调用实战:pandas数据处理代码实例

PyTorch预装库调用实战&#xff1a;pandas数据处理代码实例 1. 为什么不用自己装pandas&#xff1f;开箱即用的开发环境真香 你有没有过这样的经历&#xff1a;刚配好PyTorch环境&#xff0c;兴冲冲想读个CSV文件做数据探索&#xff0c;结果import pandas as pd报错——“Mod…

作者头像 李华
网站建设 2026/4/23 10:44:36

CANFD协议全面讲解:从基础到应用入门

以下是对您提供的博文《CANFD协议全面讲解:从基础到应用入门》的 深度润色与结构化重构版本 。本次优化严格遵循您的要求: ✅ 彻底去除AI痕迹,强化“人类专家口吻”与工程现场感 ✅ 摒弃模板化标题(如“引言”“总结”),代之以自然、有张力的技术叙事逻辑 ✅ 所有技…

作者头像 李华
网站建设 2026/4/23 12:11:56

实战语音识别预处理:FSMN-VAD离线镜像让VAD检测更简单

实战语音识别预处理&#xff1a;FSMN-VAD离线镜像让VAD检测更简单 1. 为什么语音识别前必须做端点检测&#xff1f; 你有没有试过把一段5分钟的会议录音直接喂给语音识别模型&#xff1f;结果可能是&#xff1a;开头30秒静音、中间多次长时间停顿、结尾还有20秒环境噪音——这…

作者头像 李华
网站建设 2026/4/23 9:18:35

【Python 基础】命名一

目录 1. 它是程序员之间的“潜规则” 2. 为什么要在这里初始化为 None&#xff1f; 3. 下划线的家族成员 举个直观的例子 1. 它是程序员之间的“潜规则” Python 语言本身并不像 Java 或 C 那样有严格的 private 关键字来禁止外部访问某个变量。 无下划线 (current_rgb)&am…

作者头像 李华