Espresso 不会等到 RecyclerView 从 LiveData 获取更新
Espresso doesn't wait till RecyclerView gets update from LiveData
StatsFragment 包含 recyclerview。 recylcerView 显示“Stat”。 recylcerView 适配器通过观察 ViewModel 中的 LiveData 来更新。 ViewModel 通过 Flow 从存储库获取更新。
在 StatsFragment 中是一个 FAB,它打开一个 DialogFragment,可以添加一个新的 Stat。
我怀疑问题是 espresso 没有等到适配器更新并且新数据显示在屏幕上。
如何让 espresso 等到存储库流发出它的值,将其传递给 viewModels LiveData,以便更新片段中的列表以通知适配器在 recyclerview 中显示列表?
StatsFragment
class StatsFragment : Fragment() {
private lateinit var binding: FragmentStatsBinding
private val mainViewModel: MainViewModel by viewModel()
private lateinit var recyclerView: RecyclerView
private lateinit var statsAdapter: StatsAdapter
private lateinit var layoutManager: LinearLayoutManager
private lateinit var addStats: FloatingActionButton
private lateinit var addStatFragment: DialogFragment
private val stats = mutableListOf<Stat>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentStatsBinding.inflate(inflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initObserver()
initUi()
}
private fun initObserver() {
mainViewModel.stats.observe(viewLifecycleOwner, Observer { statList ->
stats.clear()
stats.addAll(statList)
statsAdapter.notifyDataSetChanged()
})
}
private fun initUi() {
statsAdapter = StatsAdapter(stats)
layoutManager = LinearLayoutManager(requireContext())
recyclerView = binding.recyclerViewStatsData
recyclerView.layoutManager = layoutManager
recyclerView.adapter = statsAdapter
addStats = binding.fabStatsAddStat
addStats.setOnClickListener(addStatOnClickListener)
}
private val addStatOnClickListener = View.OnClickListener {
addStatFragment = AddStatFragment()
addStatFragment.show(requireActivity().supportFragmentManager, "addStatFragment")
}
}
AddStatFragment
class AddStatFragment : DialogFragment() {
private lateinit var binding: FragmentInputBinding
private val mainViewModel: MainViewModel by viewModel()
private lateinit var date: TextView
private lateinit var time: TextView
private lateinit var confirm: Button
override fun onStart() {
super.onStart()
val width = resources.displayMetrics.widthPixels
val height = resources.displayMetrics.heightPixels
dialog!!.window?.setLayout(width, WRAP_CONTENT)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentInputBinding.inflate(inflater, container, false)
binding.viewModel = mainViewModel
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUi()
}
private fun initUi() {
date = binding.textViewInputDate
time = binding.textViewInputTime
confirm = binding.buttonInputConfirm
confirm.setOnClickListener {
mainViewModel.insertStat()
dialog!!.hide()
}
}
}
MainViewModel
class MainViewModel(private val repository: Repository, application: Application) :
AndroidViewModel(application) {
private val _stats = repository.getAllStats().asLiveData()
val stats: LiveData<List<Stat>>
get() = _stats
private val _insertStatStatus = MutableLiveData<Event<Resource<Stat>>>()
val insertStatStatus: LiveData<Event<Resource<Stat>>>
get() = _insertStatStatus
val weight = MutableLiveData<String>()
val waist = MutableLiveData<String>()
val kCal = MutableLiveData<String>()
val date = MutableLiveData<String>()
val time = MutableLiveData<String>()
private fun insertStatIntoDatabase(stat: Stat) {
viewModelScope.launch {
repository.insertStat(stat)
}
}
internal fun insertStat(weight: String, waist: String, kCal: String, date: String, time: String) {
val newStat = Stat(weight.toDouble(), waist.toDouble(), kCal.toDouble(), date, time)
insertStatIntoDatabase(newStat)
_insertStatStatus.value = Event(Resource.success(newStat))
}
internal fun insertStat() {
insertStat(weight.value ?: "", waist.value ?: "", kCal.value ?: "", date.value ?: "", time.value ?: "")
}
}
FakeRepository
class FakeStatsRepositoryAndroidTest: Repository {
private val fakeStats = MutableLiveData<MutableList<Stat>>()
private val underlyingList = mutableListOf<Stat>()
private var idCounter = 1
init {
fakeStats.value = underlyingList
}
override fun getAllStats(): Flow<List<Stat>> {
return fakeStats.asFlow()
}
override suspend fun insertStat(stat: Stat) {
val newStat = Stat(stat.weight,stat.waist,stat.kCal,stat.date,stat.time,idCounter)
idCounter++
underlyingList.add(newStat)
}
override suspend fun deleteStat(stat: Stat) {
underlyingList.remove(stat)
}
override suspend fun updateStat(stat: Stat) {
if(underlyingList.removeIf { it.id == stat.id }) {
underlyingList.add(stat)
} else {
return
}
}
}
测试
@Test
fun openingAddStatFragmentWhenEnteringInformationAndPressingConfirmThenStatIsVisibleInList() {
val statToAdd = Stat(70.1,88.5,2300.0,"today", "now",id = 1)
onView(withId(R.id.fab_stats_addStat)).perform(click())
onView(withId(R.id.textInputEditText_input_weight)).perform(typeText(statToAdd.weight.toString()))
onView(withId(R.id.textInputEditText_input_waist)).perform(typeText(statToAdd.weight.toString()))
onView(withId(R.id.textInputEditText_input_kcal)).perform(typeText(statToAdd.weight.toString()))
onView(withId(R.id.button_input_confirm)).perform(click())
onView(withId(R.id.recyclerView_stats_data)).perform(RecyclerViewActions.scrollTo<StatsViewHolder>(hasDescendant(withText("70.1"))))
}
我删除了一些代码(输入值检查、时间和日期相关的东西等)
错误在别处。没有来自 Fake 存储库的任何数据,因此 liveData 没有更新。
StatsFragment 包含 recyclerview。 recylcerView 显示“Stat”。 recylcerView 适配器通过观察 ViewModel 中的 LiveData 来更新。 ViewModel 通过 Flow 从存储库获取更新。 在 StatsFragment 中是一个 FAB,它打开一个 DialogFragment,可以添加一个新的 Stat。
我怀疑问题是 espresso 没有等到适配器更新并且新数据显示在屏幕上。
如何让 espresso 等到存储库流发出它的值,将其传递给 viewModels LiveData,以便更新片段中的列表以通知适配器在 recyclerview 中显示列表?
StatsFragment
class StatsFragment : Fragment() {
private lateinit var binding: FragmentStatsBinding
private val mainViewModel: MainViewModel by viewModel()
private lateinit var recyclerView: RecyclerView
private lateinit var statsAdapter: StatsAdapter
private lateinit var layoutManager: LinearLayoutManager
private lateinit var addStats: FloatingActionButton
private lateinit var addStatFragment: DialogFragment
private val stats = mutableListOf<Stat>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentStatsBinding.inflate(inflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initObserver()
initUi()
}
private fun initObserver() {
mainViewModel.stats.observe(viewLifecycleOwner, Observer { statList ->
stats.clear()
stats.addAll(statList)
statsAdapter.notifyDataSetChanged()
})
}
private fun initUi() {
statsAdapter = StatsAdapter(stats)
layoutManager = LinearLayoutManager(requireContext())
recyclerView = binding.recyclerViewStatsData
recyclerView.layoutManager = layoutManager
recyclerView.adapter = statsAdapter
addStats = binding.fabStatsAddStat
addStats.setOnClickListener(addStatOnClickListener)
}
private val addStatOnClickListener = View.OnClickListener {
addStatFragment = AddStatFragment()
addStatFragment.show(requireActivity().supportFragmentManager, "addStatFragment")
}
}
AddStatFragment
class AddStatFragment : DialogFragment() {
private lateinit var binding: FragmentInputBinding
private val mainViewModel: MainViewModel by viewModel()
private lateinit var date: TextView
private lateinit var time: TextView
private lateinit var confirm: Button
override fun onStart() {
super.onStart()
val width = resources.displayMetrics.widthPixels
val height = resources.displayMetrics.heightPixels
dialog!!.window?.setLayout(width, WRAP_CONTENT)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentInputBinding.inflate(inflater, container, false)
binding.viewModel = mainViewModel
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUi()
}
private fun initUi() {
date = binding.textViewInputDate
time = binding.textViewInputTime
confirm = binding.buttonInputConfirm
confirm.setOnClickListener {
mainViewModel.insertStat()
dialog!!.hide()
}
}
}
MainViewModel
class MainViewModel(private val repository: Repository, application: Application) :
AndroidViewModel(application) {
private val _stats = repository.getAllStats().asLiveData()
val stats: LiveData<List<Stat>>
get() = _stats
private val _insertStatStatus = MutableLiveData<Event<Resource<Stat>>>()
val insertStatStatus: LiveData<Event<Resource<Stat>>>
get() = _insertStatStatus
val weight = MutableLiveData<String>()
val waist = MutableLiveData<String>()
val kCal = MutableLiveData<String>()
val date = MutableLiveData<String>()
val time = MutableLiveData<String>()
private fun insertStatIntoDatabase(stat: Stat) {
viewModelScope.launch {
repository.insertStat(stat)
}
}
internal fun insertStat(weight: String, waist: String, kCal: String, date: String, time: String) {
val newStat = Stat(weight.toDouble(), waist.toDouble(), kCal.toDouble(), date, time)
insertStatIntoDatabase(newStat)
_insertStatStatus.value = Event(Resource.success(newStat))
}
internal fun insertStat() {
insertStat(weight.value ?: "", waist.value ?: "", kCal.value ?: "", date.value ?: "", time.value ?: "")
}
}
FakeRepository
class FakeStatsRepositoryAndroidTest: Repository {
private val fakeStats = MutableLiveData<MutableList<Stat>>()
private val underlyingList = mutableListOf<Stat>()
private var idCounter = 1
init {
fakeStats.value = underlyingList
}
override fun getAllStats(): Flow<List<Stat>> {
return fakeStats.asFlow()
}
override suspend fun insertStat(stat: Stat) {
val newStat = Stat(stat.weight,stat.waist,stat.kCal,stat.date,stat.time,idCounter)
idCounter++
underlyingList.add(newStat)
}
override suspend fun deleteStat(stat: Stat) {
underlyingList.remove(stat)
}
override suspend fun updateStat(stat: Stat) {
if(underlyingList.removeIf { it.id == stat.id }) {
underlyingList.add(stat)
} else {
return
}
}
}
测试
@Test
fun openingAddStatFragmentWhenEnteringInformationAndPressingConfirmThenStatIsVisibleInList() {
val statToAdd = Stat(70.1,88.5,2300.0,"today", "now",id = 1)
onView(withId(R.id.fab_stats_addStat)).perform(click())
onView(withId(R.id.textInputEditText_input_weight)).perform(typeText(statToAdd.weight.toString()))
onView(withId(R.id.textInputEditText_input_waist)).perform(typeText(statToAdd.weight.toString()))
onView(withId(R.id.textInputEditText_input_kcal)).perform(typeText(statToAdd.weight.toString()))
onView(withId(R.id.button_input_confirm)).perform(click())
onView(withId(R.id.recyclerView_stats_data)).perform(RecyclerViewActions.scrollTo<StatsViewHolder>(hasDescendant(withText("70.1"))))
}
我删除了一些代码(输入值检查、时间和日期相关的东西等)
错误在别处。没有来自 Fake 存储库的任何数据,因此 liveData 没有更新。