Update: Replace CursorOwner's de-/attaching with VM's fillList

This commit is contained in:
machiav3lli 2022-01-03 23:36:04 +01:00
parent bedf5cad3f
commit 3082e7153d
7 changed files with 178 additions and 58 deletions

View File

@ -171,6 +171,97 @@ interface ProductDao : BaseDao<Product> {
return query(SimpleSQLiteQuery(builder.build())) return query(SimpleSQLiteQuery(builder.build()))
} }
@RawQuery
fun queryList(
query: SupportSQLiteQuery
): List<Product>
// TODO optimize and simplify
@Transaction
fun queryList(
installed: Boolean, updates: Boolean, searchQuery: String,
section: ProductItem.Section, order: ProductItem.Order
): List<Product> {
val builder = QueryBuilder()
val signatureMatches = """installed.${ROW_SIGNATURE} IS NOT NULL AND
product.${ROW_SIGNATURES} LIKE ('%.' || installed.${ROW_SIGNATURE} || '.%') AND
product.${ROW_SIGNATURES} != ''"""
builder += """SELECT product.rowid AS _id, product.${ROW_REPOSITORY_ID},
product.${ROW_PACKAGE_NAME}, product.${ROW_NAME},
product.${ROW_SUMMARY}, installed.${ROW_VERSION},
(COALESCE(lock.${ROW_VERSION_CODE}, -1) NOT IN (0, product.${ROW_VERSION_CODE}) AND
product.${ROW_COMPATIBLE} != 0 AND product.${ROW_VERSION_CODE} >
COALESCE(installed.${ROW_VERSION_CODE}, 0xffffffff) AND $signatureMatches)
AS ${ROW_CAN_UPDATE}, product.${ROW_COMPATIBLE},
product.${ROW_DATA_ITEM},"""
if (searchQuery.isNotEmpty()) {
builder += """(((product.${ROW_NAME} LIKE ? OR
product.${ROW_SUMMARY} LIKE ?) * 7) |
((product.${ROW_PACKAGE_NAME} LIKE ?) * 3) |
(product.${ROW_DESCRIPTION} LIKE ?)) AS ${ROW_MATCH_RANK},"""
builder %= List(4) { "%$searchQuery%" }
} else {
builder += "0 AS ${ROW_MATCH_RANK},"
}
builder += """MAX((product.${ROW_COMPATIBLE} AND
(installed.${ROW_SIGNATURE} IS NULL OR $signatureMatches)) ||
PRINTF('%016X', product.${ROW_VERSION_CODE})) FROM $ROW_PRODUCT_NAME AS product"""
builder += """JOIN $ROW_REPOSITORY_NAME AS repository
ON product.${ROW_REPOSITORY_ID} = repository.${ROW_ID}"""
builder += """LEFT JOIN $ROW_LOCK_NAME AS lock
ON product.${ROW_PACKAGE_NAME} = lock.${ROW_PACKAGE_NAME}"""
if (!installed && !updates) {
builder += "LEFT"
}
builder += """JOIN $ROW_INSTALLED_NAME AS installed
ON product.${ROW_PACKAGE_NAME} = installed.${ROW_PACKAGE_NAME}"""
if (section is ProductItem.Section.Category) {
builder += """JOIN $ROW_CATEGORY_NAME AS category
ON product.${ROW_PACKAGE_NAME} = category.${ROW_PACKAGE_NAME}"""
}
builder += """WHERE repository.${ROW_ENABLED} != 0 AND
repository.${ROW_DELETED} == 0"""
if (section is ProductItem.Section.Category) {
builder += "AND category.${ROW_NAME} = ?"
builder %= section.name
} else if (section is ProductItem.Section.Repository) {
builder += "AND product.${ROW_REPOSITORY_ID} = ?"
builder %= section.id.toString()
}
if (searchQuery.isNotEmpty()) {
builder += """AND $ROW_MATCH_RANK > 0"""
}
builder += "GROUP BY product.${ROW_PACKAGE_NAME} HAVING 1"
if (updates) {
builder += "AND $ROW_CAN_UPDATE"
}
builder += "ORDER BY"
if (searchQuery.isNotEmpty()) {
builder += """$ROW_MATCH_RANK DESC,"""
}
when (order) {
ProductItem.Order.NAME -> Unit
ProductItem.Order.DATE_ADDED -> builder += "product.${ROW_ADDED} DESC,"
ProductItem.Order.LAST_UPDATE -> builder += "product.${ROW_UPDATED} DESC,"
}::class
builder += "product.${ROW_NAME} COLLATE LOCALIZED ASC"
return queryList(SimpleSQLiteQuery(builder.build()))
}
} }
@Dao @Dao

View File

@ -234,35 +234,6 @@ class MainActivityX : AppCompatActivity(), LoaderManager.LoaderCallbacks<Cursor>
syncConnection.binder?.setUpdateNotificationBlocker(blockerFragment) syncConnection.binder?.setUpdateNotificationBlocker(blockerFragment)
} }
fun attachCursorOwner(callback: CursorOwner.Callback, request: CursorOwner.Request) {
val oldActiveRequest = viewModel.activeRequests[request.id]
if (oldActiveRequest?.callback != null &&
oldActiveRequest.callback != callback && oldActiveRequest.cursor != null
) {
oldActiveRequest.callback.onCursorData(oldActiveRequest.request, null)
}
val cursor = if (oldActiveRequest?.request == request && oldActiveRequest.cursor != null) {
callback.onCursorData(request, oldActiveRequest.cursor)
oldActiveRequest.cursor
} else {
null
}
viewModel.activeRequests[request.id] = CursorOwner.ActiveRequest(request, callback, cursor)
if (cursor == null) {
LoaderManager.getInstance(this).restartLoader(request.id, null, this)
}
}
fun detachCursorOwner(callback: CursorOwner.Callback) {
for (id in viewModel.activeRequests.keys) {
val activeRequest = viewModel.activeRequests[id]!!
if (activeRequest.callback == callback) {
viewModel.activeRequests[id] = activeRequest.copy(callback = null)
}
}
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> { override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
val request = viewModel.activeRequests[id]!!.request val request = viewModel.activeRequests[id]!!.request
return QueryLoader(this) { return QueryLoader(this) {

View File

@ -59,7 +59,7 @@ class ExploreFragment : MainNavFragmentX(), CursorOwner.Callback {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
mainActivityX.attachCursorOwner(this, viewModel.request(source)) viewModel.fillList(source)
viewModel.db.repositoryDao.allFlowable viewModel.db.repositoryDao.allFlowable
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.flatMapSingle { list -> RxUtils.querySingle { list.mapNotNull { it.trueData } } } .flatMapSingle { list -> RxUtils.querySingle { list.mapNotNull { it.trueData } } }
@ -68,12 +68,6 @@ class ExploreFragment : MainNavFragmentX(), CursorOwner.Callback {
.subscribe { repositories = it } .subscribe { repositories = it }
} }
override fun onDestroyView() {
super.onDestroyView()
mainActivityX.detachCursorOwner(this)
}
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
(binding.recyclerView.adapter as? AppListAdapter)?.apply { (binding.recyclerView.adapter as? AppListAdapter)?.apply {
this.cursor = cursor this.cursor = cursor

View File

@ -72,7 +72,7 @@ class InstalledFragment : MainNavFragmentX(), CursorOwner.Callback {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
mainActivityX.attachCursorOwner(this, viewModel.request(source)) viewModel.fillList(source)
viewModel.db.repositoryDao.allFlowable viewModel.db.repositoryDao.allFlowable
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.flatMapSingle { list -> RxUtils.querySingle { list.mapNotNull { it.trueData } } } .flatMapSingle { list -> RxUtils.querySingle { list.mapNotNull { it.trueData } } }
@ -81,12 +81,6 @@ class InstalledFragment : MainNavFragmentX(), CursorOwner.Callback {
.subscribe { repositories = it } .subscribe { repositories = it }
} }
override fun onDestroyView() {
super.onDestroyView()
mainActivityX.detachCursorOwner(this)
}
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
// TODO get a list instead of the cursor // TODO get a list instead of the cursor
// TODO use LiveData and observers instead of listeners // TODO use LiveData and observers instead of listeners

View File

@ -72,7 +72,7 @@ class LatestFragment : MainNavFragmentX(), CursorOwner.Callback {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
mainActivityX.attachCursorOwner(this, viewModel.request(source)) viewModel.fillList(source)
viewModel.db.repositoryDao.allFlowable viewModel.db.repositoryDao.allFlowable
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.flatMapSingle { list -> RxUtils.querySingle { list.mapNotNull { it.trueData } } } .flatMapSingle { list -> RxUtils.querySingle { list.mapNotNull { it.trueData } } }
@ -81,12 +81,6 @@ class LatestFragment : MainNavFragmentX(), CursorOwner.Callback {
.subscribe { repositories = it } .subscribe { repositories = it }
} }
override fun onDestroyView() {
super.onDestroyView()
mainActivityX.detachCursorOwner(this)
}
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
// TODO get a list instead of the cursor // TODO get a list instead of the cursor
// TODO use LiveData and observers instead of listeners // TODO use LiveData and observers instead of listeners

View File

@ -18,7 +18,7 @@ abstract class MainNavFragmentX : Fragment(), CursorOwner.Callback {
internal fun setSearchQuery(searchQuery: String) { internal fun setSearchQuery(searchQuery: String) {
viewModel.setSearchQuery(searchQuery) { viewModel.setSearchQuery(searchQuery) {
if (view != null) { if (view != null) {
mainActivityX.attachCursorOwner(this, viewModel.request(source)) viewModel.fillList(source)
} }
} }
} }
@ -26,7 +26,7 @@ abstract class MainNavFragmentX : Fragment(), CursorOwner.Callback {
internal fun setSection(section: ProductItem.Section) { internal fun setSection(section: ProductItem.Section) {
viewModel.setSection(section) { viewModel.setSection(section) {
if (view != null) { if (view != null) {
mainActivityX.attachCursorOwner(this, viewModel.request(source)) viewModel.fillList(source)
} }
} }
} }
@ -34,7 +34,7 @@ abstract class MainNavFragmentX : Fragment(), CursorOwner.Callback {
internal fun setOrder(order: ProductItem.Order) { internal fun setOrder(order: ProductItem.Order) {
viewModel.setOrder(order) { viewModel.setOrder(order) {
if (view != null) { if (view != null) {
mainActivityX.attachCursorOwner(this, viewModel.request(source)) viewModel.fillList(source)
} }
} }
} }
@ -44,4 +44,37 @@ enum class Source(val titleResId: Int, val sections: Boolean, val order: Boolean
AVAILABLE(R.string.available, true, true), AVAILABLE(R.string.available, true, true),
INSTALLED(R.string.installed, false, true), INSTALLED(R.string.installed, false, true),
UPDATES(R.string.updates, false, false) UPDATES(R.string.updates, false, false)
}
sealed class Request {
internal abstract val id: Int
data class ProductsAvailable(
val searchQuery: String, val section: ProductItem.Section,
val order: ProductItem.Order,
) : Request() {
override val id: Int
get() = 1
}
data class ProductsInstalled(
val searchQuery: String, val section: ProductItem.Section,
val order: ProductItem.Order,
) : Request() {
override val id: Int
get() = 2
}
data class ProductsUpdates(
val searchQuery: String, val section: ProductItem.Section,
val order: ProductItem.Order,
) : Request() {
override val id: Int
get() = 3
}
object Repositories : Request() {
override val id: Int
get() = 4
}
} }

View File

@ -1,11 +1,13 @@
package com.looker.droidify.ui.viewmodels package com.looker.droidify.ui.viewmodels
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.looker.droidify.database.CursorOwner
import com.looker.droidify.database.DatabaseX import com.looker.droidify.database.DatabaseX
import com.looker.droidify.database.Product
import com.looker.droidify.entity.ProductItem import com.looker.droidify.entity.ProductItem
import com.looker.droidify.ui.fragments.Request
import com.looker.droidify.ui.fragments.Source import com.looker.droidify.ui.fragments.Source
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -13,6 +15,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainNavFragmentViewModelX(val db: DatabaseX) : ViewModel() { class MainNavFragmentViewModelX(val db: DatabaseX) : ViewModel() {
@ -37,7 +40,7 @@ class MainNavFragmentViewModelX(val db: DatabaseX) : ViewModel() {
started = SharingStarted.WhileSubscribed(5000) started = SharingStarted.WhileSubscribed(5000)
) )
fun request(source: Source): CursorOwner.Request { fun request(source: Source): Request {
var mSearchQuery = "" var mSearchQuery = ""
var mSections: ProductItem.Section = ProductItem.Section.All var mSections: ProductItem.Section = ProductItem.Section.All
var mOrder: ProductItem.Order = ProductItem.Order.NAME var mOrder: ProductItem.Order = ProductItem.Order.NAME
@ -47,17 +50,17 @@ class MainNavFragmentViewModelX(val db: DatabaseX) : ViewModel() {
launch { order.collect { if (source.order) mOrder = it } } launch { order.collect { if (source.order) mOrder = it } }
} }
return when (source) { return when (source) {
Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable( Source.AVAILABLE -> Request.ProductsAvailable(
mSearchQuery, mSearchQuery,
mSections, mSections,
mOrder mOrder
) )
Source.INSTALLED -> CursorOwner.Request.ProductsInstalled( Source.INSTALLED -> Request.ProductsInstalled(
mSearchQuery, mSearchQuery,
mSections, mSections,
mOrder mOrder
) )
Source.UPDATES -> CursorOwner.Request.ProductsUpdates( Source.UPDATES -> Request.ProductsUpdates(
mSearchQuery, mSearchQuery,
mSections, mSections,
mOrder mOrder
@ -65,6 +68,46 @@ class MainNavFragmentViewModelX(val db: DatabaseX) : ViewModel() {
} }
} }
var productsList = MediatorLiveData<MutableList<Product>>()
fun fillList(source: Source) {
viewModelScope.launch {
productsList.value = query(request(source))?.toMutableList()
}
}
private suspend fun query(request: Request): List<Product>? {
return withContext(Dispatchers.IO) {
when (request) {
is Request.ProductsAvailable -> db.productDao
.queryList(
installed = false,
updates = false,
searchQuery = request.searchQuery,
section = request.section,
order = request.order
)
is Request.ProductsInstalled -> db.productDao
.queryList(
installed = true,
updates = false,
searchQuery = request.searchQuery,
section = request.section,
order = request.order
)
is Request.ProductsUpdates -> db.productDao
.queryList(
installed = true,
updates = true,
searchQuery = request.searchQuery,
section = request.section,
order = request.order
)
else -> listOf()
}
}
}
fun setSection(newSection: ProductItem.Section, perform: () -> Unit) { fun setSection(newSection: ProductItem.Section, perform: () -> Unit) {
viewModelScope.launch { viewModelScope.launch {
if (newSection != sections.value) { if (newSection != sections.value) {