You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多视图单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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 14:52:44