Flutter for OpenHarmony 二维码扫描App实战 - 联系人二维码生成实现
本文介绍了联系人二维码生成页面的实现方案。该页面基于vCard格式将联系人信息编码为二维码,包含姓名、电话、邮箱等字段输入,支持表单验证功能。通过TextEditingController管理各输入框状态,采用封装方法构建统一风格的输入组件,最终生成符合vCard 3.0标准的字符串。页面布局包含滚动表单和底部生成按钮,实现了便捷的商务联系人信息交换功能。
联系人二维码可以将个人信息编码成二维码,扫描后可以直接添加到通讯录。这在商务场合交换名片时特别方便,不需要手动输入联系方式。这篇文章介绍联系人二维码生成页面的实现,包括 vCard 格式、多字段输入、表单验证等功能。
vCard 格式介绍
联系人二维码使用 vCard 格式编码联系人信息。vCard 是一种电子名片的标准格式,被大多数通讯录应用支持。
基本格式如下:
BEGIN:VCARD
VERSION:3.0
N:姓名
FN:全名
TEL:电话号码
EMAIL:邮箱地址
ORG:公司名称
TITLE:职位
ADR:地址
END:VCARD
各字段说明:
N (Name):姓名,格式为"姓;名"
FN (Full Name):全名,显示名称
TEL:电话号码,可以有多个
EMAIL:邮箱地址
ORG (Organization):公司/组织名称
TITLE:职位/头衔
ADR (Address):地址
ContactQrView 的基础结构
先来看文件的导入和类定义:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../data/models/qr_record.dart';
import 'generate_controller.dart';
class ContactQrView extends StatefulWidget {
const ContactQrView({super.key});
State<ContactQrView> createState() => _ContactQrViewState();
}
导入了必要的包。ContactQrView 使用 StatefulWidget,因为需要管理多个输入框的状态。
状态类的定义
状态类中定义了多个输入控制器:
class _ContactQrViewState extends State<ContactQrView> {
final _nameController = TextEditingController();
final _phoneController = TextEditingController();
final _emailController = TextEditingController();
final _companyController = TextEditingController();
final _titleController = TextEditingController();
final _addressController = TextEditingController();
final _controller = Get.find<GenerateController>();
void dispose() {
_nameController.dispose();
_phoneController.dispose();
_emailController.dispose();
_companyController.dispose();
_titleController.dispose();
_addressController.dispose();
super.dispose();
}
六个 TextEditingController 分别控制姓名、电话、邮箱、公司、职位和地址输入框。dispose 中释放所有控制器,避免内存泄漏。
这里的字段覆盖了名片上最常见的信息,足够满足大多数场景的需求。
vCard 字符串生成方法
生成 vCard 格式字符串的方法:
String _generateVCard() {
return '''BEGIN:VCARD
VERSION:3.0
N:${_nameController.text}
FN:${_nameController.text}
TEL:${_phoneController.text}
EMAIL:${_emailController.text}
ORG:${_companyController.text}
TITLE:${_titleController.text}
ADR:${_addressController.text}
END:VCARD''';
}
使用 Dart 的多行字符串语法(三个引号)生成 vCard 内容。字符串插值将各输入框的内容填入对应字段。
VERSION:3.0 指定 vCard 版本,3.0 是最广泛支持的版本。N 和 FN 都使用姓名,简化处理。
Scaffold 和 AppBar
build 方法返回页面结构:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('联系人二维码')),
body: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
Scaffold 提供基础页面结构,AppBar 标题为"联系人二维码"。body 使用 SingleChildScrollView 包裹,因为有多个输入框,内容较长需要滚动。
输入字段列表
使用封装的方法构建输入字段:
_buildTextField(_nameController, '姓名', Icons.person, required: true),
_buildTextField(_phoneController, '电话', Icons.phone, keyboardType: TextInputType.phone),
_buildTextField(_emailController, '邮箱', Icons.email, keyboardType: TextInputType.emailAddress),
_buildTextField(_companyController, '公司', Icons.business),
_buildTextField(_titleController, '职位', Icons.work),
_buildTextField(_addressController, '地址', Icons.location_on, maxLines: 2),
],
),
),
每个字段调用 _buildTextField 方法,传入控制器、标签、图标和可选参数。姓名字段标记为必填,电话和邮箱使用对应的键盘类型,地址允许多行输入。
这种封装方式让代码更简洁,避免重复的 TextField 配置。
_buildTextField 方法
封装的输入框构建方法:
Widget _buildTextField(
TextEditingController controller,
String label,
IconData icon, {
bool required = false,
TextInputType? keyboardType,
int maxLines = 1,
}) {
return Padding(
padding: EdgeInsets.only(bottom: 16.h),
child: TextField(
controller: controller,
keyboardType: keyboardType,
maxLines: maxLines,
decoration: InputDecoration(
labelText: required ? '$label *' : label,
prefixIcon: Icon(icon),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
),
),
);
}
}
方法接收控制器、标签、图标作为必需参数,required、keyboardType、maxLines 作为可选参数。
labelText 根据 required 参数决定是否显示星号。prefixIcon 显示对应的图标。border 使用圆角边框。
Padding 在每个输入框下方添加 16.h 的间距,让表单看起来更整齐。
底部生成按钮
页面底部是生成按钮:
bottomNavigationBar: SafeArea(
child: Padding(
padding: EdgeInsets.all(16.w),
child: ElevatedButton(
onPressed: () => _controller.generateQr(_generateVCard(), QrType.contact),
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 48.h),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
),
child: const Text('生成二维码'),
),
),
),
);
}
点击按钮时调用 _generateVCard() 生成 vCard 字符串,然后调用控制器的 generateQr 方法生成二维码。
输入验证
在生成前验证必填字段:
void _handleGenerate() {
final name = _nameController.text.trim();
if (name.isEmpty) {
Get.snackbar('提示', '请输入姓名', snackPosition: SnackPosition.BOTTOM);
return;
}
// 至少需要一种联系方式
final phone = _phoneController.text.trim();
final email = _emailController.text.trim();
if (phone.isEmpty && email.isEmpty) {
Get.snackbar('提示', '请至少输入电话或邮箱', snackPosition: SnackPosition.BOTTOM);
return;
}
// 验证邮箱格式
if (email.isNotEmpty && !GetUtils.isEmail(email)) {
Get.snackbar('提示', '请输入有效的邮箱地址', snackPosition: SnackPosition.BOTTOM);
return;
}
_controller.generateQr(_generateVCard(), QrType.contact);
}
验证逻辑包括:姓名必填、至少有一种联系方式、邮箱格式正确。GetUtils.isEmail 是 GetX 提供的邮箱验证方法。
多电话号码支持
支持添加多个电话号码:
class _ContactQrViewState extends State<ContactQrView> {
final List<TextEditingController> _phoneControllers = [TextEditingController()];
void _addPhone() {
setState(() {
_phoneControllers.add(TextEditingController());
});
}
void _removePhone(int index) {
if (_phoneControllers.length > 1) {
setState(() {
_phoneControllers[index].dispose();
_phoneControllers.removeAt(index);
});
}
}
使用列表存储多个电话控制器。_addPhone 添加新的输入框,_removePhone 移除指定的输入框(至少保留一个)。
多电话输入 UI
Widget _buildPhoneFields() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('电话', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w500)),
TextButton.icon(
icon: const Icon(Icons.add, size: 18),
label: const Text('添加'),
onPressed: _addPhone,
),
],
),
SizedBox(height: 8.h),
...List.generate(_phoneControllers.length, (index) => Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: Row(
children: [
Expanded(
child: TextField(
controller: _phoneControllers[index],
keyboardType: TextInputType.phone,
decoration: InputDecoration(
hintText: '输入电话号码',
prefixIcon: const Icon(Icons.phone),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
),
),
),
if (_phoneControllers.length > 1)
IconButton(
icon: const Icon(Icons.remove_circle_outline, color: Colors.red),
onPressed: () => _removePhone(index),
),
],
),
)),
],
);
}
标题行包含"添加"按钮。每个电话输入框右侧有删除按钮(只有多于一个时显示)。
更新 vCard 生成方法
支持多电话的 vCard 生成:
String _generateVCard() {
final buffer = StringBuffer();
buffer.writeln('BEGIN:VCARD');
buffer.writeln('VERSION:3.0');
buffer.writeln('N:${_nameController.text}');
buffer.writeln('FN:${_nameController.text}');
for (final controller in _phoneControllers) {
if (controller.text.isNotEmpty) {
buffer.writeln('TEL:${controller.text}');
}
}
if (_emailController.text.isNotEmpty) {
buffer.writeln('EMAIL:${_emailController.text}');
}
if (_companyController.text.isNotEmpty) {
buffer.writeln('ORG:${_companyController.text}');
}
if (_titleController.text.isNotEmpty) {
buffer.writeln('TITLE:${_titleController.text}');
}
if (_addressController.text.isNotEmpty) {
buffer.writeln('ADR:${_addressController.text}');
}
buffer.writeln('END:VCARD');
return buffer.toString();
}
使用 StringBuffer 拼接字符串,遍历所有电话控制器添加 TEL 字段。只有非空字段才会添加到 vCard 中。
从通讯录导入
提供从通讯录导入联系人的功能:
import 'package:contacts_service/contacts_service.dart';
Future<void> _importFromContacts() async {
final contact = await ContactsService.openDeviceContactPicker();
if (contact != null) {
_nameController.text = contact.displayName ?? '';
if (contact.phones?.isNotEmpty ?? false) {
_phoneControllers.first.text = contact.phones!.first.value ?? '';
}
if (contact.emails?.isNotEmpty ?? false) {
_emailController.text = contact.emails!.first.value ?? '';
}
if (contact.company != null) {
_companyController.text = contact.company!;
}
if (contact.jobTitle != null) {
_titleController.text = contact.jobTitle!;
}
Get.snackbar('成功', '已导入联系人信息', snackPosition: SnackPosition.BOTTOM);
}
}
使用 contacts_service 插件打开系统通讯录选择器。选择联系人后,将信息填充到各输入框。
导入按钮
在 AppBar 添加导入按钮:
AppBar(
title: const Text('联系人二维码'),
actions: [
IconButton(
icon: const Icon(Icons.contacts),
onPressed: _importFromContacts,
tooltip: '从通讯录导入',
),
],
),
点击通讯录图标可以快速导入已有联系人,不需要手动输入。
名片预览
显示名片样式的预览:
Widget _buildCardPreview() {
return Card(
margin: EdgeInsets.only(bottom: 16.h),
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 30.r,
child: Text(
_nameController.text.isNotEmpty
? _nameController.text[0].toUpperCase()
: '?',
style: TextStyle(fontSize: 24.sp),
),
),
SizedBox(width: 16.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_nameController.text.isEmpty ? '姓名' : _nameController.text,
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
if (_titleController.text.isNotEmpty || _companyController.text.isNotEmpty)
Text(
'${_titleController.text} ${_companyController.text}'.trim(),
style: TextStyle(fontSize: 14.sp, color: Colors.grey),
),
],
),
),
],
),
if (_phoneControllers.first.text.isNotEmpty) ...[
SizedBox(height: 12.h),
Row(
children: [
Icon(Icons.phone, size: 16.sp, color: Colors.grey),
SizedBox(width: 8.w),
Text(_phoneControllers.first.text, style: TextStyle(fontSize: 14.sp)),
],
),
],
if (_emailController.text.isNotEmpty) ...[
SizedBox(height: 8.h),
Row(
children: [
Icon(Icons.email, size: 16.sp, color: Colors.grey),
SizedBox(width: 8.w),
Text(_emailController.text, style: TextStyle(fontSize: 14.sp)),
],
),
],
],
),
),
);
}
名片预览显示头像(使用姓名首字母)、姓名、职位公司、电话和邮箱。实时更新,让用户看到最终效果。
保存为模板
保存常用的联系人信息为模板:
void _saveAsTemplate() async {
final name = _nameController.text.trim();
if (name.isEmpty) {
Get.snackbar('提示', '请先输入姓名', snackPosition: SnackPosition.BOTTOM);
return;
}
final prefs = await SharedPreferences.getInstance();
final templates = prefs.getStringList('contact_templates') ?? [];
final template = jsonEncode({
'name': _nameController.text,
'phone': _phoneControllers.first.text,
'email': _emailController.text,
'company': _companyController.text,
'title': _titleController.text,
'address': _addressController.text,
});
templates.add(template);
await prefs.setStringList('contact_templates', templates);
Get.snackbar('成功', '已保存为模板', snackPosition: SnackPosition.BOTTOM);
}
将联系人信息序列化为 JSON 保存。下次可以快速加载模板生成二维码。
清空表单
提供清空所有输入的功能:
void _clearForm() {
_nameController.clear();
for (final controller in _phoneControllers) {
controller.clear();
}
_emailController.clear();
_companyController.clear();
_titleController.clear();
_addressController.clear();
setState(() {});
}
清空所有输入框,方便用户重新输入。
表单自动保存
自动保存表单内容,防止意外丢失:
void initState() {
super.initState();
_loadDraft();
// 监听输入变化,自动保存草稿
_nameController.addListener(_saveDraft);
_phoneControllers.first.addListener(_saveDraft);
_emailController.addListener(_saveDraft);
}
Future<void> _saveDraft() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('contact_draft', jsonEncode({
'name': _nameController.text,
'phone': _phoneControllers.first.text,
'email': _emailController.text,
'company': _companyController.text,
'title': _titleController.text,
'address': _addressController.text,
}));
}
Future<void> _loadDraft() async {
final prefs = await SharedPreferences.getInstance();
final draft = prefs.getString('contact_draft');
if (draft != null) {
final data = jsonDecode(draft);
_nameController.text = data['name'] ?? '';
_phoneControllers.first.text = data['phone'] ?? '';
_emailController.text = data['email'] ?? '';
_companyController.text = data['company'] ?? '';
_titleController.text = data['title'] ?? '';
_addressController.text = data['address'] ?? '';
}
}
输入变化时自动保存草稿,页面打开时恢复草稿内容。
小结
联系人二维码生成页面需要处理多个输入字段,使用 vCard 格式编码联系人信息。页面使用封装的输入框构建方法,让代码更简洁。
支持多电话号码、从通讯录导入、名片预览、模板保存等功能,提升了用户体验。表单验证确保生成的二维码包含有效的联系人信息。
联系人二维码在商务场合非常实用,一个好用的生成页面可以让交换名片变得更加便捷。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)