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

自定义View(DrawMap)获取Room数据库数据的ViewModel调用与数据传递问题

自定义View(DrawMap)获取Room数据库数据的ViewModel调用与数据传递问题

兄弟,刚瞅了你的代码,发现几个核心问题导致你拿不到Room的数据,咱们一步步来搞定它!

先说说你当前踩的坑

  1. 自定义View不能直接注入ViewModel/Repository:Android自定义View的构造有严格规范,必须支持带AttributeSet的构造(系统inflation流程会用到),而Hilt没法给View注入ViewModel这类依赖,强行注入只会触发初始化异常。
  2. 数据传递时机完全错了:你在onCreateView里创建DrawMap时,tile还是空列表——因为Room查询是异步操作,ViewModel的observe回调要等数据查完才会执行,这时候你已经把空列表传给DrawMap了,后续数据更新也没通知到View。
  3. ViewModel的同步调用有风险:你现在直接在主线程调用repository.getTiles(),Room默认不允许主线程查询,轻则卡UI,重则直接崩溃。

一步步解决问题

第一步:改造DrawMap,让它支持动态更新数据

自定义View的职责应该是只负责绘制,数据从外部传入就行。我们给它加一个更新数据的方法,并且确保构造符合Android规范:

class DrawMap(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
    // 用私有变量存tile列表,外部通过setter更新
    private var tileList: List<Tile> = emptyList()

    // 外部调用这个方法更新数据,更新后触发重绘
    fun setTiles(newTiles: List<Tile>) {
        tileList = newTiles
        invalidate() // 通知系统重新绘制View,这样onDraw会被触发
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 现在这里就能拿到最新的tileList了
        println("当前待绘制的tile数量:${tileList.size}")
        // 你的绘制逻辑写在这里,比如遍历tileList画元素
    }
}

第二步:改造Fragment,正确处理ViewModel的异步数据

Fragment要先创建DrawMap实例,等ViewModel的Room数据回来后,再调用setTiles更新View:

@AndroidEntryPoint
class MapFragment : Fragment() {
    private var _binding: FragmentMapBinding? = null
    private val binding get() = _binding!!
    private val viewModel: MapViewModel by viewModels()
    // 保留DrawMap的实例引用,方便后续更新数据
    private lateinit var drawMap: DrawMap

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentMapBinding.inflate(inflater, container, false)
        
        // 用标准构造创建DrawMap实例
        drawMap = DrawMap(requireContext())

        // 观察ViewModel的LiveData,数据回来后更新View
        viewModel.tiles.observe(viewLifecycleOwner) { tiles ->
            drawMap.setTiles(tiles)
            println("Fragment收到Room返回的tile数据:${tiles.size}")
        }

        // 调用ViewModel的方法触发数据查询(如果需要的话)
        viewModel.getTiles()

        // 如果你要把DrawMap作为Fragment的根View,就返回它;如果要加到布局里,就用binding的容器addView
        // 比如:binding.mapContainer.addView(drawMap) 然后 return binding.root
        return drawMap
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

第三步:修正ViewModel和Repository,确保Room异步查询

Room禁止主线程同步查询,所以我们用Flow来异步获取数据,再转换成LiveData给Fragment观察:

Repository改造
class TileRepository(private val tileDao: TileDao) {
    // Room会自动在后台线程执行这个查询,返回Flow
    fun getTiles(): Flow<List<Tile>> {
        return tileDao.getAllTiles()
    }
}
ViewModel改造
@HiltViewModel
class MapViewModel @Inject constructor(private val repository: TileRepository): ViewModel() {
    // 把Flow转换成LiveData,自动在ViewModel的作用域内管理
    val tiles: LiveData<List<Tile>> = repository.getTiles()
        .asLiveData(viewModelScope.coroutineContext)

    // 如果需要主动刷新数据,可以加个方法
    fun getTiles() {
        // 比如如果是Flow的话,不需要额外操作,因为Flow会自动监听数据库变化;如果是一次性查询,可以重新触发
    }
}

最后再划几个重点

  • 自定义View要做“纯View”:别让它依赖ViewModel、Repository这些业务层的东西,数据通过外部传参,职责单一才好维护。
  • 异步数据要等回调:Room、网络请求都是异步的,不能在创建View的时候直接传空数据,要等数据回来后再更新View。
  • Room查询必须异步:永远别用allowMainThreadQueries(除非是测试),用Flow/LiveData是标准做法。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 14:57:57