前言

本文将从前端开发者的视角出发,快速带你上手 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 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

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐