【maaath】Flutter for OpenHarmony 跨平台图书阅读应用开发实践

Flutter for OpenHarmony 跨平台图书阅读应用开发实践

作者:maaath


社区引导

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


前言

在移动应用开发领域,跨平台技术一直是开发者关注的焦点。Flutter 作为 Google 推出的 UI 框架,凭借其高性能、热重载和丰富的组件库,在跨平台开发中占据重要地位。而 OpenHarmony 作为国产操作系统,正在积极拥抱跨平台生态。本文将介绍如何使用 Flutter for OpenHarmony 开发一个图书阅读应用,并分享开发过程中的实践经验。


一、项目概述

本文将基于 Flutter 框架,在 OpenHarmony 平台上构建一个完整的图书阅读应用。该应用具备以下核心功能:

  • 图书分类浏览
  • 推荐书籍展示
  • 书架管理
  • 阅读历史记录
  • 收藏功能

1.1 技术栈

  • 框架:Flutter SDK (OpenHarmony 适配版)
  • 状态管理:Provider
  • 本地存储:shared_preferences
  • 网络请求:http (鸿蒙化适配版本)
  • 目标平台:OpenHarmony 设备

1.2 项目结构

复制代码
lib/
├── main.dart
├── models/
│   └── book_model.dart
├── services/
│   └── book_service.dart
├── providers/
│   └── book_provider.dart
├── pages/
│   ├── home_page.dart
│   ├── category_page.dart
│   ├── shelf_page.dart
│   └── detail_page.dart
└── widgets/
    └── book_card.dart

二、数据模型设计

首先,我们需要定义图书数据模型。一个完整的图书模型应包含基本信息、分类、评分等字段。

dart 复制代码
// lib/models/book_model.dart

class BookModel {
  final String id;
  final String title;
  final String author;
  final String coverUrl;
  final String description;
  final String category;
  final double rating;
  final int reviewCount;
  final int wordCount;
  final int chapterCount;
  final String status;
  final List<String> tags;
  final bool isFavorite;
  final bool isInShelf;
  final int readingProgress;

  BookModel({
    required this.id,
    required this.title,
    required this.author,
    required this.coverUrl,
    required this.description,
    required this.category,
    required this.rating,
    required this.reviewCount,
    required this.wordCount,
    required this.chapterCount,
    required this.status,
    required this.tags,
    this.isFavorite = false,
    this.isInShelf = false,
    this.readingProgress = 0,
  });

  // 从 JSON 创建 BookModel 实例
  factory BookModel.fromJson(Map<String, dynamic> json) {
    return BookModel(
      id: json['id'] ?? '',
      title: json['title'] ?? '',
      author: json['author'] ?? '',
      coverUrl: json['coverUrl'] ?? '',
      description: json['description'] ?? '',
      category: json['category'] ?? '',
      rating: (json['rating'] ?? 0).toDouble(),
      reviewCount: json['reviewCount'] ?? 0,
      wordCount: json['wordCount'] ?? 0,
      chapterCount: json['chapterCount'] ?? 0,
      status: json['status'] ?? '',
      tags: List<String>.from(json['tags'] ?? []),
      isFavorite: json['isFavorite'] ?? false,
      isInShelf: json['isInShelf'] ?? false,
      readingProgress: json['readingProgress'] ?? 0,
    );
  }

  // 转换为 JSON
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'author': author,
      'coverUrl': coverUrl,
      'description': description,
      'category': category,
      'rating': rating,
      'reviewCount': reviewCount,
      'wordCount': wordCount,
      'chapterCount': chapterCount,
      'status': status,
      'tags': tags,
      'isFavorite': isFavorite,
      'isInShelf': isInShelf,
      'readingProgress': readingProgress,
    };
  }

  // 创建副本并修改指定字段
  BookModel copyWith({
    String? id,
    String? title,
    String? author,
    String? coverUrl,
    String? description,
    String? category,
    double? rating,
    int? reviewCount,
    int? wordCount,
    int? chapterCount,
    String? status,
    List<String>? tags,
    bool? isFavorite,
    bool? isInShelf,
    int? readingProgress,
  }) {
    return BookModel(
      id: id ?? this.id,
      title: title ?? this.title,
      author: author ?? this.author,
      coverUrl: coverUrl ?? this.coverUrl,
      description: description ?? this.description,
      category: category ?? this.category,
      rating: rating ?? this.rating,
      reviewCount: reviewCount ?? this.reviewCount,
      wordCount: wordCount ?? this.wordCount,
      chapterCount: chapterCount ?? this.chapterCount,
      status: status ?? this.status,
      tags: tags ?? this.tags,
      isFavorite: isFavorite ?? this.isFavorite,
      isInShelf: isInShelf ?? this.isInShelf,
      readingProgress: readingProgress ?? this.readingProgress,
    );
  }
}

上述代码展示了如何在 Flutter 中定义一个规范的数据模型。关键点包括:

  • 使用 final 关键字确保不可变字段
  • 通过 factory 构造函数实现 JSON 序列化
  • 提供 copyWith 方法支持不可变更新模式

三、图书服务层实现

服务层负责与后端交互,获取图书数据。为了保持代码的简洁性和可测试性,我们采用本地模拟数据的方式。

dart 复制代码
// lib/services/book_service.dart

import '../models/book_model.dart';

class BookService {
  // 获取推荐书籍列表
  Future<List<BookModel>> getRecommendBooks() async {
    // 模拟网络延迟
    await Future.delayed(const Duration(milliseconds: 500));

    return _mockBooks.where((book) {
      return book.tags.contains('推荐') || book.rating >= 4.5;
    }).toList();
  }

  // 获取分类书籍
  Future<List<BookModel>> getBooksByCategory(String category) async {
    await Future.delayed(const Duration(milliseconds: 300));

    if (category == '全部') {
      return _mockBooks;
    }
    return _mockBooks.where((book) => book.category == category).toList();
  }

  // 获取书架书籍
  Future<List<BookModel>> getShelfBooks() async {
    await Future.delayed(const Duration(milliseconds: 300));
    return _mockBooks.where((book) => book.isInShelf).toList();
  }

  // 获取收藏书籍
  Future<List<BookModel>> getFavoriteBooks() async {
    await Future.delayed(const Duration(milliseconds: 300));
    return _mockBooks.where((book) => book.isFavorite).toList();
  }

  // 获取所有分类
  Future<List<String>> getCategories() async {
    await Future.delayed(const Duration(milliseconds: 200));

    final categories = _mockBooks.map((book) => book.category).toSet().toList();
    return ['全部', ...categories];
  }

  // 搜索书籍
  Future<List<BookModel>> searchBooks(String keyword) async {
    await Future.delayed(const Duration(milliseconds: 300));

    final lowerKeyword = keyword.toLowerCase();
    return _mockBooks.where((book) {
      return book.title.toLowerCase().contains(lowerKeyword) ||
          book.author.toLowerCase().contains(lowerKeyword);
    }).toList();
  }

  // 切换收藏状态
  Future<BookModel> toggleFavorite(BookModel book) async {
    await Future.delayed(const Duration(milliseconds: 100));

    final index = _mockBooks.indexWhere((b) => b.id == book.id);
    if (index != -1) {
      final updatedBook = book.copyWith(isFavorite: !book.isFavorite);
      _mockBooks[index] = updatedBook;
      return updatedBook;
    }
    return book;
  }

  // 添加到书架
  Future<BookModel> addToShelf(BookModel book) async {
    await Future.delayed(const Duration(milliseconds: 100));

    final index = _mockBooks.indexWhere((b) => b.id == book.id);
    if (index != -1) {
      final updatedBook = book.copyWith(isInShelf: true);
      _mockBooks[index] = updatedBook;
      return updatedBook;
    }
    return book;
  }

  // 从书架移除
  Future<BookModel> removeFromShelf(BookModel book) async {
    await Future.delayed(const Duration(milliseconds: 100));

    final index = _mockBooks.indexWhere((b) => b.id == book.id);
    if (index != -1) {
      final updatedBook = book.copyWith(isInShelf: false, readingProgress: 0);
      _mockBooks[index] = updatedBook;
      return updatedBook;
    }
    return book;
  }

  // 模拟数据
  static final List<BookModel> _mockBooks = [
    BookModel(
      id: '1',
      title: '斗罗大陆',
      author: '唐家三少',
      coverUrl: 'https://picsum.photos/seed/book1/200/300',
      description: '讲述了唐门外门弟子唐三,因偷学内门绝学而被迫跳崖明志,却意外重生到一个全新的世界------斗罗大陆。',
      category: '玄幻',
      rating: 4.8,
      reviewCount: 125000,
      wordCount: 3000000,
      chapterCount: 300,
      status: '已完结',
      tags: ['推荐', '热门', '玄幻'],
      isFavorite: true,
      isInShelf: true,
      readingProgress: 65,
    ),
    BookModel(
      id: '2',
      title: '全职高手',
      author: '蝴蝶蓝',
      coverUrl: 'https://picsum.photos/seed/book2/200/300',
      description: '网游荣耀中被誉为教科书级别的顶尖高手叶修,因为种种原因遭到俱乐部的驱逐。',
      category: '游戏',
      rating: 4.7,
      reviewCount: 98000,
      wordCount: 2000000,
      chapterCount: 200,
      status: '已完结',
      tags: ['推荐', '游戏'],
      isFavorite: false,
      isInShelf: true,
      readingProgress: 30,
    ),
    BookModel(
      id: '3',
      title: '庆余年',
      author: '猫腻',
      coverUrl: 'https://picsum.photos/seed/book3/200/300',
      description: '一个年轻的病人,因为一次被迫的逃亡,意外来到一个神秘的世界,开始了一段传奇的人生。',
      category: '历史',
      rating: 4.6,
      reviewCount: 86000,
      wordCount: 2500000,
      chapterCount: 250,
      status: '已完结',
      tags: ['热门', '历史'],
      isFavorite: false,
      isInShelf: false,
      readingProgress: 0,
    ),
    BookModel(
      id: '4',
      title: '凡人修仙传',
      author: '忘语',
      coverUrl: 'https://picsum.photos/seed/book4/200/300',
      description: '一个资质平庸的少年,偶然踏入修真界,历经磨难,最终成为一代大修士。',
      category: '玄幻',
      rating: 4.9,
      reviewCount: 156000,
      wordCount: 8000000,
      chapterCount: 800,
      status: '已完结',
      tags: ['推荐', '热门', '玄幻'],
      isFavorite: true,
      isInShelf: true,
      readingProgress: 45,
    ),
    BookModel(
      id: '5',
      title: '大奉打更人',
      author: '卖报小郎君',
      coverUrl: 'https://picsum.photos/seed/book5/200/300',
      description: '这个世界儒道佛三教并立,武者称霸天下。这是一个警察穿越成为打更人的故事。',
      category: '仙侠',
      rating: 4.5,
      reviewCount: 72000,
      wordCount: 3500000,
      chapterCount: 350,
      status: '已完结',
      tags: ['热门', '仙侠'],
      isFavorite: false,
      isInShelf: true,
      readingProgress: 80,
    ),
    BookModel(
      id: '6',
      title: '明朝那些事儿',
      author: '当年明月',
      coverUrl: 'https://picsum.photos/seed/book6/200/300',
      description: '以史料为基础,用轻松幽默的语言讲述了明朝三百年间的历史故事。',
      category: '历史',
      rating: 4.8,
      reviewCount: 110000,
      wordCount: 1500000,
      chapterCount: 150,
      status: '已完结',
      tags: ['推荐', '历史'],
      isFavorite: false,
      isInShelf: false,
      readingProgress: 0,
    ),
  ];
}

服务层采用单例模式,通过 Future 实现异步数据获取,便于后续接入真实 API。


四、状态管理

使用 Provider 进行状态管理,这是一种轻量级且易于理解的状态管理方案。

dart 复制代码
// lib/providers/book_provider.dart

import 'package:flutter/foundation.dart';
import '../models/book_model.dart';
import '../services/book_service.dart';

class BookProvider extends ChangeNotifier {
  final BookService _bookService = BookService();

  List<BookModel> _recommendBooks = [];
  List<BookModel> _categoryBooks = [];
  List<BookModel> _shelfBooks = [];
  List<BookModel> _favoriteBooks = [];
  List<BookModel> _searchResults = [];
  List<String> _categories = [];

  bool _isLoading = false;
  String _selectedCategory = '全部';
  String _searchKeyword = '';

  // Getters
  List<BookModel> get recommendBooks => _recommendBooks;
  List<BookModel> get categoryBooks => _categoryBooks;
  List<BookModel> get shelfBooks => _shelfBooks;
  List<BookModel> get favoriteBooks => _favoriteBooks;
  List<BookModel> get searchResults => _searchResults;
  List<String> get categories => _categories;
  bool get isLoading => _isLoading;
  String get selectedCategory => _selectedCategory;
  String get searchKeyword => _searchKeyword;

  // 初始化数据
  Future<void> initialize() async {
    await loadRecommendBooks();
    await loadCategories();
    await loadShelfBooks();
    await loadFavoriteBooks();
  }

  // 加载推荐书籍
  Future<void> loadRecommendBooks() async {
    _isLoading = true;
    notifyListeners();

    try {
      _recommendBooks = await _bookService.getRecommendBooks();
    } catch (e) {
      debugPrint('Error loading recommend books: $e');
    }

    _isLoading = false;
    notifyListeners();
  }

  // 加载分类书籍
  Future<void> loadCategoryBooks(String category) async {
    _selectedCategory = category;
    _isLoading = true;
    notifyListeners();

    try {
      _categoryBooks = await _bookService.getBooksByCategory(category);
    } catch (e) {
      debugPrint('Error loading category books: $e');
    }

    _isLoading = false;
    notifyListeners();
  }

  // 加载所有分类
  Future<void> loadCategories() async {
    try {
      _categories = await _bookService.getCategories();
    } catch (e) {
      debugPrint('Error loading categories: $e');
    }
    notifyListeners();
  }

  // 加载书架
  Future<void> loadShelfBooks() async {
    try {
      _shelfBooks = await _bookService.getShelfBooks();
      notifyListeners();
    } catch (e) {
      debugPrint('Error loading shelf books: $e');
    }
  }

  // 加载收藏
  Future<void> loadFavoriteBooks() async {
    try {
      _favoriteBooks = await _bookService.getFavoriteBooks();
      notifyListeners();
    } catch (e) {
      debugPrint('Error loading favorite books: $e');
    }
  }

  // 搜索书籍
  Future<void> searchBooks(String keyword) async {
    _searchKeyword = keyword;

    if (keyword.isEmpty) {
      _searchResults = [];
      notifyListeners();
      return;
    }

    try {
      _searchResults = await _bookService.searchBooks(keyword);
      notifyListeners();
    } catch (e) {
      debugPrint('Error searching books: $e');
    }
  }

  // 切换收藏
  Future<void> toggleFavorite(BookModel book) async {
    try {
      final updatedBook = await _bookService.toggleFavorite(book);

      // 更新本地数据
      _updateBookInList(_recommendBooks, updatedBook);
      _updateBookInList(_categoryBooks, updatedBook);
      _updateBookInList(_shelfBooks, updatedBook);

      if (updatedBook.isFavorite) {
        _favoriteBooks.add(updatedBook);
      } else {
        _favoriteBooks.removeWhere((b) => b.id == book.id);
      }

      notifyListeners();
    } catch (e) {
      debugPrint('Error toggling favorite: $e');
    }
  }

  // 添加到书架
  Future<void> addToShelf(BookModel book) async {
    try {
      final updatedBook = await _bookService.addToShelf(book);

      _updateBookInList(_recommendBooks, updatedBook);
      _updateBookInList(_categoryBooks, updatedBook);

      if (updatedBook.isInShelf) {
        _shelfBooks.add(updatedBook);
      }

      notifyListeners();
    } catch (e) {
      debugPrint('Error adding to shelf: $e');
    }
  }

  // 从书架移除
  Future<void> removeFromShelf(BookModel book) async {
    try {
      final updatedBook = await _bookService.removeFromShelf(book);

      _updateBookInList(_recommendBooks, updatedBook);
      _updateBookInList(_categoryBooks, updatedBook);
      _shelfBooks.removeWhere((b) => b.id == book.id);

      notifyListeners();
    } catch (e) {
      debugPrint('Error removing from shelf: $e');
    }
  }

  // 更新列表中的书籍
  void _updateBookInList(List<BookModel> list, BookModel updatedBook) {
    final index = list.indexWhere((b) => b.id == updatedBook.id);
    if (index != -1) {
      list[index] = updatedBook;
    }
  }
}

Provider 的优势在于代码简洁、易于理解,非常适合中小型应用的状态管理场景。


五、页面实现

5.1 首页实现

dart 复制代码
// lib/pages/home_page.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/book_provider.dart';
import '../models/book_model.dart';
import '../widgets/book_card.dart';

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  void initState() {
    super.initState();
    // 初始化加载数据
    WidgetsBinding.instance.addPostFrameCallback((_) {
      context.read<BookProvider>().initialize();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF5F5F5),
      body: SafeArea(
        child: Consumer<BookProvider>(
          builder: (context, provider, child) {
            if (provider.isLoading && provider.recommendBooks.isEmpty) {
              return const Center(child: CircularProgressIndicator());
            }

            return RefreshIndicator(
              onRefresh: () => provider.loadRecommendBooks(),
              child: CustomScrollView(
                slivers: [
                  // 应用栏
                  SliverAppBar(
                    floating: true,
                    backgroundColor: Colors.white,
                    title: const Text(
                      '图书阅读',
                      style: TextStyle(
                        color: Color(0xFF333333),
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    actions: [
                      IconButton(
                        icon: const Icon(Icons.search, color: Color(0xFF666666)),
                        onPressed: () => _showSearchDialog(context),
                      ),
                    ],
                  ),

                  // 推荐书籍标题
                  SliverToBoxAdapter(
                    child: Padding(
                      padding: const EdgeInsets.all(16),
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          const Text(
                            '热门推荐',
                            style: TextStyle(
                              fontSize: 20,
                              fontWeight: FontWeight.bold,
                              color: Color(0xFF333333),
                            ),
                          ),
                          TextButton(
                            onPressed: () {},
                            child: const Text(
                              '查看更多',
                              style: TextStyle(color: Color(0xFFFF6B6B)),
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),

                  // 推荐书籍列表
                  SliverPadding(
                    padding: const EdgeInsets.symmetric(horizontal: 16),
                    sliver: SliverToBoxAdapter(
                      child: SizedBox(
                        height: 220,
                        child: ListView.builder(
                          scrollDirection: Axis.horizontal,
                          itemCount: provider.recommendBooks.length,
                          itemBuilder: (context, index) {
                            final book = provider.recommendBooks[index];
                            return Padding(
                              padding: const EdgeInsets.only(right: 12),
                              child: _buildBookCard(book),
                            );
                          },
                        ),
                      ),
                    ),
                  ),

                  // 新书上架标题
                  const SliverToBoxAdapter(
                    child: Padding(
                      padding: EdgeInsets.all(16),
                      child: Text(
                        '新书上架',
                        style: TextStyle(
                          fontSize: 20,
                          fontWeight: FontWeight.bold,
                          color: Color(0xFF333333),
                        ),
                      ),
                    ),
                  ),

                  // 新书列表
                  SliverPadding(
                    padding: const EdgeInsets.symmetric(horizontal: 16),
                    sliver: SliverList(
                      delegate: SliverChildBuilderDelegate(
                        (context, index) {
                          final book = provider.recommendBooks[index];
                          return Padding(
                            padding: const EdgeInsets.only(bottom: 12),
                            child: _buildBookListItem(book, provider),
                          );
                        },
                        childCount: provider.recommendBooks.length > 4
                            ? 4
                            : provider.recommendBooks.length,
                      ),
                    ),
                  ),

                  // 底部间距
                  const SliverToBoxAdapter(
                    child: SizedBox(height: 100),
                  ),
                ],
              ),
            );
          },
        ),
      ),
    );
  }

  Widget _buildBookCard(BookModel book) {
    return GestureDetector(
      onTap: () => _showBookDetail(book),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 封面图
          Container(
            width: 130,
            height: 160,
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(8),
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withOpacity(0.1),
                  blurRadius: 8,
                  offset: const Offset(2, 4),
                ),
              ],
            ),
            child: ClipRRect(
              borderRadius: BorderRadius.circular(8),
              child: Image.network(
                book.coverUrl,
                fit: BoxFit.cover,
                errorBuilder: (_, __, ___) => Container(
                  color: const Color(0xFFE0E0E0),
                  child: const Icon(Icons.book, size: 40),
                ),
              ),
            ),
          ),
          const SizedBox(height: 8),
          // 书名
          SizedBox(
            width: 130,
            child: Text(
              book.title,
              style: const TextStyle(
                fontSize: 14,
                fontWeight: FontWeight.w500,
                color: Color(0xFF333333),
              ),
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
          ),
          // 作者
          SizedBox(
            width: 130,
            child: Text(
              book.author,
              style: const TextStyle(
                fontSize: 12,
                color: Color(0xFF999999),
              ),
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildBookListItem(BookModel book, BookProvider provider) {
    return GestureDetector(
      onTap: () => _showBookDetail(book),
      child: Container(
        padding: const EdgeInsets.all(12),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(12),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.05),
              blurRadius: 8,
              offset: const Offset(0, 2),
            ),
          ],
        ),
        child: Row(
          children: [
            // 封面
            ClipRRect(
              borderRadius: BorderRadius.circular(6),
              child: Image.network(
                book.coverUrl,
                width: 70,
                height: 95,
                fit: BoxFit.cover,
                errorBuilder: (_, __, ___) => Container(
                  width: 70,
                  height: 95,
                  color: const Color(0xFFE0E0E0),
                  child: const Icon(Icons.book),
                ),
              ),
            ),
            const SizedBox(width: 12),
            // 信息
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    book.title,
                    style: const TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                      color: Color(0xFF333333),
                    ),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                  const SizedBox(height: 4),
                  Text(
                    book.author,
                    style: const TextStyle(
                      fontSize: 13,
                      color: Color(0xFF666666),
                    ),
                  ),
                  const SizedBox(height: 8),
                  Row(
                    children: [
                      // 评分
                      const Icon(Icons.star, size: 14, color: Color(0xFFFFB800)),
                      const SizedBox(width: 4),
                      Text(
                        book.rating.toStringAsFixed(1),
                        style: const TextStyle(
                          fontSize: 12,
                          color: Color(0xFF666666),
                        ),
                      ),
                      const SizedBox(width: 12),
                      // 状态
                      Container(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 6,
                          vertical: 2,
                        ),
                        decoration: BoxDecoration(
                          color: book.status == '已完结'
                              ? const Color(0xFF4CAF50).withOpacity(0.1)
                              : const Color(0xFFFF9800).withOpacity(0.1),
                          borderRadius: BorderRadius.circular(4),
                        ),
                        child: Text(
                          book.status,
                          style: TextStyle(
                            fontSize: 10,
                            color: book.status == '已完结'
                                ? const Color(0xFF4CAF50)
                                : const Color(0xFFFF9800),
                          ),
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 8),
                  // 简介
                  Text(
                    book.description,
                    style: const TextStyle(
                      fontSize: 12,
                      color: Color(0xFF999999),
                    ),
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                ],
              ),
            ),
            // 收藏按钮
            IconButton(
              icon: Icon(
                book.isFavorite ? Icons.favorite : Icons.favorite_border,
                color: book.isFavorite
                    ? const Color(0xFFFF6B6B)
                    : const Color(0xFFCCCCCC),
              ),
              onPressed: () => provider.toggleFavorite(book),
            ),
          ],
        ),
      ),
    );
  }

  void _showBookDetail(BookModel book) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (_) => BookDetailPage(book: book),
      ),
    );
  }

  void _showSearchDialog(BuildContext context) {
    showSearch(context: context, delegate: BookSearchDelegate());
  }
}

// 书籍详情页
class BookDetailPage extends StatelessWidget {
  final BookModel book;

  const BookDetailPage({super.key, required this.book});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF5F5F5),
      body: CustomScrollView(
        slivers: [
          // 顶部图片区域
          SliverAppBar(
            expandedHeight: 250,
            pinned: true,
            flexibleSpace: FlexibleSpaceBar(
              background: Stack(
                fit: StackFit.expand,
                children: [
                  Image.network(
                    book.coverUrl,
                    fit: BoxFit.cover,
                    errorBuilder: (_, __, ___) => Container(
                      color: const Color(0xFFE0E0E0),
                    ),
                  ),
                  Container(
                    decoration: BoxDecoration(
                      gradient: LinearGradient(
                        begin: Alignment.topCenter,
                        end: Alignment.bottomCenter,
                        colors: [
                          Colors.transparent,
                          Colors.black.withOpacity(0.7),
                        ],
                      ),
                    ),
                  ),
                  Positioned(
                    bottom: 16,
                    left: 16,
                    child: Text(
                      book.title,
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 24,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),

          // 书籍信息
          SliverToBoxAdapter(
            child: Container(
              margin: const EdgeInsets.all(16),
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(12),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      const Icon(Icons.person, size: 16, color: Color(0xFF666666)),
                      const SizedBox(width: 4),
                      Text(
                        book.author,
                        style: const TextStyle(color: Color(0xFF666666)),
                      ),
                    ],
                  ),
                  const SizedBox(height: 8),
                  Row(
                    children: [
                      const Icon(Icons.category, size: 16, color: Color(0xFF666666)),
                      const SizedBox(width: 4),
                      Text(
                        book.category,
                        style: const TextStyle(color: Color(0xFF666666)),
                      ),
                      const SizedBox(width: 16),
                      const Icon(Icons.star, size: 16, color: Color(0xFFFFB800)),
                      const SizedBox(width: 4),
                      Text(
                        book.rating.toStringAsFixed(1),
                        style: const TextStyle(color: Color(0xFF666666)),
                      ),
                    ],
                  ),
                  const SizedBox(height: 16),
                  const Text(
                    '简介',
                    style: TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                      color: Color(0xFF333333),
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    book.description,
                    style: const TextStyle(
                      fontSize: 14,
                      color: Color(0xFF666666),
                      height: 1.6,
                    ),
                  ),
                ],
              ),
            ),
          ),

          // 底部操作按钮
          SliverToBoxAdapter(
            child: Container(
              padding: const EdgeInsets.all(16),
              child: Row(
                children: [
                  Expanded(
                    child: ElevatedButton(
                      onPressed: () {},
                      style: ElevatedButton.styleFrom(
                        backgroundColor: const Color(0xFFFF6B6B),
                        padding: const EdgeInsets.symmetric(vertical: 14),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(24),
                        ),
                      ),
                      child: const Text(
                        '开始阅读',
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: 16,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(width: 12),
                  IconButton(
                    onPressed: () {},
                    icon: Icon(
                      book.isFavorite ? Icons.favorite : Icons.favorite_border,
                      color: const Color(0xFFFF6B6B),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

// 搜索委托
class BookSearchDelegate extends SearchDelegate<BookModel?> {
  @override
  List<Widget> buildActions(BuildContext context) {
    return [
      IconButton(
        icon: const Icon(Icons.clear),
        onPressed: () => query = '',
      ),
    ];
  }

  @override
  Widget buildLeading(BuildContext context) {
    return IconButton(
      icon: const Icon(Icons.arrow_back),
      onPressed: () => close(context, null),
    );
  }

  @override
  Widget buildResults(BuildContext context) {
    if (query.isEmpty) {
      return const Center(child: Text('请输入搜索关键词'));
    }

    // 实际项目中应该调用搜索 API
    return Center(
      child: Text('搜索: $query'),
    );
  }

  @override
  Widget buildSuggestions(BuildContext context) {
    return buildResults(context);
  }
}

5.2 书架页面实现

dart 复制代码
// lib/pages/shelf_page.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/book_provider.dart';
import '../models/book_model.dart';

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

  @override
  State<ShelfPage> createState() => _ShelfPageState();
}

class _ShelfPageState extends State<ShelfPage> {
  bool _isEditMode = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      context.read<BookProvider>().loadShelfBooks();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF5F5F5),
      appBar: AppBar(
        backgroundColor: Colors.white,
        title: const Text(
          '我的书架',
          style: TextStyle(
            color: Color(0xFF333333),
            fontWeight: FontWeight.bold,
          ),
        ),
        actions: [
          TextButton(
            onPressed: () {
              setState(() {
                _isEditMode = !_isEditMode;
              });
            },
            child: Text(
              _isEditMode ? '完成' : '编辑',
              style: const TextStyle(color: Color(0xFFFF6B6B)),
            ),
          ),
        ],
      ),
      body: Consumer<BookProvider>(
        builder: (context, provider, child) {
          if (provider.shelfBooks.isEmpty) {
            return _buildEmptyState();
          }

          return GridView.builder(
            padding: const EdgeInsets.all(16),
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 3,
              childAspectRatio: 0.65,
              crossAxisSpacing: 12,
              mainAxisSpacing: 12,
            ),
            itemCount: provider.shelfBooks.length,
            itemBuilder: (context, index) {
              final book = provider.shelfBooks[index];
              return _buildShelfBookItem(book, provider);
            },
          );
        },
      ),
    );
  }

  Widget _buildEmptyState() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.library_books_outlined,
            size: 80,
            color: Colors.grey[300],
          ),
          const SizedBox(height: 16),
          Text(
            '书架空空如也',
            style: TextStyle(
              fontSize: 18,
              color: Colors.grey[400],
            ),
          ),
          const SizedBox(height: 8),
          Text(
            '快去发现喜欢的书籍吧',
            style: TextStyle(
              fontSize: 14,
              color: Colors.grey[400],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildShelfBookItem(BookModel book, BookProvider provider) {
    return GestureDetector(
      onTap: () {},
      child: Stack(
        children: [
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // 封面
              Expanded(
                child: Container(
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(8),
                    boxShadow: [
                      BoxShadow(
                        color: Colors.black.withOpacity(0.1),
                        blurRadius: 6,
                        offset: const Offset(2, 3),
                      ),
                    ],
                  ),
                  child: ClipRRect(
                    borderRadius: BorderRadius.circular(8),
                    child: Image.network(
                      book.coverUrl,
                      fit: BoxFit.cover,
                      width: double.infinity,
                      errorBuilder: (_, __, ___) => Container(
                        color: const Color(0xFFE0E0E0),
                        child: const Icon(Icons.book),
                      ),
                    ),
                  ),
                ),
              ),
              const SizedBox(height: 8),
              // 书名
              Text(
                book.title,
                style: const TextStyle(
                  fontSize: 12,
                  fontWeight: FontWeight.w500,
                  color: Color(0xFF333333),
                ),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),
              // 阅读进度
              if (book.readingProgress > 0)
                LinearProgressIndicator(
                  value: book.readingProgress / 100,
                  backgroundColor: const Color(0xFFEEEEEE),
                  valueColor: const AlwaysStoppedAnimation<Color>(
                    Color(0xFFFF6B6B),
                  ),
                ),
            ],
          ),
          // 阅读进度
          if (book.readingProgress > 0)
            Positioned(
              bottom: 40,
              left: 0,
              right: 0,
              child: Container(
                padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
                color: Colors.black.withOpacity(0.5),
                child: Text(
                  '${book.readingProgress}%',
                  style: const TextStyle(
                    color: Colors.white,
                    fontSize: 10,
                  ),
                  textAlign: TextAlign.center,
                ),
              ),
            ),
        ],
      ),
    );
  }
}

六、图书卡片组件

为了提高代码复用性,我们将图书卡片封装为独立组件。

dart 复制代码
// lib/widgets/book_card.dart

import 'package:flutter/material.dart';
import '../models/book_model.dart';

class BookCard extends StatelessWidget {
  final BookModel book;
  final VoidCallback? onTap;
  final VoidCallback? onFavorite;

  const BookCard({
    super.key,
    required this.book,
    this.onTap,
    this.onFavorite,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Container(
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(12),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.08),
              blurRadius: 10,
              offset: const Offset(0, 4),
            ),
          ],
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 封面图片
            Expanded(
              flex: 3,
              child: Stack(
                fit: StackFit.expand,
                children: [
                  ClipRRect(
                    borderRadius: const BorderRadius.vertical(
                      top: Radius.circular(12),
                    ),
                    child: Image.network(
                      book.coverUrl,
                      fit: BoxFit.cover,
                      errorBuilder: (_, __, ___) => Container(
                        color: const Color(0xFFE0E0E0),
                        child: const Icon(
                          Icons.book,
                          size: 48,
                          color: Color(0xFFCCCCCC),
                        ),
                      ),
                    ),
                  ),
                  // 状态标签
                  if (book.status == '已完结')
                    Positioned(
                      top: 8,
                      left: 8,
                      child: Container(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 6,
                          vertical: 2,
                        ),
                        decoration: BoxDecoration(
                          color: const Color(0xFF4CAF50),
                          borderRadius: BorderRadius.circular(4),
                        ),
                        child: const Text(
                          '完结',
                          style: TextStyle(
                            color: Colors.white,
                            fontSize: 10,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ),
                    ),
                  // 收藏按钮
                  Positioned(
                    top: 8,
                    right: 8,
                    child: GestureDetector(
                      onTap: onFavorite,
                      child: Container(
                        padding: const EdgeInsets.all(4),
                        decoration: BoxDecoration(
                          color: Colors.white.withOpacity(0.9),
                          shape: BoxShape.circle,
                        ),
                        child: Icon(
                          book.isFavorite
                              ? Icons.favorite
                              : Icons.favorite_border,
                          size: 16,
                          color: book.isFavorite
                              ? const Color(0xFFFF6B6B)
                              : const Color(0xFFCCCCCC),
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
            // 书籍信息
            Expanded(
              flex: 1,
              child: Padding(
                padding: const EdgeInsets.all(8),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    // 书名
                    Text(
                      book.title,
                      style: const TextStyle(
                        fontSize: 13,
                        fontWeight: FontWeight.w600,
                        color: Color(0xFF333333),
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    // 作者
                    Text(
                      book.author,
                      style: const TextStyle(
                        fontSize: 11,
                        color: Color(0xFF999999),
                      ),
                      maxLines: 1,
                      overflow: TextOverflow.ellipsis,
                    ),
                    // 评分
                    Row(
                      children: [
                        const Icon(
                          Icons.star,
                          size: 12,
                          color: Color(0xFFFFB800),
                        ),
                        const SizedBox(width: 2),
                        Text(
                          book.rating.toStringAsFixed(1),
                          style: const TextStyle(
                            fontSize: 11,
                            color: Color(0xFF666666),
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

七、主应用入口

dart 复制代码
// lib/main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/book_provider.dart';
import 'pages/home_page.dart';
import 'pages/shelf_page.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => BookProvider()),
      ],
      child: MaterialApp(
        title: '图书阅读',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          primaryColor: const Color(0xFFFF6B6B),
          scaffoldBackgroundColor: const Color(0xFFF5F5F5),
          colorScheme: ColorScheme.fromSeed(
            seedColor: const Color(0xFFFF6B6B),
          ),
          appBarTheme: const AppBarTheme(
            backgroundColor: Colors.white,
            elevation: 0,
            iconTheme: IconThemeData(color: Color(0xFF333333)),
            titleTextStyle: TextStyle(
              color: Color(0xFF333333),
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
        home: const MainNavigationPage(),
      ),
    );
  }
}

// 主导航页面
class MainNavigationPage extends StatefulWidget {
  const MainNavigationPage({super.key});

  @override
  State<MainNavigationPage> createState() => _MainNavigationPageState();
}

class _MainNavigationPageState extends State<MainNavigationPage> {
  int _currentIndex = 0;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: _pages,
      ),
      bottomNavigationBar: Container(
        decoration: BoxDecoration(
          color: Colors.white,
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.05),
              blurRadius: 10,
              offset: const Offset(0, -5),
            ),
          ],
        ),
        child: SafeArea(
          child: Padding(
            padding: const EdgeInsets.symmetric(vertical: 8),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                _buildNavItem(0, Icons.home, '首页'),
                _buildNavItem(1, Icons.library_books, '书架'),
                _buildNavItem(2, Icons.person, '我的'),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildNavItem(int index, IconData icon, String label) {
    final isSelected = _currentIndex == index;
    return GestureDetector(
      onTap: () {
        setState(() {
          _currentIndex = index;
        });
      },
      behavior: HitTestBehavior.opaque,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(
              icon,
              size: 26,
              color: isSelected
                  ? const Color(0xFFFF6B6B)
                  : const Color(0xFF999999),
            ),
            const SizedBox(height: 4),
            Text(
              label,
              style: TextStyle(
                fontSize: 12,
                color: isSelected
                    ? const Color(0xFFFF6B6B)
                    : const Color(0xFF999999),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// 个人中心页面
class ProfilePage extends StatelessWidget {
  const ProfilePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF5F5F5),
      appBar: AppBar(
        title: const Text('个人中心'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Container(
              width: 80,
              height: 80,
              decoration: BoxDecoration(
                color: const Color(0xFFE0E0E0),
                shape: BoxShape.circle,
              ),
              child: const Icon(
                Icons.person,
                size: 48,
                color: Color(0xFFCCCCCC),
              ),
            ),
            const SizedBox(height: 16),
            const Text(
              '未登录',
              style: TextStyle(
                fontSize: 18,
                color: Color(0xFF333333),
              ),
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () {},
              style: ElevatedButton.styleFrom(
                backgroundColor: const Color(0xFFFF6B6B),
                padding: const EdgeInsets.symmetric(
                  horizontal: 32,
                  vertical: 12,
                ),
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(24),
                ),
              ),
              child: const Text(
                '登录 / 注册',
                style: TextStyle(color: Colors.white),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

八、截图运行板块

8.1 项目运行截图

以下是 Flutter 图书阅读应用在 OpenHarmony 设备上的运行效果:

图1:进入页面展示

图2:推荐页面展示

图3:我的书架页面

图4:个人中心页面

8.2 运行说明

  1. 确保已安装 Flutter SDK(OpenHarmony 适配版)
  2. 连接 OpenHarmony 设备或启动模拟器
  3. 执行以下命令运行应用:
bash 复制代码
flutter run -d <设备ID>
  1. 应用启动后,将自动加载推荐书籍数据

8.3 功能验证

经过实际测试,应用在 OpenHarmony 设备上能够正常运行以下功能:

  • 图书列表展示
  • 下拉刷新
  • 收藏功能切换
  • 书架管理
  • 页面导航切换

九、代码仓库

本文完整代码已托管至 AtomGit:

仓库地址https://atomgit.com/maaath/flutter-book-reader

仓库包含完整的 Flutter 项目结构,支持直接导入 IDE 进行开发和调试。


十、总结

本文通过一个完整的图书阅读应用实例,展示了如何使用 Flutter for OpenHarmony 进行跨平台应用开发。主要涵盖了以下内容:

  1. 数据模型设计:使用 Dart 类定义规范的数据模型,支持 JSON 序列化
  2. 服务层封装:通过服务类实现数据访问的解耦,便于后续扩展
  3. 状态管理:采用 Provider 进行状态管理,代码简洁易维护
  4. 页面实现:使用 Flutter 丰富的 UI 组件构建美观的界面
  5. 组件复用:通过抽取公共组件提高代码复用性

Flutter for OpenHarmony 为开发者提供了良好的跨平台开发体验,其热重载功能大大提升了开发效率。随着 OpenHarmony 生态的不断完善,Flutter 在该平台上的应用前景将更加广阔。


十一、参考资源

  1. Flutter 官方文档:https://flutter.dev
  2. OpenHarmony 开发者文档:https://developer.harmonyos.com
  3. Provider 状态管理:https://pub.dev/packages/provider

相关推荐
maaath7 小时前
【maaath】 Flutter for OpenHarmony 新闻资讯应用实战开发
flutter·华为·harmonyos
xmdy58667 小时前
Flutter+开源鸿蒙实战|智联邻里Day2 首页UI开发+全局组件封装+鸿蒙多端适配
flutter·开源·harmonyos
特立独行的猫a7 小时前
移植 vcpkg 到鸿蒙 PC:vcpkg-tool 交叉编译与实践手记
华为·harmonyos·vcpkg·鸿蒙pc·vcpkg-tool
911hzh8 小时前
Flutter 音视频通话集成实战:WebSocket 做信令,WebRTC 传音视频,附详细事件时序图
websocket·flutter·音视频
万添裁8 小时前
huawei 机考
算法·华为·深度优先
里欧跑得慢16 小时前
15. Web可访问性最佳实践:让每个用户都能平等访问
前端·css·flutter·web
nashane17 小时前
HarmonyOS Wi-Fi连接用户操作监听全解析:从系统弹框到Promise回调
华为·harmonyos·harmonyos 5
Lanren的编程日记19 小时前
Flutter 鸿蒙应用数据版本管理实战:版本记录+版本回退+版本对比,实现全链路数据版本控制
flutter·华为·harmonyos
我是大聪明.20 小时前
DeepSeek V4 Pro + 华为昇腾910:国产大模型落地的性能实测与深度解析
人工智能·华为