Flutter for HarmonyOS 开发指南(三):登录页面开发,使用DIO发起http请求
本文介绍了使用Flutter开发HarmonyOS登录页面的完整流程。主要内容包括:1) 环境搭建和项目创建;2) 实现登录页面UI,包含Logo、输入框和按钮;3) 处理用户输入验证;4) 集成Dio库实现HTTP请求;5) 添加加载状态和错误处理。文章通过对比CSS和Flutter的样式写法,帮助前端开发者快速上手Flutter开发,并提供了完整的代码示例实现登录功能。
前言
本文将从前端开发者的视角出发,快速带你上手 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≈div -
Column/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.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' })
Dio GET 请求示例:
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
更多推荐



所有评论(0)