Last active
March 20, 2020 15:23
-
-
Save Cassie-von-Clausewitz/c35f0dc77f454dd9c07c846e84a55982 to your computer and use it in GitHub Desktop.
Coroutine Flows
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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