在这里插入图片描述

前言

用户想改个昵称、换个头像,就得来编辑资料页面。这个页面功能不复杂,但涉及到表单处理和状态管理,是个很好的练手场景。本文将详细介绍如何在Flutter for OpenHarmony环境下实现一个完整的编辑资料页面,包括表单初始化、头像上传、输入验证以及数据持久化等核心技术点。

技术要点概览

在开始实现之前,让我们先了解本页面涉及的核心技术点:

  • TextEditingController:表单输入控制器的使用
  • StatefulWidget:需要管理表单状态
  • Stack组件:头像和相机图标的叠加布局
  • 表单验证:输入内容的有效性检查
  • GetX状态管理:与ProfileController的交互

页面初始化

编辑资料页面需要显示用户当前的信息,所以要在初始化时获取数据:

class EditProfilePage extends StatefulWidget {
  const EditProfilePage({super.key});

  
  State<EditProfilePage> createState() => _EditProfilePageState();
}

class _EditProfilePageState extends State<EditProfilePage> {
  // 使用late关键字延迟初始化
  late TextEditingController _nameController;
  // 获取ProfileController实例
  final controller = Get.find<ProfileController>();

  
  void initState() {
    super.initState();
    // 用当前用户名初始化输入框
    _nameController = TextEditingController(text: controller.userName.value);
  }

初始化要点说明

关键点TextEditingControllerinitState里初始化,并且用当前的用户名作为初始值。这样用户进入页面时,输入框里已经有了现在的昵称,用户可以在此基础上修改。

late关键字告诉Dart这个变量会在使用前初始化,避免空安全检查报错。

资源释放

别忘了在dispose中释放Controller:

  
  void dispose() {
    _nameController.dispose();
    super.dispose();
  }

页面结构

页面包含头像区域和昵称输入框,AppBar右边有保存按钮:

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('编辑资料'),
        actions: [
          TextButton(
            onPressed: _save,
            child: const Text('保存', style: TextStyle(color: Colors.white)),
          ),
        ],
      ),

保存按钮放在AppBar右边是常见的设计模式,用户改完资料后顺手就能点保存。

头像区域

头像区域展示当前头像,右下角有个相机图标表示可以更换:

      body: Padding(
        padding: EdgeInsets.all(16.w),
        child: Column(
          children: [
            Center(
              child: Stack(
                children: [
                  // 头像圆形容器
                  CircleAvatar(
                    radius: 50.r,
                    backgroundColor: AppTheme.primaryColor.withOpacity(0.2),
                    child: Icon(Icons.person, size: 60.sp, color: AppTheme.primaryColor),
                  ),
                  // 相机图标定位到右下角
                  Positioned(
                    bottom: 0,
                    right: 0,
                    child: Container(
                      padding: EdgeInsets.all(4.w),
                      decoration: const BoxDecoration(
                        color: AppTheme.primaryColor,
                        shape: BoxShape.circle,
                      ),
                      child: Icon(Icons.camera_alt, size: 20.sp, color: Colors.white),
                    ),
                  ),
                ],
              ),
            ),

Stack布局说明

Stack组件实现头像和相机图标的叠加。Positioned把相机图标定位到右下角。

交互提示:相机图标暗示用户这里可以点击更换头像。实际项目中点击后应该弹出选择框,让用户从相册选择或拍照。

头像点击处理

GestureDetector(
  onTap: _showAvatarOptions,
  child: Stack(
    // ... 头像内容
  ),
)

void _showAvatarOptions() {
  Get.bottomSheet(
    Container(
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ListTile(
            leading: Icon(Icons.camera_alt),
            title: Text('拍照'),
            onTap: () {
              Get.back();
              _takePhoto();
            },
          ),
          ListTile(
            leading: Icon(Icons.photo_library),
            title: Text('从相册选择'),
            onTap: () {
              Get.back();
              _pickFromGallery();
            },
          ),
          ListTile(
            leading: Icon(Icons.person),
            title: Text('使用默认头像'),
            onTap: () {
              Get.back();
              _useDefaultAvatar();
            },
          ),
        ],
      ),
    ),
  );
}

昵称输入框

输入框使用Material Design风格,带有浮动标签:

            SizedBox(height: 32.h),
            TextField(
              controller: _nameController,
              decoration: InputDecoration(
                labelText: '昵称',
                filled: true,
                fillColor: Colors.white,
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(12.r),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

输入框设计说明

输入框用labelText而不是hintText,这样标签会在输入时浮动到上方,用户始终能看到这是昵称输入框。

增强版输入框

TextField(
  controller: _nameController,
  maxLength: 20,  // 限制最大长度
  decoration: InputDecoration(
    labelText: '昵称',
    hintText: '请输入2-20个字符',
    filled: true,
    fillColor: Colors.white,
    prefixIcon: Icon(Icons.person_outline),
    suffixIcon: IconButton(
      icon: Icon(Icons.clear),
      onPressed: () => _nameController.clear(),
    ),
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12.r),
    ),
    // 错误提示样式
    errorBorder: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12.r),
      borderSide: BorderSide(color: Colors.red),
    ),
  ),
)

保存逻辑

保存前要验证输入是否有效:

  void _save() {
    // 验证昵称不能为空
    if (_nameController.text.trim().isEmpty) {
      Get.snackbar('提示', '请输入昵称');
      return;
    }
    // 调用控制器保存数据
    controller.saveUserName(_nameController.text.trim());
    // 显示成功提示
    Get.snackbar('成功', '资料已更新');
    // 返回上一页
    Get.back();
  }
}

验证逻辑说明

验证逻辑很简单:昵称不能为空。trim()去掉首尾空格,避免用户输入纯空格。

保存成功后做三件事:

  1. 调用控制器的方法保存数据
  2. 显示成功提示
  3. 返回上一页

增强版验证

void _save() {
  final name = _nameController.text.trim();
  
  // 空值检查
  if (name.isEmpty) {
    Get.snackbar('提示', '请输入昵称');
    return;
  }
  
  // 长度检查
  if (name.length < 2) {
    Get.snackbar('提示', '昵称至少需要2个字符');
    return;
  }
  
  if (name.length > 20) {
    Get.snackbar('提示', '昵称不能超过20个字符');
    return;
  }
  
  // 敏感词检查
  if (_containsSensitiveWords(name)) {
    Get.snackbar('提示', '昵称包含敏感词,请修改');
    return;
  }
  
  // 特殊字符检查
  final validPattern = RegExp(r'^[\u4e00-\u9fa5a-zA-Z0-9_]+$');
  if (!validPattern.hasMatch(name)) {
    Get.snackbar('提示', '昵称只能包含中文、英文、数字和下划线');
    return;
  }
  
  // 保存数据
  controller.saveUserName(name);
  Get.snackbar('成功', '资料已更新');
  Get.back();
}

bool _containsSensitiveWords(String text) {
  final sensitiveWords = ['敏感词1', '敏感词2'];
  return sensitiveWords.any((word) => text.contains(word));
}

控制器里的保存方法

ProfileController里应该有对应的保存方法:

// ProfileController中的方法
class ProfileController extends GetxController {
  final userName = '用户'.obs;
  final avatarUrl = ''.obs;
  
  void saveUserName(String name) {
    userName.value = name;
    // 持久化存储
    storage.write('userName', name);
  }
  
  void saveAvatar(String url) {
    avatarUrl.value = url;
    storage.write('avatarUrl', url);
  }
  
  
  void onInit() {
    super.onInit();
    // 读取本地存储的数据
    userName.value = storage.read('userName') ?? '用户';
    avatarUrl.value = storage.read('avatarUrl') ?? '';
  }
}

数据保存到本地存储,下次打开App时读取出来,用户的昵称就不会丢失。

头像上传功能

完整的头像上传实现:

import 'package:image_picker/image_picker.dart';
import 'package:image_cropper/image_cropper.dart';

Future<void> _takePhoto() async {
  final picker = ImagePicker();
  final image = await picker.pickImage(source: ImageSource.camera);
  if (image != null) {
    await _cropAndUpload(image.path);
  }
}

Future<void> _pickFromGallery() async {
  final picker = ImagePicker();
  final image = await picker.pickImage(source: ImageSource.gallery);
  if (image != null) {
    await _cropAndUpload(image.path);
  }
}

Future<void> _cropAndUpload(String imagePath) async {
  // 裁剪图片为圆形
  final croppedFile = await ImageCropper().cropImage(
    sourcePath: imagePath,
    aspectRatio: CropAspectRatio(ratioX: 1, ratioY: 1),
    cropStyle: CropStyle.circle,
    uiSettings: [
      AndroidUiSettings(
        toolbarTitle: '裁剪头像',
        toolbarColor: AppTheme.primaryColor,
        toolbarWidgetColor: Colors.white,
      ),
    ],
  );
  
  if (croppedFile != null) {
    // 上传到服务器
    final url = await _uploadImage(croppedFile.path);
    if (url != null) {
      controller.saveAvatar(url);
      Get.snackbar('成功', '头像已更新');
    }
  }
}

Future<String?> _uploadImage(String path) async {
  try {
    // 调用上传API
    final response = await ApiService.uploadAvatar(path);
    return response.url;
  } catch (e) {
    Get.snackbar('错误', '头像上传失败');
    return null;
  }
}

更多资料字段

除了昵称,还可以让用户填写更多信息:

// 性别选择
DropdownButtonFormField<String>(
  value: _selectedGender,
  decoration: InputDecoration(labelText: '性别'),
  items: ['男', '女', '保密'].map((gender) {
    return DropdownMenuItem(value: gender, child: Text(gender));
  }).toList(),
  onChanged: (value) => setState(() => _selectedGender = value),
)

// 生日选择
ListTile(
  title: Text('生日'),
  subtitle: Text(_birthday ?? '未设置'),
  trailing: Icon(Icons.chevron_right),
  onTap: () async {
    final date = await showDatePicker(
      context: context,
      initialDate: DateTime.now(),
      firstDate: DateTime(1900),
      lastDate: DateTime.now(),
    );
    if (date != null) {
      setState(() => _birthday = DateFormat('yyyy-MM-dd').format(date));
    }
  },
)

// 个性签名
TextField(
  controller: _signatureController,
  maxLines: 3,
  maxLength: 100,
  decoration: InputDecoration(
    labelText: '个性签名',
    hintText: '介绍一下自己吧',
  ),
)

修改确认

如果用户修改了内容但没保存就返回,弹出确认框询问是否放弃修改:

Future<bool> _onWillPop() async {
  // 检查是否有修改
  if (_nameController.text.trim() != controller.userName.value) {
    final result = await Get.dialog<bool>(
      AlertDialog(
        title: Text('提示'),
        content: Text('您有未保存的修改,确定要离开吗?'),
        actions: [
          TextButton(
            onPressed: () => Get.back(result: false),
            child: Text('取消'),
          ),
          TextButton(
            onPressed: () => Get.back(result: true),
            child: Text('确定'),
          ),
        ],
      ),
    );
    return result ?? false;
  }
  return true;
}

// 在build中使用
WillPopScope(
  onWillPop: _onWillPop,
  child: Scaffold(
    // ...
  ),
)

表单处理的通用模式

编辑资料页面展示了Flutter表单处理的通用模式:

  1. 初始化:在initState里创建TextEditingController,设置初始值
  2. 绑定:把controller绑定到TextField
  3. 验证:提交前检查输入是否有效
  4. 保存:调用业务逻辑保存数据
  5. 反馈:给用户成功或失败的提示

掌握了这个模式,做其他表单页面也能举一反三。

总结

编辑资料页面虽然功能简单,但涉及的技术点很全面。本文介绍的实现方案包括:

  1. 表单初始化:使用TextEditingController管理输入状态
  2. 头像上传:图片选择、裁剪、上传的完整流程
  3. 输入验证:空值、长度、敏感词等多重验证
  4. 数据持久化:使用GetStorage保存用户数据

通过合理的表单设计和完善的验证逻辑,可以为用户提供良好的资料编辑体验。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐