Flutter实战:打造二维码名片应用

前言

二维码名片是一款现代化的名片管理应用,通过二维码技术实现快速交换联系方式。本文将带你从零开始,使用Flutter开发一个功能完整的二维码名片应用,支持个人信息展示、二维码生成、扫码添加联系人等功能。

应用特色

  • 📇 个人名片:创建和编辑个人名片信息
  • 🎨 多彩主题:8种名片颜色主题可选
  • 📱 二维码生成:自动生成vCard格式二维码
  • 📷 扫码添加:扫描二维码快速添加联系人
  • 💾 本地存储:名片和联系人本地保存
  • 📤 分享功能:分享二维码图片
  • 📋 信息复制:一键复制名片信息
  • 👥 联系人管理:查看、删除联系人
  • 🔍 详情查看:查看联系人完整信息
  • 🎯 vCard标准:符合vCard 3.0标准

效果展示

在这里插入图片描述
在这里插入图片描述

二维码名片

我的名片

创建名片

编辑信息

选择颜色

生成二维码

分享二维码

复制信息

联系人管理

扫码添加

查看列表

详情展示

删除联系人

信息字段

姓名

职位

公司

电话

邮箱

微信

地址

网站

备注

数据模型设计

名片信息模型

class BusinessCard {
  String id;
  String name;
  String title;
  String company;
  String phone;
  String email;
  String wechat;
  String address;
  String website;
  String note;
  DateTime createdAt;
  int cardColor;

  BusinessCard({
    required this.id,
    required this.name,
    this.title = '',
    this.company = '',
    this.phone = '',
    this.email = '',
    this.wechat = '',
    this.address = '',
    this.website = '',
    this.note = '',
    required this.createdAt,
    this.cardColor = 0xFF2196F3,
  });
}

字段说明

  • id:唯一标识符
  • name:姓名(必填)
  • title:职位
  • company:公司
  • phone:电话
  • email:邮箱
  • wechat:微信号
  • address:地址
  • website:网站
  • note:备注
  • createdAt:创建时间
  • cardColor:名片颜色

核心功能实现

1. vCard格式转换

vCard是电子名片的国际标准格式:

String toVCard() {
  return '''BEGIN:VCARD
VERSION:3.0
FN:$name
TITLE:$title
ORG:$company
TEL:$phone
EMAIL:$email
X-WECHAT:$wechat
ADR:$address
URL:$website
NOTE:$note
END:VCARD''';
}

vCard标准字段

  • BEGIN:VCARD / END:VCARD:开始和结束标记
  • VERSION:3.0:vCard版本
  • FN:全名(Full Name)
  • TITLE:职位
  • ORG:组织/公司
  • TEL:电话
  • EMAIL:邮箱
  • X-WECHAT:自定义字段(微信)
  • ADR:地址
  • URL:网站
  • NOTE:备注

2. vCard解析

从二维码扫描结果解析vCard:

factory BusinessCard.fromVCard(String vcard) {
  final lines = vcard.split('\n');
  String name = '';
  String title = '';
  String company = '';
  String phone = '';
  String email = '';
  String wechat = '';
  String address = '';
  String website = '';
  String note = '';

  for (var line in lines) {
    if (line.startsWith('FN:')) {
      name = line.substring(3);
    } else if (line.startsWith('TITLE:')) {
      title = line.substring(6);
    } else if (line.startsWith('ORG:')) {
      company = line.substring(4);
    } else if (line.startsWith('TEL:')) {
      phone = line.substring(4);
    } else if (line.startsWith('EMAIL:')) {
      email = line.substring(6);
    } else if (line.startsWith('X-WECHAT:')) {
      wechat = line.substring(9);
    } else if (line.startsWith('ADR:')) {
      address = line.substring(4);
    } else if (line.startsWith('URL:')) {
      website = line.substring(4);
    } else if (line.startsWith('NOTE:')) {
      note = line.substring(5);
    }
  }

  return BusinessCard(
    id: DateTime.now().millisecondsSinceEpoch.toString(),
    name: name,
    title: title,
    company: company,
    phone: phone,
    email: email,
    wechat: wechat,
    address: address,
    website: website,
    note: note,
    createdAt: DateTime.now(),
  );
}

解析流程

  1. 按行分割vCard文本
  2. 遍历每一行,识别字段前缀
  3. 提取字段值(去除前缀)
  4. 创建BusinessCard对象

3. 二维码生成

使用qr_flutter生成二维码:

QrImageView(
  data: card.toVCard(),
  version: QrVersions.auto,
  size: 200,
  backgroundColor: Colors.white,
)

参数说明

  • data:要编码的数据(vCard字符串)
  • version:二维码版本(auto自动选择)
  • size:二维码尺寸
  • backgroundColor:背景颜色

4. 二维码扫描

使用mobile_scanner扫描二维码:

MobileScanner(
  controller: cameraController,
  onDetect: _onDetect,
)

void _onDetect(BarcodeCapture capture) {
  if (_isProcessing) return;

  final List<Barcode> barcodes = capture.barcodes;
  if (barcodes.isEmpty) return;

  final barcode = barcodes.first;
  final String? code = barcode.rawValue;

  if (code != null && code.isNotEmpty) {
    _isProcessing = true;

    try {
      if (code.startsWith('BEGIN:VCARD')) {
        final card = BusinessCard.fromVCard(code);
        widget.onScanned(card);

        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('已添加 ${card.name}')),
          );
          Navigator.pop(context);
        }
      } else {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('无效的名片二维码')),
          );
        }
        _isProcessing = false;
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('解析失败: $e')),
        );
      }
      _isProcessing = false;
    }
  }
}

扫描流程

  1. 检测到二维码后触发onDetect
  2. 获取二维码内容
  3. 验证是否为vCard格式
  4. 解析vCard创建名片
  5. 添加到联系人列表
  6. 显示提示并返回

防重复处理

  • 使用_isProcessing标志防止重复扫描
  • 扫描成功后立即返回页面

5. 数据持久化

使用SharedPreferences保存数据:

Future<void> _loadData() async {
  final prefs = await SharedPreferences.getInstance();

  // 加载我的名片
  final myCardJson = prefs.getString('my_card');
  if (myCardJson != null) {
    setState(() {
      _myCard = BusinessCard.fromJson(json.decode(myCardJson));
    });
  }

  // 加载联系人
  final contactsJson = prefs.getString('contacts');
  if (contactsJson != null) {
    final List<dynamic> decoded = json.decode(contactsJson);
    setState(() {
      _contacts = decoded.map((item) => BusinessCard.fromJson(item)).toList();
    });
  }
}

Future<void> _saveMyCard(BusinessCard card) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('my_card', json.encode(card.toJson()));
  setState(() {
    _myCard = card;
  });
}

Future<void> _saveContacts() async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString(
    'contacts',
    json.encode(_contacts.map((c) => c.toJson()).toList()),
  );
}

UI组件设计

1. 名片预览

Widget _buildCardPreview(BuildContext context, BusinessCard card) {
  return Container(
    width: double.infinity,
    padding: const EdgeInsets.all(24),
    decoration: BoxDecoration(
      gradient: LinearGradient(
        colors: [
          Color(card.cardColor),
          Color(card.cardColor).withValues(alpha: 0.7),
        ],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
      borderRadius: BorderRadius.circular(16),
      boxShadow: [
        BoxShadow(
          color: Colors.black.withValues(alpha: 0.2),
          blurRadius: 10,
          offset: const Offset(0, 5),
        ),
      ],
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          card.name,
          style: const TextStyle(
            fontSize: 28,
            fontWeight: FontWeight.bold,
            color: Colors.white,
          ),
        ),
        if (card.title.isNotEmpty) ...[
          const SizedBox(height: 8),
          Text(
            card.title,
            style: const TextStyle(
              fontSize: 16,
              color: Colors.white,
            ),
          ),
        ],
        // ... 其他信息
      ],
    ),
  );
}

设计要点

  • 渐变背景:使用LinearGradient创建渐变效果
  • 圆角卡片:BorderRadius.circular(16)
  • 阴影效果:BoxShadow增加立体感
  • 条件显示:只显示非空字段

2. 二维码展示

Widget _buildQRCode(BuildContext context, BusinessCard card) {
  final qrKey = GlobalKey();

  return Container(
    padding: const EdgeInsets.all(24),
    decoration: BoxDecoration(
      color: Theme.of(context).colorScheme.surface,
      borderRadius: BorderRadius.circular(16),
      border: Border.all(
        color: Theme.of(context).colorScheme.outline,
      ),
    ),
    child: Column(
      children: [
        const Text(
          '扫描二维码添加我',
          style: TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
        ),
        const SizedBox(height: 16),
        RepaintBoundary(
          key: qrKey,
          child: Container(
            padding: const EdgeInsets.all(16),
            color: Colors.white,
            child: QrImageView(
              data: card.toVCard(),
              version: QrVersions.auto,
              size: 200,
              backgroundColor: Colors.white,
            ),
          ),
        ),
        const SizedBox(height: 16),
        FilledButton.icon(
          onPressed: () => _shareQRCode(context, qrKey, card),
          icon: const Icon(Icons.share),
          label: const Text('分享二维码'),
        ),
      ],
    ),
  );
}

RepaintBoundary的作用

  • 创建独立渲染层
  • 可以转换为图片
  • 用于分享二维码

3. 编辑表单

Form(
  key: _formKey,
  child: ListView(
    padding: const EdgeInsets.all(16),
    children: [
      TextFormField(
        controller: _nameController,
        decoration: const InputDecoration(
          labelText: '姓名 *',
          prefixIcon: Icon(Icons.person),
          border: OutlineInputBorder(),
        ),
        validator: (value) {
          if (value == null || value.isEmpty) {
            return '请输入姓名';
          }
          return null;
        },
      ),
      const SizedBox(height: 16),
      TextFormField(
        controller: _titleController,
        decoration: const InputDecoration(
          labelText: '职位',
          prefixIcon: Icon(Icons.work),
          border: OutlineInputBorder(),
        ),
      ),
      // ... 其他字段
    ],
  ),
)

表单验证

  • 姓名为必填字段
  • 使用validator进行验证
  • 其他字段为可选

4. 颜色选择器

Wrap(
  spacing: 12,
  runSpacing: 12,
  children: _cardColors.map((color) {
    return InkWell(
      onTap: () {
        setState(() {
          _selectedColor = color;
        });
      },
      child: Container(
        width: 50,
        height: 50,
        decoration: BoxDecoration(
          color: Color(color),
          shape: BoxShape.circle,
          border: Border.all(
            color: _selectedColor == color
                ? Colors.black
                : Colors.transparent,
            width: 3,
          ),
        ),
        child: _selectedColor == color
            ? const Icon(Icons.check, color: Colors.white)
            : null,
      ),
    );
  }).toList(),
)

8种预设颜色

  • Blue (蓝色)
  • Green (绿色)
  • Orange (橙色)
  • Pink (粉色)
  • Purple (紫色)
  • Cyan (青色)
  • Deep Orange (深橙)
  • Blue Grey (蓝灰)

5. 扫描界面

Stack(
  children: [
    MobileScanner(
      controller: cameraController,
      onDetect: _onDetect,
    ),
    Center(
      child: Container(
        width: 250,
        height: 250,
        decoration: BoxDecoration(
          border: Border.all(
            color: Colors.white,
            width: 2,
          ),
          borderRadius: BorderRadius.circular(12),
        ),
      ),
    ),
    Positioned(
      bottom: 50,
      left: 0,
      right: 0,
      child: Center(
        child: Container(
          padding: const EdgeInsets.symmetric(
            horizontal: 24,
            vertical: 12,
          ),
          decoration: BoxDecoration(
            color: Colors.black.withValues(alpha: 0.7),
            borderRadius: BorderRadius.circular(24),
          ),
          child: const Text(
            '将二维码放入框内',
            style: TextStyle(
              color: Colors.white,
              fontSize: 16,
            ),
          ),
        ),
      ),
    ),
  ],
)

界面元素

  • 相机预览:MobileScanner
  • 扫描框:白色边框提示扫描区域
  • 提示文字:底部提示信息

技术要点详解

1. vCard标准

vCard是电子名片的国际标准格式,广泛应用于:

  • 通讯录导入导出
  • 邮件签名
  • 二维码名片
  • NFC名片

vCard 3.0主要字段

字段 说明 示例
FN 全名 FN:张三
N 姓名结构 N:张;三;;;
TITLE 职位 TITLE:产品经理
ORG 组织 ORG:某某公司
TEL 电话 TEL:13800138000
EMAIL 邮箱 EMAIL:zhangsan@example.com
ADR 地址 ADR:;;北京市朝阳区;;100000;中国
URL 网站 URL:https://example.com
NOTE 备注 NOTE:欢迎交流

2. 二维码容量

二维码版本与容量关系:

版本 模块数 数字容量 字母容量 字节容量
1 21×21 41 25 17
10 57×57 652 395 271
20 97×97 1,852 1,125 773
40 177×177 7,089 4,296 2,953

vCard二维码建议

  • 控制信息长度在500字节以内
  • 使用Version 10-15即可
  • 过大的二维码难以扫描

3. JSON序列化

Map<String, dynamic> toJson() => {
  'id': id,
  'name': name,
  'title': title,
  'company': company,
  'phone': phone,
  'email': email,
  'wechat': wechat,
  'address': address,
  'website': website,
  'note': note,
  'createdAt': createdAt.toIso8601String(),
  'cardColor': cardColor,
};

factory BusinessCard.fromJson(Map<String, dynamic> json) {
  return BusinessCard(
    id: json['id'],
    name: json['name'],
    title: json['title'] ?? '',
    company: json['company'] ?? '',
    phone: json['phone'] ?? '',
    email: json['email'] ?? '',
    wechat: json['wechat'] ?? '',
    address: json['address'] ?? '',
    website: json['website'] ?? '',
    note: json['note'] ?? '',
    createdAt: DateTime.parse(json['createdAt']),
    cardColor: json['cardColor'] ?? 0xFF2196F3,
  );
}

注意事项

  • DateTime使用ISO 8601格式
  • 可选字段使用??提供默认值
  • Color使用int值存储

4. 相机权限

AndroidManifest.xml中添加:

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />

Info.plist中添加:

<key>NSCameraUsageDescription</key>
<string>需要使用相机扫描二维码</string>

应用场景

1. 商务交流

class BusinessMeeting {
  String meetingId;
  List<BusinessCard> attendees;
  DateTime meetingTime;
  
  void exchangeCards(BusinessCard card1, BusinessCard card2) {
    // 交换名片逻辑
  }
}

2. 展会活动

class ExhibitionBooth {
  String boothNumber;
  BusinessCard exhibitorCard;
  List<BusinessCard> visitors;
  
  void collectVisitorCard(BusinessCard card) {
    visitors.add(card);
  }
}

3. 社交网络

class SocialProfile {
  BusinessCard card;
  String avatar;
  List<String> socialLinks;
  
  String generateProfileQR() {
    // 生成包含社交链接的二维码
    return card.toVCard();
  }
}

功能扩展建议

1. NFC名片

import 'package:nfc_manager/nfc_manager.dart';

Future<void> writeNFC(BusinessCard card) async {
  await NfcManager.instance.startSession(onDiscovered: (NfcTag tag) async {
    var ndef = Ndef.from(tag);
    if (ndef == null || !ndef.isWritable) {
      return;
    }

    NdefMessage message = NdefMessage([
      NdefRecord.createText(card.toVCard()),
    ]);

    await ndef.write(message);
  });
}

2. 名片模板

class CardTemplate {
  String id;
  String name;
  String layout;
  Map<String, dynamic> style;
  
  Widget buildCard(BusinessCard card) {
    // 根据模板渲染名片
    return Container();
  }
}

3. 批量导入

Future<List<BusinessCard>> importFromCSV(String csvPath) async {
  final file = File(csvPath);
  final lines = await file.readAsLines();
  
  List<BusinessCard> cards = [];
  for (var line in lines.skip(1)) {
    final fields = line.split(',');
    cards.add(BusinessCard(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      name: fields[0],
      title: fields[1],
      company: fields[2],
      phone: fields[3],
      email: fields[4],
      createdAt: DateTime.now(),
    ));
  }
  
  return cards;
}

4. 云端同步

class CloudSync {
  Future<void> uploadCard(BusinessCard card) async {
    // 上传到云端
  }
  
  Future<List<BusinessCard>> downloadContacts() async {
    // 从云端下载
    return [];
  }
  
  Future<void> syncAll() async {
    // 双向同步
  }
}

5. 名片识别

import 'package:google_ml_kit/google_ml_kit.dart';

Future<BusinessCard> recognizeCard(String imagePath) async {
  final inputImage = InputImage.fromFilePath(imagePath);
  final textRecognizer = TextRecognizer();
  final recognizedText = await textRecognizer.processImage(inputImage);
  
  // 解析识别的文本
  String name = '';
  String phone = '';
  String email = '';
  
  for (var block in recognizedText.blocks) {
    final text = block.text;
    if (text.contains('@')) {
      email = text;
    } else if (RegExp(r'\d{11}').hasMatch(text)) {
      phone = text;
    }
  }
  
  return BusinessCard(
    id: DateTime.now().millisecondsSinceEpoch.toString(),
    name: name,
    phone: phone,
    email: email,
    createdAt: DateTime.now(),
  );
}

6. 统计分析

class CardAnalytics {
  int getTotalContacts() {
    return contacts.length;
  }
  
  Map<String, int> getContactsByCompany() {
    Map<String, int> stats = {};
    for (var contact in contacts) {
      stats[contact.company] = (stats[contact.company] ?? 0) + 1;
    }
    return stats;
  }
  
  List<BusinessCard> getRecentContacts(int days) {
    final cutoff = DateTime.now().subtract(Duration(days: days));
    return contacts.where((c) => c.createdAt.isAfter(cutoff)).toList();
  }
}

性能优化

1. 二维码缓存

class QRCodeCache {
  static final Map<String, Uint8List> _cache = {};
  
  static Future<Uint8List> getQRCode(String data) async {
    if (_cache.containsKey(data)) {
      return _cache[data]!;
    }
    
    // 生成二维码
    final qrImage = await _generateQRImage(data);
    _cache[data] = qrImage;
    
    return qrImage;
  }
  
  static void clearCache() {
    _cache.clear();
  }
}

2. 图片压缩

Future<Uint8List> compressQRCode(Uint8List bytes) async {
  final image = img.decodeImage(bytes);
  if (image == null) return bytes;
  
  final resized = img.copyResize(image, width: 500);
  return Uint8List.fromList(img.encodePng(resized));
}

3. 懒加载

ListView.builder(
  itemCount: contacts.length,
  itemBuilder: (context, index) {
    final contact = contacts[index];
    return ListTile(
      leading: CircleAvatar(
        backgroundColor: Color(contact.cardColor),
        child: Text(contact.name[0]),
      ),
      title: Text(contact.name),
      subtitle: Text(contact.company),
    );
  },
)

常见问题解答

Q1: 如何提高二维码扫描成功率?

A:

  • 保持二维码清晰完整
  • 控制信息长度
  • 使用适当的纠错级别
  • 保持良好的光线条件

Q2: vCard格式兼容性如何?

A: vCard 3.0是广泛支持的标准,兼容大多数通讯录应用。

Q3: 如何处理特殊字符?

A: 在vCard中,特殊字符需要转义,如逗号、分号等。

项目结构

lib/
├── main.dart                    # 主程序入口
├── models/
│   └── business_card.dart      # 名片模型
├── screens/
│   ├── home_page.dart          # 主页
│   ├── my_card_page.dart       # 我的名片页
│   ├── edit_card_page.dart     # 编辑名片页
│   ├── contacts_page.dart      # 联系人页
│   └── qr_scanner_page.dart    # 扫描页
├── widgets/
│   ├── card_preview.dart       # 名片预览组件
│   ├── qr_code_widget.dart     # 二维码组件
│   └── contact_item.dart       # 联系人项组件
└── utils/
    ├── vcard_parser.dart       # vCard解析工具
    └── storage_helper.dart     # 存储辅助工具

总结

本文实现了一个功能完整的二维码名片应用,涵盖了以下核心技术:

  1. vCard标准:符合国际标准的电子名片格式
  2. 二维码技术:qr_flutter生成、mobile_scanner扫描
  3. 数据持久化:SharedPreferences本地存储
  4. JSON序列化:数据模型的序列化和反序列化
  5. 相机操作:调用相机扫描二维码
  6. 图片分享:RepaintBoundary转换和分享

通过本项目,你不仅学会了如何实现二维码名片应用,还掌握了Flutter中二维码处理、相机操作、数据存储的核心技术。这些知识可以应用到更多需要二维码功能的应用开发。

扫码交换,快速建立联系!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐