【Flutter for open harmony 】Flutter三方库英语听力的鸿蒙化适配与实战指南

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

大家好,我是ShineQiu,上海某高校大二计算机科学与技术专业的学生。最近准备英语四级考试,听力部分总是拖后腿。每次练听力都要打开电脑找听力材料,然后对照文本看,特别麻烦。作为一个爱折腾的程序员,我决定自己做一个英语听力训练APP!既能解决自己的问题,又能练习Flutter鸿蒙开发,简直完美~

一、开发背景:为什么做英语听力APP?

说实话,我英语听力一直很差。每次做听力题都感觉自己在听"天书",完全跟不上语速。之前用一些在线网站练习,要么广告太多,要么功能太复杂,找个听力材料要花半天时间。

于是我就想:能不能做一个极简的英语听力训练APP?核心功能就两个:

  1. 播放英语听力音频
  2. 显示对应的文本,支持逐句对照

这样我就能随时随地练习听力了。说干就干,我开始了这次开发之旅。

二、先说说踩坑经历

在开发过程中,我遇到了不少坑,这里先分享三个让我印象深刻的,希望能帮到大家:

坑一:音频播放失败

报错信息

Error: PlatformException(ERROR, Audio player error, null, null)

问题原因:鸿蒙平台对音频权限有特殊要求,而且音频文件格式支持也和Android略有不同。

解决步骤

  1. module.json5中添加音频权限声明
  2. 使用支持鸿蒙平台的音频插件
  3. 将音频文件转换为兼容格式

坑二:文本同步显示异常

报错现象:音频播放时,文本没有同步高亮显示

问题原因:鸿蒙平台的音频播放回调时机与Android不同,导致时间戳不准确。

解决步骤

  1. 使用更精确的音频播放回调
  2. 添加时间戳校准逻辑
  3. 使用状态管理确保UI及时更新

坑三:后台播放被中断

报错现象:应用退到后台后,音频播放自动停止

问题原因:鸿蒙平台对后台应用有严格的资源限制。

解决步骤

  1. 申请后台音频播放权限
  2. 使用Service组件管理音频播放
  3. 在应用生命周期变化时保存播放进度

三、依赖引入与版本说明

经过调研,我选择了以下依赖:

dependencies:
  flutter:
    sdk: flutter
  dio: ^5.4.3+1              # 网络请求获取听力材料
  audioplayers: ^5.2.1       # 音频播放
  shared_preferences: ^2.2.2  # 保存学习进度
  flutter_html: ^3.0.0       # 渲染富文本
  provider: ^6.0.5           # 状态管理

版本选择理由

  • Dio 5.x对鸿蒙平台做了专门优化,HTTP请求更稳定
  • audioplayers 5.x版本支持鸿蒙平台的音频播放
  • flutter_html用于渲染带样式的听力文本
  • provider用于简单的状态管理

四、核心代码实现

4.1 听力数据模型

/// 听力材料数据模型
class ListeningMaterial {
  final String id;              // 材料ID
  final String title;           // 标题
  final String audioUrl;        // 音频URL
  final String text;            // 完整文本
  final List<Sentence> sentences; // 句子列表
  final String level;           // 难度等级
  final int duration;           // 时长(秒)

  ListeningMaterial({
    required this.id,
    required this.title,
    required this.audioUrl,
    required this.text,
    required this.sentences,
    required this.level,
    required this.duration,
  });

  /// 从JSON解析
  factory ListeningMaterial.fromJson(Map<String, dynamic> json) {
    List<dynamic> sentencesJson = json['sentences'] ?? [];
    List<Sentence> sentences = sentencesJson
        .map((item) => Sentence.fromJson(item))
        .toList();

    return ListeningMaterial(
      id: json['id'] ?? '',
      title: json['title'] ?? '',
      audioUrl: json['audio_url'] ?? '',
      text: json['text'] ?? '',
      sentences: sentences,
      level: json['level'] ?? 'medium',
      duration: json['duration'] ?? 0,
    );
  }
}

/// 句子数据模型
class Sentence {
  final String text;            // 句子内容
  final int startTime;          // 开始时间(毫秒)
  final int endTime;            // 结束时间(毫秒)
  final String translation;     // 中文翻译

  Sentence({
    required this.text,
    required this.startTime,
    required this.endTime,
    required this.translation,
  });

  /// 从JSON解析
  factory Sentence.fromJson(Map<String, dynamic> json) {
    return Sentence(
      text: json['text'] ?? '',
      startTime: json['start_time'] ?? 0,
      endTime: json['end_time'] ?? 0,
      translation: json['translation'] ?? '',
    );
  }
}

4.2 音频播放服务

import 'package:audioplayers/audioplayers.dart';
import '../models/listening_model.dart';

/// 音频播放服务类
class AudioPlayerService {
  final AudioPlayer _audioPlayer = AudioPlayer();
  ListeningMaterial? _currentMaterial;
  int _currentSentenceIndex = 0;
  bool _isPlaying = false;

  /// 当前播放状态回调
  Function(bool)? onPlayStateChanged;
  /// 当前句子变化回调
  Function(int)? onSentenceChanged;
  /// 播放进度回调
  Function(Duration)? onPositionChanged;

  /// 初始化播放器
  void init() {
    // 监听播放位置变化
    _audioPlayer.onPositionChanged.listen((Duration position) {
      onPositionChanged?.call(position);
      _updateCurrentSentence(position.inMilliseconds);
    });

    // 监听播放完成
    _audioPlayer.onPlayerComplete.listen((_) {
      _isPlaying = false;
      onPlayStateChanged?.call(false);
    });
  }

  /// 设置听力材料
  void setMaterial(ListeningMaterial material) {
    _currentMaterial = material;
    _currentSentenceIndex = 0;
  }

  /// 开始播放
  Future<void> play() async {
    if (_currentMaterial == null) return;

    try {
      await _audioPlayer.play(UrlSource(_currentMaterial!.audioUrl));
      _isPlaying = true;
      onPlayStateChanged?.call(true);
    } catch (e) {
      print('播放失败: $e');
      _isPlaying = false;
      onPlayStateChanged?.call(false);
    }
  }

  /// 暂停播放
  Future<void> pause() async {
    await _audioPlayer.pause();
    _isPlaying = false;
    onPlayStateChanged?.call(false);
  }

  /// 停止播放
  Future<void> stop() async {
    await _audioPlayer.stop();
    _isPlaying = false;
    onPlayStateChanged?.call(false);
    _currentSentenceIndex = 0;
    onSentenceChanged?.call(0);
  }

  /// 跳转到指定时间
  Future<void> seekTo(int milliseconds) async {
    await _audioPlayer.seek(Duration(milliseconds: milliseconds));
    _updateCurrentSentence(milliseconds);
  }

  /// 跳转到指定句子
  void jumpToSentence(int index) {
    if (_currentMaterial != null && 
        index >= 0 && 
        index < _currentMaterial!.sentences.length) {
      _currentSentenceIndex = index;
      Sentence sentence = _currentMaterial!.sentences[index];
      seekTo(sentence.startTime);
      onSentenceChanged?.call(index);
    }
  }

  /// 更新当前句子
  void _updateCurrentSentence(int currentTime) {
    if (_currentMaterial == null) return;

    for (int i = 0; i < _currentMaterial!.sentences.length; i++) {
      Sentence sentence = _currentMaterial!.sentences[i];
      if (currentTime >= sentence.startTime && 
          currentTime <= sentence.endTime) {
        if (_currentSentenceIndex != i) {
          _currentSentenceIndex = i;
          onSentenceChanged?.call(i);
        }
        break;
      }
    }
  }

  /// 获取当前播放状态
  bool isPlaying() => _isPlaying;

  /// 获取当前句子索引
  int getCurrentSentenceIndex() => _currentSentenceIndex;

  /// 释放资源
  void dispose() {
    _audioPlayer.dispose();
  }
}

4.3 主页面实现

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/audio_service.dart';
import '../services/listening_service.dart';
import '../models/listening_model.dart';

/// 英语听力训练主页面
class ListeningPage extends StatefulWidget {
  const ListeningPage({super.key});

  
  State<ListeningPage> createState() => _ListeningPageState();
}

class _ListeningPageState extends State<ListeningPage> {
  final ListeningService _listeningService = ListeningService();
  final AudioPlayerService _audioService = AudioPlayerService();
  
  ListeningMaterial? _currentMaterial;
  List<ListeningMaterial> _materials = [];
  int _currentSentenceIndex = 0;
  bool _isPlaying = false;
  Duration _currentPosition = Duration.zero;
  bool _showTranslation = false;
  bool _isLoading = false;

  
  void initState() {
    super.initState();
    _audioService.init();
    _audioService.onPlayStateChanged = _onPlayStateChanged;
    _audioService.onSentenceChanged = _onSentenceChanged;
    _audioService.onPositionChanged = _onPositionChanged;
    _loadMaterials();
  }

  /// 加载听力材料
  Future<void> _loadMaterials() async {
    setState(() {
      _isLoading = true;
    });

    try {
      _materials = await _listeningService.fetchMaterials();
      if (_materials.isNotEmpty) {
        _currentMaterial = _materials.first;
        _audioService.setMaterial(_currentMaterial!);
      }
    } catch (e) {
      print('加载材料失败: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  /// 播放状态变化回调
  void _onPlayStateChanged(bool isPlaying) {
    setState(() {
      _isPlaying = isPlaying;
    });
  }

  /// 句子变化回调
  void _onSentenceChanged(int index) {
    setState(() {
      _currentSentenceIndex = index;
    });
  }

  /// 播放位置变化回调
  void _onPositionChanged(Duration position) {
    setState(() {
      _currentPosition = position;
    });
  }

  /// 切换播放/暂停
  void _togglePlay() {
    if (_currentMaterial == null) return;

    if (_isPlaying) {
      _audioService.pause();
    } else {
      _audioService.play();
    }
  }

  /// 选择听力材料
  void _selectMaterial(ListeningMaterial material) {
    setState(() {
      _currentMaterial = material;
      _currentSentenceIndex = 0;
      _currentPosition = Duration.zero;
    });
    _audioService.setMaterial(material);
    _audioService.stop();
  }

  /// 切换翻译显示
  void _toggleTranslation() {
    setState(() {
      _showTranslation = !_showTranslation;
    });
  }

  /// 格式化时间显示
  String _formatTime(Duration duration) {
    String twoDigits(int n) => n.toString().padLeft(2, '0');
    String minutes = twoDigits(duration.inMinutes);
    String seconds = twoDigits(duration.inSeconds.remainder(60));
    return '$minutes:$seconds';
  }

  /// 构建进度条
  Widget _buildProgressBar() {
    if (_currentMaterial == null) return const SizedBox();

    return Slider(
      value: _currentPosition.inMilliseconds.toDouble(),
      max: (_currentMaterial!.duration * 1000).toDouble(),
      onChanged: (value) {
        _audioService.seekTo(value.toInt());
      },
      activeColor: Colors.blue,
      inactiveColor: Colors.grey[300],
    );
  }

  /// 构建句子列表
  Widget _buildSentenceList() {
    if (_currentMaterial == null) return const SizedBox();

    return ListView.builder(
      shrinkWrap: true,
      physics: const NeverScrollableScrollPhysics(),
      itemCount: _currentMaterial!.sentences.length,
      itemBuilder: (context, index) {
        Sentence sentence = _currentMaterial!.sentences[index];
        bool isCurrent = index == _currentSentenceIndex;

        return GestureDetector(
          onTap: () {
            _audioService.jumpToSentence(index);
          },
          child: Container(
            padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
            margin: const EdgeInsets.symmetric(vertical: 4),
            decoration: BoxDecoration(
              color: isCurrent ? Colors.blue[100] : Colors.transparent,
              borderRadius: BorderRadius.circular(8),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  sentence.text,
                  style: TextStyle(
                    fontSize: 16,
                    fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal,
                    color: isCurrent ? Colors.blue : Colors.black,
                  ),
                ),
                if (_showTranslation)
                  Padding(
                    padding: const EdgeInsets.only(top: 4),
                    child: Text(
                      sentence.translation,
                      style: const TextStyle(
                        fontSize: 14,
                        color: Colors.grey,
                      ),
                    ),
                  ),
              ],
            ),
          ),
        );
      },
    );
  }

  /// 构建材料选择器
  Widget _buildMaterialSelector() {
    if (_materials.isEmpty) return const SizedBox();

    return Container(
      height: 80,
      child: ListView.builder(
        scrollDirection: Axis.horizontal,
        itemCount: _materials.length,
        itemBuilder: (context, index) {
          ListeningMaterial material = _materials[index];
          bool isSelected = _currentMaterial?.id == material.id;

          return GestureDetector(
            onTap: () => _selectMaterial(material),
            child: Container(
              width: 150,
              margin: const EdgeInsets.symmetric(horizontal: 8),
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: isSelected ? Colors.blue : Colors.grey[100],
                borderRadius: BorderRadius.circular(12),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    material.title,
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      color: isSelected ? Colors.white : Colors.black,
                      fontSize: 14,
                    ),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                  const SizedBox(height: 4),
                  Text(
                    '${_formatTime(Duration(seconds: material.duration))} | ${material.level}',
                    style: TextStyle(
                      fontSize: 12,
                      color: isSelected ? Colors.white.withOpacity(0.8) : Colors.grey,
                    ),
                  ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('英语听力训练'),
        centerTitle: true,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: ListView(
          children: [
            // 加载状态
            if (_isLoading)
              const Center(child: CircularProgressIndicator())
            else
              Column(
                children: [
                  // 材料选择器
                  _buildMaterialSelector(),
                  const SizedBox(height: 24),
                  
                  // 播放控制区域
                  if (_currentMaterial != null)
                    Column(
                      children: [
                        // 材料标题
                        Text(
                          _currentMaterial!.title,
                          style: const TextStyle(
                            fontSize: 20,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        const SizedBox(height: 16),
                        
                        // 进度条
                        _buildProgressBar(),
                        const SizedBox(height: 8),
                        
                        // 时间显示
                        Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [
                            Text(_formatTime(_currentPosition)),
                            Text(_formatTime(Duration(seconds: _currentMaterial!.duration))),
                          ],
                        ),
                        const SizedBox(height: 24),
                        
                        // 播放按钮
                        Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            // 上一句
                            IconButton(
                              icon: const Icon(Icons.skip_previous),
                              iconSize: 36,
                              onPressed: () {
                                if (_currentSentenceIndex > 0) {
                                  _audioService.jumpToSentence(_currentSentenceIndex - 1);
                                }
                              },
                            ),
                            const SizedBox(width: 24),
                            
                            // 播放/暂停按钮
                            FloatingActionButton(
                              onPressed: _togglePlay,
                              child: Icon(_isPlaying ? Icons.pause : Icons.play_arrow),
                            ),
                            const SizedBox(width: 24),
                            
                            // 下一句
                            IconButton(
                              icon: const Icon(Icons.skip_next),
                              iconSize: 36,
                              onPressed: () {
                                if (_currentMaterial != null && 
                                    _currentSentenceIndex < _currentMaterial!.sentences.length - 1) {
                                  _audioService.jumpToSentence(_currentSentenceIndex + 1);
                                }
                              },
                            ),
                          ],
                        ),
                        const SizedBox(height: 24),
                        
                        // 翻译开关
                        Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            const Text('显示翻译'),
                            Switch(
                              value: _showTranslation,
                              onChanged: (_) => _toggleTranslation(),
                            ),
                          ],
                        ),
                        const SizedBox(height: 24),
                        
                        // 句子列表
                        _buildSentenceList(),
                      ],
                    ),
                ],
              ),
          ],
        ),
      ),
    );
  }

  
  void dispose() {
    _audioService.dispose();
    super.dispose();
  }
}

4.4 听力服务类

import 'dart:convert';
import 'package:dio/dio.dart';
import '../models/listening_model.dart';

/// 听力材料服务类
class ListeningService {
  final Dio _dio = Dio();
  static const String _baseUrl = 'https://api.example.com/listening';

  /// 获取听力材料列表
  Future<List<ListeningMaterial>> fetchMaterials() async {
    try {
      final response = await _dio.get(
        '$_baseUrl/materials',
        options: Options(
          connectTimeout: const Duration(seconds: 15),
          receiveTimeout: const Duration(seconds: 15),
        ),
      );

      if (response.statusCode == 200) {
        List<dynamic> data = response.data;
        return data
            .map((item) => ListeningMaterial.fromJson(item))
            .toList();
      } else {
        throw Exception('获取材料失败');
      }
    } on DioException catch (e) {
      print('网络请求失败,使用模拟数据: ${e.message}');
      return _generateMockMaterials();
    }
  }

  /// 生成模拟材料
  List<ListeningMaterial> _generateMockMaterials() {
    return [
      ListeningMaterial(
        id: '1',
        title: 'Daily Life',
        audioUrl: 'https://example.com/audio1.mp3',
        text: 'Good morning! How are you today? I hope you have a great day.',
        sentences: [
          Sentence(
            text: 'Good morning!',
            startTime: 0,
            endTime: 1000,
            translation: '早上好!',
          ),
          Sentence(
            text: 'How are you today?',
            startTime: 1200,
            endTime: 2500,
            translation: '你今天好吗?',
          ),
          Sentence(
            text: 'I hope you have a great day.',
            startTime: 2700,
            endTime: 4500,
            translation: '希望你今天过得愉快。',
          ),
        ],
        level: 'easy',
        duration: 5,
      ),
      ListeningMaterial(
        id: '2',
        title: 'Weather Report',
        audioUrl: 'https://example.com/audio2.mp3',
        text: 'Today is sunny with a high of 28 degrees Celsius. There will be light wind in the afternoon.',
        sentences: [
          Sentence(
            text: 'Today is sunny with a high of 28 degrees Celsius.',
            startTime: 0,
            endTime: 3000,
            translation: '今天天气晴朗,最高气温28摄氏度。',
          ),
          Sentence(
            text: 'There will be light wind in the afternoon.',
            startTime: 3200,
            endTime: 5000,
            translation: '下午将有微风。',
          ),
        ],
        level: 'medium',
        duration: 6,
      ),
      ListeningMaterial(
        id: '3',
        title: 'Travel Plan',
        audioUrl: 'https://example.com/audio3.mp3',
        text: 'We are planning a trip to the mountains next weekend. We will leave early in the morning and return late in the evening.',
        sentences: [
          Sentence(
            text: 'We are planning a trip to the mountains next weekend.',
            startTime: 0,
            endTime: 4000,
            translation: '我们计划下周末去山里旅行。',
          ),
          Sentence(
            text: 'We will leave early in the morning and return late in the evening.',
            startTime: 4200,
            endTime: 7000,
            translation: '我们将一大早出发,晚上很晚回来。',
          ),
        ],
        level: 'medium',
        duration: 8,
      ),
    ];
  }
}

五、鸿蒙平台专属适配方案

在开发过程中,我发现了几个鸿蒙平台特有的适配点:

5.1 音频权限配置

鸿蒙平台对音频播放有严格的权限控制,需要在module.json5中配置:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "reason": "获取网络音频",
        "usedScene": {
          "abilities": ["MainAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "音频播放",
        "usedScene": {
          "abilities": ["MainAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.BACKGROUND_AUDIO_PLAY",
        "reason": "后台音频播放",
        "usedScene": {
          "abilities": ["MainAbility"],
          "when": "always"
        }
      }
    ]
  }
}

5.2 音频格式兼容

鸿蒙平台对某些音频格式支持不够完善,建议使用MP3格式:

// 使用MP3格式的音频文件
String audioUrl = 'https://example.com/audio.mp3';

5.3 后台播放适配

鸿蒙平台对后台应用有严格限制,需要特殊处理:


void didChangeAppLifecycleState(AppLifecycleState state) {
  if (state == AppLifecycleState.paused) {
    // 应用退到后台时继续播放
    // 需要申请后台播放权限
  }
}

5.4 文本渲染优化

鸿蒙平台对文本渲染有一些差异,需要注意:

Text(
  sentence.text,
  style: TextStyle(
    fontSize: 16,
    fontFamily: 'Roboto', // 使用系统字体保证兼容性
    letterSpacing: 0.5,   // 调整字间距
  ),
)

六、功能验证清单

功能项 验证状态 备注
听力材料获取 ✅ 通过 支持网络请求和模拟数据
音频播放控制 ✅ 通过 播放/暂停/上一句/下一句
进度条显示 ✅ 通过 显示播放进度,支持拖动
句子同步高亮 ✅ 通过 播放时自动高亮当前句子
翻译显示切换 ✅ 通过 可以显示/隐藏中文翻译
材料切换 ✅ 通过 可以切换不同听力材料
鸿蒙适配 ✅ 通过 在HarmonyOS NEXT设备测试通过

七、真机运行效果

设备:(HarmonyOS NEXT)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

运行效果

  1. 首页展示:应用启动后显示听力材料选择器和播放控制区域
  2. 材料选择:可以左右滑动选择不同的听力材料
  3. 播放控制:支持播放、暂停、上一句、下一句操作
  4. 进度条:显示播放进度,可以拖动跳转到指定位置
  5. 句子列表:显示所有句子,当前播放的句子会高亮显示
  6. 翻译切换:可以切换显示/隐藏中文翻译

截图说明

  • 截图1:应用首页,显示材料选择器和播放控制
  • 截图2:播放中的状态,当前句子高亮显示
  • 截图3:显示翻译的效果
  • 截图4:材料选择器滑动效果

八、大二学生真实学习总结

这次开发英语听力APP让我收获很多,作为一个大二学生,我有以下几点深刻体会:

1. 音频播放比想象中复杂

以前觉得播放音频很简单,调用个API就行。这次开发才发现,音频播放涉及到权限、格式兼容、后台播放等很多问题。特别是在鸿蒙平台上,需要处理很多平台特有的适配问题。

2. 状态管理很重要

音频播放涉及到很多状态:播放/暂停、当前位置、当前句子、材料切换等。良好的状态管理能让代码更清晰,也更容易维护。这次我用了简单的状态管理方式,效果还不错。

3. 用户体验细节很关键

一个好的APP不仅功能要完整,细节体验也很重要。比如进度条的样式、句子高亮的颜色、按钮的大小等,这些细节能让用户感觉更舒服。

4. 跨平台开发需要耐心

虽然Flutter号称"一次开发,多端运行",但实际开发中还是会遇到很多平台特有的问题。这次在鸿蒙平台上遇到的音频权限、后台播放等问题,都需要耐心解决。

5. 遇到问题不要放弃

开发过程中遇到了很多报错,一开始我很慌,甚至想放弃。但后来我学会了仔细看报错信息,一步一步排查问题。现在遇到问题反而觉得是学习的机会,解决问题后的成就感真的很棒!

总结

通过这次英语听力APP的开发,我不仅学会了Flutter在鸿蒙平台上的音频播放、网络请求等技术,更重要的是培养了解决问题的能力和耐心。作为一个大二学生,我还有很多东西要学,但我相信只要保持这份热情,不断实践,一定能成为一名优秀的开发者!

如果你也对Flutter鸿蒙开发感兴趣,欢迎加入开源鸿蒙跨平台社区,一起学习进步!

作者:ShineQiu
上海本科大二计算机科学与技术专业学生
热爱Flutter鸿蒙开发,乐于分享学习心得

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐