From b75e380c3a6cd5c8b915956b5889f9cbf3fbb7f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sikora?= Date: Sun, 28 Mar 2021 17:53:21 +0200 Subject: [PATCH] Navigate from supervised feature to supervisor --- library/docs/changelog.md | 3 + .../laboratory/inspector/FeatureAdapter.kt | 7 +- .../inspector/FeatureCoordinates.kt | 6 + .../laboratory/inspector/FeatureViewHolder.kt | 35 +++-- .../laboratory/inspector/GroupAdapter.kt | 16 --- .../inspector/InspectorViewModel.kt | 22 ++- .../inspector/LaboratoryActivity.kt | 18 ++- .../laboratory/inspector/SectionAdapter.kt | 28 ++++ .../{GroupFragment.kt => SectionFragment.kt} | 35 +++-- .../SmoothScrollingLinearLayoutManager.kt | 20 +++ .../io_mehow_laboratory_feature_group.xml | 2 +- .../inspector/src/main/res/values/strings.xml | 3 +- .../inspector/InspectorModelNavigationSpec.kt | 129 ++++++++++++++++++ 13 files changed, 278 insertions(+), 46 deletions(-) create mode 100644 library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureCoordinates.kt delete mode 100644 library/inspector/src/main/java/io/mehow/laboratory/inspector/GroupAdapter.kt create mode 100644 library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionAdapter.kt rename library/inspector/src/main/java/io/mehow/laboratory/inspector/{GroupFragment.kt => SectionFragment.kt} (57%) create mode 100644 library/inspector/src/main/java/io/mehow/laboratory/inspector/SmoothScrollingLinearLayoutManager.kt create mode 100644 library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelNavigationSpec.kt diff --git a/library/docs/changelog.md b/library/docs/changelog.md index 22649cc1a..6912c2647 100644 --- a/library/docs/changelog.md +++ b/library/docs/changelog.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Navigation from supervised feature to supervisor. + ### Changed - Upgrade to Kotlin `1.4.32`. - Upgrade to LifecycleViewmodelKtx `2.3.1`. diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureAdapter.kt b/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureAdapter.kt index e24f7afc2..490229a0f 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureAdapter.kt +++ b/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureAdapter.kt @@ -4,6 +4,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil.ItemCallback import androidx.recyclerview.widget.ListAdapter +import io.mehow.laboratory.Feature import io.mehow.laboratory.inspector.OptionViewGroup.OnSelectFeatureListener import io.mehow.laboratory.inspector.SourceViewGroup.OnSelectSourceListener @@ -28,5 +29,9 @@ internal class FeatureAdapter( override fun getChangePayload(old: FeatureUiModel, new: FeatureUiModel) = Unit } - interface Listener : OnSelectFeatureListener, OnSelectSourceListener + interface Listener : OnSelectFeatureListener, OnSelectSourceListener { + @JvmDefault override fun onSelectSource(feature: Feature<*>) = onSelectFeature(feature) + + fun onGoToFeature(feature: Class>) + } } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureCoordinates.kt b/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureCoordinates.kt new file mode 100644 index 000000000..21355bf13 --- /dev/null +++ b/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureCoordinates.kt @@ -0,0 +1,6 @@ +package io.mehow.laboratory.inspector + +internal data class FeatureCoordinates( + val sectionIndex: Int, + val featureIndex: Int, +) diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureViewHolder.kt b/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureViewHolder.kt index a2653d0f1..de6a64269 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureViewHolder.kt +++ b/library/inspector/src/main/java/io/mehow/laboratory/inspector/FeatureViewHolder.kt @@ -1,8 +1,11 @@ package io.mehow.laboratory.inspector import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG +import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan import android.view.View +import androidx.core.text.buildSpannedString import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.google.android.material.textview.MaterialTextView @@ -15,25 +18,36 @@ internal class FeatureViewHolder( itemView: View, listener: FeatureAdapter.Listener, ) : ViewHolder(itemView) { + private var uiModel: FeatureUiModel? = null + private val context = itemView.context private val nameControl = itemView.findViewById(R.id.io_mehow_laboratory_feature_name) private val supervisorControl = itemView.findViewById(R.id.io_mehow_laboratory_feature_supervisor) private val descriptionControl = itemView.findViewById(R.id.io_mehow_laboratory_feature_description) private val sourcesControl = itemView.findViewById(R.id.io_mehow_laboratory_feature_sources) private val dividerControl = itemView.findViewById(R.id.io_mehow_laboratory_sources_divider) private val optionsControl = itemView.findViewById(R.id.io_mehow_laboratory_feature_options) + private val goToSupervisor = object : ClickableSpan() { + override fun onClick(widget: View) { + listener.onGoToFeature(uiModel!!.supervisorOption!!.javaClass) + } + } init { sourcesControl.setOnSelectSourceListener(listener) optionsControl.setOnSelectFeatureListener(listener) descriptionControl.movementMethod = LinkMovementMethod.getInstance() + supervisorControl.movementMethod = LinkMovementMethod.getInstance() } - fun bind(uiModel: FeatureUiModel) = with(uiModel) { - bindName() - bindSupervisor() - bindDescription() - bindSources() - bindOptions() + fun bind(uiModel: FeatureUiModel) { + this.uiModel = uiModel + with(uiModel) { + bindName() + bindSupervisor() + bindDescription() + bindSources() + bindOptions() + } } private fun FeatureUiModel.bindName() { @@ -48,8 +62,13 @@ internal class FeatureViewHolder( private fun FeatureUiModel.bindSupervisor() = with(type) { supervisorControl.isVisible = supervisorOption != null supervisorControl.text = supervisorOption?.let { supervisorOption -> - val supervisorName = supervisorOption::class.simpleName - itemView.context.getString(R.string.io_mehow_laboratory_feature_supervisor, supervisorName, supervisorOption) + buildSpannedString { + append(context.getString(R.string.io_mehow_laboratory_feature_supervisor_prefix)) + val linkStart = length + append(supervisorOption::class.simpleName) + setSpan(goToSupervisor, linkStart, length, SPAN_EXCLUSIVE_EXCLUSIVE) + append(context.getString(R.string.io_mehow_laboratory_feature_supervisor_suffix, supervisorOption)) + } } } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/GroupAdapter.kt b/library/inspector/src/main/java/io/mehow/laboratory/inspector/GroupAdapter.kt deleted file mode 100644 index 63b589fe6..000000000 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/GroupAdapter.kt +++ /dev/null @@ -1,16 +0,0 @@ -package io.mehow.laboratory.inspector - -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.viewpager2.adapter.FragmentStateAdapter - -internal class GroupAdapter( - fragmentActivity: FragmentActivity, - private val sections: List, -) : FragmentStateAdapter(fragmentActivity) { - override fun getItemCount(): Int = sections.size - - override fun createFragment(position: Int): Fragment { - return GroupFragment.create(sections[position]) - } -} diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/InspectorViewModel.kt b/library/inspector/src/main/java/io/mehow/laboratory/inspector/InspectorViewModel.kt index 75704fcb8..7158918ac 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/InspectorViewModel.kt +++ b/library/inspector/src/main/java/io/mehow/laboratory/inspector/InspectorViewModel.kt @@ -14,17 +14,23 @@ import kotlinx.coroutines.CoroutineStart.UNDISPATCHED import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.withIndex import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.time.milliseconds @@ -42,7 +48,7 @@ internal class InspectorViewModel( emitAll(searchQueries) }.distinctUntilChanged() - private val groupFlows = featureFactories.mapValues { (_, featureFactory) -> + private val sectionFlows = featureFactories.mapValues { (_, featureFactory) -> flow { val groups = withContext(Dispatchers.Default) { featureFactory.create() @@ -57,12 +63,24 @@ internal class InspectorViewModel( }.shareIn(viewModelScope, SharingStarted.Lazily, replay = 1) } - fun sectionFlow(sectionName: String) = groupFlows[sectionName] ?: emptyFlow() + fun sectionFlow(sectionName: String) = sectionFlows[sectionName] ?: emptyFlow() fun selectFeature(feature: Feature<*>) { viewModelScope.launch(start = UNDISPATCHED) { laboratory.setOption(feature) } } + private val mutableNavigationFlow = MutableSharedFlow() + + val featureCoordinatesFlow: Flow get() = mutableNavigationFlow + + suspend fun goTo(feature: Class>) = sectionFlows.values.asFlow().withIndex() + .mapNotNull { (sectionIndex, sectionFlow) -> + val listIndex = sectionFlow.first().map(FeatureUiModel::type).indexOf(feature) + if (listIndex == -1) null else FeatureCoordinates(sectionIndex, listIndex) + } + .firstOrNull() + ?.also { mutableNavigationFlow.emit(it) } + private class FeatureMetadata( private val feature: Class>, private val deprecationHandler: DeprecationHandler, diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/LaboratoryActivity.kt b/library/inspector/src/main/java/io/mehow/laboratory/inspector/LaboratoryActivity.kt index 345b5f37a..052655b4d 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/LaboratoryActivity.kt +++ b/library/inspector/src/main/java/io/mehow/laboratory/inspector/LaboratoryActivity.kt @@ -17,6 +17,8 @@ import com.google.android.material.tabs.TabLayoutMediator import com.willowtreeapps.hyperion.plugin.v1.HyperionIgnore import io.mehow.laboratory.FeatureFactory import io.mehow.laboratory.Laboratory +import io.mehow.laboratory.inspector.LaboratoryActivity.Configuration.OffscreenSectionsBehavior.Limited +import io.mehow.laboratory.inspector.LaboratoryActivity.Configuration.OffscreenSectionsBehavior.Unlimited import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -26,7 +28,11 @@ import kotlinx.coroutines.launch */ @HyperionIgnore // https://github.com/willowtreeapps/Hyperion-Android/issues/194 public class LaboratoryActivity : AppCompatActivity(R.layout.io_mehow_laboratory_inspector) { + private val sectionNames = configuration.sectionNames.toList() private val searchViewModel by viewModels { SearchViewModel.Factory } + private val inspectorViewModel by viewModels { + InspectorViewModel.Factory(configuration, searchViewModel) + } override fun onCreate(inState: Bundle?) { super.onCreate(inState) @@ -46,11 +52,12 @@ public class LaboratoryActivity : AppCompatActivity(R.layout.io_mehow_laboratory } private fun setUpViewPager() { - val sectionNames = configuration.sectionNames.toList() val viewPager = findViewById(R.id.io_mehow_laboratory_view_pager).apply { - adapter = GroupAdapter(this@LaboratoryActivity, sectionNames) + adapter = SectionAdapter(this@LaboratoryActivity, sectionNames) disableScrollEffect() } + observeNavigationEvents(viewPager) + if (sectionNames.size <= 1) return val tabLayout = findViewById(R.id.io_mehow_laboratory_tab_layout).apply { isVisible = true @@ -60,6 +67,13 @@ public class LaboratoryActivity : AppCompatActivity(R.layout.io_mehow_laboratory }.attach() } + private fun observeNavigationEvents(viewPager: ViewPager2) = inspectorViewModel.featureCoordinatesFlow + .onEach { (sectionIndex, featureIndex) -> + viewPager.currentItem = sectionIndex + (viewPager.adapter as SectionAdapter).awaitSectionFragment(sectionNames[sectionIndex]).scrollTo(featureIndex) + } + .launchIn(lifecycleScope) + private fun resetFeatureFlags() = lifecycleScope.launch { val isCleared = configuration.laboratory.clear() val messageId = if (isCleared) { diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionAdapter.kt b/library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionAdapter.kt new file mode 100644 index 000000000..c8164bad3 --- /dev/null +++ b/library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionAdapter.kt @@ -0,0 +1,28 @@ +package io.mehow.laboratory.inspector + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import kotlinx.coroutines.delay +import java.lang.ref.WeakReference + +internal class SectionAdapter( + activity: FragmentActivity, + private val sectionNames: List, +) : FragmentStateAdapter(activity) { + private val pages = mutableMapOf>() + + override fun getItemCount() = sectionNames.size + + override fun createFragment(position: Int): Fragment { + val sectionName = sectionNames[position] + return SectionFragment.create(sectionName).also { + pages[sectionName] = WeakReference(it) + } + } + + suspend fun awaitSectionFragment(sectionName: String): SectionFragment = pages[sectionName]?.get() ?: run { + delay(100) // ¯\_(ツ)_/¯ + awaitSectionFragment(sectionName) + } +} diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/GroupFragment.kt b/library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionFragment.kt similarity index 57% rename from library/inspector/src/main/java/io/mehow/laboratory/inspector/GroupFragment.kt rename to library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionFragment.kt index 84ec49c81..9783c375c 100644 --- a/library/inspector/src/main/java/io/mehow/laboratory/inspector/GroupFragment.kt +++ b/library/inspector/src/main/java/io/mehow/laboratory/inspector/SectionFragment.kt @@ -5,46 +5,51 @@ import android.view.View import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.mehow.laboratory.Feature import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch -internal class GroupFragment : Fragment(R.layout.io_mehow_laboratory_feature_group) { - private val sectionName get() = requireStringArgument(sectionKey) - private val searchViewModel by activityViewModels { SearchViewModel.Factory } - val viewModel by viewModels { +internal class SectionFragment : Fragment(R.layout.io_mehow_laboratory_feature_group) { + val inspectorViewModel by activityViewModels { InspectorViewModel.Factory(LaboratoryActivity.configuration, searchViewModel) } + private val searchViewModel by activityViewModels { SearchViewModel.Factory } + private val sectionName get() = requireStringArgument(sectionKey) + + private lateinit var layoutManager: SmoothScrollingLinearLayoutManager private val featureAdapter = FeatureAdapter(object : FeatureAdapter.Listener { - override fun onSelectFeature(feature: Feature<*>) { - viewModel.selectFeature(feature) - } + override fun onSelectFeature(feature: Feature<*>) = inspectorViewModel.selectFeature(feature) - override fun onSelectSource(feature: Feature<*>) = viewModel.selectFeature(feature) + override fun onGoToFeature(feature: Class>) { + lifecycleScope.launch { inspectorViewModel.goTo(feature) } + } }) override fun onViewCreated(view: View, inState: Bundle?) { - view.findViewById(R.id.io_mehow_laboratory_feature_group).apply { - layoutManager = LinearLayoutManager(requireActivity()) + view.findViewById(R.id.io_mehow_laboratory_feature_section).apply { + layoutManager = SmoothScrollingLinearLayoutManager(requireActivity()).also { + this@SectionFragment.layoutManager = it + } adapter = featureAdapter hideKeyboardOnScroll() } observeGroup() } - private fun observeGroup() = viewModel.sectionFlow(sectionName) + private fun observeGroup() = inspectorViewModel.sectionFlow(sectionName) .onEach { featureAdapter.submitList(it) } .launchIn(viewLifecycleOwner.lifecycleScope) + fun scrollTo(index: Int) = layoutManager.smoothScrollTo(index) + companion object { private const val sectionKey = "Section.Key" - fun create(section: String): GroupFragment { - return GroupFragment().apply { + fun create(section: String): SectionFragment { + return SectionFragment().apply { arguments = bundleOf(sectionKey to section) } } diff --git a/library/inspector/src/main/java/io/mehow/laboratory/inspector/SmoothScrollingLinearLayoutManager.kt b/library/inspector/src/main/java/io/mehow/laboratory/inspector/SmoothScrollingLinearLayoutManager.kt new file mode 100644 index 000000000..f352cdfef --- /dev/null +++ b/library/inspector/src/main/java/io/mehow/laboratory/inspector/SmoothScrollingLinearLayoutManager.kt @@ -0,0 +1,20 @@ +package io.mehow.laboratory.inspector + +import android.content.Context +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSmoothScroller + +internal class SmoothScrollingLinearLayoutManager( + context: Context, +) : LinearLayoutManager(context) { + private val scroller = object : LinearSmoothScroller(context) { + override fun getVerticalSnapPreference() = SNAP_TO_START + + override fun getHorizontalSnapPreference() = SNAP_TO_START + } + + fun smoothScrollTo(index: Int) { + scroller.targetPosition = index + startSmoothScroll(scroller) + } +} diff --git a/library/inspector/src/main/res/layout/io_mehow_laboratory_feature_group.xml b/library/inspector/src/main/res/layout/io_mehow_laboratory_feature_group.xml index 54d4bd093..1d2821b7a 100644 --- a/library/inspector/src/main/res/layout/io_mehow_laboratory_feature_group.xml +++ b/library/inspector/src/main/res/layout/io_mehow_laboratory_feature_group.xml @@ -1,7 +1,7 @@ Search features… Clear query Close search - Turned on if %1$s is %2$s + "Turned on if " + " is %1$s" diff --git a/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelNavigationSpec.kt b/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelNavigationSpec.kt new file mode 100644 index 000000000..31c0df595 --- /dev/null +++ b/library/inspector/src/test/java/io/mehow/laboratory/inspector/InspectorModelNavigationSpec.kt @@ -0,0 +1,129 @@ +package io.mehow.laboratory.inspector + +import app.cash.turbine.test +import io.kotest.assertions.fail +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.collections.shouldBeIn +import io.kotest.matchers.shouldBe +import io.mehow.laboratory.Feature +import io.mehow.laboratory.FeatureFactory +import io.mehow.laboratory.Laboratory +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf + +@Suppress("UNCHECKED_CAST") +internal class InspectorViewModelNavigationSpec : DescribeSpec({ + setMainDispatcher() + + describe("feature flag coordinates") { + it("are found") { + val viewModel = InspectorViewModel() + + viewModel.goTo(SectionOneFeatureA::class.java as Class>) shouldBe FeatureCoordinates(0, 0) + viewModel.goTo(SectionOneFeatureB::class.java as Class>) shouldBe FeatureCoordinates(0, 1) + viewModel.goTo(SectionTwoFeature::class.java as Class>) shouldBe FeatureCoordinates(1, 0) + } + + it("are not found when feature is registered") { + val viewModel = InspectorViewModel() + + viewModel.goTo(UnregisteredFeature::class.java as Class>) shouldBe null + } + + it("are not found when feature is filtered") { + val viewModel = InspectorViewModel(searchFlow = flowOf(SearchQuery("Foo"))) + + viewModel.goTo(SectionOneFeatureA::class.java as Class>) shouldBe null + } + + it("are found when feature is registerd twice") { + val viewModel = InspectorViewModel(mapOf("A1" to SectionAFactory, "A2" to SectionAFactory)) + + viewModel.goTo(SectionOneFeatureA::class.java as Class>) shouldBeIn listOf( + FeatureCoordinates(0, 0), + FeatureCoordinates(1, 0), + ) + } + + it("can be observed") { + val viewModel = InspectorViewModel() + + viewModel.featureCoordinatesFlow.test { + expectNoEvents() + + viewModel.goTo(SectionOneFeatureA::class.java as Class>) + expectItem() shouldBe FeatureCoordinates(0, 0) + + cancel() + } + } + + it("do not cache emissions") { + val viewModel = InspectorViewModel() + + viewModel.goTo(SectionOneFeatureA::class.java as Class>) + + viewModel.featureCoordinatesFlow.test { + cancel() + } + } + } +}) + +private object SectionAFactory : FeatureFactory { + override fun create(): Set>> { + @Suppress("UNCHECKED_CAST") + return setOf(SectionOneFeatureA::class.java, SectionOneFeatureB::class.java) as Set>> + } +} + +private object SectionBFactory : FeatureFactory { + override fun create(): Set>> { + @Suppress("UNCHECKED_CAST") + return setOf(SectionTwoFeature::class.java) as Set>> + } +} + +private enum class SectionOneFeatureA : Feature { + Option, + ; + + override val defaultOption: SectionOneFeatureA + get() = Option +} + +private enum class SectionOneFeatureB : Feature { + Option, + ; + + override val defaultOption: SectionOneFeatureB + get() = Option +} + +private enum class SectionTwoFeature : Feature { + Option, + ; + + override val defaultOption: SectionTwoFeature + get() = Option +} + +private enum class UnregisteredFeature : Feature { + Option, + ; + + override val defaultOption: UnregisteredFeature + get() = Option +} + +@Suppress("TestFunctionName") +private fun InspectorViewModel( + featureFactories: Map = mapOf("A" to SectionAFactory, "B" to SectionBFactory), + searchFlow: Flow = emptyFlow(), +) = InspectorViewModel( + Laboratory.inMemory(), + searchFlow, + featureFactories, + DeprecationHandler({ fail("Unexpected call") }, { fail("Unexpected call") }), +)