Flutter观影记录账本应用开发教程

项目简介

这是一款功能全面的观影记录账本应用,为电影爱好者提供专业的观影管理和统计分析功能。应用采用Material Design 3设计风格,支持电影信息记录、观影状态管理、评分系统、统计分析、搜索筛选等功能,界面简洁美观,操作流畅便捷,是管理个人观影历程的理想工具。
运行效果图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

核心特性

  • 完整电影信息:标题、导演、演员、年份、时长、类型等详细信息
  • 观影状态管理:已观看、正在看、想看、弃坑四种状态
  • 评分系统:1-10分精确评分和观影感受记录
  • 智能搜索:支持标题、导演、演员的模糊搜索
  • 多维筛选:按类型、状态、评分等条件筛选
  • 统计分析:观影数据的可视化统计和趋势分析
  • 观影清单:想看和在看电影的专门管理
  • 数据管理:支持数据备份、恢复和导入导出
  • 个性化设置:灵活的排序和显示选项
  • 精美界面:渐变设计和流畅的用户体验

技术栈

  • Flutter 3.x
  • Material Design 3
  • 数据持久化
  • 动画控制器(AnimationController)
  • 状态管理
  • 图表统计

项目架构

MovieRecordHomePage

MovieListPage

StatisticsPage

WishlistPage

SettingsPage

MovieListHeader

SearchAndFilter

MovieList

AddMovieDialog

HeaderCards

SearchBar

FilterChips

MovieCard

MovieDetailsDialog

EditDialog

DeleteConfirmation

OverviewStats

GenreStats

YearlyStats

RatingDistribution

StatCards

ProgressBars

YearList

RatingChart

WishlistTabs

StatusLists

PlannedTab

WatchingTab

MovieStatusList

DataManagement

AboutSection

BackupRestore

ClearData

VersionInfo

HelpDialog

MovieRecord

WatchStatus

SortBy

AnimationController

数据模型设计

MovieRecord(电影记录模型)

class MovieRecord {
  final String id;                      // 唯一标识
  final String title;                   // 电影标题
  final String director;                // 导演
  final List<String> genres;            // 类型列表
  final int year;                       // 年份
  final int duration;                   // 时长(分钟)
  final double rating;                  // 评分(1-10)
  final String review;                  // 观影感受
  final DateTime watchDate;             // 观看日期
  final String poster;                  // 海报URL
  final List<String> actors;            // 演员列表
  final String country;                 // 国家/地区
  final String language;                // 语言
  final WatchStatus status;             // 观看状态
  final List<String> tags;              // 标签列表

  const MovieRecord({
    required this.id,
    required this.title,
    required this.director,
    required this.genres,
    required this.year,
    required this.duration,
    required this.rating,
    required this.review,
    required this.watchDate,
    this.poster = '',
    this.actors = const [],
    this.country = '',
    this.language = '',
    this.status = WatchStatus.watched,
    this.tags = const [],
  });
}

设计要点

  • id使用时间戳确保唯一性
  • genres和actors使用列表支持多个值
  • rating使用double支持精确评分
  • watchDate记录具体观看时间
  • status使用枚举确保数据一致性

WatchStatus(观看状态枚举)

enum WatchStatus {
  watched,    // 已观看
  watching,   // 正在观看
  planned,    // 计划观看
  dropped,    // 弃坑
}

SortBy(排序方式枚举)

enum SortBy {
  watchDate,  // 观看日期
  rating,     // 评分
  title,      // 标题
  year,       // 年份
  duration,   // 时长
}

数据关系图

渲染错误: Mermaid 渲染失败: Parse error on line 32: ... duration } MovieReco ----------------------^ Expecting 'ATTRIBUTE_WORD', got 'BLOCK_STOP'

统计数据模型

// 统计信息
int _totalMovies = 0;                    // 总观影数
int _totalHours = 0;                     // 总观影时长
double _averageRating = 0.0;             // 平均评分
Map<String, int> _genreStats = {};       // 类型统计

// 筛选条件
String _searchQuery = '';                // 搜索关键词
List<String> _selectedGenres = [];       // 选中的类型
WatchStatus? _selectedStatus;            // 选中的状态
SortBy _sortBy = SortBy.watchDate;       // 排序方式
bool _sortAscending = false;             // 排序方向

核心功能实现

1. 电影记录管理

实现完整的电影信息录入和管理功能。

void _showAddMovieDialog() {
  showDialog(
    context: context,
    builder: (context) => _AddMovieDialog(
      onSave: (movie) {
        setState(() {
          _movieRecords.add(movie);
        });
        _updateStatistics();
      },
    ),
  );
}

class _AddMovieDialog extends StatefulWidget {
  final Function(MovieRecord) onSave;

  const _AddMovieDialog({required this.onSave});

  
  State<_AddMovieDialog> createState() => _AddMovieDialogState();
}

class _AddMovieDialogState extends State<_AddMovieDialog> {
  final _formKey = GlobalKey<FormState>();
  final _titleController = TextEditingController();
  final _directorController = TextEditingController();
  final _yearController = TextEditingController();
  final _durationController = TextEditingController();
  final _reviewController = TextEditingController();

  double _rating = 8.0;
  List<String> _selectedGenres = [];
  WatchStatus _status = WatchStatus.watched;
  DateTime _watchDate = DateTime.now();

  void _saveMovie() {
    if (_formKey.currentState!.validate()) {
      final movie = MovieRecord(
        id: DateTime.now().millisecondsSinceEpoch.toString(),
        title: _titleController.text,
        director: _directorController.text,
        genres: _selectedGenres,
        year: int.tryParse(_yearController.text) ?? DateTime.now().year,
        duration: int.tryParse(_durationController.text) ?? 120,
        rating: _rating,
        review: _reviewController.text,
        watchDate: _watchDate,
        status: _status,
      );

      widget.onSave(movie);
      Navigator.pop(context);
    }
  }
}

功能特点

  • 表单验证:必填字段的完整性检查
  • 类型选择:多选FilterChip支持多种类型
  • 评分滑块:直观的1-10分评分选择
  • 日期选择:DatePicker选择观看日期
  • 状态管理:下拉选择观看状态

2. 搜索和筛选系统

实现强大的搜索和多维度筛选功能。

List<MovieRecord> _getFilteredMovies() {
  var filtered = _movieRecords.where((movie) {
    // 搜索过滤
    if (_searchQuery.isNotEmpty) {
      final query = _searchQuery.toLowerCase();
      if (!movie.title.toLowerCase().contains(query) &&
          !movie.director.toLowerCase().contains(query) &&
          !movie.actors.any((actor) => actor.toLowerCase().contains(query))) {
        return false;
      }
    }

    // 类型过滤
    if (_selectedGenres.isNotEmpty) {
      if (!movie.genres.any((genre) => _selectedGenres.contains(genre))) {
        return false;
      }
    }

    // 状态过滤
    if (_selectedStatus != null && movie.status != _selectedStatus) {
      return false;
    }

    return true;
  }).toList();

  // 排序
  filtered.sort((a, b) {
    switch (_sortBy) {
      case SortBy.watchDate:
        return _sortAscending 
            ? a.watchDate.compareTo(b.watchDate)
            : b.watchDate.compareTo(a.watchDate);
      case SortBy.rating:
        return _sortAscending 
            ? a.rating.compareTo(b.rating)
            : b.rating.compareTo(a.rating);
      case SortBy.title:
        return _sortAscending 
            ? a.title.compareTo(b.title)
            : b.title.compareTo(a.title);
      case SortBy.year:
        return _sortAscending 
            ? a.year.compareTo(b.year)
            : b.year.compareTo(a.year);
      case SortBy.duration:
        return _sortAscending 
            ? a.duration.compareTo(b.duration)
            : b.duration.compareTo(a.duration);
    }
  });

  return filtered;
}

Widget _buildSearchAndFilter() {
  return Container(
    padding: const EdgeInsets.all(16),
    child: Column(
      children: [
        TextField(
          decoration: const InputDecoration(
            hintText: '搜索电影标题、导演或演员...',
            prefixIcon: Icon(Icons.search),
            border: OutlineInputBorder(),
          ),
          onChanged: (value) {
            setState(() {
              _searchQuery = value;
            });
          },
        ),
        const SizedBox(height: 12),
        Row(
          children: [
            Expanded(
              child: _buildFilterChip('筛选类型', 
                _selectedGenres.isEmpty ? '全部' : _selectedGenres.join(', '), 
                () => _showGenreFilter()),
            ),
            const SizedBox(width: 8),
            Expanded(
              child: _buildFilterChip('观看状态', 
                _getStatusName(_selectedStatus), 
                () => _showStatusFilter()),
            ),
            const SizedBox(width: 8),
            Expanded(
              child: _buildFilterChip('排序', 
                _getSortName(_sortBy), 
                () => _showSortOptions()),
            ),
          ],
        ),
      ],
    ),
  );
}

搜索筛选特点

  • 模糊搜索:支持标题、导演、演员的模糊匹配
  • 多维筛选:类型、状态的组合筛选
  • 灵活排序:多种排序方式和升降序切换
  • 实时更新:输入即时更新搜索结果

3. 统计分析系统

提供详细的观影数据统计和可视化分析。

void _updateStatistics() {
  setState(() {
    _totalMovies = _movieRecords.where((m) => m.status == WatchStatus.watched).length;
    _totalHours = _movieRecords
        .where((m) => m.status == WatchStatus.watched)
        .fold(0, (sum, movie) => sum + movie.duration) ~/ 60;
    
    final watchedMovies = _movieRecords.where((m) => m.status == WatchStatus.watched).toList();
    if (watchedMovies.isNotEmpty) {
      _averageRating = watchedMovies
          .map((m) => m.rating)
          .reduce((a, b) => a + b) / watchedMovies.length;
    }

    _genreStats.clear();
    for (final movie in watchedMovies) {
      for (final genre in movie.genres) {
        _genreStats[genre] = (_genreStats[genre] ?? 0) + 1;
      }
    }
  });
}

Widget _buildGenreStats() {
  if (_genreStats.isEmpty) {
    return const SizedBox.shrink();
  }

  final sortedGenres = _genreStats.entries.toList()
    ..sort((a, b) => b.value.compareTo(a.value));

  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '类型偏好',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          ...sortedGenres.take(5).map((entry) {
            final percentage = (entry.value / _totalMovies * 100).round();
            return Padding(
              padding: const EdgeInsets.only(bottom: 12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text(entry.key),
                      Text('${entry.value}部 ($percentage%)'),
                    ],
                  ),
                  const SizedBox(height: 4),
                  LinearProgressIndicator(
                    value: entry.value / _totalMovies,
                    backgroundColor: Colors.grey.shade200,
                    valueColor: AlwaysStoppedAnimation<Color>(Colors.indigo.shade400),
                  ),
                ],
              ),
            );
          }),
        ],
      ),
    ),
  );
}

Widget _buildRatingDistribution() {
  final watchedMovies = _movieRecords.where((m) => m.status == WatchStatus.watched).toList();
  final ratingRanges = <String, int>{
    '9-10分': 0,
    '8-9分': 0,
    '7-8分': 0,
    '6-7分': 0,
    '6分以下': 0,
  };

  for (final movie in watchedMovies) {
    if (movie.rating >= 9) {
      ratingRanges['9-10分'] = ratingRanges['9-10分']! + 1;
    } else if (movie.rating >= 8) {
      ratingRanges['8-9分'] = ratingRanges['8-9分']! + 1;
    } else if (movie.rating >= 7) {
      ratingRanges['7-8分'] = ratingRanges['7-8分']! + 1;
    } else if (movie.rating >= 6) {
      ratingRanges['6-7分'] = ratingRanges['6-7分']! + 1;
    } else {
      ratingRanges['6分以下'] = ratingRanges['6分以下']! + 1;
    }
  }

  return Card(
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            '评分分布',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          ...ratingRanges.entries.map((entry) {
            return Padding(
              padding: const EdgeInsets.only(bottom: 12),
              child: Row(
                children: [
                  SizedBox(
                    width: 60,
                    child: Text(entry.key),
                  ),
                  Expanded(
                    child: LinearProgressIndicator(
                      value: _totalMovies > 0 ? entry.value / _totalMovies : 0,
                      backgroundColor: Colors.grey.shade200,
                      valueColor: AlwaysStoppedAnimation<Color>(Colors.amber.shade600),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Text('${entry.value}部'),
                ],
              ),
            );
          }),
        ],
      ),
    ),
  );
}

统计分析特点

  • 多维统计:观影数量、时长、评分等多维度统计
  • 类型偏好:可视化显示最喜欢的电影类型
  • 评分分布:直观展示评分区间分布
  • 年度统计:按年份统计观影数量趋势

4. 电影卡片展示

设计美观的电影信息卡片展示。

Widget _buildMovieCard(MovieRecord movie) {
  return Card(
    margin: const EdgeInsets.only(bottom: 16),
    child: InkWell(
      onTap: () => _showMovieDetails(movie),
      borderRadius: BorderRadius.circular(12),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 海报占位符
            Container(
              width: 60,
              height: 90,
              decoration: BoxDecoration(
                color: Colors.grey.shade300,
                borderRadius: BorderRadius.circular(8),
              ),
              child: movie.poster.isEmpty
                  ? const Icon(Icons.movie, size: 30, color: Colors.grey)
                  : ClipRRect(
                      borderRadius: BorderRadius.circular(8),
                      child: Image.network(
                        movie.poster,
                        fit: BoxFit.cover,
                        errorBuilder: (context, error, stackTrace) {
                          return const Icon(Icons.movie, size: 30, color: Colors.grey);
                        },
                      ),
                    ),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      Expanded(
                        child: Text(
                          movie.title,
                          style: const TextStyle(
                            fontSize: 16,
                            fontWeight: FontWeight.bold,
                          ),
                          maxLines: 2,
                          overflow: TextOverflow.ellipsis,
                        ),
                      ),
                      Container(
                        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                        decoration: BoxDecoration(
                          color: _getStatusColor(movie.status),
                          borderRadius: BorderRadius.circular(12),
                        ),
                        child: Text(
                          _getStatusName(movie.status),
                          style: const TextStyle(
                            fontSize: 10,
                            color: Colors.white,
                            fontWeight: FontWeight.w500,
                          ),
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 4),
                  Text(
                    '导演: ${movie.director}',
                    style: TextStyle(
                      fontSize: 14,
                      color: Colors.grey.shade600,
                    ),
                  ),
                  const SizedBox(height: 4),
                  Row(
                    children: [
                      Text(
                        '${movie.year}',
                        style: TextStyle(
                          fontSize: 12,
                          color: Colors.grey.shade600,
                        ),
                      ),
                      const SizedBox(width: 8),
                      Text(
                        '${movie.duration}分钟',
                        style: TextStyle(
                          fontSize: 12,
                          color: Colors.grey.shade600,
                        ),
                      ),
                      const Spacer(),
                      Row(
                        children: [
                          const Icon(Icons.star, size: 16, color: Colors.amber),
                          const SizedBox(width: 4),
                          Text(
                            movie.rating.toStringAsFixed(1),
                            style: const TextStyle(
                              fontSize: 14,
                              fontWeight: FontWeight.w500,
                            ),
                          ),
                        ],
                      ),
                    ],
                  ),
                  const SizedBox(height: 8),
                  Wrap(
                    spacing: 4,
                    runSpacing: 4,
                    children: movie.genres.map((genre) {
                      return Container(
                        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
                        decoration: BoxDecoration(
                          color: Colors.indigo.shade50,
                          borderRadius: BorderRadius.circular(12),
                        ),
                        child: Text(
                          genre,
                          style: TextStyle(
                            fontSize: 10,
                            color: Colors.indigo.shade700,
                          ),
                        ),
                      );
                    }).toList(),
                  ),
                  if (movie.review.isNotEmpty) ...[
                    const SizedBox(height: 8),
                    Text(
                      movie.review,
                      style: TextStyle(
                        fontSize: 12,
                        color: Colors.grey.shade700,
                        fontStyle: FontStyle.italic,
                      ),
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                    ),
                  ],
                ],
              ),
            ),
          ],
        ),
      ),
    ),
  );
}

卡片设计特点

  • 信息层次:清晰的信息层次和视觉重点
  • 状态标识:彩色标签显示观看状态
  • 评分展示:星级图标和数字评分
  • 类型标签:彩色标签展示电影类型
  • 交互反馈:点击效果和详情跳转

5. 观影清单管理

专门的观影计划和进度管理功能。

Widget _buildWishlistContent() {
  final plannedMovies = _movieRecords.where((m) => m.status == WatchStatus.planned).toList();
  final watchingMovies = _movieRecords.where((m) => m.status == WatchStatus.watching).toList();

  return DefaultTabController(
    length: 2,
    child: Column(
      children: [
        const TabBar(
          tabs: [
            Tab(text: '想看'),
            Tab(text: '在看'),
          ],
        ),
        Expanded(
          child: TabBarView(
            children: [
              _buildMovieStatusList(plannedMovies, '暂无想看的电影'),
              _buildMovieStatusList(watchingMovies, '暂无正在观看的电影'),
            ],
          ),
        ),
      ],
    ),
  );
}

Widget _buildMovieStatusList(List<MovieRecord> movies, String emptyMessage) {
  if (movies.isEmpty) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.bookmark_outline, size: 64, color: Colors.grey.shade400),
          const SizedBox(height: 16),
          Text(emptyMessage, style: TextStyle(fontSize: 16, color: Colors.grey.shade600)),
        ],
      ),
    );
  }

  return ListView.builder(
    padding: const EdgeInsets.all(16),
    itemCount: movies.length,
    itemBuilder: (context, index) {
      return _buildMovieCard(movies[index]);
    },
  );
}

清单管理特点

  • 分类管理:想看和在看的分别管理
  • 状态切换:便捷的状态更新功能
  • 进度跟踪:观影进度的可视化展示
  • 空状态处理:友好的空列表提示

6. 电影详情展示

详细的电影信息展示和操作功能。

void _showMovieDetails(MovieRecord movie) {
  showDialog(
    context: context,
    builder: (context) => _MovieDetailsDialog(
      movie: movie,
      onEdit: (editedMovie) {
        setState(() {
          final index = _movieRecords.indexWhere((m) => m.id == movie.id);
          if (index != -1) {
            _movieRecords[index] = editedMovie;
          }
        });
        _updateStatistics();
      },
      onDelete: () {
        setState(() {
          _movieRecords.removeWhere((m) => m.id == movie.id);
        });
        _updateStatistics();
      },
    ),
  );
}

class _MovieDetailsDialog extends StatelessWidget {
  final MovieRecord movie;
  final Function(MovieRecord) onEdit;
  final VoidCallback onDelete;

  const _MovieDetailsDialog({
    required this.movie,
    required this.onEdit,
    required this.onDelete,
  });

  
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text(movie.title),
      content: SizedBox(
        width: 400,
        height: 500,
        child: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              _buildDetailRow('导演', movie.director),
              _buildDetailRow('年份', '${movie.year}'),
              _buildDetailRow('时长', '${movie.duration}分钟'),
              _buildDetailRow('评分', '${movie.rating}/10'),
              _buildDetailRow('状态', _getStatusName(movie.status)),
              _buildDetailRow('观看日期', _formatDate(movie.watchDate)),
              if (movie.genres.isNotEmpty)
                _buildDetailRow('类型', movie.genres.join(', ')),
              if (movie.actors.isNotEmpty)
                _buildDetailRow('演员', movie.actors.join(', ')),
              if (movie.country.isNotEmpty)
                _buildDetailRow('国家', movie.country),
              if (movie.language.isNotEmpty)
                _buildDetailRow('语言', movie.language),
              if (movie.review.isNotEmpty) ...[
                const SizedBox(height: 16),
                const Text('观影感受:', style: TextStyle(fontWeight: FontWeight.bold)),
                const SizedBox(height: 8),
                Text(movie.review),
              ],
            ],
          ),
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('关闭'),
        ),
        TextButton(
          onPressed: () {
            Navigator.pop(context);
            _showEditDialog(context);
          },
          child: const Text('编辑'),
        ),
        TextButton(
          onPressed: () {
            Navigator.pop(context);
            _showDeleteConfirmation(context);
          },
          child: const Text('删除', style: TextStyle(color: Colors.red)),
        ),
      ],
    );
  }

  Widget _buildDetailRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 80,
            child: Text(
              '$label:',
              style: const TextStyle(fontWeight: FontWeight.w500),
            ),
          ),
          Expanded(child: Text(value)),
        ],
      ),
    );
  }
}

详情展示特点

  • 完整信息:展示所有电影相关信息
  • 操作按钮:编辑、删除等操作功能
  • 格式化显示:清晰的标签-值格式
  • 滚动支持:长内容的滚动查看

UI组件设计

1. 导航栏设计

采用Material Design 3的NavigationBar组件,提供清晰的页面导航。

NavigationBar(
  selectedIndex: _selectedIndex,
  onDestinationSelected: (index) {
    setState(() {
      _selectedIndex = index;
    });
  },
  destinations: const [
    NavigationDestination(
      icon: Icon(Icons.movie_outlined),
      selectedIcon: Icon(Icons.movie),
      label: '观影记录',
    ),
    NavigationDestination(
      icon: Icon(Icons.analytics_outlined),
      selectedIcon: Icon(Icons.analytics),
      label: '统计分析',
    ),
    NavigationDestination(
      icon: Icon(Icons.bookmark_outlined),
      selectedIcon: Icon(Icons.bookmark),
      label: '观影清单',
    ),
    NavigationDestination(
      icon: Icon(Icons.settings_outlined),
      selectedIcon: Icon(Icons.settings),
      label: '设置',
    ),
  ],
)

设计特点

  • 图标状态:选中和未选中状态的不同图标
  • 标签清晰:简洁明了的功能标签
  • 视觉反馈:选中状态的视觉突出
  • 一致性:统一的设计语言

2. 页面头部设计

每个页面都有独特的渐变色头部,增强视觉层次。

Widget _buildMovieListHeader() {
  return Container(
    padding: const EdgeInsets.fromLTRB(16, 48, 16, 16),
    decoration: BoxDecoration(
      gradient: LinearGradient(
        colors: [Colors.indigo.shade600, Colors.indigo.shade400],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ),
    ),
    child: Column(
      children: [
        // 标题和图标
        Row(
          children: [
            const Icon(Icons.movie, color: Colors.white, size: 32),
            const SizedBox(width: 12),
            const Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    '观影记录账本',
                    style: TextStyle(
                      fontSize: 24,
                      fontWeight: FontWeight.bold,
                      color: Colors.white,
                    ),
                  ),
                  Text(
                    '记录每一部精彩的电影',
                    style: TextStyle(
                      fontSize: 14,
                      color: Colors.white70,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
        const SizedBox(height: 16),
        // 统计卡片
        Row(
          children: [
            Expanded(
              child: _buildHeaderCard(
                '观影总数',
                '$_totalMovies',
                Icons.movie_creation,
              ),
            ),
            // 更多统计卡片...
          ],
        ),
      ],
    ),
  );
}

头部特色

  • 渐变背景:美观的渐变色彩
  • 统计展示:关键数据的快速预览
  • 层次分明:清晰的信息层次结构
  • 响应式:适配不同屏幕尺寸

3. 卡片组件设计

统一的卡片设计语言,提供一致的用户体验。

Widget _buildStatCard(String title, String value, IconData icon, Color color) {
  return Container(
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
      color: color.withValues(alpha: 0.1),
      borderRadius: BorderRadius.circular(12),
      border: Border.all(color: color.withValues(alpha: 0.3)),
    ),
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(icon, color: color, size: 24),
        const SizedBox(height: 8),
        Text(
          value,
          style: TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.bold,
            color: color,
          ),
        ),
        Text(
          title,
          style: TextStyle(
            fontSize: 12,
            color: Colors.grey.shade600,
          ),
        ),
      ],
    ),
  );
}

卡片特点

  • 圆角设计:现代化的圆角边框
  • 颜色主题:统一的颜色体系
  • 阴影效果:适度的阴影增强层次
  • 内容布局:合理的内容间距和对齐

4. 表单组件设计

用户友好的表单输入体验。

TextFormField(
  controller: _titleController,
  decoration: const InputDecoration(
    labelText: '电影标题 *',
    border: OutlineInputBorder(),
    prefixIcon: Icon(Icons.movie),
  ),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return '请输入电影标题';
    }
    return null;
  },
)

表单特色

  • 清晰标签:明确的字段标识
  • 输入验证:实时的输入验证反馈
  • 图标提示:直观的功能图标
  • 错误处理:友好的错误提示信息

功能扩展建议

1. 数据同步功能

实现云端数据同步,支持多设备数据共享。

class CloudSyncService {
  // 上传数据到云端
  Future<void> uploadData(List<MovieRecord> records) async {
    // 实现云端上传逻辑
  }
  
  // 从云端下载数据
  Future<List<MovieRecord>> downloadData() async {
    // 实现云端下载逻辑
    return [];
  }
  
  // 数据同步
  Future<void> syncData() async {
    // 实现增量同步逻辑
  }
}

扩展价值

  • 多设备同步:手机、平板、电脑数据一致
  • 数据安全:云端备份防止数据丢失
  • 离线支持:离线操作,联网时自动同步

2. 社交分享功能

添加观影记录的社交分享能力。

class SocialShareService {
  // 分享到社交平台
  Future<void> shareMovie(MovieRecord movie) async {
    final text = '我刚看了《${movie.title}》,评分${movie.rating}/10分!${movie.review}';
    // 调用分享插件
  }
  
  // 生成观影海报
  Future<Uint8List> generateMoviePoster(MovieRecord movie) async {
    // 生成精美的观影分享海报
    return Uint8List(0);
  }
}

3. 推荐系统

基于观影历史的智能推荐功能。

class RecommendationEngine {
  // 基于类型偏好推荐
  List<String> recommendByGenre(List<MovieRecord> history) {
    // 分析用户类型偏好,推荐相似电影
    return [];
  }
  
  // 基于评分推荐
  List<String> recommendByRating(List<MovieRecord> history) {
    // 分析高评分电影特征,推荐类似作品
    return [];
  }
}

4. 观影计划提醒

智能的观影计划和提醒功能。

class WatchReminderService {
  // 设置观影提醒
  Future<void> setWatchReminder(MovieRecord movie, DateTime reminderTime) async {
    // 设置本地通知提醒
  }
  
  // 智能推荐观影时间
  DateTime suggestWatchTime(MovieRecord movie) {
    // 基于电影时长和用户习惯推荐观影时间
    return DateTime.now();
  }
}

5. 数据导入导出

支持多种格式的数据导入导出。

class DataImportExport {
  // 导出为CSV格式
  Future<String> exportToCSV(List<MovieRecord> records) async {
    // 生成CSV格式数据
    return '';
  }
  
  // 导出为JSON格式
  Future<String> exportToJSON(List<MovieRecord> records) async {
    // 生成JSON格式数据
    return '';
  }
  
  // 从豆瓣导入
  Future<List<MovieRecord>> importFromDouban(String userId) async {
    // 从豆瓣API导入观影记录
    return [];
  }
}

6. 高级统计分析

更丰富的数据分析和可视化功能。

class AdvancedAnalytics {
  // 观影趋势分析
  Map<String, dynamic> analyzeTrends(List<MovieRecord> records) {
    // 分析观影频率、类型变化等趋势
    return {};
  }
  
  // 导演偏好分析
  Map<String, int> analyzeDirectorPreference(List<MovieRecord> records) {
    // 统计最喜欢的导演
    return {};
  }
  
  // 观影时间分析
  Map<String, dynamic> analyzeWatchingTime(List<MovieRecord> records) {
    // 分析观影时间分布
    return {};
  }
}

7. 电影信息自动获取

集成电影数据库API,自动获取电影信息。

class MovieInfoService {
  // 搜索电影信息
  Future<List<MovieInfo>> searchMovie(String title) async {
    // 调用TMDB或豆瓣API搜索电影
    return [];
  }
  
  // 获取详细信息
  Future<MovieInfo> getMovieDetails(String movieId) async {
    // 获取电影详细信息
    return MovieInfo();
  }
}

8. 个性化主题

支持多种主题和个性化设置。

class ThemeService {
  // 切换主题
  void switchTheme(ThemeMode mode) {
    // 切换亮色/暗色主题
  }
  
  // 自定义颜色
  void setCustomColor(Color primaryColor) {
    // 设置自定义主色调
  }
}

性能优化建议

1. 列表性能优化

对于大量电影记录的高效渲染。

// 使用ListView.builder进行懒加载
ListView.builder(
  itemCount: filteredMovies.length,
  itemBuilder: (context, index) {
    return _buildMovieCard(filteredMovies[index]);
  },
  // 添加缓存范围
  cacheExtent: 1000,
)

// 实现虚拟滚动
class VirtualizedMovieList extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        SliverList(
          delegate: SliverChildBuilderDelegate(
            (context, index) => _buildMovieCard(movies[index]),
            childCount: movies.length,
          ),
        ),
      ],
    );
  }
}

2. 图片加载优化

优化海报图片的加载和缓存。

// 使用cached_network_image插件
CachedNetworkImage(
  imageUrl: movie.poster,
  placeholder: (context, url) => const CircularProgressIndicator(),
  errorWidget: (context, url, error) => const Icon(Icons.movie),
  fit: BoxFit.cover,
  memCacheWidth: 200, // 限制内存中的图片尺寸
  memCacheHeight: 300,
)

// 预加载关键图片
void preloadImages() {
  for (final movie in _movieRecords.take(10)) {
    if (movie.poster.isNotEmpty) {
      precacheImage(NetworkImage(movie.poster), context);
    }
  }
}

3. 数据存储优化

高效的本地数据存储方案。

// 使用Hive进行高性能本地存储
class MovieStorage {
  static late Box<MovieRecord> _movieBox;
  
  static Future<void> init() async {
    Hive.registerAdapter(MovieRecordAdapter());
    _movieBox = await Hive.openBox<MovieRecord>('movies');
  }
  
  static Future<void> saveMovie(MovieRecord movie) async {
    await _movieBox.put(movie.id, movie);
  }
  
  static List<MovieRecord> getAllMovies() {
    return _movieBox.values.toList();
  }
}

// 实现增量更新
class IncrementalUpdate {
  static void updateStatistics(MovieRecord newMovie) {
    // 只更新受影响的统计数据,避免全量重计算
  }
}

4. 搜索性能优化

实现高效的搜索算法。

class SearchOptimizer {
  // 建立搜索索引
  static Map<String, List<MovieRecord>> _searchIndex = {};
  
  static void buildSearchIndex(List<MovieRecord> movies) {
    _searchIndex.clear();
    for (final movie in movies) {
      // 为标题、导演、演员建立索引
      _addToIndex(movie.title.toLowerCase(), movie);
      _addToIndex(movie.director.toLowerCase(), movie);
      for (final actor in movie.actors) {
        _addToIndex(actor.toLowerCase(), movie);
      }
    }
  }
  
  static List<MovieRecord> search(String query) {
    final results = <MovieRecord>{};
    final keywords = query.toLowerCase().split(' ');
    
    for (final keyword in keywords) {
      final matches = _searchIndex[keyword] ?? [];
      results.addAll(matches);
    }
    
    return results.toList();
  }
}

5. 内存管理优化

避免内存泄漏和过度使用。

class MemoryManager {
  // 及时释放资源
  
  void dispose() {
    _fadeController.dispose();
    _scaleController.dispose();
    _titleController.dispose();
    // 释放所有控制器和监听器
    super.dispose();
  }
  
  // 使用弱引用避免循环引用
  WeakReference<MovieRecord>? _currentMovie;
  
  // 限制缓存大小
  static const int maxCacheSize = 100;
  static final LRUCache<String, MovieRecord> _cache = 
      LRUCache<String, MovieRecord>(maxCacheSize);
}

测试建议

1. 单元测试

测试核心业务逻辑和数据处理。

// test/movie_record_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:movie_record_app/models/movie_record.dart';

void main() {
  group('MovieRecord Tests', () {
    test('should create movie record with required fields', () {
      final movie = MovieRecord(
        id: '1',
        title: '测试电影',
        director: '测试导演',
        genres: ['剧情'],
        year: 2024,
        duration: 120,
        rating: 8.5,
        review: '很好看',
        watchDate: DateTime.now(),
      );
      
      expect(movie.title, '测试电影');
      expect(movie.rating, 8.5);
      expect(movie.status, WatchStatus.watched);
    });
    
    test('should validate rating range', () {
      expect(() => MovieRecord(
        id: '1',
        title: '测试电影',
        director: '测试导演',
        genres: ['剧情'],
        year: 2024,
        duration: 120,
        rating: 11.0, // 超出范围
        review: '很好看',
        watchDate: DateTime.now(),
      ), throwsArgumentError);
    });
  });
}

2. Widget测试

测试UI组件的渲染和交互。

// test/widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:movie_record_app/main.dart';

void main() {
  testWidgets('should display movie list', (WidgetTester tester) async {
    await tester.pumpWidget(const MovieRecordApp());
    
    // 验证导航栏存在
    expect(find.text('观影记录'), findsOneWidget);
    expect(find.text('统计分析'), findsOneWidget);
    
    // 验证添加按钮存在
    expect(find.byType(FloatingActionButton), findsOneWidget);
    
    // 点击添加按钮
    await tester.tap(find.byType(FloatingActionButton));
    await tester.pumpAndSettle();
    
    // 验证对话框打开
    expect(find.text('添加观影记录'), findsOneWidget);
  });
  
  testWidgets('should filter movies by search query', (WidgetTester tester) async {
    await tester.pumpWidget(const MovieRecordApp());
    
    // 输入搜索关键词
    await tester.enterText(find.byType(TextField), '肖申克');
    await tester.pumpAndSettle();
    
    // 验证搜索结果
    expect(find.text('肖申克的救赎'), findsOneWidget);
  });
}

3. 集成测试

测试完整的用户流程。

// integration_test/app_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:movie_record_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  
  group('Movie Record App Integration Tests', () {
    testWidgets('complete movie adding flow', (WidgetTester tester) async {
      app.main();
      await tester.pumpAndSettle();
      
      // 点击添加按钮
      await tester.tap(find.byType(FloatingActionButton));
      await tester.pumpAndSettle();
      
      // 填写电影信息
      await tester.enterText(find.byKey(const Key('title_field')), '新电影');
      await tester.enterText(find.byKey(const Key('director_field')), '新导演');
      
      // 选择类型
      await tester.tap(find.text('剧情'));
      
      // 保存电影
      await tester.tap(find.text('保存'));
      await tester.pumpAndSettle();
      
      // 验证电影已添加
      expect(find.text('新电影'), findsOneWidget);
    });
  });
}

4. 性能测试

测试应用性能和响应速度。

// test/performance_test.dart
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('Performance Tests', () {
    test('should handle large dataset efficiently', () {
      final stopwatch = Stopwatch()..start();
      
      // 创建大量测试数据
      final movies = List.generate(1000, (index) => MovieRecord(
        id: '$index',
        title: '电影$index',
        director: '导演$index',
        genres: ['剧情'],
        year: 2024,
        duration: 120,
        rating: 8.0,
        review: '测试评论',
        watchDate: DateTime.now(),
      ));
      
      // 测试搜索性能
      final filteredMovies = movies.where((movie) => 
          movie.title.contains('100')).toList();
      
      stopwatch.stop();
      
      // 验证性能要求(例如:1000条记录搜索应在100ms内完成)
      expect(stopwatch.elapsedMilliseconds, lessThan(100));
      expect(filteredMovies.length, greaterThan(0));
    });
  });
}

部署指南

1. Android部署

配置Android应用的构建和发布。

# android/app/build.gradle
android {
    compileSdkVersion 34
    
    defaultConfig {
        applicationId "com.example.movie_record_app"
        minSdkVersion 21
        targetSdkVersion 34
        versionCode 1
        versionName "1.0.0"
    }
    
    signingConfigs {
        release {
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
            storePassword keystoreProperties['storePassword']
        }
    }
    
    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

构建命令

# 构建APK
flutter build apk --release

# 构建App Bundle
flutter build appbundle --release

# 安装到设备
flutter install --release

2. iOS部署

配置iOS应用的构建和发布。

<!-- ios/Runner/Info.plist -->
<dict>
    <key>CFBundleName</key>
    <string>观影记录账本</string>
    <key>CFBundleDisplayName</key>
    <string>观影记录账本</string>
    <key>CFBundleVersion</key>
    <string>1.0.0</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0.0</string>
</dict>

构建命令

# 构建iOS应用
flutter build ios --release

# 使用Xcode构建
open ios/Runner.xcworkspace

3. Web部署

配置Web应用的构建和部署。

# web/index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>观影记录账本</title>
    <link rel="manifest" href="manifest.json">
</head>
<body>
    <script src="main.dart.js" type="application/javascript"></script>
</body>
</html>

构建和部署

# 构建Web应用
flutter build web --release

# 部署到Firebase Hosting
firebase deploy --only hosting

# 部署到GitHub Pages
gh-pages -d build/web

4. 桌面应用部署

配置Windows、macOS、Linux桌面应用。

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  window_manager: ^0.3.7  # 窗口管理
  
dev_dependencies:
  msix: ^3.16.7  # Windows打包

构建命令

# Windows
flutter build windows --release

# macOS
flutter build macos --release

# Linux
flutter build linux --release

5. 持续集成/持续部署 (CI/CD)

使用GitHub Actions自动化构建和部署。

# .github/workflows/build.yml
name: Build and Deploy

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Flutter
      uses: subosito/flutter-action@v2
      with:
        flutter-version: '3.16.0'
    
    - name: Install dependencies
      run: flutter pub get
    
    - name: Run tests
      run: flutter test
    
    - name: Build APK
      run: flutter build apk --release
    
    - name: Build Web
      run: flutter build web --release
    
    - name: Deploy to Firebase
      uses: FirebaseExtended/action-hosting-deploy@v0
      with:
        repoToken: '${{ secrets.GITHUB_TOKEN }}'
        firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT }}'
        projectId: movie-record-app

项目总结

技术成果

本项目成功实现了一个功能完整的观影记录账本应用,具备以下技术特点:

  1. 架构设计:采用清晰的分层架构,数据模型、业务逻辑、UI组件分离明确
  2. 状态管理:使用StatefulWidget进行本地状态管理,响应式更新UI
  3. 数据处理:实现了完整的CRUD操作,支持复杂的搜索筛选和统计分析
  4. 用户体验:Material Design 3设计风格,流畅的动画和交互效果
  5. 性能优化:列表懒加载、图片缓存、搜索优化等性能提升措施

功能亮点

  • 全面的电影信息管理:支持详细的电影信息录入和管理
  • 智能搜索筛选:多维度的搜索和筛选功能
  • 丰富的统计分析:可视化的观影数据统计和趋势分析
  • 灵活的状态管理:支持多种观影状态的管理和切换
  • 优雅的界面设计:现代化的UI设计和良好的用户体验

学习价值

通过本项目的开发,可以学习到:

  1. Flutter基础:Widget组件、状态管理、导航路由等核心概念
  2. 数据建模:复杂数据结构的设计和管理
  3. UI设计:Material Design设计规范的实际应用
  4. 性能优化:Flutter应用性能优化的实践方法
  5. 项目架构:中等复杂度应用的架构设计思路

扩展方向

本应用具有良好的扩展性,可以在以下方向继续发展:

  1. 云端同步:实现多设备数据同步功能
  2. 社交功能:添加观影记录分享和社区交流
  3. AI推荐:基于机器学习的个性化推荐系统
  4. 数据分析:更深入的观影习惯分析和洞察
  5. 跨平台:扩展到更多平台和设备

开发建议

对于想要学习或改进本项目的开发者:

  1. 循序渐进:从基础功能开始,逐步添加复杂特性
  2. 代码规范:保持良好的代码风格和注释习惯
  3. 测试驱动:编写充分的单元测试和集成测试
  4. 用户反馈:重视用户体验,持续优化界面和交互
  5. 技术更新:关注Flutter生态的最新发展,及时更新技术栈

本项目展示了Flutter在移动应用开发中的强大能力,通过合理的架构设计和精心的功能实现,创造了一个实用且美观的观影记录管理工具。希望这个项目能够为Flutter学习者提供有价值的参考和启发。

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

Logo

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

更多推荐