From ecc4c15b81ff4bb164723444ccf326141e23133c Mon Sep 17 00:00:00 2001 From: Yigit Boyar Date: Mon, 7 Oct 2019 19:59:01 -0700 Subject: [PATCH 1/3] Use livedata builders instead of setting values on live data --- app/build.gradle | 2 +- build.gradle | 4 +- .../dribbble/ui/shot/ShotViewModel.kt | 71 +++++++-------- .../dribbble/ui/shot/ShotViewModelTest.kt | 87 ++++++++++++------- gradle/wrapper/gradle-wrapper.properties | 3 +- 5 files changed, 95 insertions(+), 72 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index eac705a89..5157c2627 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,4 @@ -/* + /* * Copyright 2015 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/build.gradle b/build.gradle index b94fdfad8..6dce5aa1d 100644 --- a/build.gradle +++ b/build.gradle @@ -42,7 +42,7 @@ buildscript { scriptHandler -> 'kotlin' : '1.3.50', 'ktlint' : '0.24.0', 'legacyCoreUtils' : '1.0.0', - 'lifecycle' : '2.2.0-alpha03', + 'lifecycle' : '2.2.0-alpha05', 'material' : '1.1.0-alpha05', 'mockito' : '2.23.0', 'mockito_kotlin' : '2.0.0-RC3', @@ -60,7 +60,7 @@ buildscript { scriptHandler -> ] dependencies { - classpath 'com.android.tools.build:gradle:3.6.0-alpha07' + classpath 'com.android.tools.build:gradle:3.6.0-alpha12' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "com.google.gms:google-services:${versions.googleServices}" classpath "io.fabric.tools:gradle:${versions.fabric}" diff --git a/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotViewModel.kt b/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotViewModel.kt index 1080b8b8c..ea347184c 100644 --- a/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotViewModel.kt +++ b/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotViewModel.kt @@ -16,19 +16,22 @@ package io.plaidapp.dribbble.ui.shot -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.asFlow +import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope import io.plaidapp.core.data.CoroutinesDispatcherProvider import io.plaidapp.core.data.Result import io.plaidapp.core.dribbble.data.ShotsRepository -import io.plaidapp.core.dribbble.data.api.model.Shot import io.plaidapp.core.util.event.Event import io.plaidapp.dribbble.domain.CreateShotUiModelUseCase import io.plaidapp.dribbble.domain.GetShareShotInfoUseCase -import io.plaidapp.dribbble.domain.ShareShotInfo -import kotlinx.coroutines.launch +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ConflatedBroadcastChannel +import kotlinx.coroutines.channels.consume +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield import javax.inject.Inject /** @@ -41,43 +44,48 @@ class ShotViewModel @Inject constructor( private val getShareShotInfo: GetShareShotInfoUseCase, private val dispatcherProvider: CoroutinesDispatcherProvider ) : ViewModel() { - - private val _shotUiModel = MutableLiveData() - val shotUiModel: LiveData - get() = _shotUiModel - - private val _openLink = MutableLiveData>() - val openLink: LiveData> - get() = _openLink - - private val _shareShot = MutableLiveData>() - val shareShot: LiveData> - get() = _shareShot - - init { + //private val _shotUiModel = MutableLiveData() + val shotUiModel = liveData( + context = viewModelScope.coroutineContext + ) { val result = shotsRepository.getShot(shotId) if (result is Result.Success) { - _shotUiModel.value = result.data.toShotUiModel() - processUiModel(result.data) + emit(result.data.toShotUiModel()) + // this is for testing so that it can see two values :/. + yield() + emit(createShotUiModel(result.data)) } else { // TODO re-throw Error.exception once Loading state removed. throw IllegalStateException("Could not retrieve shot $shotId") } } - fun shareShotRequested() { - _shotUiModel.value?.let { model -> - viewModelScope.launch(dispatcherProvider.io) { + private val openLinkRequest = Channel(Channel.CONFLATED) + val openLink = liveData { + openLinkRequest.consume { + val model = shotUiModel.asFlow().first() + emit(Event(model.url)) + } + } + + private val shareShotRequest = Channel(Channel.CONFLATED) + + val shareShot = liveData { + shareShotRequest.consume { + withContext(dispatcherProvider.io) { + val model = shotUiModel.asFlow().first() val shareInfo = getShareShotInfo(model) - _shareShot.postValue(Event(shareInfo)) + emit(Event(shareInfo)) } } } + fun shareShotRequested() { + shareShotRequest.offer(Unit) + } + fun viewShotRequested() { - _shotUiModel.value?.let { model -> - _openLink.value = Event(model.url) - } + openLinkRequest.offer(Unit) } fun getAssistWebUrl(): String { @@ -87,11 +95,4 @@ class ShotViewModel @Inject constructor( fun getShotId(): Long { return shotUiModel.value?.id ?: -1L } - - private fun processUiModel(shot: Shot) { - viewModelScope.launch(dispatcherProvider.main) { - val uiModel = createShotUiModel(shot) - _shotUiModel.value = uiModel - } - } } diff --git a/dribbble/src/test/java/io/plaidapp/dribbble/ui/shot/ShotViewModelTest.kt b/dribbble/src/test/java/io/plaidapp/dribbble/ui/shot/ShotViewModelTest.kt index 24e903edf..b992015c2 100644 --- a/dribbble/src/test/java/io/plaidapp/dribbble/ui/shot/ShotViewModelTest.kt +++ b/dribbble/src/test/java/io/plaidapp/dribbble/ui/shot/ShotViewModelTest.kt @@ -17,6 +17,8 @@ package io.plaidapp.dribbble.ui.shot import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.LiveData +import androidx.lifecycle.asFlow import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.doReturn import com.nhaarman.mockitokotlin2.mock @@ -30,16 +32,22 @@ import io.plaidapp.dribbble.domain.GetShareShotInfoUseCase import io.plaidapp.dribbble.domain.ShareShotInfo import io.plaidapp.dribbble.testShot import io.plaidapp.dribbble.testShotUiModel -import io.plaidapp.test.shared.LiveDataTestUtil import io.plaidapp.test.shared.provideFakeCoroutinesDispatcherProvider +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Rule import org.junit.Test @@ -61,24 +69,35 @@ class ShotViewModelTest { } private val testCoroutineDispatcher = TestCoroutineDispatcher() + @Before + fun initMainDispatcher() { + Dispatchers.setMain(testCoroutineDispatcher) + } + @After fun tearDown() { testCoroutineDispatcher.cleanupTestCoroutines() } @Test - fun loadShot_existsInRepo() { + fun loadShot_existsInRepo() = testCoroutineDispatcher.runBlockingTest { // Given that the repo successfully returns the requested shot // When view model is constructed val viewModel = withViewModel() - // Then a shotUiModel is present - val result: ShotUiModel? = LiveDataTestUtil.getValue(viewModel.shotUiModel) + val result: ShotUiModel? = viewModel.shotUiModel.currentOrNextValue() assertNotNull(result) } + private suspend fun LiveData.currentOrNextValue() = try { + this.asFlow().first() + } catch (_: NoSuchElementException) { + null + } + + @Test(expected = IllegalStateException::class) - fun loadShot_notInRepo() { + fun loadShot_notInRepo() = testCoroutineDispatcher.runBlockingTest { // Given that the repo fails to return the requested shot whenever(repo.getShot(shotId)).thenReturn(Result.Error(Exception())) @@ -89,29 +108,28 @@ class ShotViewModelTest { createShotUiModel, getShareShotInfoUseCase, provideFakeCoroutinesDispatcherProvider() - ) + ).shotUiModel.currentOrNextValue() // Then it throws } @Test - fun shotClicked_sendsOpenLinkEvent() = runBlocking { + fun shotClicked_sendsOpenLinkEvent() = testCoroutineDispatcher.runBlockingTest { // Given a view model with a shot with a known URL val url = "https://dribbble.com/shots/2344334-Plaid-Product-Icon" val mockShotUiModel = mock { on { this.url } doReturn url } whenever(createShotUiModel.invoke(any())).thenReturn(mockShotUiModel) val viewModel = withViewModel(shot = testShot.copy(htmlUrl = url)) - // When there is a request to view the shot viewModel.viewShotRequested() // Then an event is emitted to open the given url - val openLinkEvent: Event? = LiveDataTestUtil.getValue(viewModel.openLink) + val openLinkEvent: Event? = viewModel.openLink.currentOrNextValue() assertNotNull(openLinkEvent) assertEquals(url, openLinkEvent!!.peek()) } @Test - fun shotShareClicked_sendsShareInfoEvent() { + fun shotShareClicked_sendsShareInfoEvent() = testCoroutineDispatcher.runBlockingTest { // Given a VM with a mocked use case which return a known Share Info object val expected = ShareShotInfo(mock(), "Title", "Share Text", "Mime") val viewModel = withViewModel(shareInfo = expected) @@ -120,18 +138,20 @@ class ShotViewModelTest { viewModel.shareShotRequested() // Then an event is raised with the expected info - val shareInfoEvent: Event? = LiveDataTestUtil.getValue(viewModel.shareShot) + val shareInfoEvent: Event? = viewModel.shareShot.currentOrNextValue() assertNotNull(shareInfoEvent) assertEquals(expected, shareInfoEvent!!.peek()) } @Test - fun getAssistWebUrl_returnsShotUrl() { + fun getAssistWebUrl_returnsShotUrl() = testCoroutineDispatcher.runBlockingTest{ // Given a view model with a shot with a known URL val url = "https://dribbble.com/shots/2344334-Plaid-Product-Icon" val mockShotUiModel = mock { on { this.url } doReturn url } - runBlocking { whenever(createShotUiModel.invoke(any())).thenReturn(mockShotUiModel) } + whenever(createShotUiModel.invoke(any())).thenReturn(mockShotUiModel) val viewModel = withViewModel(shot = testShot.copy(htmlUrl = url)) + // ensure it is observed + viewModel.shotUiModel.currentOrNextValue() // When there is a request to share the shot val assistWebUrl = viewModel.getAssistWebUrl() @@ -141,13 +161,14 @@ class ShotViewModelTest { } @Test - fun getShotId_returnsId() { + fun getShotId_returnsId() = testCoroutineDispatcher.runBlockingTest { // Given a view model with a shot with a known ID val id = 1234L val mockShotUiModel = mock { on { this.id } doReturn id } - runBlocking { whenever(createShotUiModel.invoke(any())).thenReturn(mockShotUiModel) } + whenever(createShotUiModel.invoke(any())).thenReturn(mockShotUiModel) val viewModel = withViewModel(shot = testShot.copy(id = id)) - + // ensure it is observed + viewModel.shotUiModel.currentOrNextValue() // When there is a request to share the shot val shotId = viewModel.getShotId() @@ -160,19 +181,17 @@ class ShotViewModelTest { // Given coroutines have not started yet and the View Model is created testCoroutineDispatcher.pauseDispatcher() val viewModel = withViewModel() - - // Then the fast result has been emitted - val fastResult: ShotUiModel? = LiveDataTestUtil.getValue(viewModel.shotUiModel) - assertNotNull(fastResult) - assertTrue(fastResult!!.formattedDescription.isEmpty()) - - // When the coroutine starts - testCoroutineDispatcher.resumeDispatcher() - - // Then the slow result has been emitted - val slowResult: ShotUiModel? = LiveDataTestUtil.getValue(viewModel.shotUiModel) - assertNotNull(slowResult) - assertTrue(slowResult!!.formattedDescription.isNotEmpty()) + val collection = async { + viewModel.shotUiModel.asFlow().take(2) + .onEach { + println("hello $it") + } + .toList() + } + testCoroutineDispatcher.runCurrent() + assertEquals(collection.await().map { + it.formattedDescription.isEmpty() + }, listOf(true, false)) } private fun withViewModel( @@ -190,8 +209,10 @@ class ShotViewModelTest { repo, createShotUiModel, getShareShotInfoUseCase, - provideFakeCoroutinesDispatcherProvider(testCoroutineDispatcher, - testCoroutineDispatcher, testCoroutineDispatcher) + provideFakeCoroutinesDispatcherProvider( + testCoroutineDispatcher, + testCoroutineDispatcher, testCoroutineDispatcher + ) ) } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b0acbdcd7..1c16bafbf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Mon Oct 07 18:04:50 PDT 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-all.zip From db56835ff370e5f6cc70d2af4a2f3a0d16eabd91 Mon Sep 17 00:00:00 2001 From: Yigit Boyar Date: Mon, 7 Oct 2019 20:26:46 -0700 Subject: [PATCH 2/3] use channels instead of live data for events --- dribbble/build.gradle | 2 +- .../plaidapp/dribbble/ui/shot/ShotActivity.kt | 17 +++++++- .../dribbble/ui/shot/ShotViewModel.kt | 40 +++++++++++-------- .../dribbble/ui/shot/ShotViewModelTest.kt | 13 +++--- 4 files changed, 46 insertions(+), 26 deletions(-) diff --git a/dribbble/build.gradle b/dribbble/build.gradle index 908930084..0fdb5cab6 100644 --- a/dribbble/build.gradle +++ b/dribbble/build.gradle @@ -56,7 +56,7 @@ android { dependencies { implementation project(':app') implementation project(':core') - + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${versions.lifecycle}" implementation "com.android.support:customtabs:${versions.supportLibrary}" implementation "com.android.support:palette-v7:${versions.supportLibrary}" implementation "com.github.bumptech.glide:recyclerview-integration:${versions.glide}" diff --git a/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotActivity.kt b/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotActivity.kt index 034a8d9e7..0e2a40220 100644 --- a/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotActivity.kt +++ b/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotActivity.kt @@ -30,6 +30,7 @@ import androidx.core.app.ShareCompat import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope import androidx.palette.graphics.Palette import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException @@ -119,13 +120,25 @@ class ShotActivity : AppCompatActivity() { largeAvatarSize = resources.getDimensionPixelSize(io.plaidapp.R.dimen.large_avatar_size) binding.viewModel = viewModel.also { vm -> - vm.openLink.observe(this, EventObserver { openLink(it) }) - vm.shareShot.observe(this, EventObserver { shareShot(it) }) vm.shotUiModel.observe(this, Observer { binding.uiModel = it }) } + lifecycleScope.launchWhenStarted { + for(event in viewModel.events) { + when(event) { + is ShotViewModel.UserAction.OpenLink -> { + openLink(event.url) + } + is ShotViewModel.UserAction.ShareShot -> { + shareShot(event.info) + } + } + } + } + + binding.shotLoadListener = shotLoadListener binding.apply { bodyScroll.setOnScrollChangeListener { _, _, scrollY, _, _ -> diff --git a/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotViewModel.kt b/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotViewModel.kt index ea347184c..8657af20e 100644 --- a/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotViewModel.kt +++ b/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotViewModel.kt @@ -23,14 +23,14 @@ import androidx.lifecycle.viewModelScope import io.plaidapp.core.data.CoroutinesDispatcherProvider import io.plaidapp.core.data.Result import io.plaidapp.core.dribbble.data.ShotsRepository -import io.plaidapp.core.util.event.Event import io.plaidapp.dribbble.domain.CreateShotUiModelUseCase import io.plaidapp.dribbble.domain.GetShareShotInfoUseCase +import io.plaidapp.dribbble.domain.ShareShotInfo +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ConflatedBroadcastChannel -import kotlinx.coroutines.channels.consume +import kotlinx.coroutines.channels.produce import kotlinx.coroutines.flow.first -import kotlinx.coroutines.withContext +import kotlinx.coroutines.selects.select import kotlinx.coroutines.yield import javax.inject.Inject @@ -44,7 +44,6 @@ class ShotViewModel @Inject constructor( private val getShareShotInfo: GetShareShotInfoUseCase, private val dispatcherProvider: CoroutinesDispatcherProvider ) : ViewModel() { - //private val _shotUiModel = MutableLiveData() val shotUiModel = liveData( context = viewModelScope.coroutineContext ) { @@ -61,22 +60,24 @@ class ShotViewModel @Inject constructor( } private val openLinkRequest = Channel(Channel.CONFLATED) - val openLink = liveData { - openLinkRequest.consume { - val model = shotUiModel.asFlow().first() - emit(Event(model.url)) - } - } private val shareShotRequest = Channel(Channel.CONFLATED) - val shareShot = liveData { - shareShotRequest.consume { - withContext(dispatcherProvider.io) { - val model = shotUiModel.asFlow().first() - val shareInfo = getShareShotInfo(model) - emit(Event(shareInfo)) + @ExperimentalCoroutinesApi + val events = viewModelScope.produce(dispatcherProvider.computation) { + while (true) { + val action = select { + shareShotRequest.onReceive { + val model = shotUiModel.asFlow().first() + val shareInfo = getShareShotInfo(model) + UserAction.ShareShot(shareInfo) + } + openLinkRequest.onReceive { + val model = shotUiModel.asFlow().first() + UserAction.OpenLink(model.url) + } } + send(action) } } @@ -95,4 +96,9 @@ class ShotViewModel @Inject constructor( fun getShotId(): Long { return shotUiModel.value?.id ?: -1L } + + sealed class UserAction { + class ShareShot(val info: ShareShotInfo) : UserAction() + class OpenLink(val url: String) : UserAction() + } } diff --git a/dribbble/src/test/java/io/plaidapp/dribbble/ui/shot/ShotViewModelTest.kt b/dribbble/src/test/java/io/plaidapp/dribbble/ui/shot/ShotViewModelTest.kt index b992015c2..e7f945e14 100644 --- a/dribbble/src/test/java/io/plaidapp/dribbble/ui/shot/ShotViewModelTest.kt +++ b/dribbble/src/test/java/io/plaidapp/dribbble/ui/shot/ShotViewModelTest.kt @@ -26,12 +26,13 @@ import com.nhaarman.mockitokotlin2.whenever import io.plaidapp.core.data.Result import io.plaidapp.core.dribbble.data.ShotsRepository import io.plaidapp.core.dribbble.data.api.model.Shot -import io.plaidapp.core.util.event.Event import io.plaidapp.dribbble.domain.CreateShotUiModelUseCase import io.plaidapp.dribbble.domain.GetShareShotInfoUseCase import io.plaidapp.dribbble.domain.ShareShotInfo import io.plaidapp.dribbble.testShot import io.plaidapp.dribbble.testShotUiModel +import io.plaidapp.dribbble.ui.shot.ShotViewModel.UserAction.OpenLink +import io.plaidapp.dribbble.ui.shot.ShotViewModel.UserAction.ShareShot import io.plaidapp.test.shared.provideFakeCoroutinesDispatcherProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -123,9 +124,9 @@ class ShotViewModelTest { viewModel.viewShotRequested() // Then an event is emitted to open the given url - val openLinkEvent: Event? = viewModel.openLink.currentOrNextValue() + val openLinkEvent = viewModel.events.poll() assertNotNull(openLinkEvent) - assertEquals(url, openLinkEvent!!.peek()) + assertEquals(url, (openLinkEvent as OpenLink).url) } @Test @@ -138,13 +139,13 @@ class ShotViewModelTest { viewModel.shareShotRequested() // Then an event is raised with the expected info - val shareInfoEvent: Event? = viewModel.shareShot.currentOrNextValue() + val shareInfoEvent = viewModel.events.poll() assertNotNull(shareInfoEvent) - assertEquals(expected, shareInfoEvent!!.peek()) + assertEquals(expected, (shareInfoEvent as ShareShot).info) } @Test - fun getAssistWebUrl_returnsShotUrl() = testCoroutineDispatcher.runBlockingTest{ + fun getAssistWebUrl_returnsShotUrl() = testCoroutineDispatcher.runBlockingTest { // Given a view model with a shot with a known URL val url = "https://dribbble.com/shots/2344334-Plaid-Product-Icon" val mockShotUiModel = mock { on { this.url } doReturn url } From c46c8d181073f25ea83ee89497f50a51eb11a369 Mon Sep 17 00:00:00 2001 From: Yigit Boyar Date: Mon, 7 Oct 2019 20:36:33 -0700 Subject: [PATCH 3/3] spotless --- .../main/java/io/plaidapp/dribbble/ui/shot/ShotActivity.kt | 6 ++---- .../java/io/plaidapp/dribbble/ui/shot/ShotViewModelTest.kt | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotActivity.kt b/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotActivity.kt index 0e2a40220..a20f14d83 100644 --- a/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotActivity.kt +++ b/dribbble/src/main/java/io/plaidapp/dribbble/ui/shot/ShotActivity.kt @@ -43,7 +43,6 @@ import io.plaidapp.core.util.ColorUtils import io.plaidapp.core.util.ViewUtils import io.plaidapp.core.util.customtabs.CustomTabActivityHelper import io.plaidapp.core.util.delegates.contentView -import io.plaidapp.core.util.event.EventObserver import io.plaidapp.core.util.glide.getBitmap import io.plaidapp.dribbble.R import io.plaidapp.dribbble.dagger.inject @@ -126,8 +125,8 @@ class ShotActivity : AppCompatActivity() { } lifecycleScope.launchWhenStarted { - for(event in viewModel.events) { - when(event) { + for (event in viewModel.events) { + when (event) { is ShotViewModel.UserAction.OpenLink -> { openLink(event.url) } @@ -138,7 +137,6 @@ class ShotActivity : AppCompatActivity() { } } - binding.shotLoadListener = shotLoadListener binding.apply { bodyScroll.setOnScrollChangeListener { _, _, scrollY, _, _ -> diff --git a/dribbble/src/test/java/io/plaidapp/dribbble/ui/shot/ShotViewModelTest.kt b/dribbble/src/test/java/io/plaidapp/dribbble/ui/shot/ShotViewModelTest.kt index e7f945e14..cf1e5ef07 100644 --- a/dribbble/src/test/java/io/plaidapp/dribbble/ui/shot/ShotViewModelTest.kt +++ b/dribbble/src/test/java/io/plaidapp/dribbble/ui/shot/ShotViewModelTest.kt @@ -96,7 +96,6 @@ class ShotViewModelTest { null } - @Test(expected = IllegalStateException::class) fun loadShot_notInRepo() = testCoroutineDispatcher.runBlockingTest { // Given that the repo fails to return the requested shot