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

目录

前言:表格组件适配的技术探索

在移动应用开发中,表格是一种常见且重要的UI组件,用于清晰地展示和管理结构化数据。当我们将Flutter应用适配到OpenHarmony平台时,如何实现一个功能完善、交互友好的表格组件成为了一个关键挑战。

本次开发中,我们参考了Flutter三方库shadcn_ui的设计理念,实现了一个适配OpenHarmony平台的表格组件。这个组件不仅具备基本的表格展示功能,还支持行点击交互、奇偶行交替颜色、水平滚动等特性,为用户提供了良好的使用体验。

通过本次实践,我们不仅掌握了在OpenHarmony平台上实现复杂UI组件的方法,也积累了跨平台开发的宝贵经验。本文将详细介绍表格组件的实现过程、技术要点以及开发中遇到的问题和解决方案。

混合工程结构深度解析

项目目录架构

当Flutter项目集成鸿蒙支持后,典型的项目结构会发生显著变化。以下是经过ohos_flutter插件初始化后的项目结构:

my_flutter_harmony_app/
├── lib/                          # Flutter业务代码(基本不变)
│   ├── main.dart                 # 应用入口
│   ├── table_widget.dart         # 表格组件
├── pubspec.yaml                  # Flutter依赖配置
├── ohos/                         # 鸿蒙原生层(核心适配区)
│   ├── entry/                    # 主模块
│   │   └── src/main/
│   │       ├── ets/              # ArkTS代码
│   │       │   ├── entryability/
│   │       │   │   ├── EntryAbility.ets       # 主Ability
│   │       │   └── pages/
│   │       │       ├── Index.ets           # 主页面
│   │       ├── resources/        # 鸿蒙资源文件
│   │       │   ├── base/
│   │       │   │   ├── element/  # 字符串等
│   │       │   │   ├── media/    # 图片资源
│   │       │   │   └── profile/  # 配置文件
│   │       └── module.json5       # 应用核心配置
└── README.md

展示效果图片

flutter 实时预览 效果展示
在这里插入图片描述

运行到鸿蒙虚拟设备中效果展示
在这里插入图片描述

引入第三方库

在本次开发中,我们使用了shadcn_ui第三方库的设计理念来实现表格功能。虽然我们没有直接使用shadcn_ui库的代码,但我们参考了其设计风格和交互模式,实现了一个类似的表格组件。

在pubspec.yaml文件中,我们添加了shadcn_ui依赖:

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8
  shadcn_ui: ^0.1.0

功能代码实现

1. 表格组件开发

我们创建了一个名为TableWidget的自定义组件,它是一个StatefulWidget,用于生成和显示表格控件。这个组件支持多种配置选项,包括列配置、行数据、样式设置和交互回调等。

核心数据结构

首先,我们定义了两个核心数据结构:TableColumnTableRowData,用于描述表格的列和行数据。

class TableColumn {
  final String title;
  final double? width;
  final Alignment align;

  TableColumn({
    required this.title,
    this.width,
    this.align = Alignment.centerLeft,
  });
}

class TableRowData {
  final Map<String, dynamic> cells;
  final bool isHeader;

  TableRowData({
    required this.cells,
    this.isHeader = false,
  });
}

表格组件实现

接下来,我们实现了TableWidget组件,它包含了表格的核心逻辑和UI渲染。

class TableWidget extends StatefulWidget {
  final List<TableColumn> columns;
  final List<TableRowData> rows;
  final Color? headerColor;
  final Color? rowColor;
  final Color? alternatingRowColor;
  final double? rowHeight;
  final Function(int, Map<String, dynamic>)? onRowTap;
  final bool showBorder;
  final Color? borderColor;

  const TableWidget({
    Key? key,
    required this.columns,
    required this.rows,
    this.headerColor,
    this.rowColor,
    this.alternatingRowColor,
    this.rowHeight,
    this.onRowTap,
    this.showBorder = true,
    this.borderColor,
  }) : super(key: key);

  
  _TableWidgetState createState() => _TableWidgetState();
}

状态管理

_TableWidgetState负责管理组件的状态,包括选中行的索引。当用户点击表格行时,它会更新选中状态并触发回调函数。

class _TableWidgetState extends State<TableWidget> {
  int? _selectedRowIndex;

  void _handleRowTap(int index) {
    setState(() {
      _selectedRowIndex = _selectedRowIndex == index ? null : index;
    });
    if (widget.onRowTap != null) {
      widget.onRowTap!(index, widget.rows[index].cells);
    }
  }

  
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: Container(
        decoration: BoxDecoration(
          border: widget.showBorder
              ? Border.all(
                  color: widget.borderColor ?? Colors.grey[300]!,
                  width: 1,
                )
              : null,
          borderRadius: BorderRadius.circular(4),
        ),
        child: Column(
          children: [
            // Header row
            _buildRow(
              widget.rows.firstWhere((row) => row.isHeader, orElse: () => TableRowData(cells: {})),
              true,
              -1,
              false,
              false,
            ),
            // Data rows
            ...widget.rows
                .where((row) => !row.isHeader)
                .toList()
                .asMap()
                .entries
                .map((entry) {
              int index = entry.key;
              TableRowData row = entry.value;
              bool isAlternating = index % 2 == 1;
              bool isSelected = _selectedRowIndex == index;

              return GestureDetector(
                onTap: () => _handleRowTap(index),
                child: _buildRow(
                  row,
                  false,
                  index,
                  isAlternating,
                  isSelected,
                ),
              );
            }).toList(),
          ],
        ),
      ),
    );
  }

  Widget _buildRow(
    TableRowData row,
    bool isHeader,
    int index,
    bool isAlternating,
    bool isSelected,
  ) {
    Color backgroundColor;
    if (isHeader) {
      backgroundColor = widget.headerColor ?? Colors.grey[100]!;
    } else if (isSelected) {
      backgroundColor = Colors.blue[50]!;
    } else if (isAlternating && widget.alternatingRowColor != null) {
      backgroundColor = widget.alternatingRowColor!;
    } else {
      backgroundColor = widget.rowColor ?? Colors.white;
    }

    return Container(
      height: widget.rowHeight ?? 56,
      color: backgroundColor,
      child: Row(
        children: widget.columns.map((column) {
          String cellKey = column.title.toLowerCase().replaceAll(' ', '_');
          dynamic cellValue = row.cells[cellKey] ?? '';

          return Container(
            width: column.width,
            padding: const EdgeInsets.symmetric(horizontal: 16),
            alignment: column.align,
            decoration: widget.showBorder
                ? BoxDecoration(
                    border: Border(
                      right: BorderSide(
                        color: widget.borderColor ?? Colors.grey[300]!,
                        width: 1,
                      ),
                    ),
                  )
                : null,
            child: Text(
              cellValue.toString(),
              style: TextStyle(
                fontWeight: isHeader ? FontWeight.bold : FontWeight.normal,
                color: isHeader ? Colors.black : Colors.grey[800],
              ),
            ),
          );
        }).toList(),
      ),
    );
  }
}

2. 主应用集成

main.dart文件中,我们集成了TableWidget组件,并添加了交互功能,实现了表格的展示和点击效果。

import 'package:flutter/material.dart';
import 'table_widget.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter for openHarmony',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      debugShowCheckedModeBanner: false,
      home: const MyHomePage(title: 'Flutter for openHarmony'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late List<TableColumn> _columns;
  late List<TableRowData> _rows;
  String _selectedRowInfo = '';

  
  void initState() {
    super.initState();
    _initializeTableData();
  }

  void _initializeTableData() {
    // Define table columns
    _columns = [
      TableColumn(title: 'ID', width: 80, align: Alignment.center),
      TableColumn(title: 'Name', width: 150),
      TableColumn(title: 'Email', width: 200),
      TableColumn(title: 'Status', width: 100, align: Alignment.center),
    ];

    // Define table rows
    _rows = [
      TableRowData(
        cells: {
          'id': 'ID',
          'name': 'Name',
          'email': 'Email',
          'status': 'Status',
        },
        isHeader: true,
      ),
      TableRowData(
        cells: {
          'id': '1',
          'name': 'John Doe',
          'email': 'john@example.com',
          'status': 'Active',
        },
      ),
      TableRowData(
        cells: {
          'id': '2',
          'name': 'Jane Smith',
          'email': 'jane@example.com',
          'status': 'Inactive',
        },
      ),
      TableRowData(
        cells: {
          'id': '3',
          'name': 'Bob Johnson',
          'email': 'bob@example.com',
          'status': 'Active',
        },
      ),
      TableRowData(
        cells: {
          'id': '4',
          'name': 'Alice Brown',
          'email': 'alice@example.com',
          'status': 'Active',
        },
      ),
      TableRowData(
        cells: {
          'id': '5',
          'name': 'Charlie Davis',
          'email': 'charlie@example.com',
          'status': 'Inactive',
        },
      ),
    ];
  }

  void _handleRowTap(int index, Map<String, dynamic> cells) {
    setState(() {
      _selectedRowInfo = 'Selected: ${cells['name']} (${cells['email']})';
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[
            const Text(
              'Shadcn UI - 表格',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 10),
            const Text(
              '点击行查看详情',
              style: TextStyle(fontSize: 16, color: Colors.grey),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 20),
            Expanded(
              child: TableWidget(
                columns: _columns,
                rows: _rows,
                headerColor: Colors.grey[100],
                rowColor: Colors.white,
                alternatingRowColor: Colors.grey[50],
                rowHeight: 56,
                onRowTap: _handleRowTap,
                showBorder: true,
                borderColor: Colors.grey[300],
              ),
            ),
            const SizedBox(height: 20),
            Text(
              _selectedRowInfo,
              style: const TextStyle(fontSize: 16, color: Colors.blue),
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }
}

3. 使用方法

要使用TableWidget组件,只需在需要显示表格的地方添加以下代码:

TableWidget(
  columns: [
    TableColumn(title: 'ID', width: 80, align: Alignment.center),
    TableColumn(title: 'Name', width: 150),
    TableColumn(title: 'Email', width: 200),
    TableColumn(title: 'Status', width: 100, align: Alignment.center),
  ],
  rows: [
    TableRowData(
      cells: {
        'id': 'ID',
        'name': 'Name',
        'email': 'Email',
        'status': 'Status',
      },
      isHeader: true,
    ),
    TableRowData(
      cells: {
        'id': '1',
        'name': 'John Doe',
        'email': 'john@example.com',
        'status': 'Active',
      },
    ),
    // 更多行数据...
  ],
  headerColor: Colors.grey[100],
  rowColor: Colors.white,
  alternatingRowColor: Colors.grey[50],
  rowHeight: 56,
  onRowTap: (index, cells) {
    // 处理行点击事件
    print('Selected row: $index, data: $cells');
  },
  showBorder: true,
  borderColor: Colors.grey[300],
)

配置选项

  • columns:表格列配置列表
  • rows:表格行数据列表
  • headerColor:表头背景颜色
  • rowColor:行背景颜色
  • alternatingRowColor:交替行背景颜色
  • rowHeight:行高度
  • onRowTap:行点击回调函数
  • showBorder:是否显示边框
  • borderColor:边框颜色

本次开发中容易遇到的问题

  1. 类型不匹配问题:在开发过程中,我们遇到了TextAlignAlignment类型不匹配的问题。最初我们使用了TextAlign来设置列的对齐方式,但在Flutter中,Containeralignment属性需要的是Alignment类型。解决方案是将TableColumn中的align属性类型从TextAlign改为Alignment

  2. 水平溢出问题:当表格列数较多或列宽较宽时,会出现水平溢出的问题。解决方案是将表格包裹在SingleChildScrollView中,并设置scrollDirection: Axis.horizontal,使表格可以水平滚动。

  3. 可选参数语法错误:在定义_buildRow方法时,我们使用了可选参数语法,但在Dart中,可选参数需要用大括号括起来。解决方案是将可选参数改为普通参数,并在调用时传入默认值。

  4. asMap方法使用错误:我们尝试在Iterable上直接调用asMap方法,但asMap方法是List的方法。解决方案是先将Iterable转换为List,然后再调用asMap方法。

  5. 状态管理问题:在处理行点击事件时,我们需要更新选中行的状态并触发UI刷新。解决方案是使用setState方法来更新状态,确保UI能够正确反映当前的选中状态。

总结本次开发中用到的技术点

  1. 自定义组件开发:我们创建了一个可复用的TableWidget组件,支持多种配置选项和交互功能。

  2. 状态管理:使用StatefulWidgetsetState方法来管理表格的选中状态。

  3. 布局和滚动:使用SingleChildScrollView实现表格的水平滚动,解决了水平溢出的问题。

  4. 数据结构设计:定义了TableColumnTableRowData数据结构,用于描述表格的列和行数据。

  5. 事件处理:实现了行点击事件的处理,支持点击行查看详情的功能。

  6. 样式定制:支持自定义表头颜色、行颜色、交替行颜色、边框等样式选项。

  7. 跨平台适配:确保表格组件在OpenHarmony平台上能够正常显示和交互。

  8. 代码组织:将表格组件抽离到单独的文件中,提高了代码的可维护性和复用性。

通过本次开发,我们不仅实现了一个功能完善的表格组件,也积累了在OpenHarmony平台上开发Flutter应用的经验。这个表格组件可以直接应用到实际项目中,为用户提供良好的数据展示和交互体验。

Logo

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

更多推荐