mirror of
https://github.com/AllanWang/Frost-for-Facebook.git
synced 2024-11-10 04:52:38 +01:00
Begin replacing observables with channels
This commit is contained in:
parent
7d85262ada
commit
e6dcbd7b32
@ -100,6 +100,8 @@ android {
|
||||
resValue "string", "frost_name", "Frost Debug"
|
||||
resValue "string", "frost_web", "Frost Web Debug"
|
||||
ext.enableBugsnag = false
|
||||
|
||||
kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"]
|
||||
}
|
||||
releaseTest {
|
||||
minifyEnabled true
|
||||
@ -141,6 +143,7 @@ android {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
@ -72,6 +72,7 @@ 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 okhttp3.HttpUrl
|
||||
|
||||
/**
|
||||
@ -181,7 +182,6 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseAc
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
setFrameContentView(R.layout.activity_web_overlay)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(true)
|
||||
|
@ -20,6 +20,10 @@ 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.
|
||||
@ -29,7 +33,7 @@ import io.reactivex.subjects.PublishSubject
|
||||
* Contract for the underlying parent,
|
||||
* binds to activities & fragments
|
||||
*/
|
||||
interface FrostContentContainer {
|
||||
interface FrostContentContainer : CoroutineScope {
|
||||
|
||||
val baseUrl: String
|
||||
|
||||
@ -45,8 +49,11 @@ interface FrostContentContainer {
|
||||
* Contract for components shared among
|
||||
* all content providers
|
||||
*/
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
interface FrostContentParent : DynamicUiContract {
|
||||
|
||||
val scope: CoroutineScope
|
||||
|
||||
val core: FrostContentCore
|
||||
|
||||
/**
|
||||
@ -54,16 +61,22 @@ interface FrostContentParent : DynamicUiContract {
|
||||
*/
|
||||
val refreshObservable: PublishSubject<Boolean>
|
||||
|
||||
val refreshChannel: BroadcastChannel<Boolean>
|
||||
|
||||
/**
|
||||
* Observable to get data on refresh progress, with range [0, 100]
|
||||
*/
|
||||
val progressObservable: PublishSubject<Int>
|
||||
|
||||
val progressChannel: BroadcastChannel<Int>
|
||||
|
||||
/**
|
||||
* Observable to get new title data (unique values only)
|
||||
*/
|
||||
val titleObservable: BehaviorSubject<String>
|
||||
|
||||
val titleChannel: BroadcastChannel<String>
|
||||
|
||||
var baseUrl: String
|
||||
|
||||
var baseEnum: FbItem?
|
||||
@ -106,6 +119,9 @@ interface FrostContentParent : DynamicUiContract {
|
||||
*/
|
||||
interface FrostContentCore : DynamicUiContract {
|
||||
|
||||
val scope: CoroutineScope
|
||||
get() = parent.scope
|
||||
|
||||
/**
|
||||
* Reference to parent
|
||||
* Bound through calling [FrostContentParent.bind]
|
||||
|
@ -23,6 +23,8 @@ 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
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-11-07.
|
||||
@ -102,8 +104,9 @@ interface RecyclerContentContract {
|
||||
|
||||
/**
|
||||
* Completely handle data reloading
|
||||
* Optional progress emission update
|
||||
* Callback returns [true] for success, [false] otherwise
|
||||
* The progress function allows optional emission of progress values (between 0 and 100).
|
||||
* This can be called from any thread.
|
||||
* Returns [true] for success, [false] otherwise
|
||||
*/
|
||||
fun reload(progress: (Int) -> Unit, callback: (Boolean) -> Unit)
|
||||
suspend fun reload(progress: (Int) -> Unit): Boolean
|
||||
}
|
||||
|
@ -29,16 +29,18 @@ import com.pitchedapps.frost.facebook.parsers.ParseResponse
|
||||
import com.pitchedapps.frost.utils.L
|
||||
import com.pitchedapps.frost.utils.frostJsoup
|
||||
import com.pitchedapps.frost.views.FrostRecyclerView
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 27/12/17.
|
||||
*/
|
||||
abstract class RecyclerFragment : BaseFragment(), RecyclerContentContract {
|
||||
abstract class RecyclerFragment<T, Item : IItem<*, *>> : BaseFragment(), RecyclerContentContract {
|
||||
|
||||
override val layoutRes: Int = R.layout.view_content_recycler
|
||||
|
||||
abstract val adapter: ModelAdapter<T, Item>
|
||||
|
||||
override fun firstLoadRequest() {
|
||||
val core = core ?: return
|
||||
if (firstLoad) {
|
||||
@ -47,23 +49,30 @@ abstract class RecyclerFragment : BaseFragment(), RecyclerContentContract {
|
||||
}
|
||||
}
|
||||
|
||||
final override fun reload(progress: (Int) -> Unit, callback: (Boolean) -> Unit) {
|
||||
reloadImpl(progress) {
|
||||
if (it)
|
||||
callback(it)
|
||||
else
|
||||
valid = false
|
||||
final override suspend fun reload(progress: (Int) -> Unit): Boolean {
|
||||
val data = try {
|
||||
reloadImpl(progress)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
if (data == null) {
|
||||
valid = false
|
||||
return false
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.setNewList(data)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
protected abstract fun reloadImpl(progress: (Int) -> Unit, callback: (Boolean) -> Unit)
|
||||
protected abstract suspend fun reloadImpl(progress: (Int) -> Unit): List<T>?
|
||||
}
|
||||
|
||||
abstract class GenericRecyclerFragment<T, Item : IItem<*, *>> : RecyclerFragment() {
|
||||
abstract class GenericRecyclerFragment<T, Item : IItem<*, *>> : RecyclerFragment<T, Item>() {
|
||||
|
||||
abstract fun mapper(data: T): Item
|
||||
|
||||
val adapter: ModelAdapter<T, Item> = ModelAdapter { this.mapper(it) }
|
||||
override val adapter: ModelAdapter<T, Item> = ModelAdapter { this.mapper(it) }
|
||||
|
||||
final override fun bind(recyclerView: FrostRecyclerView) {
|
||||
recyclerView.adapter = getAdapter()
|
||||
@ -83,7 +92,7 @@ abstract class GenericRecyclerFragment<T, Item : IItem<*, *>> : RecyclerFragment
|
||||
open fun getAdapter(): FastAdapter<IItem<*, *>> = fastAdapter(this.adapter)
|
||||
}
|
||||
|
||||
abstract class FrostParserFragment<T : Any, Item : IItem<*, *>> : RecyclerFragment() {
|
||||
abstract class FrostParserFragment<T : Any, Item : IItem<*, *>> : RecyclerFragment<Item, Item>() {
|
||||
|
||||
/**
|
||||
* The parser to make this all happen
|
||||
@ -94,7 +103,7 @@ abstract class FrostParserFragment<T : Any, Item : IItem<*, *>> : RecyclerFragme
|
||||
|
||||
abstract fun toItems(response: ParseResponse<T>): List<Item>
|
||||
|
||||
val adapter: ItemAdapter<Item> = ItemAdapter()
|
||||
override val adapter: ItemAdapter<Item> = ItemAdapter()
|
||||
|
||||
final override fun bind(recyclerView: FrostRecyclerView) {
|
||||
recyclerView.adapter = getAdapter()
|
||||
@ -113,23 +122,20 @@ abstract class FrostParserFragment<T : Any, Item : IItem<*, *>> : RecyclerFragme
|
||||
*/
|
||||
open fun getAdapter(): FastAdapter<IItem<*, *>> = fastAdapter(this.adapter)
|
||||
|
||||
override fun reloadImpl(progress: (Int) -> Unit, callback: (Boolean) -> Unit) {
|
||||
doAsync {
|
||||
progress(10)
|
||||
val cookie = FbCookie.webCookie
|
||||
val doc = getDoc(cookie)
|
||||
progress(60)
|
||||
val response = parser.parse(cookie, doc)
|
||||
if (response == null) {
|
||||
L.i { "RecyclerFragment failed for ${baseEnum.name}" }
|
||||
return@doAsync callback(false)
|
||||
}
|
||||
progress(80)
|
||||
val items = toItems(response)
|
||||
progress(97)
|
||||
uiThread { adapter.setNewList(items) }
|
||||
callback(true)
|
||||
override suspend fun reloadImpl(progress: (Int) -> Unit): List<Item>? = withContext(Dispatchers.IO) {
|
||||
progress(10)
|
||||
val cookie = FbCookie.webCookie
|
||||
val doc = getDoc(cookie)
|
||||
progress(60)
|
||||
val response = parser.parse(cookie, doc)
|
||||
if (response == null) {
|
||||
L.i { "RecyclerFragment failed for ${baseEnum.name}" }
|
||||
return@withContext null
|
||||
}
|
||||
progress(80)
|
||||
val items = toItems(response)
|
||||
progress(97)
|
||||
return@withContext items
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ import com.pitchedapps.frost.facebook.requests.MenuFooterItem
|
||||
import com.pitchedapps.frost.facebook.requests.MenuHeader
|
||||
import com.pitchedapps.frost.facebook.requests.MenuItem
|
||||
import com.pitchedapps.frost.facebook.requests.MenuItemData
|
||||
import com.pitchedapps.frost.facebook.requests.fbRequest
|
||||
import com.pitchedapps.frost.facebook.requests.fbAuth
|
||||
import com.pitchedapps.frost.facebook.requests.getMenuData
|
||||
import com.pitchedapps.frost.iitems.ClickableIItemContract
|
||||
import com.pitchedapps.frost.iitems.MenuContentIItem
|
||||
@ -36,8 +36,9 @@ import com.pitchedapps.frost.iitems.MenuHeaderIItem
|
||||
import com.pitchedapps.frost.iitems.NotificationIItem
|
||||
import com.pitchedapps.frost.utils.frostJsoup
|
||||
import com.pitchedapps.frost.views.FrostRecyclerView
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 27/12/17.
|
||||
@ -71,20 +72,16 @@ class MenuFragment : GenericRecyclerFragment<MenuItemData, IItem<*, *>>() {
|
||||
ClickableIItemContract.bindEvents(adapter)
|
||||
}
|
||||
|
||||
override fun reloadImpl(progress: (Int) -> Unit, callback: (Boolean) -> Unit) {
|
||||
doAsync {
|
||||
val cookie = FbCookie.webCookie
|
||||
progress(10)
|
||||
cookie.fbRequest({ callback(false) }) {
|
||||
progress(30)
|
||||
val data = getMenuData().invoke() ?: return@fbRequest callback(false)
|
||||
if (data.data.isEmpty()) return@fbRequest callback(false)
|
||||
progress(70)
|
||||
val items = data.flatMapValid()
|
||||
progress(90)
|
||||
uiThread { adapter.add(items) }
|
||||
callback(true)
|
||||
}
|
||||
}
|
||||
override suspend fun reloadImpl(progress: (Int) -> Unit): List<MenuItemData>? = withContext(Dispatchers.IO) {
|
||||
val cookie = FbCookie.webCookie ?: return@withContext null
|
||||
progress(10)
|
||||
val auth = fbAuth.fetch(cookie)
|
||||
progress(30)
|
||||
val data = auth.getMenuData().invoke() ?: return@withContext null
|
||||
if (data.data.isEmpty()) return@withContext null
|
||||
progress(70)
|
||||
val items = data.flatMapValid()
|
||||
progress(90)
|
||||
return@withContext items
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +45,13 @@ import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.rxkotlin.addTo
|
||||
import io.reactivex.subjects.BehaviorSubject
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FrostContentWeb @JvmOverloads constructor(
|
||||
context: Context,
|
||||
@ -66,6 +73,7 @@ class FrostContentRecycler @JvmOverloads constructor(
|
||||
override val layoutRes: Int = R.layout.view_content_base_recycler
|
||||
}
|
||||
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
abstract class FrostContentView<out T> @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@ -85,7 +93,11 @@ abstract class FrostContentView<out T> @JvmOverloads constructor(
|
||||
override val refreshObservable: PublishSubject<Boolean> = PublishSubject.create()
|
||||
override val titleObservable: BehaviorSubject<String> = BehaviorSubject.create()
|
||||
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
override val refreshChannel: BroadcastChannel<Boolean> = BroadcastChannel(Channel.UNLIMITED)
|
||||
override val progressChannel: BroadcastChannel<Int> = BroadcastChannel(Channel.UNLIMITED)
|
||||
override val titleChannel: BroadcastChannel<String> = BroadcastChannel(Channel.UNLIMITED)
|
||||
|
||||
override lateinit var scope: CoroutineScope
|
||||
|
||||
override lateinit var baseUrl: String
|
||||
override var baseEnum: FbItem? = null
|
||||
@ -107,24 +119,6 @@ abstract class FrostContentView<out T> @JvmOverloads constructor(
|
||||
protected fun init() {
|
||||
inflate(context, layoutRes, this)
|
||||
coreView.parent = this
|
||||
|
||||
// bind observables
|
||||
progressObservable.observeOn(AndroidSchedulers.mainThread()).subscribe {
|
||||
progress.invisibleIf(it == 100)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
progress.setProgress(it, true)
|
||||
else
|
||||
progress.progress = it
|
||||
}.addTo(compositeDisposable)
|
||||
|
||||
refreshObservable
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
refresh.isRefreshing = it
|
||||
refresh.isEnabled = true
|
||||
}.addTo(compositeDisposable)
|
||||
refresh.setOnRefreshListener { coreView.reload(true) }
|
||||
|
||||
reloadThemeSelf()
|
||||
}
|
||||
|
||||
@ -132,7 +126,34 @@ abstract class FrostContentView<out T> @JvmOverloads constructor(
|
||||
baseUrl = container.baseUrl
|
||||
baseEnum = container.baseEnum
|
||||
init()
|
||||
scope = container
|
||||
core.bind(container)
|
||||
refresh.setOnRefreshListener {
|
||||
with(coreView) {
|
||||
reload(true)
|
||||
}
|
||||
}
|
||||
scope.launch(Dispatchers.Default) {
|
||||
launch {
|
||||
for (r in refreshChannel.openSubscription()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
refresh.isRefreshing = r
|
||||
refresh.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
for (p in progressChannel.openSubscription()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
progress.invisibleIf(p == 100)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
progress.setProgress(p, true)
|
||||
else
|
||||
progress.progress = p
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun reloadTheme() {
|
||||
@ -155,11 +176,10 @@ abstract class FrostContentView<out T> @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
titleObservable.onComplete()
|
||||
progressObservable.onComplete()
|
||||
refreshObservable.onComplete()
|
||||
titleChannel.close()
|
||||
progressChannel.close()
|
||||
refreshChannel.close()
|
||||
core.destroy()
|
||||
compositeDisposable.dispose()
|
||||
}
|
||||
|
||||
private var dispose: Disposable? = null
|
||||
|
@ -23,11 +23,14 @@ 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
|
||||
|
||||
/**
|
||||
* Created by Allan Wang on 2017-05-29.
|
||||
@ -69,11 +72,12 @@ class FrostRecyclerView @JvmOverloads constructor(
|
||||
|
||||
override fun reloadBase(animate: Boolean) {
|
||||
if (Prefs.animate) fadeOut(onFinish = onReloadClear)
|
||||
parent.refreshObservable.onNext(true)
|
||||
recyclerContract.reload({ parent.progressObservable.onNext(it) }) {
|
||||
parent.progressObservable.onNext(100)
|
||||
parent.refreshObservable.onNext(false)
|
||||
if (Prefs.animate) post { circularReveal() }
|
||||
scope.launch {
|
||||
parent.refreshChannel.send(true)
|
||||
val loaded = recyclerContract.reload { parent.progressChannel.offer(it) }
|
||||
parent.progressChannel.send(100)
|
||||
parent.refreshChannel.send(false)
|
||||
if (Prefs.animate) circularReveal()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,8 +43,9 @@ import io.reactivex.subjects.Subject
|
||||
*/
|
||||
class FrostChromeClient(web: FrostWebView) : WebChromeClient() {
|
||||
|
||||
private val progress: Subject<Int> = web.parent.progressObservable
|
||||
private val title: BehaviorSubject<String> = web.parent.titleObservable
|
||||
private val progress = web.parent.progressChannel
|
||||
private val title = web.parent.titleChannel
|
||||
private var prevTitle: String? = null
|
||||
private val activity = (web.context as? ActivityContract)
|
||||
private val context = web.context!!
|
||||
|
||||
@ -55,13 +56,14 @@ class FrostChromeClient(web: FrostWebView) : WebChromeClient() {
|
||||
|
||||
override fun onReceivedTitle(view: WebView, title: String) {
|
||||
super.onReceivedTitle(view, title)
|
||||
if (title.startsWith("http") || this.title.value == title) return
|
||||
this.title.onNext(title)
|
||||
if (title.startsWith("http") || prevTitle == title) return
|
||||
prevTitle = title
|
||||
this.title.offer(title)
|
||||
}
|
||||
|
||||
override fun onProgressChanged(view: WebView, newProgress: Int) {
|
||||
super.onProgressChanged(view, newProgress)
|
||||
progress.onNext(newProgress)
|
||||
progress.offer(newProgress)
|
||||
}
|
||||
|
||||
override fun onShowFileChooser(
|
||||
|
@ -16,10 +16,16 @@
|
||||
*/
|
||||
package com.pitchedapps.frost
|
||||
|
||||
import com.pitchedapps.frost.facebook.requests.call
|
||||
import com.pitchedapps.frost.facebook.requests.zip
|
||||
import okhttp3.Request
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
@ -48,14 +54,30 @@ class MiscTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun a() {
|
||||
val s = Request.Builder()
|
||||
.url("https://www.allanwang.ca/ecse429/magenta.png")
|
||||
.get()
|
||||
.call().execute().body()!!.string()
|
||||
"<EFBFBD>PNG\n\u001A\nIDA<EFBFBD>c<EFBFBD><EFBFBD><EFBFBD><EFBFBD>?\u0000\u0006<EFBFBD>\u0002<EFBFBD><EFBFBD>p<EFBFBD>\u0000\u0000\u0000\u0000IEND<EFBFBD>B`<60>"
|
||||
println("Hello")
|
||||
println(s)
|
||||
@Test
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
fun channel() {
|
||||
val c = BroadcastChannel<Int>(100)
|
||||
runBlocking {
|
||||
launch(Dispatchers.IO) {
|
||||
println("1 start ${Thread.currentThread()}")
|
||||
for (i in c.openSubscription()) {
|
||||
println("1 $i")
|
||||
}
|
||||
println("1 end ${Thread.currentThread()}")
|
||||
}
|
||||
launch(Dispatchers.IO) {
|
||||
println("2 start ${Thread.currentThread()}")
|
||||
for (i in c.openSubscription()) {
|
||||
println("2 $i")
|
||||
}
|
||||
println("2 end ${Thread.currentThread()}")
|
||||
}
|
||||
c.send(1)
|
||||
c.send(2)
|
||||
c.send(3)
|
||||
delay(1000)
|
||||
c.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,110 @@
|
||||
package com.pitchedapps.frost.views
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExecutorCoroutineDispatcher
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Collection of tests around the view thread logic
|
||||
*/
|
||||
@UseExperimental(ExperimentalCoroutinesApi::class)
|
||||
class FrostContentViewAsyncTest {
|
||||
|
||||
/**
|
||||
* Single threaded dispatcher with thread name "main"
|
||||
* Mimics the usage of Android's main dispatcher
|
||||
*/
|
||||
private lateinit var mainDispatcher: ExecutorCoroutineDispatcher
|
||||
|
||||
@BeforeTest
|
||||
fun before() {
|
||||
mainDispatcher = Executors.newSingleThreadExecutor { r ->
|
||||
Thread(r, "main")
|
||||
}.asCoroutineDispatcher()
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun after() {
|
||||
mainDispatcher.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks onto the refresh channel for one true -> false cycle.
|
||||
* Returns the list of event ids that were emitted
|
||||
*/
|
||||
private suspend fun transition(channel: ReceiveChannel<Pair<Boolean, Int>>): List<Pair<Boolean, Int>> {
|
||||
var refreshed = false
|
||||
return listen(channel) { (refreshing, _) ->
|
||||
if (refreshed && !refreshing)
|
||||
return@listen true
|
||||
if (refreshing)
|
||||
refreshed = true
|
||||
return@listen false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> listen(channel: ReceiveChannel<T>, shouldEnd: (T) -> Boolean = { false }): List<T> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val data = mutableListOf<T>()
|
||||
for (c in channel) {
|
||||
data.add(c)
|
||||
if (shouldEnd(c)) break
|
||||
}
|
||||
channel.cancel()
|
||||
return@withContext data
|
||||
}
|
||||
|
||||
/**
|
||||
* When refreshing, we have a temporary subscriber that hooks onto a single cycle.
|
||||
* The refresh channel only contains booleans, but for the sake of identification,
|
||||
* each boolean will have a unique integer attached.
|
||||
*
|
||||
* Things to note:
|
||||
* Subscription should be opened outside of async, since we don't want to miss any events.
|
||||
*/
|
||||
@Test
|
||||
fun refreshSubscriptions() {
|
||||
val refreshChannel = BroadcastChannel<Pair<Boolean, Int>>(100)
|
||||
runBlocking {
|
||||
// Listen to all events
|
||||
val fullReceiver = refreshChannel.openSubscription()
|
||||
val fullDeferred = async { listen(fullReceiver) }
|
||||
|
||||
refreshChannel.send(true to 1)
|
||||
refreshChannel.send(false to 2)
|
||||
refreshChannel.send(true to 3)
|
||||
|
||||
val partialReceiver = refreshChannel.openSubscription()
|
||||
val partialDeferred = async { transition(partialReceiver) }
|
||||
refreshChannel.send(false to 4)
|
||||
refreshChannel.send(true to 5)
|
||||
refreshChannel.send(false to 6)
|
||||
refreshChannel.send(true to 7)
|
||||
refreshChannel.close()
|
||||
val fullStream = fullDeferred.await()
|
||||
val partialStream = partialDeferred.await()
|
||||
|
||||
assertEquals(
|
||||
7,
|
||||
fullStream.size,
|
||||
"Full stream should contain all events"
|
||||
)
|
||||
assertEquals(
|
||||
listOf(false to 4, true to 5, false to 6),
|
||||
partialStream,
|
||||
"Partial stream should include up until first true false pair"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user