diff options
author | Allan Wang <me@allanwang.ca> | 2018-12-28 20:19:18 -0500 |
---|---|---|
committer | Allan Wang <me@allanwang.ca> | 2018-12-28 20:19:18 -0500 |
commit | e5e83736d3feff8ac9cc4ae38fad0fa827a6b21d (patch) | |
tree | 248e2886613e6eb30ae2e4a1196da2a484371aee | |
parent | c970d707105bbb614a630a1ac7340d6044e8fee1 (diff) | |
download | frost-e5e83736d3feff8ac9cc4ae38fad0fa827a6b21d.tar.gz frost-e5e83736d3feff8ac9cc4ae38fad0fa827a6b21d.tar.bz2 frost-e5e83736d3feff8ac9cc4ae38fad0fa827a6b21d.zip |
Switch remaining primary observables
17 files changed, 150 insertions, 95 deletions
diff --git a/app/build.gradle b/app/build.gradle index 75ffed98..84d2d694 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -101,7 +101,7 @@ android { resValue "string", "frost_web", "Frost Web Debug" ext.enableBugsnag = false - kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"] + kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental", "-XXLanguage:+InlineClasses"] } releaseTest { minifyEnabled true diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt index a110071c..283477d7 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt @@ -176,7 +176,8 @@ class AboutActivity : AboutActivityBase(null, { } val set = ConstraintSet() set.clone(container) - set.createHorizontalChain(ConstraintSet.PARENT_ID, + set.createHorizontalChain( + ConstraintSet.PARENT_ID, ConstraintSet.LEFT, ConstraintSet.PARENT_ID, ConstraintSet.RIGHT, diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt index 20b5727f..78f6bfb9 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -341,7 +341,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, private fun refreshAll() { L.d { "Refresh all" } - fragmentSubject.onNext(REQUEST_REFRESH) + fragmentChannel.offer(REQUEST_REFRESH) } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -424,9 +424,9 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, /* * These results can be stacked */ - if (resultCode and REQUEST_REFRESH > 0) fragmentSubject.onNext(REQUEST_REFRESH) + if (resultCode and REQUEST_REFRESH > 0) fragmentChannel.offer(REQUEST_REFRESH) if (resultCode and REQUEST_NAV > 0) frostNavigationBar() - if (resultCode and REQUEST_TEXT_ZOOM > 0) fragmentSubject.onNext(REQUEST_TEXT_ZOOM) + if (resultCode and REQUEST_TEXT_ZOOM > 0) fragmentChannel.offer(REQUEST_TEXT_ZOOM) if (resultCode and REQUEST_SEARCH > 0) invalidateOptionsMenu() } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt index d03c6496..cc375800 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt @@ -24,12 +24,15 @@ import com.pitchedapps.frost.views.BadgedIcon import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import io.reactivex.subjects.PublishSubject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.BroadcastChannel import org.jsoup.Jsoup import java.util.concurrent.TimeUnit +@UseExperimental(ExperimentalCoroutinesApi::class) class MainActivity : BaseMainActivity() { - override val fragmentSubject = PublishSubject.create<Int>() + override val fragmentChannel = BroadcastChannel<Int>(10) var lastPosition = -1 val headerBadgeObservable = PublishSubject.create<String>() @@ -43,8 +46,8 @@ class MainActivity : BaseMainActivity() { override fun onPageSelected(position: Int) { super.onPageSelected(position) if (lastPosition == position) return - if (lastPosition != -1) fragmentSubject.onNext(-(lastPosition + 1)) - fragmentSubject.onNext(position) + if (lastPosition != -1) fragmentChannel.offer(-(lastPosition + 1)) + fragmentChannel.offer(position) lastPosition = position } @@ -62,7 +65,7 @@ class MainActivity : BaseMainActivity() { } } }) - viewPager.post { fragmentSubject.onNext(0); lastPosition = 0 } //trigger hook so title is set + viewPager.post { fragmentChannel.offer(0); lastPosition = 0 } //trigger hook so title is set } private fun setupTabs() { @@ -101,8 +104,9 @@ class MainActivity : BaseMainActivity() { } }.disposeOnDestroy() adapter.pages.forEach { - tabs.addTab(tabs.newTab() - .setCustomView(BadgedIcon(this).apply { iicon = it.icon }) + tabs.addTab( + tabs.newTab() + .setCustomView(BadgedIcon(this).apply { iicon = it.icon }) ) } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt index 816524ba..19a1109f 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt @@ -67,13 +67,12 @@ import com.pitchedapps.frost.utils.Showcase import com.pitchedapps.frost.utils.frostSnackbar import com.pitchedapps.frost.utils.materialDialogThemed import com.pitchedapps.frost.utils.setFrostColors +import com.pitchedapps.frost.utils.uniqueOnly import com.pitchedapps.frost.views.FrostContentWeb import com.pitchedapps.frost.views.FrostVideoViewer import com.pitchedapps.frost.views.FrostWebView -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.HttpUrl @@ -91,6 +90,7 @@ import okhttp3.HttpUrl * Used by notifications. Unlike the other overlays, this runs as a singleInstance * Going back will bring you back to the previous app */ +@UseExperimental(ExperimentalCoroutinesApi::class) class FrostWebActivity : WebOverlayActivityBase(false) { override fun onCreate(savedInstanceState: Bundle?) { @@ -151,6 +151,7 @@ class WebOverlayBasicActivity : WebOverlayActivityBase(true) class WebOverlayActivity : WebOverlayActivityBase(false) @SuppressLint("Registered") +@UseExperimental(ExperimentalCoroutinesApi::class) open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseActivity(), ActivityContract, FrostContentContainer, VideoViewHolder, FileChooserContract by FileChooserDelegate() { @@ -203,7 +204,7 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseAc content.bind(this) - val titleReceiver = content.titleChannel.openSubscription() + val titleReceiver = content.titleChannel.openSubscription().uniqueOnly(this) launch { for (t in titleReceiver) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt index 2ce83871..9a70436e 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt @@ -19,7 +19,7 @@ package com.pitchedapps.frost.contracts import com.mikepenz.iconics.typeface.IIcon import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.fragments.BaseFragment -import io.reactivex.subjects.PublishSubject +import kotlinx.coroutines.channels.BroadcastChannel /** * All the contracts for [MainActivity] @@ -27,7 +27,7 @@ import io.reactivex.subjects.PublishSubject interface ActivityContract : FileChooserActivityContract interface MainActivityContract : ActivityContract, MainFabContract { - val fragmentSubject: PublishSubject<Int> + val fragmentChannel: BroadcastChannel<Int> fun setTitle(res: Int) fun setTitle(text: CharSequence) /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt index 0f8c49d3..8a6e57af 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt @@ -18,12 +18,9 @@ package com.pitchedapps.frost.contracts import android.view.View import com.pitchedapps.frost.facebook.FbItem -import io.reactivex.subjects.BehaviorSubject -import io.reactivex.subjects.PublishSubject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.channels.Channel /** * Created by Allan Wang on 20/12/17. diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt index 2c46edbc..c5880840 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt @@ -39,12 +39,13 @@ import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.REQUEST_REFRESH import com.pitchedapps.frost.utils.REQUEST_TEXT_ZOOM import com.pitchedapps.frost.utils.frostEvent -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlin.coroutines.CoroutineContext /** @@ -87,7 +88,7 @@ abstract class BaseFragment : Fragment(), CoroutineScope, FragmentContract, Dyna override var valid: Boolean get() = arguments!!.getBoolean(ARG_VALID, true) set(value) { - if (value || this is WebFragment) return + if (!isActive || value || this is WebFragment) return arguments!!.putBoolean(ARG_VALID, value) L.e { "Invalidating position $position" } frostEvent( @@ -98,7 +99,7 @@ abstract class BaseFragment : Fragment(), CoroutineScope, FragmentContract, Dyna } override var firstLoad: Boolean = true - private var activityDisposable: Disposable? = null + private var activityReceiver: ReceiveChannel<Int>? = null private var onCreateRunnable: ((FragmentContract) -> Unit)? = null override var content: FrostContentParent? = null @@ -154,29 +155,34 @@ abstract class BaseFragment : Fragment(), CoroutineScope, FragmentContract, Dyna (context as? MainActivityContract)?.setTitle(title) } - override fun attachMainObservable(contract: MainActivityContract): Disposable = - contract.fragmentSubject.observeOn(AndroidSchedulers.mainThread()).subscribe { - when (it) { - REQUEST_REFRESH -> { - core?.apply { - clearHistory() - firstLoad = true - firstLoadRequest() + override fun attachMainObservable(contract: MainActivityContract): ReceiveChannel<Int> { + val receiver = contract.fragmentChannel.openSubscription() + launch { + for (flag in receiver) { + when (flag) { + REQUEST_REFRESH -> { + core?.apply { + clearHistory() + firstLoad = true + firstLoadRequest() + } + } + position -> { + contract.setTitle(baseEnum.titleId) + updateFab(contract) + core?.active = true + } + -(position + 1) -> { + core?.active = false + } + REQUEST_TEXT_ZOOM -> { + reloadTextSize() } - } - position -> { - contract.setTitle(baseEnum.titleId) - updateFab(contract) - core?.active = true - } - -(position + 1) -> { - core?.active = false - } - REQUEST_TEXT_ZOOM -> { - reloadTextSize() } } } + return receiver + } override fun updateFab(contract: MainFabContract) { contract.hideFab() // default @@ -195,14 +201,14 @@ abstract class BaseFragment : Fragment(), CoroutineScope, FragmentContract, Dyna } override fun detachMainObservable() { - activityDisposable?.dispose() + activityReceiver?.cancel() } override fun onAttach(context: Context) { super.onAttach(context) detachMainObservable() if (context is MainActivityContract) - activityDisposable = attachMainObservable(context) + activityReceiver = attachMainObservable(context) } override fun onDetach() { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt index eaef17e5..10c612c5 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt @@ -22,7 +22,7 @@ import com.pitchedapps.frost.contracts.FrostContentParent import com.pitchedapps.frost.contracts.MainActivityContract import com.pitchedapps.frost.contracts.MainFabContract import com.pitchedapps.frost.views.FrostRecyclerView -import io.reactivex.disposables.Disposable +import kotlinx.coroutines.channels.ReceiveChannel /** * Created by Allan Wang on 2017-11-07. @@ -34,8 +34,9 @@ interface FragmentContract : FrostContentContainer { /** * Defines whether the fragment is valid in the viewpager - * Or if it needs to be recreated - * May be called from any thread to toggle status + * or if it needs to be recreated + * May be called from any thread to toggle status. + * Note that calls beyond the fragment lifecycle will be ignored */ var valid: Boolean @@ -75,9 +76,10 @@ interface FragmentContract : FrostContentContainer { /** * Call whenever a fragment is attached so that it may listen - * to activity emissions + * to activity emissions. + * Returns a means of closing the listener, which can be called from [detachMainObservable] */ - fun attachMainObservable(contract: MainActivityContract): Disposable + fun attachMainObservable(contract: MainActivityContract): ReceiveChannel<Int> /** * Call when fragment is detached so that any existing diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt index ddb9fde6..c03ac0e2 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt @@ -21,7 +21,6 @@ import com.mikepenz.fastadapter.FastAdapter import com.mikepenz.fastadapter.IItem import com.mikepenz.fastadapter.adapters.ItemAdapter import com.mikepenz.fastadapter.adapters.ModelAdapter -import com.mikepenz.fastadapter_extensions.items.ProgressItem import com.pitchedapps.frost.R import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.parsers.FrostParser @@ -142,31 +141,3 @@ abstract class FrostParserFragment<T : Any, Item : IItem<*, *>> : RecyclerFragme return@withContext items } } - -//abstract class PagedRecyclerFragment<T : Any, Item : IItem<*, *>> : RecyclerFragment<T, Item>() { -// -// var allowPagedLoading = true -// -// val footerAdapter = ItemAdapter<FrostProgress>() -// -// val footerScrollListener = object : EndlessRecyclerOnScrollListener(footerAdapter) { -// override fun onLoadMore(currentPage: Int) { -// TODO("not implemented") -// -// } -// -// } -// -// override fun getAdapter() = fastAdapter(adapter, footerAdapter) -// -// override fun bindImpl(recyclerView: FrostRecyclerView) { -// recyclerView.addOnScrollListener(footerScrollListener) -// } -// -// override fun reload(progress: (Int) -> Unit, callback: (Boolean) -> Unit) { -// footerScrollListener. -// super.reload(progress, callback) -// } -//} - -class FrostProgress : ProgressItem() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt index 45cf2290..f7ed9937 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt @@ -37,7 +37,6 @@ import com.pitchedapps.frost.iitems.NotificationIItem import com.pitchedapps.frost.utils.frostJsoup import com.pitchedapps.frost.views.FrostRecyclerView import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/KotlinUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/KotlinUtils.kt new file mode 100644 index 00000000..320aeb69 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/KotlinUtils.kt @@ -0,0 +1,20 @@ +package com.pitchedapps.frost.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.produce +import kotlinx.coroutines.isActive + +@UseExperimental(ExperimentalCoroutinesApi::class) +fun <T> ReceiveChannel<T>.uniqueOnly(scope: CoroutineScope): ReceiveChannel<T> = scope.produce { + var previous: T? = null + for (current in this@uniqueOnly) { + if (!scope.isActive) { + cancel() + } else if (previous != current) { + previous = current + send(current) + } + } +}
\ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt index 38591d38..9619eecc 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt @@ -40,8 +40,6 @@ import com.pitchedapps.frost.facebook.WEB_LOAD_DELAY import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import io.reactivex.disposables.Disposable -import io.reactivex.subjects.BehaviorSubject -import io.reactivex.subjects.PublishSubject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt index 2ba78c5e..f7cb2214 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt @@ -23,13 +23,11 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import ca.allanwang.kau.utils.circularReveal import ca.allanwang.kau.utils.fadeOut -import com.pitchedapps.frost.R.string.reload import com.pitchedapps.frost.contracts.FrostContentContainer import com.pitchedapps.frost.contracts.FrostContentCore import com.pitchedapps.frost.contracts.FrostContentParent import com.pitchedapps.frost.fragments.RecyclerContentContract import com.pitchedapps.frost.utils.Prefs -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt index f3538ec3..da90e7e5 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt @@ -29,9 +29,6 @@ import com.pitchedapps.frost.contracts.ActivityContract import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.frostSnackbar import com.pitchedapps.frost.views.FrostWebView -import io.reactivex.subjects.BehaviorSubject -import io.reactivex.subjects.Subject -import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.SendChannel /** @@ -47,7 +44,6 @@ class FrostChromeClient(web: FrostWebView) : WebChromeClient() { private val progress: SendChannel<Int> = web.parent.progressChannel private val title: SendChannel<String> = web.parent.titleChannel - private var prevTitle: String? = null private val activity = (web.context as? ActivityContract) private val context = web.context!! @@ -58,8 +54,7 @@ class FrostChromeClient(web: FrostWebView) : WebChromeClient() { override fun onReceivedTitle(view: WebView, title: String) { super.onReceivedTitle(view, title) - if (title.startsWith("http") || prevTitle == title) return - prevTitle = title + if (title.startsWith("http")) return this.title.offer(title) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt index 5a137c44..cb212b0a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt @@ -40,7 +40,6 @@ import com.pitchedapps.frost.utils.isIndirectImageUrl import com.pitchedapps.frost.utils.launchImageActivity import com.pitchedapps.frost.utils.resolveActivityForUri import com.pitchedapps.frost.views.FrostWebView -import io.reactivex.subjects.Subject import kotlinx.coroutines.channels.SendChannel import org.jetbrains.anko.withAlpha diff --git a/app/src/test/kotlin/com/pitchedapps/frost/views/FrostContentViewAsyncTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt index 13c40aa3..f930e529 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/views/FrostContentViewAsyncTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/utils/CoroutineTest.kt @@ -1,12 +1,14 @@ -package com.pitchedapps.frost.views +package com.pitchedapps.frost.utils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.async import kotlinx.coroutines.channels.BroadcastChannel +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.count +import kotlinx.coroutines.delay import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -19,10 +21,10 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue /** - * Collection of tests around the view thread logic + * Collection of tests around coroutines */ @UseExperimental(ExperimentalCoroutinesApi::class) -class FrostContentViewAsyncTest { +class CoroutineTest { /** * Hooks onto the refresh channel for one true -> false cycle. @@ -39,7 +41,7 @@ class FrostContentViewAsyncTest { } } - private suspend fun <T> listen(channel: ReceiveChannel<T>, shouldEnd: (T) -> Boolean = { false }): List<T> = + private suspend fun <T> listen(channel: ReceiveChannel<T>, shouldEnd: suspend (T) -> Boolean = { false }): List<T> = withContext(Dispatchers.IO) { val data = mutableListOf<T>() for (c in channel) { @@ -127,4 +129,66 @@ class FrostContentViewAsyncTest { assertEquals(4, receiver2.count(), "Not all events received") } } + + /** + * Not a true throttle, but for things like fetching header badges, we want to avoid simultaneous fetches. + * As a result, I want to test that the usage of offer along with a rendezvous channel will work as I expect. + * Events should be consumed when there is no pending consumer on previous elements. + */ + @Test + fun throttledChannel() { + val channel = Channel<Int>(Channel.RENDEZVOUS) + runBlocking { + val deferred = async { + listen(channel) { + // Throttle consumer + delay(10) + return@listen false + } + } + (0..100).forEach { + channel.offer(it) + delay(1) + } + channel.close() + val received = deferred.await() + assertTrue( + received.size < 20, + "Received data should be throttled; expected that around 1/10th of all events are consumed" + ) + println(received) + } + } + + @Test + fun uniqueOnly() { + val channel = BroadcastChannel<Int>(100) + runBlocking { + val fullReceiver = channel.openSubscription() + val uniqueReceiver = channel.openSubscription().uniqueOnly(this) + + val fullDeferred = async { listen(fullReceiver) } + val uniqueDeferred = async { listen(uniqueReceiver) } + + listOf(0, 1, 2, 3, 3, 3, 4, 3, 5, 5, 1).forEach { + channel.offer(it) + } + channel.close() + + val fullData = fullDeferred.await() + val uniqueData = uniqueDeferred.await() + + assertEquals( + listOf(0, 1, 2, 3, 3, 3, 4, 3, 5, 5, 1), + fullData, + "Full receiver should get all channel events" + ) + assertEquals( + listOf(0, 1, 2, 3, 4, 3, 5, 1), + uniqueData, + "Unique receiver should not have two consecutive events that are equal" + ) + + } + } }
\ No newline at end of file |