MVVM架构下PreferenceFragment读取的CSV数据如何传递到ChartFragment
实现方案
核心基于你使用的MVVM架构,采用宿主Activity范围的共享ViewModel作为数据中转,无需Fragment直接耦合即可完成数据传递,同时修复现有代码的潜在问题。
步骤1:创建共享ViewModel
在项目中新增共享ViewModel类,用于存储选中的CSV文件Uri,两个Fragment都将读取这个ViewModel的状态:
import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel class SharedCsvViewModel : ViewModel() { // 存储选中的CSV文件Uri,内部用MutableLiveData修改,外部暴露不可变LiveData private val _selectedCsvUri = MutableLiveData<Uri?>() val selectedCsvUri: LiveData<Uri?> = _selectedCsvUri fun updateCsvUri(uri: Uri?) { _selectedCsvUri.value = uri } }
步骤2:修改SettingsFragment代码
修复原有代码的初始化时机错误,改用ActivityResult协议获取选中文件Uri并写入共享ViewModel:
import android.content.Intent import android.net.Uri import android.util.Log import androidx.activity.result.contract.ActivityResultContracts import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.lifecycle.viewModels class SettingsFragment : PreferenceFragmentCompat() { private val SETTINGS_DEBUG = "PROFILE_DEBUG" // 绑定宿主Activity范围的共享ViewModel private val sharedVm by viewModels<SharedCsvViewModel>( ownerProducer = { requireActivity() } ) // 注册文件选择器协议 private val openCsvLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> uri?.let { // 授予URI持久化权限,避免后续切Fragment后无法读取文件 requireActivity().contentResolver.takePersistableUriPermission( it, Intent.FLAG_GRANT_READ_URI_PERMISSION ) sharedVm.updateCsvUri(it) } } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.root_preferences, rootKey) val myPref: Preference? = findPreference("load_csv_file") myPref?.onPreferenceClickListener = Preference.OnPreferenceClickListener { try { // 限制仅选择CSV类型文件 openCsvLauncher.launch(arrayOf("text/csv", "text/comma-separated-values")) } catch (exc: Exception) { Log.d(SETTINGS_DEBUG, exc.message.toString()) } true } } }
步骤3:修改ChartFragment代码
观察共享ViewModel的Uri变化,触发CSV读取和图表渲染,迁移相关工具方法到当前Fragment:
import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewModels import com.github.mikephil.charting.data.Entry import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineDataSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.BufferedReader import java.io.InputStreamReader class ChartFragment : Fragment() { private var _binding: FragmentChartBinding? = null private val binding get() = _binding!! // 绑定同一个宿主Activity范围的共享ViewModel private val sharedVm by viewModels<SharedCsvViewModel>( ownerProducer = { requireActivity() } ) private val lineChartVm by viewModels<ChartViewModel>() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { _binding = FragmentChartBinding.inflate(inflater, container, false) val markerView = CustomMarker(activity?.applicationContext, R.layout.marker_view) binding.lineChart.marker = markerView // 观察CSV Uri变化,触发读取和渲染 sharedVm.selectedCsvUri.observe(viewLifecycleOwner) { uri -> uri ?: return@observe // 用协程在IO线程读文件,避免阻塞主线程 viewLifecycleOwner.lifecycleScope.launch { val lines = readCSV(uri) val newEntries = lines.map { line -> toEntry(line) } .groupingBy { entry -> entry.x } .reduce { _, accumulator, element -> if (accumulator.y > element.y) accumulator else element }.values renderChart(newEntries.toList()) } } return binding.root } // 读取CSV方法迁移到当前Fragment private suspend fun readCSV(uri: Uri): List<String> = withContext(Dispatchers.IO) { val csvFile = requireActivity().contentResolver.openInputStream(uri) val isr = InputStreamReader(csvFile) BufferedReader(isr).readLines() } private fun toEntry(line: String): Entry { val split = line.split(";") val time = split[1] val secondsStartIdx = time.length - 2 val minutesStartIdx = time.length - 4 val hoursStartIdx = (time.length - 6).coerceAtLeast(0) val hour = time.substring(hoursStartIdx, hoursStartIdx + time.length - 4) val minutest = time.substring(minutesStartIdx, minutesStartIdx + 2) val seconds = time.substring(secondsStartIdx, secondsStartIdx + 2) val newTime = hour.toFloat() * 10000 + (minutest.toFloat() * 100 / 60).toInt() * 100 + (seconds.toFloat() * 100 / 60).toInt() return Entry(newTime, split[2].toFloat()) } // 图表渲染逻辑抽成单独方法 private fun renderChart(entries: List<Entry>) { val lineChart = binding.lineChart val vl = LineDataSet(entries.take(4000), "cost") vl.setDrawValues(false) vl.setDrawFilled(true) vl.lineWidth = 1.5f vl.fillColor = R.color.gray vl.fillAlpha = R.color.red vl.setDrawCircles(false) lineChart.data = LineData(vl) lineChart.notifyDataSetChanged() lineChart.animateX(1800, Easing.EaseInExpo) lineChart.description.isEnabled = false lineChart.isHighlightPerDragEnabled = false lineChart.isScaleYEnabled = false lineChart.axisRight.isEnabled = false } override fun onDestroyView() { super.onDestroyView() _binding = null } }
注意事项
- 如果你的CSV文件体积较大,上述协程异步读取逻辑已经避免了主线程阻塞
- 如果需要持久化选中的文件路径,可以将Uri字符串存入SharedPreferences,下次启动直接读取
- 已自动添加URI持久化权限,重启应用后仍可读取之前选中的文件
内容的提问来源于stack exchange,提问作者MaksChojniak1
相关产品推荐
相关产品推荐

