多视图单ViewModel/单视图多ViewModel?Android音准应用架构疑问
针对Android吉他辨音应用的架构设计建议
看起来你已经搭建了不错的基础框架,针对你纠结的「ViewModel拆分与代码复用」问题,我推荐采用核心逻辑抽离+模块独立ViewModel的方案,既能避免代码重复,又能保证游戏、调音器模块的独立性,具体可以这么落地:
1. 把核心识别逻辑抽离到独立Repository
首先要做的是打破「分析逻辑绑定ViewModel」的现状,把音符/和弦识别的核心算法、音频数据处理逻辑从ViewModel中剥离出来,封装到一个独立的NoteRecognitionRepository类里:
- 这个Repository负责管理音频录制(如果合适的话,也可以把你现有的录制AsyncTask逻辑整合进来)、后台分析、识别结果的输出;
- 内部用
MutableLiveData(或Jetpack Compose推荐的Flow)来暴露识别出的音符/和弦数据; - 所有和「识别算法」相关的修改都只需要在这个类里进行,游戏和调音器模块只需要依赖它,不用重复写核心代码。
2. 为游戏、调音器分别创建专属ViewModel
游戏模块对应GameViewModel,调音器模块对应TunerViewModel,各自只处理自身模块的业务逻辑:
GameViewModel:负责维护游戏的目标音符、得分、关卡进度,监听Repository的识别结果并和目标音符对比,更新游戏状态;TunerViewModel:负责计算当前识别音符与标准音的偏差、显示校准状态,监听Repository的结果并转换为调音需要的UI数据;- 两个ViewModel都通过依赖注入(或构造函数传入)的方式获取
NoteRecognitionRepository实例,观察其输出的识别结果,这样每个View只需要关联自己的专属ViewModel,完全不需要绑定两个ViewModel。
3. 简单代码示例参考
核心识别Repository
class NoteRecognitionRepository { private val _recognizedNote = MutableLiveData<Note>() val recognizedNote: LiveData<Note> = _recognizedNote fun startRecognition() { // 用Coroutines替代AsyncTask(AsyncTask已被弃用,更推荐协程处理后台任务) CoroutineScope(Dispatchers.IO).launch { // 这里放你的音频录制、分析、识别逻辑 val detectedNote = analyzeAudioData() _recognizedNote.postValue(detectedNote) } } fun stopRecognition() { // 停止录制与分析的逻辑 } private suspend fun analyzeAudioData(): Note { // 你的核心音符识别算法实现 return Note("C", 261.63f) } }
游戏ViewModel
class GameViewModel(private val recognitionRepo: NoteRecognitionRepository) : ViewModel() { private val _gameScore = MutableLiveData(0) val gameScore: LiveData<Int> = _gameScore private val _targetNote = MutableLiveData<Note>() val targetNote: LiveData<Note> = _targetNote init { // 监听识别结果,判断是否匹配目标音符 recognitionRepo.recognizedNote.observeForever { detectedNote -> if (detectedNote == _targetNote.value) { _gameScore.value = _gameScore.value?.plus(10) ?: 10 } } } fun startNewRound() { _targetNote.value = generateRandomNote() recognitionRepo.startRecognition() } fun endGame() { recognitionRepo.stopRecognition() } private fun generateRandomNote(): Note { // 生成游戏目标音符的逻辑 return Note("G", 392.00f) } }
调音器ViewModel
class TunerViewModel(private val recognitionRepo: NoteRecognitionRepository) : ViewModel() { private val _pitchDeviation = MutableLiveData(0.0f) val pitchDeviation: LiveData<Float> = _pitchDeviation init { recognitionRepo.recognizedNote.observeForever { detectedNote -> _pitchDeviation.value = calculateDeviationFromStandard(detectedNote) } } fun startTuning() { recognitionRepo.startRecognition() } fun stopTuning() { recognitionRepo.stopRecognition() } private fun calculateDeviationFromStandard(note: Note): Float { // 计算当前音符与标准音的偏差值逻辑 return note.pitch - getStandardPitch(note.name) } }
4. 方案优势
- 单一职责:每个组件只做一件事——Repository管识别,ViewModel管模块业务,View只管UI展示;
- 代码复用:核心识别逻辑只写一次,游戏和调音器模块无缝复用;
- 可维护性:修改识别算法只需要改Repository,修改游戏规则只改
GameViewModel,互不干扰; - 可测试性:Repository和ViewModel都能单独编写单元测试,不需要依赖UI层。
内容的提问来源于stack exchange,提问作者Flendor
相关产品推荐
相关产品推荐

