Parcourir la source

Mostly migrate rxbinding to Kotlin Flow version

arkon il y a 5 ans
Parent
commit
bdf322ceb0

+ 7 - 2
app/build.gradle

@@ -232,10 +232,15 @@ dependencies {
 
     // RxBindings
     final rxbindings_version = '1.0.1'
-    implementation "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
     implementation "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
     implementation "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
-    implementation "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
+
+    // FlowBinding
+    final flowbinding_version = '0.10.2'
+    implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
+    implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
+    implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
+    implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbinding_version"
 
     // Tests
     testImplementation 'junit:junit:4.13'

+ 12 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryController.kt

@@ -10,7 +10,6 @@ import androidx.appcompat.view.ActionMode
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import com.google.android.material.snackbar.Snackbar
-import com.jakewharton.rxbinding.view.clicks
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.SelectableAdapter
 import eu.davidea.flexibleadapter.helpers.UndoHelper
@@ -19,6 +18,11 @@ import eu.kanade.tachiyomi.data.database.models.Category
 import eu.kanade.tachiyomi.databinding.CategoriesControllerBinding
 import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 import eu.kanade.tachiyomi.util.system.toast
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import reactivecircus.flowbinding.android.view.clicks
 
 /**
  * Controller to manage the categories for the users' library.
@@ -47,6 +51,8 @@ class CategoryController : NucleusController<CategoryPresenter>(),
      */
     private var undoHelper: UndoHelper? = null
 
+    private val uiScope = CoroutineScope(Dispatchers.Main)
+
     private lateinit var binding: CategoriesControllerBinding
 
     /**
@@ -87,9 +93,11 @@ class CategoryController : NucleusController<CategoryPresenter>(),
         adapter?.isHandleDragEnabled = true
         adapter?.isPermanentDelete = false
 
-        binding.fab.clicks().subscribeUntilDestroy {
-            CategoryCreateDialog(this@CategoryController).showDialog(router, null)
-        }
+        binding.fab.clicks()
+            .onEach {
+                CategoryCreateDialog(this@CategoryController).showDialog(router, null)
+            }
+            .launchIn(uiScope)
     }
 
     /**

+ 18 - 10
app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionController.kt

@@ -12,8 +12,6 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
 import com.bluelinelabs.conductor.ControllerChangeType
 import com.bluelinelabs.conductor.RouterTransaction
 import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
-import com.jakewharton.rxbinding.support.v4.widget.refreshes
-import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.IFlexible
 import eu.kanade.tachiyomi.R
@@ -24,6 +22,13 @@ import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
 import eu.kanade.tachiyomi.extension.model.Extension
 import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import reactivecircus.flowbinding.appcompat.queryTextChanges
+import reactivecircus.flowbinding.swiperefreshlayout.refreshes
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
@@ -47,6 +52,8 @@ open class ExtensionController : NucleusController<ExtensionPresenter>(),
 
     private var query = ""
 
+    private val uiScope = CoroutineScope(Dispatchers.Main)
+
     private lateinit var binding: ExtensionControllerBinding
 
     init {
@@ -70,9 +77,9 @@ open class ExtensionController : NucleusController<ExtensionPresenter>(),
         super.onViewCreated(view)
 
         binding.extSwipeRefresh.isRefreshing = true
-        binding.extSwipeRefresh.refreshes().subscribeUntilDestroy {
-            presenter.findAvailableExtensions()
-        }
+        binding.extSwipeRefresh.refreshes()
+            .onEach { presenter.findAvailableExtensions() }
+            .launchIn(uiScope)
 
         // Initialize adapter, scroll listener and recycler views
         adapter = ExtensionAdapter(this)
@@ -146,11 +153,12 @@ open class ExtensionController : NucleusController<ExtensionPresenter>(),
         }
 
         searchView.queryTextChanges()
-                .filter { router.backstack.lastOrNull()?.controller() == this }
-                .subscribeUntilDestroy {
-                    query = it.toString()
-                    drawExtensions()
-                }
+            .filter { router.backstack.lastOrNull()?.controller() == this }
+            .onEach {
+                query = it.toString()
+                drawExtensions()
+            }
+            .launchIn(uiScope)
 
         // Fixes problem with the overflow icon showing up in lieu of search
         searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })

+ 10 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionDetailsController.kt

@@ -22,7 +22,6 @@ import androidx.preference.PreferenceScreen
 import androidx.recyclerview.widget.DividerItemDecoration
 import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
 import androidx.recyclerview.widget.LinearLayoutManager
-import com.jakewharton.rxbinding.view.clicks
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
 import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
@@ -33,6 +32,11 @@ import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 import eu.kanade.tachiyomi.util.preference.preferenceCategory
 import eu.kanade.tachiyomi.util.system.LocaleHelper
 import eu.kanade.tachiyomi.util.view.visible
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import reactivecircus.flowbinding.android.view.clicks
 
 @SuppressLint("RestrictedApi")
 class ExtensionDetailsController(bundle: Bundle? = null) :
@@ -44,6 +48,8 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
 
     private var preferenceScreen: PreferenceScreen? = null
 
+    private val uiScope = CoroutineScope(Dispatchers.Main)
+
     private lateinit var binding: ExtensionDetailControllerBinding
 
     constructor(pkgName: String) : this(Bundle().apply {
@@ -76,9 +82,9 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
         binding.extensionLang.text = context.getString(R.string.ext_language_info, LocaleHelper.getDisplayName(extension.lang, context))
         binding.extensionPkg.text = extension.pkgName
         extension.getApplicationIcon(context)?.let { binding.extensionIcon.setImageDrawable(it) }
-        binding.extensionUninstallButton.clicks().subscribeUntilDestroy {
-            presenter.uninstallExtension()
-        }
+        binding.extensionUninstallButton.clicks()
+            .onEach { presenter.uninstallExtension() }
+            .launchIn(uiScope)
 
         if (extension.isObsolete) {
             binding.extensionObsolete.visible()

+ 15 - 10
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt

@@ -20,7 +20,6 @@ import com.bluelinelabs.conductor.ControllerChangeType
 import com.f2prateek.rx.preferences.Preference
 import com.google.android.material.tabs.TabLayout
 import com.jakewharton.rxbinding.support.v4.view.pageSelections
-import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
 import com.jakewharton.rxrelay.BehaviorRelay
 import com.jakewharton.rxrelay.PublishRelay
 import eu.kanade.tachiyomi.R
@@ -40,6 +39,12 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
 import eu.kanade.tachiyomi.util.system.toast
 import java.io.IOException
 import kotlinx.android.synthetic.main.main_activity.tabs
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import reactivecircus.flowbinding.appcompat.queryTextChanges
 import rx.Subscription
 import timber.log.Timber
 import uy.kohesive.injekt.Injekt
@@ -123,7 +128,7 @@ class LibraryController(
 
     private var tabsVisibilitySubscription: Subscription? = null
 
-    private var searchViewSubscription: Subscription? = null
+    private val uiScope = CoroutineScope(Dispatchers.Main)
 
     private lateinit var binding: LibraryControllerBinding
 
@@ -335,14 +340,14 @@ class LibraryController(
         // Mutate the filter icon because it needs to be tinted and the resource is shared.
         menu.findItem(R.id.action_filter).icon.mutate()
 
-        searchViewSubscription?.unsubscribe()
-        searchViewSubscription = searchView.queryTextChanges()
-                // Ignore events if this controller isn't at the top
-                .filter { router.backstack.lastOrNull()?.controller() == this }
-                .subscribeUntilDestroy {
-                    query = it.toString()
-                    searchRelay.call(query)
-                }
+        searchView.queryTextChanges()
+            // Ignore events if this controller isn't at the top
+            .filter { router.backstack.lastOrNull()?.controller() == this }
+            .onEach {
+                query = it.toString()
+                searchRelay.call(query)
+            }
+            .launchIn(uiScope)
 
         searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
     }

+ 30 - 19
app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersController.kt

@@ -16,8 +16,6 @@ import androidx.core.graphics.drawable.DrawableCompat
 import androidx.recyclerview.widget.DividerItemDecoration
 import androidx.recyclerview.widget.LinearLayoutManager
 import com.google.android.material.snackbar.Snackbar
-import com.jakewharton.rxbinding.support.v4.widget.refreshes
-import com.jakewharton.rxbinding.view.clicks
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.SelectableAdapter
 import eu.kanade.tachiyomi.R
@@ -36,6 +34,12 @@ import eu.kanade.tachiyomi.util.view.gone
 import eu.kanade.tachiyomi.util.view.shrinkOnScroll
 import eu.kanade.tachiyomi.util.view.snack
 import eu.kanade.tachiyomi.util.view.visible
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import reactivecircus.flowbinding.android.view.clicks
+import reactivecircus.flowbinding.swiperefreshlayout.refreshes
 import timber.log.Timber
 
 class ChaptersController : NucleusController<ChaptersPresenter>(),
@@ -62,6 +66,8 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
 
     private var lastClickPosition = -1
 
+    private val uiScope = CoroutineScope(Dispatchers.Main)
+
     private lateinit var binding: ChaptersControllerBinding
 
     init {
@@ -91,27 +97,32 @@ class ChaptersController : NucleusController<ChaptersPresenter>(),
         binding.recycler.setHasFixedSize(true)
         adapter?.fastScroller = binding.fastScroller
 
-        binding.swipeRefresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() }
-
-        binding.fab.clicks().subscribeUntilDestroy {
-            val item = presenter.getNextUnreadChapter()
-            if (item != null) {
-                // Create animation listener
-                val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() {
-                    override fun onAnimationStart(animation: Animator?) {
-                        openChapter(item.chapter, true)
+        binding.swipeRefresh.refreshes()
+            .onEach { fetchChaptersFromSource() }
+            .launchIn(uiScope)
+
+        binding.fab.clicks()
+            .onEach {
+                val item = presenter.getNextUnreadChapter()
+                if (item != null) {
+                    // Create animation listener
+                    val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() {
+                        override fun onAnimationStart(animation: Animator?) {
+                            openChapter(item.chapter, true)
+                        }
                     }
-                }
 
-                // Get coordinates and start animation
-                val coordinates = binding.fab.getCoordinates()
-                if (!binding.revealView.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) {
-                    openChapter(item.chapter)
+                    // Get coordinates and start animation
+                    val coordinates = binding.fab.getCoordinates()
+                    if (!binding.revealView.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) {
+                        openChapter(item.chapter)
+                    }
+                } else {
+                    view.context.toast(R.string.no_next_chapter)
                 }
-            } else {
-                view.context.toast(R.string.no_next_chapter)
             }
-        }
+            .launchIn(uiScope)
+
         binding.fab.shrinkOnScroll(binding.recycler)
     }
 

+ 64 - 32
app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt

@@ -27,9 +27,6 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners
 import com.bumptech.glide.request.target.CustomTarget
 import com.bumptech.glide.request.transition.Transition
 import com.google.android.material.chip.Chip
-import com.jakewharton.rxbinding.support.v4.widget.refreshes
-import com.jakewharton.rxbinding.view.clicks
-import com.jakewharton.rxbinding.view.longClicks
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.Category
 import eu.kanade.tachiyomi.data.database.models.Manga
@@ -56,6 +53,13 @@ import eu.kanade.tachiyomi.util.system.toast
 import eu.kanade.tachiyomi.util.view.snack
 import eu.kanade.tachiyomi.util.view.visible
 import jp.wasabeef.glide.transformations.CropSquareTransformation
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import reactivecircus.flowbinding.android.view.clicks
+import reactivecircus.flowbinding.android.view.longClicks
+import reactivecircus.flowbinding.swiperefreshlayout.refreshes
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 import uy.kohesive.injekt.injectLazy
@@ -70,6 +74,8 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
 
     private val preferences: PreferencesHelper by injectLazy()
 
+    private val uiScope = CoroutineScope(Dispatchers.Main)
+
     private lateinit var binding: MangaInfoControllerBinding
 
     init {
@@ -91,53 +97,79 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
         super.onViewCreated(view)
 
         // Set onclickListener to toggle favorite when favorite button clicked.
-        binding.btnFavorite.clicks().subscribeUntilDestroy { onFavoriteClick() }
+        binding.btnFavorite.clicks()
+            .onEach { onFavoriteClick() }
+            .launchIn(uiScope)
 
         // Set onLongClickListener to manage categories when favorite button is clicked.
-        binding.btnFavorite.longClicks().subscribeUntilDestroy { onFavoriteLongClick() }
+        binding.btnFavorite.longClicks()
+            .onEach { onFavoriteLongClick() }
+            .launchIn(uiScope)
 
         if (presenter.source is HttpSource) {
             binding.btnWebview.visible()
             binding.btnShare.visible()
 
-            binding.btnWebview.clicks().subscribeUntilDestroy { openInWebView() }
-            binding.btnShare.clicks().subscribeUntilDestroy { shareManga() }
+            binding.btnWebview.clicks()
+                .onEach { openInWebView() }
+                .launchIn(uiScope)
+            binding.btnShare.clicks()
+                .onEach { shareManga() }
+                .launchIn(uiScope)
         }
 
         // Set SwipeRefresh to refresh manga data.
-        binding.swipeRefresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
+        binding.swipeRefresh.refreshes()
+            .onEach { fetchMangaFromSource() }
+            .launchIn(uiScope)
 
-        binding.mangaFullTitle.longClicks().subscribeUntilDestroy {
-            copyToClipboard(view.context.getString(R.string.title), binding.mangaFullTitle.text.toString())
-        }
+        binding.mangaFullTitle.longClicks()
+            .onEach {
+                copyToClipboard(view.context.getString(R.string.title), binding.mangaFullTitle.text.toString())
+            }
+            .launchIn(uiScope)
 
-        binding.mangaFullTitle.clicks().subscribeUntilDestroy {
-            performGlobalSearch(binding.mangaFullTitle.text.toString())
-        }
+        binding.mangaFullTitle.clicks()
+            .onEach {
+                performGlobalSearch(binding.mangaFullTitle.text.toString())
+            }
+            .launchIn(uiScope)
 
-        binding.mangaArtist.longClicks().subscribeUntilDestroy {
-            copyToClipboard(binding.mangaArtistLabel.text.toString(), binding.mangaArtist.text.toString())
-        }
+        binding.mangaArtist.longClicks()
+            .onEach {
+                copyToClipboard(binding.mangaArtistLabel.text.toString(), binding.mangaArtist.text.toString())
+            }
+            .launchIn(uiScope)
 
-        binding.mangaArtist.clicks().subscribeUntilDestroy {
-            performGlobalSearch(binding.mangaArtist.text.toString())
-        }
+        binding.mangaArtist.clicks()
+            .onEach {
+                performGlobalSearch(binding.mangaArtist.text.toString())
+            }
+            .launchIn(uiScope)
 
-        binding.mangaAuthor.longClicks().subscribeUntilDestroy {
-            copyToClipboard(binding.mangaAuthor.text.toString(), binding.mangaAuthor.text.toString())
-        }
+        binding.mangaAuthor.longClicks()
+            .onEach {
+                copyToClipboard(binding.mangaAuthor.text.toString(), binding.mangaAuthor.text.toString())
+            }
+            .launchIn(uiScope)
 
-        binding.mangaAuthor.clicks().subscribeUntilDestroy {
-            performGlobalSearch(binding.mangaAuthor.text.toString())
-        }
+        binding.mangaAuthor.clicks()
+            .onEach {
+                performGlobalSearch(binding.mangaAuthor.text.toString())
+            }
+            .launchIn(uiScope)
 
-        binding.mangaSummary.longClicks().subscribeUntilDestroy {
-            copyToClipboard(view.context.getString(R.string.description), binding.mangaSummary.text.toString())
-        }
+        binding.mangaSummary.longClicks()
+            .onEach {
+                copyToClipboard(view.context.getString(R.string.description), binding.mangaSummary.text.toString())
+            }
+            .launchIn(uiScope)
 
-        binding.mangaCover.longClicks().subscribeUntilDestroy {
-            copyToClipboard(view.context.getString(R.string.title), presenter.manga.title)
-        }
+        binding.mangaCover.longClicks()
+            .onEach {
+                copyToClipboard(view.context.getString(R.string.title), presenter.manga.title)
+            }
+            .launchIn(uiScope)
     }
 
     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {

+ 10 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt

@@ -6,13 +6,17 @@ import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import androidx.recyclerview.widget.LinearLayoutManager
-import com.jakewharton.rxbinding.support.v4.widget.refreshes
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.track.model.TrackSearch
 import eu.kanade.tachiyomi.databinding.TrackControllerBinding
 import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 import eu.kanade.tachiyomi.ui.manga.MangaController
 import eu.kanade.tachiyomi.util.system.toast
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import reactivecircus.flowbinding.swiperefreshlayout.refreshes
 import timber.log.Timber
 
 class TrackController : NucleusController<TrackPresenter>(),
@@ -23,6 +27,8 @@ class TrackController : NucleusController<TrackPresenter>(),
 
     private var adapter: TrackAdapter? = null
 
+    private val uiScope = CoroutineScope(Dispatchers.Main)
+
     private lateinit var binding: TrackControllerBinding
 
     init {
@@ -47,7 +53,9 @@ class TrackController : NucleusController<TrackPresenter>(),
         binding.trackRecycler.layoutManager = LinearLayoutManager(view.context)
         binding.trackRecycler.adapter = adapter
         binding.swipeRefresh.isEnabled = false
-        binding.swipeRefresh.refreshes().subscribeUntilDestroy { presenter.refresh() }
+        binding.swipeRefresh.refreshes()
+            .onEach { presenter.refresh() }
+            .launchIn(uiScope)
     }
 
     override fun onDestroyView(view: View) {

+ 21 - 28
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt

@@ -4,24 +4,27 @@ import android.app.Dialog
 import android.os.Bundle
 import android.view.View
 import com.afollestad.materialdialogs.MaterialDialog
-import com.jakewharton.rxbinding.widget.itemClicks
-import com.jakewharton.rxbinding.widget.textChanges
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.Track
 import eu.kanade.tachiyomi.data.track.TrackManager
 import eu.kanade.tachiyomi.data.track.TrackService
 import eu.kanade.tachiyomi.data.track.model.TrackSearch
 import eu.kanade.tachiyomi.ui.base.controller.DialogController
-import eu.kanade.tachiyomi.util.lang.plusAssign
 import eu.kanade.tachiyomi.util.view.invisible
 import eu.kanade.tachiyomi.util.view.visible
 import java.util.concurrent.TimeUnit
 import kotlinx.android.synthetic.main.track_search_dialog.view.progress
 import kotlinx.android.synthetic.main.track_search_dialog.view.track_search
 import kotlinx.android.synthetic.main.track_search_dialog.view.track_search_list
-import rx.Subscription
-import rx.android.schedulers.AndroidSchedulers
-import rx.subscriptions.CompositeSubscription
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import reactivecircus.flowbinding.android.widget.itemClicks
+import reactivecircus.flowbinding.android.widget.textChanges
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
@@ -35,13 +38,11 @@ class TrackSearchDialog : DialogController {
 
     private val service: TrackService
 
-    private var subscriptions = CompositeSubscription()
-
-    private var searchTextSubscription: Subscription? = null
-
     private val trackController
         get() = targetController as TrackController
 
+    private val uiScope = CoroutineScope(Dispatchers.Main)
+
     constructor(target: TrackController, service: TrackService) : super(Bundle().apply {
         putInt(KEY_SERVICE, service.id)
     }) {
@@ -64,10 +65,6 @@ class TrackSearchDialog : DialogController {
                 .onNeutral { _, _ -> onRemoveButtonClick() }
                 .build()
 
-        if (subscriptions.isUnsubscribed) {
-            subscriptions = CompositeSubscription()
-        }
-
         dialogView = dialog.view
         onViewCreated(dialog.view, savedViewState)
 
@@ -83,9 +80,11 @@ class TrackSearchDialog : DialogController {
         // Set listeners
         selectedItem = null
 
-        subscriptions += view.track_search_list.itemClicks().subscribe { position ->
-            selectedItem = adapter.getItem(position)
-        }
+        view.track_search_list.itemClicks()
+            .onEach { position ->
+                selectedItem = adapter.getItem(position)
+            }
+            .launchIn(uiScope)
 
         // Do an initial search based on the manga's title
         if (savedState == null) {
@@ -97,24 +96,18 @@ class TrackSearchDialog : DialogController {
 
     override fun onDestroyView(view: View) {
         super.onDestroyView(view)
-        subscriptions.unsubscribe()
         dialogView = null
         adapter = null
     }
 
     override fun onAttach(view: View) {
         super.onAttach(view)
-        searchTextSubscription = dialogView!!.track_search.textChanges()
-                .skip(1)
-                .debounce(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
+        dialogView!!.track_search.textChanges(false)
+                .debounce(TimeUnit.SECONDS.toMillis(1))
                 .map { it.toString() }
-                .filter(String::isNotBlank)
-                .subscribe { search(it) }
-    }
-
-    override fun onDetach(view: View) {
-        super.onDetach(view)
-        searchTextSubscription?.unsubscribe()
+                .filter { it.isNotBlank() }
+                .onEach { search(it) }
+                .launchIn(uiScope)
     }
 
     private fun search(query: String) {

+ 22 - 12
app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt

@@ -10,8 +10,6 @@ import androidx.appcompat.app.AppCompatActivity
 import androidx.appcompat.view.ActionMode
 import androidx.recyclerview.widget.DividerItemDecoration
 import androidx.recyclerview.widget.LinearLayoutManager
-import com.jakewharton.rxbinding.support.v4.widget.refreshes
-import com.jakewharton.rxbinding.support.v7.widget.scrollStateChanges
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.SelectableAdapter
 import eu.davidea.flexibleadapter.items.IFlexible
@@ -29,6 +27,12 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.util.system.notificationManager
 import eu.kanade.tachiyomi.util.system.toast
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import reactivecircus.flowbinding.recyclerview.scrollStateChanges
+import reactivecircus.flowbinding.swiperefreshlayout.refreshes
 import timber.log.Timber
 
 /**
@@ -57,6 +61,8 @@ class UpdatesController : NucleusController<UpdatesPresenter>(),
     var adapter: UpdatesAdapter? = null
         private set
 
+    private val uiScope = CoroutineScope(Dispatchers.Main)
+
     private lateinit var binding: UpdatesControllerBinding
 
     init {
@@ -92,19 +98,23 @@ class UpdatesController : NucleusController<UpdatesPresenter>(),
         adapter = UpdatesAdapter(this@UpdatesController)
         binding.recycler.adapter = adapter
 
-        binding.recycler.scrollStateChanges().subscribeUntilDestroy {
-            // Disable swipe refresh when view is not at the top
-            val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition()
-            binding.swipeRefresh.isEnabled = firstPos <= 0
-        }
+        binding.recycler.scrollStateChanges()
+            .onEach {
+                // Disable swipe refresh when view is not at the top
+                val firstPos = layoutManager.findFirstCompletelyVisibleItemPosition()
+                binding.swipeRefresh.isEnabled = firstPos <= 0
+            }
+            .launchIn(uiScope)
 
         binding.swipeRefresh.setDistanceToTriggerSync((2 * 64 * view.resources.displayMetrics.density).toInt())
-        binding.swipeRefresh.refreshes().subscribeUntilDestroy {
-            updateLibrary()
+        binding.swipeRefresh.refreshes()
+            .onEach {
+                updateLibrary()
 
-            // It can be a very long operation, so we disable swipe refresh and show a toast.
-            binding.swipeRefresh.isRefreshing = false
-        }
+                // It can be a very long operation, so we disable swipe refresh and show a toast.
+                binding.swipeRefresh.isRefreshing = false
+            }
+            .launchIn(uiScope)
     }
 
     override fun onDestroyView(view: View) {

+ 13 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/source/SourceController.kt

@@ -14,7 +14,6 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
 import com.bluelinelabs.conductor.ControllerChangeType
 import com.bluelinelabs.conductor.RouterTransaction
 import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
-import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
 import eu.davidea.flexibleadapter.FlexibleAdapter
 import eu.davidea.flexibleadapter.items.IFlexible
 import eu.kanade.tachiyomi.R
@@ -31,6 +30,13 @@ import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
 import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController
 import eu.kanade.tachiyomi.ui.source.global_search.GlobalSearchController
 import eu.kanade.tachiyomi.ui.source.latest.LatestUpdatesController
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import reactivecircus.flowbinding.appcompat.QueryTextEvent
+import reactivecircus.flowbinding.appcompat.queryTextEvents
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
@@ -54,6 +60,8 @@ class SourceController : NucleusController<SourcePresenter>(),
      */
     private var adapter: SourceAdapter? = null
 
+    private val uiScope = CoroutineScope(Dispatchers.Main)
+
     private lateinit var binding: SourceMainControllerBinding
 
     init {
@@ -192,9 +200,10 @@ class SourceController : NucleusController<SourcePresenter>(),
         searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
 
         // Create query listener which opens the global search view.
-        searchView.queryTextChangeEvents()
-                .filter { it.isSubmitted }
-                .subscribeUntilDestroy { performGlobalSearch(it.queryText().toString()) }
+        searchView.queryTextEvents()
+            .filter { it is QueryTextEvent.QuerySubmitted }
+            .onEach { performGlobalSearch(it.queryText.toString()) }
+            .launchIn(uiScope)
     }
 
     fun performGlobalSearch(query: String) {

+ 17 - 8
app/src/main/java/eu/kanade/tachiyomi/ui/source/global_search/GlobalSearchController.kt

@@ -9,7 +9,6 @@ import android.view.View
 import android.view.ViewGroup
 import androidx.appcompat.widget.SearchView
 import androidx.recyclerview.widget.LinearLayoutManager
-import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding
@@ -17,6 +16,13 @@ import eu.kanade.tachiyomi.source.CatalogueSource
 import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
 import eu.kanade.tachiyomi.ui.manga.MangaController
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import reactivecircus.flowbinding.appcompat.QueryTextEvent
+import reactivecircus.flowbinding.appcompat.queryTextEvents
 
 /**
  * This controller shows and manages the different search result in global search.
@@ -34,6 +40,8 @@ open class GlobalSearchController(
      */
     protected var adapter: GlobalSearchAdapter? = null
 
+    private val uiScope = CoroutineScope(Dispatchers.Main)
+
     private lateinit var binding: GlobalSearchControllerBinding
 
     /**
@@ -119,13 +127,14 @@ open class GlobalSearchController(
             }
         })
 
-        searchView.queryTextChangeEvents()
-                .filter { it.isSubmitted }
-                .subscribeUntilDestroy {
-                    presenter.search(it.queryText().toString())
-                    searchItem.collapseActionView()
-                    setTitle() // Update toolbar title
-                }
+        searchView.queryTextEvents()
+            .filter { it is QueryTextEvent.QuerySubmitted }
+            .onEach {
+                presenter.search(it.queryText.toString())
+                searchItem.collapseActionView()
+                setTitle() // Update toolbar title
+            }
+            .launchIn(uiScope)
     }
 
     /**