Android使用Dispatchers.IO读取本地文件遇重复执行问题及原因问询
背景
Android官方文档提到:
Dispatchers.IO - 此调度器针对在主线程外执行磁盘或网络IO进行了优化。示例包括使用Room组件、读写文件及运行任何网络操作。
我需要读取assets目录下的.json文件,通过Moshi解析后用Jetpack Compose展示到界面。按照文档描述,我认为这类操作应该使用Dispatchers.IO,于是编写了如下代码:
ViewModel代码
@HiltViewModel class FileViewModel @Inject constructor( private val getFileUseCase: GetFileUseCase, @Dispatcher(LPlusDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { private val _uiState = MutableStateFlow<FileUiState>(FileUiState.Loading) val uiState: StateFlow<FileUiState> = _uiState.asStateFlow() fun loadData(fileRequest: FileRequest) { _uiState.value = FileUiState.Loading viewModelScope.launch(ioDispatcher) { try { val result = getFileUseCase(fileRequest) _uiState.value = FileUiState.Loaded(FileItemUiState(result)) } catch (error: Exception) { _uiState.value = FileUiState.Error(ExceptionParser.getMessage(error)) } } } //... }
UseCase代码
class GetFileUseCase @Inject constructor( private val fileRepository: LocalFileRepository ) { suspend operator fun invoke(fileRequest: FileRequest): MutableList<FileResponse> = fileRepository.getFile(fileRequest) }
仓库层代码
override suspend fun getFile(fileRequest: FileRequest): MutableList<FileResponse> { val fileResponse = assetProvider.getFiles(fileRequest.fileName) val moshi = Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Content::class.java, "type") .withSubtype(Paragraphus::class.java, "p") .withSubtype(Rubrica::class.java, "r") .withSubtype(Titulus::class.java, "t") //... ) .add(KotlinJsonAdapterFactory()) .build() fileResponse.forEach { if (books.contains(it.fileName)) { it.text = moshi.adapter(Book::class.java).fromJson(it.text.toString()) // ... } } return fileResponse }
问题现象
当使用viewModelScope.launch(ioDispatcher)时,代码会持续重复执行——断点会不断触发;但只用viewModelScope.launch()时,代码能正常执行,只读取一次目标文件。
Compose中状态使用代码
@Composable fun FileScreen( modifier: Modifier = Modifier, fileRequest: FileRequest, viewModel: FileViewModel = hiltViewModel(), ) { viewModel.loadData(fileRequest) val uiState by viewModel.uiState.collectAsStateWithLifecycle() FileScreen(modifier = modifier, uiState = uiState) }
@Composable fun FileScreen( modifier: Modifier, uiState: FileViewModel.FileUiState ) { when (uiState) { FileViewModel.FileUiState.Empty -> EmptyState() is FileViewModel.FileUiState.Error -> ErrorState() is FileViewModel.FileUiState.Loaded -> { uiState.itemState.allData.forEach { Text(text = it.text) } } FileViewModel.FileUiState.Loading -> LoadingState() } }
核心问题与解答
是不是不需要用Dispatchers.IO?
不是,读取assets文件属于IO操作,完全应该用Dispatchers.IO,避免阻塞主线程导致UI卡顿。问题的根源不在调度器,而是你在Composable中的调用时机错误。
重复执行的原因
你在FileScreen Composable的根节点直接调用viewModel.loadData(fileRequest),而Composable会因为多种原因触发重组(比如UI状态更新、屏幕旋转、配置变化等),每次重组都会重新执行这个方法,进而重复发起文件读取请求。
为什么用默认调度器(Main)时看起来正常?因为Main调度器是主线程,协程里的IO操作会阻塞主线程,导致UI重组被延迟,直到任务完成才会触发下一次重组,这时候可能因为状态已经变为Loaded,没触发重复调用,但这本质是错误的做法——阻塞主线程会让UI失去响应,影响用户体验。
而用IO调度器时,协程在后台线程执行,速度更快,状态更新会立刻触发UI重组,重组又会调用loadData,形成无限循环,所以断点会不断触发。
解决方案
用LaunchedEffect包裹loadData调用,确保只有当指定参数变化时才执行一次:
@Composable fun FileScreen( modifier: Modifier = Modifier, fileRequest: FileRequest, viewModel: FileViewModel = hiltViewModel(), ) { // 仅当fileRequest变化时,才会重新执行loadData LaunchedEffect(fileRequest) { viewModel.loadData(fileRequest) } val uiState by viewModel.uiState.collectAsStateWithLifecycle() FileScreen(modifier = modifier, uiState = uiState) }
这样就能避免重复执行的问题,同时保留Dispatchers.IO来安全执行IO操作,不会阻塞主线程。
内容的提问来源于stack exchange,提问作者A. Cedano

