Flutter for OpenHarmony音乐播放器App实战11:创建歌单实现

创建歌单是音乐播放器中一个基础但重要的功能。用户可以创建自己的歌单来整理和收藏喜欢的音乐。本篇文章将详细介绍如何实现一个简洁实用的创建歌单页面,包括封面上传、名称输入、隐私设置等功能。

页面基础结构

创建歌单页面使用StatefulWidget,因为需要管理输入框内容和开关状态。

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

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

  @override
  State<CreatePlaylistPage> createState() => _CreatePlaylistPageState();
}

页面继承自StatefulWidget,使用GetX进行路由管理。创建歌单的交互相对简单,但需要响应用户的输入和开关操作。

状态变量定义

页面需要管理输入控制器和隐私开关状态。

dart 复制代码
class _CreatePlaylistPageState extends State<CreatePlaylistPage> {
  final _nameController = TextEditingController();
  bool _isPrivate = false;

  @override
  void dispose() {
    _nameController.dispose();
    super.dispose();
  }

_nameController用于控制歌单名称输入框,_isPrivate标识歌单是否设为私密。在dispose方法中释放控制器资源,这是Flutter开发中的标准做法。

AppBar设计

AppBar包含标题和完成按钮。

dart 复制代码
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('创建歌单'),
        actions: [
          TextButton(
            onPressed: () => _createPlaylist(),
            child: const Text(
              '完成',
              style: TextStyle(color: Color(0xFFE91E63)),
            ),
          ),
        ],
      ),

完成按钮使用主题色,放在AppBar右侧。点击后调用_createPlaylist方法提交创建请求。这种设计符合用户的操作习惯。

页面主体布局

页面主体使用Padding包裹Column,垂直排列各个组件。

dart 复制代码
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _buildCoverPicker(),
            const SizedBox(height: 24),
            _buildNameInput(),
            const SizedBox(height: 16),
            _buildPrivacySwitch(),
          ],
        ),
      ),
    );
  }

页面包含三个主要部分:封面选择、名称输入和隐私设置。使用SizedBox控制各部分之间的间距。

封面选择组件

封面选择区域居中显示,点击可以选择图片。

dart 复制代码
  Widget _buildCoverPicker() {
    return Center(
      child: GestureDetector(
        onTap: () => _pickCoverImage(),
        child: Container(
          width: 120,
          height: 120,
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(12),
            color: const Color(0xFF1E1E1E),
          ),
          child: const Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Icon(Icons.add_photo_alternate, size: 40, color: Colors.grey),
              SizedBox(height: 8),
              Text('添加封面', style: TextStyle(color: Colors.grey, fontSize: 12)),
            ],
          ),
        ),
      ),
    );
  }

封面容器使用深色背景,圆角设置为12。内部垂直排列图标和文字,提示用户点击添加封面。GestureDetector包裹整个容器,扩大点击区域。

选择封面图片

点击封面区域后调用图片选择方法。

dart 复制代码
  void _pickCoverImage() async {
    // 实际项目中使用image_picker插件
    Get.bottomSheet(
      Container(
        decoration: const BoxDecoration(
          color: Color(0xFF1E1E1E),
          borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              leading: const Icon(Icons.camera_alt),
              title: const Text('拍照'),
              onTap: () {
                Get.back();
                Get.snackbar('提示', '相机功能开发中');
              },
            ),
            ListTile(
              leading: const Icon(Icons.photo_library),
              title: const Text('从相册选择'),
              onTap: () {
                Get.back();
                Get.snackbar('提示', '相册功能开发中');
              },
            ),
            const SizedBox(height: 16),
          ],
        ),
      ),
    );
  }

使用底部菜单提供拍照和从相册选择两个选项。实际项目中需要使用image_picker插件来实现真正的图片选择功能。

名称输入组件

歌单名称输入区域包含标签和输入框。

dart 复制代码
  Widget _buildNameInput() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '歌单名称',
          style: TextStyle(fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 8),
        TextField(
          controller: _nameController,
          decoration: InputDecoration(
            hintText: '请输入歌单名称',
            filled: true,
            fillColor: const Color(0xFF1E1E1E),
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(12),
              borderSide: BorderSide.none,
            ),
          ),
        ),
      ],
    );
  }

输入框使用深色填充背景,无边框设计。hintText提示用户输入内容。圆角与封面容器保持一致,视觉上更加协调。

隐私设置开关

使用SwitchListTile实现隐私设置。

dart 复制代码
  Widget _buildPrivacySwitch() {
    return SwitchListTile(
      title: const Text('设为私密'),
      subtitle: const Text('私密歌单仅自己可见'),
      value: _isPrivate,
      onChanged: (v) => setState(() => _isPrivate = v),
      activeColor: const Color(0xFFE91E63),
      contentPadding: EdgeInsets.zero,
    );
  }

SwitchListTile集成了标题、副标题和开关,非常适合这种设置项。activeColor设置为主题色,contentPadding设为零与其他组件对齐。

创建歌单方法

点击完成按钮后执行创建逻辑。

dart 复制代码
  void _createPlaylist() {
    final name = _nameController.text.trim();
    
    if (name.isEmpty) {
      Get.snackbar(
        '提示',
        '请输入歌单名称',
        snackPosition: SnackPosition.BOTTOM,
        backgroundColor: Colors.red.withOpacity(0.8),
        colorText: Colors.white,
      );
      return;
    }
    
    // 模拟创建歌单
    Get.back(result: {
      'name': name,
      'isPrivate': _isPrivate,
    });
    
    Get.snackbar(
      '成功',
      '歌单创建成功',
      snackPosition: SnackPosition.BOTTOM,
      backgroundColor: Colors.green.withOpacity(0.8),
      colorText: Colors.white,
    );
  }

首先验证歌单名称是否为空,为空时显示错误提示。创建成功后返回上一页并传递创建结果,同时显示成功提示。

输入验证增强

可以添加更多的输入验证规则。

dart 复制代码
  bool _validateInput() {
    final name = _nameController.text.trim();
    
    if (name.isEmpty) {
      _showError('请输入歌单名称');
      return false;
    }
    
    if (name.length > 40) {
      _showError('歌单名称不能超过40个字符');
      return false;
    }
    
    // 检查是否包含特殊字符
    final regex = RegExp(r'[<>"\\/|?*]');
    if (regex.hasMatch(name)) {
      _showError('歌单名称不能包含特殊字符');
      return false;
    }
    
    return true;
  }
  
  void _showError(String message) {
    Get.snackbar(
      '提示',
      message,
      snackPosition: SnackPosition.BOTTOM,
      backgroundColor: Colors.red.withOpacity(0.8),
      colorText: Colors.white,
    );
  }

验证规则包括非空检查、长度限制和特殊字符检查。将错误提示抽取为独立方法,避免代码重复。

歌单描述输入

可以添加歌单描述输入框。

dart 复制代码
  Widget _buildDescriptionInput() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '歌单简介',
          style: TextStyle(fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 8),
        TextField(
          controller: _descController,
          maxLines: 4,
          maxLength: 200,
          decoration: InputDecoration(
            hintText: '介绍一下这个歌单吧(选填)',
            filled: true,
            fillColor: const Color(0xFF1E1E1E),
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(12),
              borderSide: BorderSide.none,
            ),
          ),
        ),
      ],
    );
  }

描述输入框设置maxLines为4,允许多行输入。maxLength限制最大字符数,输入框会自动显示字符计数。

标签选择功能

可以为歌单添加标签。

dart 复制代码
  final List<String> _availableTags = ['流行', '摇滚', '民谣', '电子', '古典', '爵士', 'R&B', '说唱'];
  final Set<String> _selectedTags = {};

  Widget _buildTagSelector() {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          '歌单标签',
          style: TextStyle(fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 8),
        Wrap(
          spacing: 8,
          runSpacing: 8,
          children: _availableTags.map((tag) {
            final isSelected = _selectedTags.contains(tag);
            return GestureDetector(
              onTap: () {
                setState(() {
                  if (isSelected) {
                    _selectedTags.remove(tag);
                  } else if (_selectedTags.length < 3) {
                    _selectedTags.add(tag);
                  } else {
                    Get.snackbar('提示', '最多选择3个标签');
                  }
                });
              },
              child: Chip(
                label: Text(tag),
                backgroundColor: isSelected ? const Color(0xFFE91E63) : const Color(0xFF1E1E1E),
                labelStyle: TextStyle(
                  color: isSelected ? Colors.white : Colors.grey,
                ),
              ),
            );
          }).toList(),
        ),
      ],
    );
  }

使用Wrap组件实现流式布局,标签会自动换行。选中的标签使用主题色背景,限制最多选择3个标签。

封面预览

选择封面后显示预览。

dart 复制代码
  String? _coverPath;

  Widget _buildCoverPreview() {
    return Center(
      child: GestureDetector(
        onTap: () => _pickCoverImage(),
        child: Container(
          width: 120,
          height: 120,
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(12),
            color: const Color(0xFF1E1E1E),
            image: _coverPath != null
                ? DecorationImage(
                    image: FileImage(File(_coverPath!)),
                    fit: BoxFit.cover,
                  )
                : null,
          ),
          child: _coverPath == null
              ? const Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Icon(Icons.add_photo_alternate, size: 40, color: Colors.grey),
                    SizedBox(height: 8),
                    Text('添加封面', style: TextStyle(color: Colors.grey, fontSize: 12)),
                  ],
                )
              : Stack(
                  children: [
                    Positioned(
                      right: 4,
                      top: 4,
                      child: Container(
                        padding: const EdgeInsets.all(4),
                        decoration: const BoxDecoration(
                          color: Colors.black54,
                          shape: BoxShape.circle,
                        ),
                        child: const Icon(Icons.edit, size: 16, color: Colors.white),
                      ),
                    ),
                  ],
                ),
        ),
      ),
    );
  }

如果已选择封面,使用DecorationImage显示图片,右上角显示编辑图标。未选择时显示添加提示。

加载状态处理

创建歌单时显示加载状态。

dart 复制代码
  bool _isLoading = false;

  void _createPlaylist() async {
    if (!_validateInput()) return;
    
    setState(() => _isLoading = true);
    
    try {
      // 模拟网络请求
      await Future.delayed(const Duration(seconds: 1));
      
      Get.back(result: {
        'name': _nameController.text.trim(),
        'isPrivate': _isPrivate,
        'tags': _selectedTags.toList(),
      });
      
      Get.snackbar('成功', '歌单创建成功');
    } catch (e) {
      Get.snackbar('错误', '创建失败,请重试');
    } finally {
      if (mounted) {
        setState(() => _isLoading = false);
      }
    }
  }

创建过程中设置_isLoading为true,完成后恢复。使用try-catch处理可能的错误,finally确保状态恢复。

完成按钮状态

根据加载状态和输入内容控制按钮状态。

dart 复制代码
  Widget _buildSubmitButton() {
    return TextButton(
      onPressed: _isLoading || _nameController.text.trim().isEmpty
          ? null
          : () => _createPlaylist(),
      child: _isLoading
          ? const SizedBox(
              width: 20,
              height: 20,
              child: CircularProgressIndicator(
                strokeWidth: 2,
                valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFE91E63)),
              ),
            )
          : const Text(
              '完成',
              style: TextStyle(color: Color(0xFFE91E63)),
            ),
    );
  }

加载中显示CircularProgressIndicator,名称为空时按钮禁用。这种设计提供了清晰的状态反馈。

总结

创建歌单页面虽然功能相对简单,但涉及到表单输入、状态管理、输入验证等多个Flutter开发中的常见场景。通过合理的组件拆分和状态管理,让代码结构清晰、易于维护。在实际项目中,还需要对接后端接口实现真正的歌单创建功能,以及使用image_picker等插件实现图片选择。

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

相关推荐
茉莉玫瑰花茶2 小时前
C++ 17 详细特性解析(3)
开发语言·c++
java1234_小锋2 小时前
高频面试题:Java中如何安全地停止线程?
java·开发语言
一晌小贪欢2 小时前
Python 操作 Excel 高阶技巧:用 openpyxl 玩转循环与 Decimal 精度控制
开发语言·python·excel·openpyxl·python办公·python读取excel
Mr Xu_2 小时前
解决 Vue + Axios 热更新导致响应拦截器重复注册的问题
前端·javascript·vue.js
一起养小猫2 小时前
Flutter for OpenHarmony 实战:网络监控登录系统完整开发指南
网络·flutter·harmonyos
一起养小猫2 小时前
Flutter for OpenHarmony 进阶:手势识别与碰撞检测算法深度解析
算法·flutter·harmonyos
小哥Mark2 小时前
一篇验证Flutter框架核心接口在鸿蒙应用中的可能性
flutter·华为·harmonyos
Coder_preston2 小时前
JavaScript学习指南
开发语言·javascript·ecmascript
阿猿收手吧!2 小时前
【C++】无锁原子栈:CAS实现线程安全
开发语言·c++·安全