自定义View(DrawMap)获取Room数据库数据的ViewModel调用与数据传递问题
自定义View(DrawMap)获取Room数据库数据的ViewModel调用与数据传递问题
兄弟,刚瞅了你的代码,发现几个核心问题导致你拿不到Room的数据,咱们一步步来搞定它!
先说说你当前踩的坑
- 自定义View不能直接注入ViewModel/Repository:Android自定义View的构造有严格规范,必须支持带
AttributeSet的构造(系统inflation流程会用到),而Hilt没法给View注入ViewModel这类依赖,强行注入只会触发初始化异常。 - 数据传递时机完全错了:你在
onCreateView里创建DrawMap时,tile还是空列表——因为Room查询是异步操作,ViewModel的observe回调要等数据查完才会执行,这时候你已经把空列表传给DrawMap了,后续数据更新也没通知到View。 - 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
相关产品推荐
相关产品推荐

