【maaath】Flutter for OpenHarmony 的手办展示应用开发实践

Flutter for OpenHarmony 的手办展示应用开发实践

前言

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

在移动应用开发领域,Flutter 以其高效的跨平台能力和出色的性能表现,已经成为众多开发者的首选框架。随着 OpenHarmony 生态的蓬勃发展,Flutter for OpenHarmony 的支持也日趋完善。本文将基于实际项目经验,分享如何使用 Flutter for OpenHarmony 开发一个精美的手办展示应用,为读者提供可复用的开发指导。

项目概述

本文将带领读者从零开始,使用 Flutter for OpenHarmony 构建一个手办展示应用。该应用具备以下核心功能:

  • 手办列表展示(支持推荐、新品排序)
  • 手办详情查看(包含3D旋转预览效果)
  • 收藏功能
  • 个人中心页面
  • 底部导航栏切换

通过这个实战项目,读者将掌握 Flutter 跨平台开发的核心技巧,以及如何在 OpenHarmony 设备上部署运行。

环境准备

在开始之前,需要确保开发环境满足以下要求:

  • DevEco Studio 最新版本
  • Flutter SDK 3.7.0 及以上版本
  • OpenHarmony SDK
  • 一台支持 OpenHarmony 的设备或模拟器

项目采用 Flutter 3.x 版本,结合 flutter_ohos 插件实现 OpenHarmony 平台的原生适配。Flutter 的声明式 UI 范式与 ArkUI 类似,但提供了更加统一的跨平台开发体验。

项目结构设计

良好的项目结构是维护大型应用的基础。本次开发采用标准的三层架构设计:

复制代码
lib/
├── main.dart                 # 应用入口
├── model/                   # 数据模型层
│   └── figure_model.dart
├── service/                 # 业务服务层
│   └── figure_service.dart
├── view/                    # 视图层
│   ├── pages/
│   │   ├── home_page.dart
│   │   ├── detail_page.dart
│   │   ├── favorite_page.dart
│   │   └── profile_page.dart
│   └── widgets/
│       ├── figure_card.dart
│       └── bottom_nav.dart
└── viewmodel/               # 视图模型层
    └── figure_viewmodel.dart

这种分层架构清晰地分离了数据、业务逻辑和界面展示,便于团队协作开发和后续维护。

数据模型定义

首先定义手办数据模型,这是整个应用的数据基础:

dart 复制代码
class Figure {
  final String id;
  final String name;
  final String series;
  final int price;
  final int originalPrice;
  final String imageUrl;
  final String description;
  final String manufacturer;
  final String scale;
  final String releaseDate;
  final String status; // presale, onsale, soldout
  final double rating;
  final int salesCount;
  bool isFavorite;
  final List<String> images;

  Figure({
    required this.id,
    required this.name,
    required this.series,
    required this.price,
    required this.originalPrice,
    required this.imageUrl,
    required this.description,
    required this.manufacturer,
    required this.scale,
    required this.releaseDate,
    required this.status,
    required this.rating,
    required this.salesCount,
    this.isFavorite = false,
    required this.images,
  });

  factory Figure.fromJson(Map<String, dynamic> json) {
    return Figure(
      id: json['id'] ?? '',
      name: json['name'] ?? '',
      series: json['series'] ?? '',
      price: json['price'] ?? 0,
      originalPrice: json['originalPrice'] ?? 0,
      imageUrl: json['imageUrl'] ?? '',
      description: json['description'] ?? '',
      manufacturer: json['manufacturer'] ?? '',
      scale: json['scale'] ?? '',
      releaseDate: json['releaseDate'] ?? '',
      status: json['status'] ?? 'onsale',
      rating: (json['rating'] ?? 0).toDouble(),
      salesCount: json['salesCount'] ?? 0,
      isFavorite: json['isFavorite'] ?? false,
      images: List<String>.from(json['images'] ?? []),
    );
  }

  Figure copyWith({
    String? id,
    String? name,
    String? series,
    int? price,
    int? originalPrice,
    String? imageUrl,
    String? description,
    String? manufacturer,
    String? scale,
    String? releaseDate,
    String? status,
    double? rating,
    int? salesCount,
    bool? isFavorite,
    List<String>? images,
  }) {
    return Figure(
      id: id ?? this.id,
      name: name ?? this.name,
      series: series ?? this.series,
      price: price ?? this.price,
      originalPrice: originalPrice ?? this.originalPrice,
      imageUrl: imageUrl ?? this.imageUrl,
      description: description ?? this.description,
      manufacturer: manufacturer ?? this.manufacturer,
      scale: scale ?? this.scale,
      releaseDate: releaseDate ?? this.releaseDate,
      status: status ?? this.status,
      rating: rating ?? this.rating,
      salesCount: salesCount ?? this.salesCount,
      isFavorite: isFavorite ?? this.isFavorite,
      images: images ?? this.images,
    );
  }
}

模型类采用了不可变数据类的设计模式,通过 copyWith 方法实现数据的不可变性更新,这与 Flutter 的响应式编程理念高度契合。在实际开发中,建议将数据模型与业务逻辑解耦,便于单元测试和代码复用。

服务层实现

服务层负责数据的获取和处理逻辑。本次演示采用模拟数据的方式,真实项目中可替换为实际的 HTTP 请求:

dart 复制代码
import 'dart:async';
import '../model/figure_model.dart';

class FigureService {
  static final FigureService _instance = FigureService._internal();
  factory FigureService() => _instance;
  FigureService._internal();

  final Set<String> _favorites = {'2', '4', '7'};

  final List<Figure> _mockFigures = [
    Figure(
      id: '1',
      name: 'Saber Alter - 命运/Stay Night',
      series: 'Fate/Stay Night',
      price: 1299,
      originalPrice: 1599,
      imageUrl: 'https://picsum.photos/seed/figure1/400/400',
      description: '高品质手办,精致的涂装和细节处理,还原角色的魅力。',
      manufacturer: 'Good Smile Company',
      scale: '1/7',
      releaseDate: '2024-03-15',
      status: 'onsale',
      rating: 4.8,
      salesCount: 2580,
      images: ['https://picsum.photos/seed/figure1/400/400'],
    ),
    Figure(
      id: '2',
      name: '雷姆 - Re:从零开始的异世界生活',
      series: 'Re:Zero',
      price: 899,
      originalPrice: 1099,
      imageUrl: 'https://picsum.photos/seed/figure2/400/400',
      description: '可爱的女仆手办,表情刻画细腻,服装细节丰富。',
      manufacturer: 'Alter',
      scale: '1/8',
      releaseDate: '2024-02-20',
      status: 'onsale',
      rating: 4.9,
      salesCount: 3200,
      images: ['https://picsum.photos/seed/figure2/400/400'],
    ),
    // ... 更多模拟数据
  ];

  Future<List<Figure>> fetchFigures({
    String sortBy = 'recommend',
    int page = 1,
    int pageSize = 10,
    String? keyword,
  }) async {
    await Future.delayed(const Duration(milliseconds: 800));

    List<Figure> result = List.from(_mockFigures);

    // 根据排序类型处理
    switch (sortBy) {
      case 'newest':
        result.sort((a, b) => DateTime.parse(b.releaseDate)
            .compareTo(DateTime.parse(a.releaseDate)));
        break;
      case 'price_asc':
        result.sort((a, b) => a.price.compareTo(b.price));
        break;
      case 'price_desc':
        result.sort((a, b) => b.price.compareTo(a.price));
        break;
      case 'sales':
        result.sort((a, b) => b.salesCount.compareTo(a.salesCount));
        break;
      default:
        result.sort((a, b) => b.rating.compareTo(a.rating));
    }

    // 搜索过滤
    if (keyword != null && keyword.isNotEmpty) {
      final kw = keyword.toLowerCase();
      result = result.where((f) =>
        f.name.toLowerCase().contains(kw) ||
        f.series.toLowerCase().contains(kw)
      ).toList();
    }

    // 分页
    final start = (page - 1) * pageSize;
    final end = start + pageSize;
    if (start >= result.length) return [];

    final paginatedResult = result.sublist(
      start,
      end > result.length ? result.length : end,
    );

    // 更新收藏状态
    return paginatedResult.map((f) {
      return f.copyWith(isFavorite: _favorites.contains(f.id));
    }).toList();
  }

  Future<List<Figure>> fetchFavorites() async {
    await Future.delayed(const Duration(milliseconds: 500));
    return _mockFigures
        .where((f) => _favorites.contains(f.id))
        .map((f) => f.copyWith(isFavorite: true))
        .toList();
  }

  Future<bool> toggleFavorite(String figureId) async {
    await Future.delayed(const Duration(milliseconds: 200));
    if (_favorites.contains(figureId)) {
      _favorites.remove(figureId);
      return false;
    } else {
      _favorites.add(figureId);
      return true;
    }
  }

  Future<Figure?> fetchFigureDetail(String id) async {
    await Future.delayed(const Duration(milliseconds: 600));
    try {
      final figure = _mockFigures.firstWhere((f) => f.id == id);
      return figure.copyWith(isFavorite: _favorites.contains(id));
    } catch (e) {
      return null;
    }
  }
}

服务层采用了单例模式,确保全局只有一个数据服务实例,避免了资源浪费和数据不一致的问题。模拟数据的延迟返回模拟了真实网络请求的响应时间,便于后续的性能优化和用户体验测试。

主页面实现

主页面采用 Flutter 经典的底部导航栏设计,通过 IndexedStack 保持各 Tab 页面的状态:

dart 复制代码
import 'package:flutter/material.dart';
import 'home_page.dart';
import 'favorite_page.dart';
import 'profile_page.dart';

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

  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  int _currentIndex = 0;

  final List<Widget> _pages = const [
    HomePage(),
    FavoritePage(),
    ProfilePage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: _pages,
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        selectedItemColor: const Color(0xFFFF4081),
        unselectedItemColor: Colors.grey,
        type: BottomNavigationBarType.fixed,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.grid_view_outlined),
            activeIcon: Icon(Icons.grid_view),
            label: '推荐',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.favorite_outline),
            activeIcon: Icon(Icons.favorite),
            label: '收藏',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person_outline),
            activeIcon: Icon(Icons.person),
            label: '我的',
          ),
        ],
      ),
    );
  }
}

IndexedStack 组件是该实现的关键,它能够在切换 Tab 时保持子页面的状态,避免数据重复加载。对于内容丰富的列表页面,这种优化能显著提升用户体验。

手办卡片组件

手办卡片是列表展示的核心组件,需要同时展示图片、名称、价格、系列和收藏状态:

dart 复制代码
import 'package:flutter/material.dart';
import '../model/figure_model.dart';

class FigureCard extends StatelessWidget {
  final Figure figure;
  final VoidCallback onTap;
  final VoidCallback onFavoriteTap;

  const FigureCard({
    super.key,
    required this.figure,
    required this.onTap,
    required this.onFavoriteTap,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Container(
        margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
        padding: const EdgeInsets.all(12),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(12),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.08),
              blurRadius: 8,
              offset: const Offset(0, 2),
            ),
          ],
        ),
        child: Row(
          children: [
            // 手办图片
            Stack(
              alignment: Alignment.topLeft,
              children: [
                ClipRRect(
                  borderRadius: BorderRadius.circular(8),
                  child: Image.network(
                    figure.imageUrl,
                    width: 120,
                    height: 120,
                    fit: BoxFit.cover,
                    loadingBuilder: (context, child, progress) {
                      if (progress == null) return child;
                      return Container(
                        width: 120,
                        height: 120,
                        color: Colors.grey[200],
                        child: const Center(
                          child: CircularProgressIndicator(strokeWidth: 2),
                        ),
                      );
                    },
                    errorBuilder: (context, error, stack) {
                      return Container(
                        width: 120,
                        height: 120,
                        color: Colors.grey[200],
                        child: const Icon(Icons.image_not_supported),
                      );
                    },
                  ),
                ),
                // 状态标签
                if (figure.status == 'presale')
                  _StatusBadge(text: '预售', color: Colors.orange),
                if (figure.status == 'soldout')
                  _StatusBadge(text: '售罄', color: Colors.grey),
              ],
            ),
            const SizedBox(width: 12),
            // 右侧信息
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        figure.name,
                        style: const TextStyle(
                          fontSize: 14,
                          fontWeight: FontWeight.w500,
                          color: Color(0xFF333333),
                        ),
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                      const SizedBox(height: 4),
                      Text(
                        figure.series,
                        style: TextStyle(
                          fontSize: 12,
                          color: Colors.grey[600],
                        ),
                      ),
                    ],
                  ),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Row(
                            crossAxisAlignment: CrossAxisAlignment.end,
                            children: [
                              Text(
                                '¥',
                                style: TextStyle(
                                  fontSize: 12,
                                  color: const Color(0xFFFF4081),
                                  fontWeight: FontWeight.bold,
                                ),
                              ),
                              Text(
                                '${figure.price}',
                                style: const TextStyle(
                                  fontSize: 18,
                                  color: Color(0xFFFF4081),
                                  fontWeight: FontWeight.bold,
                                ),
                              ),
                            ],
                          ),
                          if (figure.originalPrice > figure.price)
                            Text(
                              '¥${figure.originalPrice}',
                              style: TextStyle(
                                fontSize: 10,
                                color: Colors.grey[400],
                                decoration: TextDecoration.lineThrough,
                              ),
                            ),
                        ],
                      ),
                      GestureDetector(
                        onTap: onFavoriteTap,
                        child: Icon(
                          figure.isFavorite
                              ? Icons.favorite
                              : Icons.favorite_border,
                          color: figure.isFavorite
                              ? const Color(0xFFFF4081)
                              : Colors.grey[400],
                          size: 24,
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _StatusBadge extends StatelessWidget {
  final String text;
  final Color color;

  const _StatusBadge({required this.text, required this.color});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
      decoration: BoxDecoration(
        color: color,
        borderRadius: const BorderRadius.only(
          topLeft: Radius.circular(8),
          bottomRight: Radius.circular(8),
        ),
      ),
      child: Text(
        text,
        style: const TextStyle(
          fontSize: 10,
          color: Colors.white,
        ),
      ),
    );
  }
}

该组件充分展示了 Flutter 声明式 UI 的优势。通过组合各种基础组件,可以构建出复杂的自定义卡片。GestureDetector 提供了完善的点击反馈,Stack 实现元素层叠,NetworkImage 则处理了网络图片的加载和错误处理。

详情页面实现

详情页面需要展示完整的手办信息,并支持 3D 旋转预览效果:

dart 复制代码
import 'package:flutter/material.dart';
import '../model/figure_model.dart';
import '../service/figure_service.dart';

class DetailPage extends StatefulWidget {
  final String figureId;

  const DetailPage({super.key, required this.figureId});

  @override
  State<DetailPage> createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage>
    with SingleTickerProviderStateMixin {
  final FigureService _service = FigureService();
  Figure? _figure;
  bool _isLoading = true;
  bool _is3DMode = false;
  double _rotationY = 0;

  @override
  void initState() {
    super.initState();
    _loadDetail();
  }

  Future<void> _loadDetail() async {
    final figure = await _service.fetchFigureDetail(widget.figureId);
    if (mounted) {
      setState(() {
        _figure = figure;
        _isLoading = false;
      });
    }
  }

  Future<void> _toggleFavorite() async {
    if (_figure == null) return;
    final result = await _service.toggleFavorite(_figure!.id);
    setState(() {
      _figure = _figure!.copyWith(isFavorite: result);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF5F5F5),
      body: _isLoading
          ? const Center(
              child: CircularProgressIndicator(color: Color(0xFFFF4081)),
            )
          : _figure == null
              ? const Center(child: Text('商品不存在'))
              : CustomScrollView(
                  slivers: [
                    SliverAppBar(
                      expandedHeight: MediaQuery.of(context).size.width,
                      pinned: true,
                      backgroundColor: Colors.white,
                      flexibleSpace: FlexibleSpaceBar(
                        background: _buildImageSection(),
                      ),
                    ),
                    SliverToBoxAdapter(
                      child: _buildInfoSection(),
                    ),
                  ],
                ),
      bottomNavigationBar: _figure != null ? _buildBottomBar() : null,
    );
  }

  Widget _buildImageSection() {
    return GestureDetector(
      onPanUpdate: _is3DMode
          ? (details) {
              setState(() {
                _rotationY += details.delta.dx * 0.5;
              });
            }
          : null,
      child: Stack(
        fit: StackFit.expand,
        children: [
          Transform(
            alignment: Alignment.center,
            transform: Matrix4.identity()
              ..setEntry(3, 2, 0.001)
              ..rotateY(_rotationY * 3.14159 / 180),
            child: Image.network(
              _figure!.imageUrl,
              fit: BoxFit.contain,
            ),
          ),
          Positioned(
            top: MediaQuery.of(context).padding.top + 8,
            right: 16,
            child: GestureDetector(
              onTap: () {
                setState(() {
                  _is3DMode = !_is3DMode;
                  if (!_is3DMode) _rotationY = 0;
                });
              },
              child: Container(
                padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
                decoration: BoxDecoration(
                  color: Colors.black.withOpacity(0.5),
                  borderRadius: BorderRadius.circular(20),
                ),
                child: Text(
                  _is3DMode ? '普通模式' : '3D旋转',
                  style: const TextStyle(color: Colors.white, fontSize: 12),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildInfoSection() {
    return Container(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 价格区域
          Container(
            padding: const EdgeInsets.all(16),
            color: Colors.white,
            child: Row(
              children: [
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      crossAxisAlignment: CrossAxisAlignment.end,
                      children: [
                        const Text(
                          '¥',
                          style: TextStyle(
                            fontSize: 16,
                            color: Color(0xFFFF4081),
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        Text(
                          '${_figure!.price}',
                          style: const TextStyle(
                            fontSize: 28,
                            color: Color(0xFFFF4081),
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        if (_figure!.originalPrice > _figure!.price)
                          Padding(
                            padding: const EdgeInsets.only(left: 8),
                            child: Text(
                              '¥${_figure!.originalPrice}',
                              style: TextStyle(
                                fontSize: 14,
                                color: Colors.grey[400],
                                decoration: TextDecoration.lineThrough,
                              ),
                            ),
                          ),
                      ],
                    ),
                    const SizedBox(height: 4),
                    Row(
                      children: [
                        _buildStatusBadge(),
                        const SizedBox(width: 8),
                        Text(
                          '销量 ${_figure!.salesCount}',
                          style: TextStyle(fontSize: 12, color: Colors.grey[600]),
                        ),
                      ],
                    ),
                  ],
                ),
              ],
            ),
          ),
          const SizedBox(height: 8),
          // 基本信息
          Container(
            padding: const EdgeInsets.all(16),
            color: Colors.white,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  _figure!.name,
                  style: const TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                    color: Color(0xFF333333),
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  _figure!.description,
                  style: TextStyle(fontSize: 14, color: Colors.grey[600]),
                ),
                const Divider(height: 24),
                _buildInfoRow('系列', _figure!.series),
                _buildInfoRow('厂商', _figure!.manufacturer),
                _buildInfoRow('比例', _figure!.scale),
                _buildInfoRow('发售日期', _figure!.releaseDate),
                _buildInfoRow('评分', '${_figure!.rating}'),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildStatusBadge() {
    Color color;
    String text;
    switch (_figure!.status) {
      case 'presale':
        color = Colors.orange;
        text = '预售';
        break;
      case 'soldout':
        color = Colors.grey;
        text = '售罄';
        break;
      default:
        color = Colors.green;
        text = '在售';
    }
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
      decoration: BoxDecoration(
        color: color,
        borderRadius: BorderRadius.circular(4),
      ),
      child: Text(
        text,
        style: const TextStyle(fontSize: 10, color: Colors.white),
      ),
    );
  }

  Widget _buildInfoRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 12),
      child: Row(
        children: [
          SizedBox(
            width: 80,
            child: Text(
              label,
              style: TextStyle(fontSize: 14, color: Colors.grey[600]),
            ),
          ),
          Expanded(
            child: Text(
              value,
              style: const TextStyle(fontSize: 14, color: Color(0xFF333333)),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildBottomBar() {
    return Container(
      height: 80,
      padding: const EdgeInsets.symmetric(horizontal: 20),
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 10,
            offset: const Offset(0, -2),
          ),
        ],
      ),
      child: Row(
        children: [
          GestureDetector(
            onTap: _toggleFavorite,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(
                  _figure!.isFavorite ? Icons.favorite : Icons.favorite_border,
                  color: _figure!.isFavorite
                      ? const Color(0xFFFF4081)
                      : Colors.grey,
                ),
                Text(
                  _figure!.isFavorite ? '已收藏' : '收藏',
                  style: const TextStyle(fontSize: 10, color: Colors.grey),
                ),
              ],
            ),
          ),
          const SizedBox(width: 40),
          Expanded(
            child: ElevatedButton(
              onPressed: _figure!.status != 'soldout'
                  ? () {
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(content: Text('购买功能开发中...')),
                      );
                    }
                  : null,
              style: ElevatedButton.styleFrom(
                backgroundColor: const Color(0xFFFF4081),
                foregroundColor: Colors.white,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(24),
                ),
                padding: const EdgeInsets.symmetric(vertical: 12),
              ),
              child: const Text(
                '立即购买',
                style: TextStyle(fontSize: 16),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

详情页的实现展示了 Flutter 动画和手势处理的强大能力。通过 Matrix4 变换实现 3D 旋转效果,GestureDetector 处理用户的拖拽手势,这些在 Flutter 中都得到了优雅的封装。

应用入口

最后是应用的主入口文件:

dart 复制代码
import 'package:flutter/material.dart';
import 'view/main_page.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '手办商城',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primaryColor: const Color(0xFFFF4081),
        scaffoldBackgroundColor: const Color(0xFFF5F5F5),
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFFFF4081),
        ),
        useMaterial3: true,
      ),
      home: const SplashPage(),
    );
  }
}

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

  @override
  State<SplashPage> createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> {
  @override
  void initState() {
    super.initState();
    Future.delayed(const Duration(seconds: 1), () {
      if (mounted) {
        Navigator.of(context).pushReplacement(
          MaterialPageRoute(builder: (_) => const MainPage()),
        );
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const [
            Text(
              '手办商城',
              style: TextStyle(
                fontSize: 28,
                fontWeight: FontWeight.bold,
                color: Color(0xFFFF4081),
              ),
            ),
            SizedBox(height: 12),
            Text(
              '精致手办 触手可及',
              style: TextStyle(
                fontSize: 14,
                color: Colors.grey,
              ),
            ),
            SizedBox(height: 40),
            CircularProgressIndicator(
              color: Color(0xFFFF4081),
              strokeWidth: 2,
            ),
          ],
        ),
      ),
    );
  }
}

启动页采用经典的渐变过渡设计,提供了良好的首屏体验。MaterialApp 配置了应用的主题色彩,使用 Color(0xFFFF4081) 作为品牌粉色,符合手办展示应用的目标用户审美。

截图运行

经过以上开发步骤,应用已成功在 OpenHarmony 设备上运行。以下是运行效果截图:

启动页截图

展示应用启动时的品牌 Logo 和加载动画,确保用户在等待期间获得视觉反馈。

主页面截图

底部导航栏正常显示,点击切换流畅,列表数据加载完整。

详情页截图

手办图片清晰展示,价格和状态标签准确呈现,3D 旋转模式切换正常。

收藏页截图

收藏状态同步正常,点击取消收藏后列表实时更新。

代码托管

本文涉及的完整示例代码已托管至 AtomGit 平台,仓库地址如下:

复制代码
https://atomgit.com/maaath/figure_app_flutter_ohos

读者可通过该仓库获取完整项目代码,并在本地进行运行和调试。代码遵循 Apache 2.0 开源协议,欢迎 Star 和 Fork。

总结与展望

通过本文的实战演练,我们完整掌握了使用 Flutter for OpenHarmony 开发跨平台应用的核心技能。从项目结构设计到数据模型定义,从服务层封装到 UI 组件开发,每个环节都有其最佳实践可循。

Flutter 的声明式编程范式大大简化了 UI 开发的复杂度,其丰富的组件库和活跃的社区生态为开发者提供了强有力的支持。随着 OpenHarmony 平台的持续发展,Flutter for OpenHarmony 将成为越来越多跨平台应用的首选方案。

下一步建议读者尝试以下扩展方向:将模拟数据替换为真实的 HTTP 接口,添加下拉刷新和上拉加载功能,优化大图加载的性能表现,以及实现本地数据持久化存储。期待读者在实践中发现更多有趣的开发技巧。


参考资源:

相关推荐
jiejiejiejie_12 小时前
Flutter for OpenHarmony 心情日记功能实战指南
flutter·华为
jiejiejiejie_13 小时前
Flutter for OpenHarmony 倒计时功能实战开发
flutter
Math_teacher_fan14 小时前
Flutter 跨平台开发实战:鸿蒙与音乐律动艺术(六)、Lissajous 利萨茹曲线:频率耦合的轨迹艺术
flutter·ui·数学建模·华为·harmonyos·鸿蒙系统
里欧跑得慢14 小时前
17. Flutter Hero动画实现:让界面过渡更加优雅
前端·css·flutter·web
liulian091615 小时前
Flutter for OpenHarmony 跨平台开发:秒表功能实战指南
flutter
xmdy586615 小时前
Flutter+开源鸿蒙实战|智安盾电商溯源平台Day3 溯源查询逻辑+鸿蒙网络请求适配
flutter·开源·harmonyos
maaath16 小时前
【maaath】Flutter 跨平台日历日程应用开发实战
flutter·华为·harmonyos
LeesonWong17 小时前
架构困境与四层结构化设计
harmonyos
梦想不只是梦与想18 小时前
鸿蒙 应用市场更新功能:版本检测与更新提醒
harmonyos·鸿蒙·版本更新