Skip to content

Instantly share code, notes, and snippets.

@Cassie-von-Clausewitz
Last active March 20, 2020 15:23
Show Gist options
  • Save Cassie-von-Clausewitz/c35f0dc77f454dd9c07c846e84a55982 to your computer and use it in GitHub Desktop.
Save Cassie-von-Clausewitz/c35f0dc77f454dd9c07c846e84a55982 to your computer and use it in GitHub Desktop.
Coroutine Flows
class FilteredLogPolicy @Inject constructor(
private val logDao: LogDao,
private val filterRepository: FilterRepository,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend operator fun invoke() = withContext(dispatcher) {
filterRepository.filter.catch {
logError(it)
emit(LogFilters())
}.flatMapLatest {
if (it.levelFilter.isNotEmpty && it.tagFilter.isEmpty) {
logDao.getLogsFilteredByLevel(it.levelFilter.intArray)
} else if (it.levelFilter.isEmpty && it.tagFilter.isNotEmpty) {
logDao.getLogsFilteredByTag(it.tagFilter.array)
} else if (it.levelFilter.isNotEmpty && it.tagFilter.isNotEmpty) {
logDao.getLogsFilteredByLevelAndTag(it.levelFilter.intArray, it.tagFilter.array)
} else {
logDao.getLogs()
}
}.catch {
logError(it)
emit(emptyList())
}
}
private fun logError(t: Throwable) = e(t) { "Error in FilteredLogPolicy" }
}
class FilteredLogPolicyTest {
@MockK lateinit var logDao: LogDao
@MockK lateinit var filterRepository: FilterRepository
val dispatcher: CoroutineDispatcher = TestCoroutineDispatcher()
lateinit var spy: FilteredLogPolicy
@Before
fun setUp() {
MockKAnnotations.init(this, relaxUnitFun = true)
spy = spyk(FilteredLogPolicy(logDao, filterRepository, dispatcher))
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `return all logs for empty filter`(): Unit = runBlockingTest {
coEvery { filterRepository.filter } returns flowOf(LogFilters(LevelFilter(), TagFilter()))
coEvery { logDao.getLogs() } returns flowOf(listOf(Log(LogLevel.Verbose, "Tag", "message")))
spy.invoke().collect { assertThat(it).isNotEmpty() }
coVerifyOrder {
spy.invoke()
filterRepository.filter
logDao.getLogs()
}
confirmVerified(filterRepository, logDao, spy)
}
@Test
fun `return logs filtered by tag for tag filter and empty level filter`(): Unit = runBlockingTest {
val tag = LogTag("Device")
coEvery { filterRepository.filter } returns flowOf(LogFilters(LevelFilter(), TagFilter(listOf(tag))))
coEvery { logDao.getLogsFilteredByTag(arrayOf("Device")) } returns flowOf(listOf(Log(LogLevel.Verbose, "Device", "message")))
spy.invoke().collect { assertThat(it).isNotEmpty() }
coVerifyOrder {
spy.invoke()
filterRepository.filter
logDao.getLogsFilteredByTag(arrayOf("Device"))
}
confirmVerified(filterRepository, logDao, spy)
}
@Test
fun `return logs filtered by level for level filter and empty tag filter`(): Unit = runBlockingTest {
coEvery { filterRepository.filter } returns flowOf(LogFilters(LevelFilter(info = true), TagFilter()))
coEvery { logDao.getLogsFilteredByLevel(intArrayOf(LogLevel.Info.level)) } returns flowOf(listOf(Log(LogLevel.Info, "Device", "message")))
spy.invoke().collect { assertThat(it).isNotEmpty() }
coVerifyOrder {
spy.invoke()
filterRepository.filter
logDao.getLogsFilteredByLevel(intArrayOf(LogLevel.Info.level))
}
confirmVerified(filterRepository, logDao, spy)
}
@Test
fun `return logs filtered by level and tag for level and tag filters`(): Unit = runBlockingTest {
val tag = LogTag("Device")
coEvery { filterRepository.filter } returns flowOf(LogFilters(LevelFilter(info = true), TagFilter(listOf(tag))))
coEvery { logDao.getLogsFilteredByLevelAndTag(intArrayOf(LogLevel.Info.level), arrayOf("Device")) } returns flowOf(listOf(Log(LogLevel.Info, "Device", "message")))
spy.invoke().collect { assertThat(it).isNotEmpty() }
coVerifyOrder {
spy.invoke()
filterRepository.filter
logDao.getLogsFilteredByLevelAndTag(intArrayOf(LogLevel.Info.level), arrayOf("Device"))
}
confirmVerified(filterRepository, logDao, spy)
}
@Test
fun `returns all logs if filter errors`(): Unit = runBlockingTest {
coEvery { filterRepository.filter } returns flow { error("Oops") }
coEvery { logDao.getLogs() } returns flowOf(listOf(Log(LogLevel.Verbose, "Tag", "message")))
spy.invoke().collect { assertThat(it).isNotEmpty() }
coVerifyOrder {
spy.invoke()
filterRepository.filter
logDao.getLogs()
}
confirmVerified(filterRepository, logDao, spy)
}
@Test
fun `returns empty list if database lookup fails`(): Unit = runBlockingTest {
coEvery { filterRepository.filter } returns flow { error("Oops") }
coEvery { logDao.getLogs() } returns flow { error("Oops") }
spy.invoke().collect { assertThat(it).isEmpty() }
coVerifyOrder {
spy.invoke()
filterRepository.filter
logDao.getLogs()
}
confirmVerified(filterRepository, logDao, spy)
}
}
class LogListFragment : BaseFragment<LogListViewModel, FragmentLogListBinding>() {
override val viewModel: LogListViewModel by viewModels { viewModelFactory }
override val layoutId = R.layout.fragment_log_list
private lateinit var listAdapter: LogAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupListAdapter()
lifecycleScope.launchWhenResumed {
viewModel.filteredItems().collect {
listAdapter.submitList(it)
}
}
binding.fab.setOnClickListener {
binding.fab.isExpanded = true
}
binding.logList.addOnScrollListener(object:RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING -> binding.fab.isExpanded = false
}
}
})
lifecycleScope.launchWhenResumed {
viewModel.levelSettings.collect { filter ->
binding.verboseChip.isChecked = filter.verbose
binding.debugChip.isChecked = filter.debug
binding.infoChip.isChecked = filter.info
binding.warnChip.isChecked = filter.warn
binding.errorChip.isChecked = filter.error
binding.assertChip.isChecked = filter.assert
binding.unknownChip.isChecked = filter.unknown
}
}
lifecycleScope.launchWhenResumed {
viewModel.tagSettings.collect { list ->
binding.chipGroup.removeAllViews()
list.forEach {
val chip = Chip(binding.chipGroup.context)
chip.isCheckable = true
chip.text = it.tag
chip.isChecked = it.selected
chip.setOnClickListener { _ ->
viewModel.toggleTagFilter(it)
}
binding.chipGroup.addView(chip)
}
}
}
}
private fun setupListAdapter() {
listAdapter = LogAdapter(findNavController())
binding.logList.adapter = listAdapter
}
}
class LogListViewModel @AssistedInject constructor(
@Assisted private val handle: SavedStateHandle,
private val filterRepository: FilterRepository,
private val filteredLogPolicy: FilteredLogPolicy,
private val logLocalDataSource: LogLocalDataSource
): ViewModel() {
suspend fun filteredItems() = filteredLogPolicy.invoke()
val levelSettings = filterRepository.levelFilter
val tags = logLocalDataSource.tags()
val tagSettings = combine(filterRepository.tagFilter, logLocalDataSource.tags()) { filters, tags ->
tags.map { it.copy(selected = filters.logTags.contains(it)) }
}
// this is still cleaner than using databinding to pass the log level from the view
fun toggleVerbose() = toggleLevelFilter(LogLevel.Verbose)
fun toggleDebug() = toggleLevelFilter(LogLevel.Debug)
fun toggleInfo() = toggleLevelFilter(LogLevel.Info)
fun toggleWarn() = toggleLevelFilter(LogLevel.Warn)
fun toggleError() = toggleLevelFilter(LogLevel.Error)
fun toggleAssert() = toggleLevelFilter(LogLevel.Assert)
fun toggleUnknown() = toggleLevelFilter(LogLevel.Unknown)
fun toggleLevelFilter(logLevel: LogLevel) = viewModelScope.launch {
filterRepository.toggleLogLevel(logLevel)
}
fun toggleTagFilter(logTag: LogTag) = viewModelScope.launch {
filterRepository.toggleLogTag(logTag)
}
fun clearFilters() = viewModelScope.launch {
filterRepository.clearFilter()
}
@AssistedInject.Factory
interface Factory : ViewModelAssistedFactory<LogListViewModel>
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment