Add onPause\onResume persistence to searchView. Fixes issue #3627 (#4494)

* Add onPause\onResume persistence to searchView. Fixes issue #3627

* New controller subclass with built-in SearchView support

* Implement new SearchableNucleusController in SourceController

* Add query to BasePresenter (for one field it is not worth create a subclass in my opinion), convert BrowseSourceController to inherit from SearchableNucleusController

* move to flows to fix an issue in GlobalSearch where it would trigger the search multiple times

* Continue conversion to SearchableNucleusController

* Convert LibraryController, convert to flows, Known ISSUE with empty string being posted after setting the query upon creation of UI

* Fix issues with the post being tide to the SearchView queue which is not processed until shown. Add COLLAPSING state capture which should wrap this up.

* refactoring & enforce @StringRes for queryHint
This commit is contained in:
Antoine Gaudreau Simard 2021-03-27 16:38:41 -04:00 committed by GitHub
parent 14c114756d
commit 2911fe7a1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 280 additions and 177 deletions

View file

@ -121,7 +121,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
* [expandActionViewFromInteraction] should be set to true in [onOptionsItemSelected] when the expandable item is selected
* This method should be called as part of [MenuItem.OnActionExpandListener.onMenuItemActionExpand]
*/
fun invalidateMenuOnExpand(): Boolean {
open fun invalidateMenuOnExpand(): Boolean {
return if (expandActionViewFromInteraction) {
activity?.invalidateOptionsMenu()
false

View file

@ -0,0 +1,196 @@
package eu.kanade.tachiyomi.ui.base.controller
import android.app.Activity
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView
import androidx.viewbinding.ViewBinding
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
/**
* Implementation of the NucleusController that has a built-in ViewSearch
*/
abstract class SearchableNucleusController<VB : ViewBinding, P : BasePresenter<*>>
(bundle: Bundle? = null) : NucleusController<VB, P>(bundle) {
enum class SearchViewState { LOADING, LOADED, COLLAPSING, FOCUSED }
/**
* Used to bypass the initial searchView being set to empty string after an onResume
*/
private var currentSearchViewState: SearchViewState = SearchViewState.LOADING
/**
* Store the query text that has not been submitted to reassign it after an onResume, UI-only
*/
protected var nonSubmittedQuery: String = ""
/**
* To be called by classes that extend this subclass in onCreateOptionsMenu
*/
protected fun createOptionsMenu(
menu: Menu,
inflater: MenuInflater,
menuId: Int,
searchItemId: Int,
@StringRes queryHint: Int? = null,
restoreCurrentQuery: Boolean = true
) {
// Inflate menu
inflater.inflate(menuId, menu)
// Initialize search option.
val searchItem = menu.findItem(searchItemId)
val searchView = searchItem.actionView as SearchView
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
searchView.maxWidth = Int.MAX_VALUE
searchView.queryTextEvents()
.onEach {
val newText = it.queryText.toString()
if (newText.isNotBlank() or acceptEmptyQuery()) {
if (it is QueryTextEvent.QuerySubmitted) {
// Abstract function for implementation
// Run it first in case the old query data is needed (like BrowseSourceController)
onSearchViewQueryTextSubmit(newText)
presenter.query = newText
nonSubmittedQuery = ""
} else if ((it is QueryTextEvent.QueryChanged) && (presenter.query != newText)) {
nonSubmittedQuery = newText
// Abstract function for implementation
onSearchViewQueryTextChange(newText)
}
}
// clear the collapsing flag
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.COLLAPSING)
}
.launchIn(viewScope)
val query = presenter.query
// Restoring a query the user had not submitted
if (nonSubmittedQuery.isNotBlank() and (nonSubmittedQuery != query)) {
searchItem.expandActionView()
searchView.setQuery(nonSubmittedQuery, false)
onSearchViewQueryTextChange(nonSubmittedQuery)
} else {
if (queryHint != null) {
searchView.queryHint = applicationContext?.getString(queryHint)
}
if (restoreCurrentQuery) {
// Restoring a query the user had submitted
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
onSearchViewQueryTextChange(query)
onSearchViewQueryTextSubmit(query)
}
}
}
// Workaround for weird behavior where searchView gets empty text change despite
// query being set already, prevents the query from being cleared
binding.root.post {
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.LOADING)
}
searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
if (hasFocus) {
setCurrentSearchViewState(SearchViewState.FOCUSED)
} else {
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.FOCUSED)
}
}
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
onSearchMenuItemActionExpand(item)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
val localSearchView = searchItem.actionView as SearchView
// if it is blank the flow event won't trigger so we would stay in a COLLAPSING state
if (localSearchView.toString().isNotBlank()) {
setCurrentSearchViewState(SearchViewState.COLLAPSING)
}
onSearchMenuItemActionCollapse(item)
return true
}
}
)
}
override fun onActivityResumed(activity: Activity) {
super.onActivityResumed(activity)
// Until everything is up and running don't accept empty queries
setCurrentSearchViewState(SearchViewState.LOADING)
}
private fun acceptEmptyQuery(): Boolean {
return when (currentSearchViewState) {
SearchViewState.COLLAPSING, SearchViewState.FOCUSED -> true
else -> false
}
}
private fun setCurrentSearchViewState(to: SearchViewState, from: SearchViewState? = null) {
// When loading ignore all requests other than loaded
if ((currentSearchViewState == SearchViewState.LOADING) && (to != SearchViewState.LOADED)) {
return
}
// Prevent changing back to an unwanted state when using async flows (ie onFocus event doing
// COLLAPSING -> LOADED)
if ((from != null) && (currentSearchViewState != from)) {
return
}
currentSearchViewState = to
}
/**
* Called by the SearchView since since the implementation of these can vary in subclasses
* Not abstract as they are optional
*/
protected open fun onSearchViewQueryTextChange(newText: String?) {
}
protected open fun onSearchViewQueryTextSubmit(query: String?) {
}
protected open fun onSearchMenuItemActionExpand(item: MenuItem?) {
}
protected open fun onSearchMenuItemActionCollapse(item: MenuItem?) {
}
/**
* During the conversion to SearchableNucleusController (after which I plan to merge its code
* into BaseController) this addresses an issue where the searchView.onTextFocus event is not
* triggered
*/
override fun invalidateMenuOnExpand(): Boolean {
return if (expandActionViewFromInteraction) {
activity?.invalidateOptionsMenu()
setCurrentSearchViewState(SearchViewState.FOCUSED) // we are technically focused here
false
} else {
true
}
}
}

View file

@ -12,6 +12,11 @@ open class BasePresenter<V> : RxPresenter<V>() {
lateinit var presenterScope: CoroutineScope
/**
* Query from the view where applicable
*/
var query: String = ""
override fun onCreate(savedState: Bundle?) {
try {
super.onCreate(savedState)

View file

@ -9,7 +9,6 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems
@ -25,19 +24,11 @@ import eu.kanade.tachiyomi.databinding.SourceMainControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.base.controller.*
import eu.kanade.tachiyomi.ui.browse.BrowseController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import kotlinx.coroutines.flow.filterIsInstance
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
@ -48,7 +39,7 @@ import uy.kohesive.injekt.api.get
* [SourceAdapter.OnLatestClickListener] call function data on latest item click
*/
class SourceController :
NucleusController<SourceMainControllerBinding, SourcePresenter>(),
SearchableNucleusController<SourceMainControllerBinding, SourcePresenter>(),
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
SourceAdapter.OnSourceClickListener {
@ -200,37 +191,6 @@ class SourceController :
parentController!!.router.pushController(controller.withFadeTransaction())
}
/**
* Adds items to the options menu.
*
* @param menu menu containing options.
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu
inflater.inflate(R.menu.source_main, menu)
// Initialize search option.
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
// Change hint to show global search.
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
// Create query listener which opens the global search view.
searchView.queryTextEvents()
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach { performGlobalSearch(it.queryText.toString()) }
.launchIn(viewScope)
}
private fun performGlobalSearch(query: String) {
parentController!!.router.pushController(
GlobalSearchController(query).withFadeTransaction()
)
}
/**
* Called when an option menu item has been selected by the user.
*
@ -290,4 +250,21 @@ class SourceController :
}
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
createOptionsMenu(
menu,
inflater,
R.menu.source_main,
R.id.action_search,
R.string.action_global_search_hint,
false // GlobalSearch handles the searching here
)
}
override fun onSearchViewQueryTextSubmit(query: String?) {
parentController!!.router.pushController(
GlobalSearchController(query).withFadeTransaction()
)
}
}

View file

@ -8,7 +8,6 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.GridLayoutManager
@ -33,7 +32,7 @@ import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
@ -51,12 +50,8 @@ import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.EmptyView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
@ -64,7 +59,7 @@ import uy.kohesive.injekt.injectLazy
* Controller to manage the catalogues available in the app.
*/
open class BrowseSourceController(bundle: Bundle) :
NucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
SearchableNucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
FabController,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
@ -259,25 +254,8 @@ open class BrowseSourceController(bundle: Bundle) :
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.source_browse, menu)
// Initialize search menu
createOptionsMenu(menu, inflater, R.menu.source_browse, R.id.action_search)
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
val query = presenter.query
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
searchView.queryTextEvents()
.filter { router.backstack.lastOrNull()?.controller() == this@BrowseSourceController }
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach { searchWithQuery(it.queryText.toString()) }
.launchIn(viewScope)
searchItem.fixExpand(
onExpand = { invalidateMenuOnExpand() },
@ -300,6 +278,10 @@ open class BrowseSourceController(bundle: Bundle) :
menu.findItem(displayItem).isChecked = true
}
override fun onSearchViewQueryTextSubmit(query: String?) {
searchWithQuery(query ?: "")
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)

View file

@ -66,12 +66,6 @@ open class BrowseSourcePresenter(
*/
lateinit var source: CatalogueSource
/**
* Query from the view.
*/
var query = searchQuery ?: ""
private set
/**
* Modifiable list of filters.
*/
@ -108,6 +102,10 @@ open class BrowseSourcePresenter(
*/
private var pageSubscription: Subscription? = null
init {
query = searchQuery ?: ""
}
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)

View file

@ -1,12 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.*
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
@ -15,15 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.manga.MangaController
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
import uy.kohesive.injekt.injectLazy
/**
@ -34,7 +24,7 @@ import uy.kohesive.injekt.injectLazy
open class GlobalSearchController(
protected val initialQuery: String? = null,
protected val extensionFilter: String? = null
) : NucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(),
) : SearchableNucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(),
GlobalSearchCardAdapter.OnMangaClickListener,
GlobalSearchAdapter.OnTitleClickListener {
@ -45,6 +35,11 @@ open class GlobalSearchController(
*/
protected var adapter: GlobalSearchAdapter? = null
/**
* Ref to the OptionsMenu.SearchItem created in onCreateOptionsMenu
*/
private var optionsMenuSearchItem: MenuItem? = null
init {
setHasOptionsMenu(true)
}
@ -100,36 +95,32 @@ open class GlobalSearchController(
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu.
inflater.inflate(R.menu.global_search, menu)
// Initialize search menu
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
searchView.onActionViewExpanded() // Required to show the query in the view
searchView.setQuery(presenter.query, false)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
return true
}
}
createOptionsMenu(
menu,
inflater,
R.menu.global_search,
R.id.action_search,
null,
false // the onMenuItemActionExpand will handle this
)
searchView.queryTextEvents()
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach {
presenter.search(it.queryText.toString())
searchItem.collapseActionView()
setTitle() // Update toolbar title
optionsMenuSearchItem = menu.findItem(R.id.action_search)
}
.launchIn(viewScope)
override fun onSearchMenuItemActionExpand(item: MenuItem?) {
super.onSearchMenuItemActionExpand(item)
val searchView = optionsMenuSearchItem?.actionView as SearchView
searchView.onActionViewExpanded() // Required to show the query in the view
if (nonSubmittedQuery.isBlank()) {
searchView.setQuery(presenter.query, false)
}
}
override fun onSearchViewQueryTextSubmit(query: String?) {
presenter.search(query ?: "")
optionsMenuSearchItem?.collapseActionView()
setTitle() // Update toolbar title
}
/**

View file

@ -47,12 +47,6 @@ open class GlobalSearchPresenter(
*/
val sources by lazy { getSourcesToQuery() }
/**
* Query from the view.
*/
var query = ""
private set
/**
* Fetches the different sources by user settings.
*/

View file

@ -10,7 +10,6 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.isVisible
import com.bluelinelabs.conductor.ControllerChangeHandler
@ -27,21 +26,16 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.databinding.LibraryControllerBinding
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.base.controller.*
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
import reactivecircus.flowbinding.appcompat.queryTextChanges
import reactivecircus.flowbinding.viewpager.pageSelections
import rx.Subscription
import uy.kohesive.injekt.Injekt
@ -50,7 +44,7 @@ import uy.kohesive.injekt.api.get
class LibraryController(
bundle: Bundle? = null,
private val preferences: PreferencesHelper = Injekt.get()
) : NucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
) : SearchableNucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
RootController,
TabbedController,
ActionMode.Callback,
@ -67,11 +61,6 @@ class LibraryController(
*/
private var actionMode: ActionMode? = null
/**
* Library search query.
*/
private var query: String = ""
/**
* Currently selected mangas.
*/
@ -212,7 +201,7 @@ class LibraryController(
binding.btnGlobalSearch.clicks()
.onEach {
router.pushController(
GlobalSearchController(query).withFadeTransaction()
GlobalSearchController(presenter.query).withFadeTransaction()
)
}
.launchIn(viewScope)
@ -384,52 +373,21 @@ class LibraryController(
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.library, menu)
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
if (query.isNotEmpty()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
performSearch()
// Workaround for weird behavior where searchview gets empty text change despite
// query being set already
searchView.postDelayed({ initSearchHandler(searchView) }, 500)
} else {
initSearchHandler(searchView)
}
createOptionsMenu(menu, inflater, R.menu.library, R.id.action_search)
// Mutate the filter icon because it needs to be tinted and the resource is shared.
menu.findItem(R.id.action_filter).icon.mutate()
}
fun search(query: String) {
this.query = query
}
private fun initSearchHandler(searchView: SearchView) {
searchView.queryTextChanges()
// Ignore events if this controller isn't at the top to avoid query being reset
.filter { router.backstack.lastOrNull()?.controller() == this }
.onEach {
query = it.toString()
performSearch()
}
.launchIn(viewScope)
presenter.query = query
}
private fun performSearch() {
searchRelay.call(query)
if (query.isNotEmpty()) {
searchRelay.call(presenter.query)
if (presenter.query.isNotEmpty()) {
binding.btnGlobalSearch.isVisible = true
binding.btnGlobalSearch.text =
resources?.getString(R.string.action_global_search_query, query)
resources?.getString(R.string.action_global_search_query, presenter.query)
} else {
binding.btnGlobalSearch.isVisible = false
}
@ -611,4 +569,12 @@ class LibraryController(
selectInverseRelay.call(it)
}
}
override fun onSearchViewQueryTextChange(newText: String?) {
// Ignore events if this controller isn't at the top to avoid query being reset
if (router.backstack.lastOrNull()?.controller() == this) {
presenter.query = newText ?: ""
performSearch()
}
}
}

View file

@ -12,12 +12,6 @@ import uy.kohesive.injekt.api.get
*/
open class SettingsSearchPresenter : BasePresenter<SettingsSearchController>() {
/**
* Query from the view.
*/
var query = ""
private set
val preferences: PreferencesHelper = Injekt.get()
override fun onCreate(savedState: Bundle?) {