From e6dcbd7b32dc49b11184b6beca598819c3f071fd Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 27 Dec 2018 02:15:10 -0500 Subject: Begin replacing observables with channels --- .../frost/activities/WebOverlayActivity.kt | 2 +- .../frost/contracts/FrostContentContract.kt | 18 +++++- .../frost/fragments/FragmentContract.kt | 9 ++- .../frost/fragments/RecyclerFragmentBase.kt | 66 ++++++++++++---------- .../frost/fragments/RecyclerFragments.kt | 33 +++++------ .../pitchedapps/frost/views/FrostContentView.kt | 66 ++++++++++++++-------- .../pitchedapps/frost/views/FrostRecyclerView.kt | 14 +++-- .../pitchedapps/frost/web/FrostChromeClients.kt | 12 ++-- 8 files changed, 134 insertions(+), 86 deletions(-) (limited to 'app/src/main') 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 b706d467..bf04c524 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt @@ -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) 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 50c2fe77..a2c2b05a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FrostContentContract.kt @@ -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 + val refreshChannel: BroadcastChannel + /** * Observable to get data on refresh progress, with range [0, 100] */ val progressObservable: PublishSubject + val progressChannel: BroadcastChannel + /** * Observable to get new title data (unique values only) */ val titleObservable: BehaviorSubject + val titleChannel: BroadcastChannel + 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] 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 e24e8308..95322c1c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt @@ -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 } 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 f77f83ea..98c8f750 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragmentBase.kt @@ -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> : BaseFragment(), RecyclerContentContract { override val layoutRes: Int = R.layout.view_content_recycler + abstract val adapter: ModelAdapter + 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? } -abstract class GenericRecyclerFragment> : RecyclerFragment() { +abstract class GenericRecyclerFragment> : RecyclerFragment() { abstract fun mapper(data: T): Item - val adapter: ModelAdapter = ModelAdapter { this.mapper(it) } + override val adapter: ModelAdapter = ModelAdapter { this.mapper(it) } final override fun bind(recyclerView: FrostRecyclerView) { recyclerView.adapter = getAdapter() @@ -83,7 +92,7 @@ abstract class GenericRecyclerFragment> : RecyclerFragment open fun getAdapter(): FastAdapter> = fastAdapter(this.adapter) } -abstract class FrostParserFragment> : RecyclerFragment() { +abstract class FrostParserFragment> : RecyclerFragment() { /** * The parser to make this all happen @@ -94,7 +103,7 @@ abstract class FrostParserFragment> : RecyclerFragme abstract fun toItems(response: ParseResponse): List - val adapter: ItemAdapter = ItemAdapter() + override val adapter: ItemAdapter = ItemAdapter() final override fun bind(recyclerView: FrostRecyclerView) { recyclerView.adapter = getAdapter() @@ -113,23 +122,20 @@ abstract class FrostParserFragment> : RecyclerFragme */ open fun getAdapter(): FastAdapter> = 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? = 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 } } 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 ff37b66d..45cf2290 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/RecyclerFragments.kt @@ -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>() { 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? = 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 } } 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 d17a424c..72b81b37 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostContentView.kt @@ -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 @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -85,7 +93,11 @@ abstract class FrostContentView @JvmOverloads constructor( override val refreshObservable: PublishSubject = PublishSubject.create() override val titleObservable: BehaviorSubject = BehaviorSubject.create() - private val compositeDisposable = CompositeDisposable() + override val refreshChannel: BroadcastChannel = BroadcastChannel(Channel.UNLIMITED) + override val progressChannel: BroadcastChannel = BroadcastChannel(Channel.UNLIMITED) + override val titleChannel: BroadcastChannel = 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 @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 @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 @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 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 2b9e8f9c..fb20c3ba 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostRecyclerView.kt @@ -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() } } 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 12df8000..1f8118da 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt @@ -43,8 +43,9 @@ import io.reactivex.subjects.Subject */ class FrostChromeClient(web: FrostWebView) : WebChromeClient() { - private val progress: Subject = web.parent.progressObservable - private val title: BehaviorSubject = 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( -- cgit v1.2.3