Switched to using the AsyncListDiffer in EntriesAdapter.kt

This commit is contained in:
Arty Bishop committed 2021-03-20 14:36:20 +00:00
1 parent ca3a9e900f
commit 46bf23171f
7 files changed
+168 -137

No files matched your search

@@ -33,10 +33,7 @@ interface SatelliteDao {
fun getAllSatItems(): Flow<List<SatItem>>
@Query("SELECT tle FROM entries WHERE isSelected = 1")
suspend fun getSelectedSatellites(): List<Satellite>
@Query("SELECT catNum FROM entries WHERE isSelected = 1")
suspend fun getSelectedCatNums(): List<Int>
fun getSelectedSatellitesFlow(): Flow<List<Satellite>>
@Query("SELECT * FROM transmitters WHERE isAlive = 1 ORDER BY mode ASC")
fun getAllTransmitters(): Flow<List<SatTrans>>
@@ -44,6 +41,12 @@ interface SatelliteDao {
@Query("SELECT * FROM transmitters WHERE isAlive = 1 and catNum = :catNum")
fun getTransmittersByCatNum(catNum: Int): Flow<List<SatTrans>>
@Query("SELECT tle FROM entries WHERE isSelected = 1")
suspend fun getSelectedSatellites(): List<Satellite>
@Query("SELECT catNum FROM entries WHERE isSelected = 1")
suspend fun getSelectedCatNums(): List<Int>
// Insert
@Insert(onConflict = OnConflictStrategy.REPLACE)
@@ -54,31 +57,31 @@ interface SatelliteDao {
// Update
@Query("UPDATE entries SET isSelected = 0")
suspend fun clearSelection()
@Query("UPDATE entries SET isSelected = :isSelected WHERE catNum = :catNum")
suspend fun updateSelectionByCatNum(catNum: Int, isSelected: Boolean)
@Transaction
suspend fun updateSelection(catNums: List<Int>) {
clearSelection()
suspend fun updateEntriesSelection(catNums: List<Int>, isSelected: Boolean) {
clearEntriesSelection()
catNums.forEach { catNum ->
updateSelectionByCatNum(catNum, true)
updateItemSelection(catNum, true)
}
}
@Query("UPDATE entries SET isSelected = 0")
suspend fun clearEntriesSelection()
@Query("UPDATE entries SET isSelected = :isSelected WHERE catNum = :catNum")
suspend fun updateItemSelection(catNum: Int, isSelected: Boolean)
// Delete
@Query("DELETE from entries")
suspend fun deleteEntries()
@Query("DELETE from transmitters")
suspend fun deleteTransmitters()
@Transaction
suspend fun deleteAllData() {
deleteEntries()
deleteTransmitters()
}
@Query("DELETE from entries")
suspend fun deleteEntries()
@Query("DELETE from transmitters")
suspend fun deleteTransmitters()
}
@@ -21,12 +21,13 @@ import android.content.ContentResolver
import android.net.Uri
import com.github.amsacode.predict4java.Satellite
import com.github.amsacode.predict4java.TLE
import com.rtbishop.look4sat.data.api.SatelliteService
import com.rtbishop.look4sat.data.database.SatelliteDao
import com.rtbishop.look4sat.data.model.SatEntry
import com.rtbishop.look4sat.data.model.SatItem
import com.rtbishop.look4sat.data.model.SatTrans
import com.rtbishop.look4sat.data.model.TleSource
import com.rtbishop.look4sat.di.IoDispatcher
import com.rtbishop.look4sat.data.database.SatelliteDao
import com.rtbishop.look4sat.data.api.SatelliteService
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
@@ -45,9 +46,21 @@ class SatelliteRepo @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) {
val satDataFlow = satelliteDao.getAllSatItems()
// Query
fun getTransmittersForSat(catNum: Int): Flow<List<SatTrans>> {
fun getAllSatItems(): Flow<List<SatItem>> {
return satelliteDao.getAllSatItems()
}
fun getSelectedSatellitesFlow(): Flow<List<Satellite>> {
return satelliteDao.getSelectedSatellitesFlow()
}
fun getAllTransmitters(): Flow<List<SatTrans>> {
return satelliteDao.getAllTransmitters()
}
fun getSatTransmitters(catNum: Int): Flow<List<SatTrans>> {
return satelliteDao.getTransmittersByCatNum(catNum)
}
@@ -55,11 +68,9 @@ class SatelliteRepo @Inject constructor(
return satelliteDao.getSelectedSatellites()
}
suspend fun updateEntriesSelection(catNums: List<Int>) {
satelliteDao.updateSelection(catNums)
}
// Insert
suspend fun updateSatDataFromFile(uri: Uri) {
suspend fun importSatDataFromFile(uri: Uri) {
runCatching {
resolver.openInputStream(uri)?.use { stream ->
val entries = importEntriesFromStreams(listOf(stream))
@@ -70,20 +81,20 @@ class SatelliteRepo @Inject constructor(
}
}
suspend fun updateSatDataFromWeb(sources: List<TleSource>) {
suspend fun importSatDataFromWeb(sources: List<TleSource>) {
coroutineScope {
launch { updateEntriesFromSources(sources) }
launch { updateTransmitters() }
launch { importEntriesFromSources(sources) }
launch { importTransmitters() }
}
}
private suspend fun updateEntriesFromSources(sources: List<TleSource>) {
private suspend fun importEntriesFromSources(sources: List<TleSource>) {
val streams = getStreamsForSources(sources)
val entries = importEntriesFromStreams(streams)
insertEntriesAndRestoreSelection(entries)
}
private suspend fun updateTransmitters() {
private suspend fun importTransmitters() {
val transmitters = satelliteService.fetchTransmitters()
satelliteDao.insertTransmitters(transmitters)
}
@@ -128,6 +139,22 @@ class SatelliteRepo @Inject constructor(
private suspend fun insertEntriesAndRestoreSelection(entries: List<SatEntry>) {
val selectedCatNums = satelliteDao.getSelectedCatNums()
satelliteDao.insertEntries(entries)
satelliteDao.updateSelection(selectedCatNums)
satelliteDao.updateEntriesSelection(selectedCatNums, true)
}
}
// Update
suspend fun updateEntriesSelection(catNums: List<Int>, isSelected: Boolean) {
satelliteDao.updateEntriesSelection(catNums, isSelected)
}
suspend fun updateItemSelection(catNum: Int, isSelected: Boolean) {
satelliteDao.updateItemSelection(catNum, isSelected)
}
// Delete
suspend fun deleteAllData() {
satelliteDao.deleteAllData()
}
}
@@ -57,7 +57,7 @@ class SharedViewModel @Inject constructor(
}
}
fun getTransmittersForSat(satId: Int) = satelliteRepo.getTransmittersForSat(satId).asLiveData()
fun getTransmittersForSat(satId: Int) = satelliteRepo.getSatTransmitters(satId).asLiveData()
fun calculatePasses(dateNow: Date = Date(System.currentTimeMillis())) {
_passes.value = Result.InProgress
@@ -19,58 +19,46 @@ package com.rtbishop.look4sat.ui.entriesScreen
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.SearchView
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.data.model.SatItem
import com.rtbishop.look4sat.databinding.ItemSatEntryBinding
import java.util.*
class EntriesAdapter : RecyclerView.Adapter<EntriesAdapter.SatItemHolder>(),
SearchView.OnQueryTextListener {
private val allItems = mutableListOf<SatItem>()
private val currentItems = mutableListOf<SatItem>()
private var shouldSearchAll = true
fun getItems(): List<SatItem> {
return allItems
class EntriesAdapter : RecyclerView.Adapter<EntriesAdapter.SatItemHolder>() {
private var allItems = listOf<SatItem>()
private var selectAll = true
private lateinit var entriesClickListener: EntriesClickListener
fun submitAllItems(items: List<SatItem>) {
allItems = items
listDiffer.submitList(items)
}
fun setItems(items: List<SatItem>) {
allItems.clear()
allItems.addAll(items)
currentItems.clear()
currentItems.addAll(items)
notifyDataSetChanged()
private fun submitCurrentItems(items: List<SatItem>) {
listDiffer.submitList(items)
}
fun selectAllItems() {
currentItems.forEach { it.isSelected = shouldSearchAll }
shouldSearchAll = shouldSearchAll.not()
notifyDataSetChanged()
fun setEntriesClickListener(listener: EntriesClickListener) {
entriesClickListener = listener
}
override fun onQueryTextChange(newText: String): Boolean {
currentItems.clear()
currentItems.addAll(filterItems(allItems, newText).toMutableList())
notifyDataSetChanged()
return false
}
override fun onQueryTextSubmit(query: String): Boolean {
return false
}
private fun filterItems(list: List<SatItem>, query: String): List<SatItem> {
shouldSearchAll = true
if (query.isEmpty()) return list
return try {
filterByCatNum(list, query.toInt())
} catch (e: NumberFormatException) {
filterByName(list, query)
fun filterItems(query: String) {
selectAll = true
if (query.isEmpty()) {
submitCurrentItems(allItems)
} else {
try {
val catNum = query.toInt()
submitCurrentItems(filterByCatNum(allItems, catNum))
} catch (e: NumberFormatException) {
submitCurrentItems(filterByName(allItems, query))
}
}
}
private fun filterByCatNum(list: List<SatItem>, catNum: Int): List<SatItem> {
return list.filter { it.catNum == catNum }
}
@@ -84,31 +72,52 @@ class EntriesAdapter : RecyclerView.Adapter<EntriesAdapter.SatItemHolder>(),
}
}
override fun getItemCount(): Int = currentItems.size
fun selectAllItems() {
// currentItems.forEach { it.isSelected = selectAll }
// selectAll = selectAll.not()
// notifyDataSetChanged()
// val catNums = listDiffer.currentList.map { it.catNum }
// entriesClickListener.onSelectAllClick(catNums, selectAll)
// selectAll = selectAll.not()
}
interface EntriesClickListener {
fun onItemClick(catNum: Int, isSelected: Boolean)
fun onSelectAllClick(catNums: List<Int>, isSelected: Boolean)
}
private val diffCallback = object : DiffUtil.ItemCallback<SatItem>() {
override fun areItemsTheSame(oldItem: SatItem, newItem: SatItem): Boolean {
return oldItem.catNum == newItem.catNum
}
override fun areContentsTheSame(oldItem: SatItem, newItem: SatItem): Boolean {
return oldItem.isSelected == newItem.isSelected
}
}
private val listDiffer = AsyncListDiffer(this, diffCallback)
override fun getItemCount(): Int = listDiffer.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SatItemHolder {
return SatItemHolder.from(parent)
val inflater = LayoutInflater.from(parent.context)
return SatItemHolder(ItemSatEntryBinding.inflate(inflater, parent, false))
}
override fun onBindViewHolder(holder: SatItemHolder, position: Int) {
holder.bind(currentItems[position])
holder.bind(listDiffer.currentList[position])
}
class SatItemHolder private constructor(private val binding: ItemSatEntryBinding) :
inner class SatItemHolder(private val binding: ItemSatEntryBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: SatItem) {
binding.satEntryCheckbox.text = item.name
binding.satEntryCheckbox.isChecked = item.isSelected
itemView.setOnClickListener {
item.isSelected = binding.satEntryCheckbox.isChecked
}
}
companion object {
fun from(parent: ViewGroup): SatItemHolder {
val inflater = LayoutInflater.from(parent.context)
return SatItemHolder(ItemSatEntryBinding.inflate(inflater, parent, false))
entriesClickListener.onItemClick(item.catNum, !item.isSelected)
}
}
}
@@ -20,11 +20,12 @@ package com.rtbishop.look4sat.ui.entriesScreen
import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.snackbar.Snackbar
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.data.model.Result
@@ -32,24 +33,21 @@ import com.rtbishop.look4sat.data.model.TleSource
import com.rtbishop.look4sat.databinding.FragmentEntriesBinding
import com.rtbishop.look4sat.utility.RecyclerDivider
import com.rtbishop.look4sat.utility.getNavResult
import com.rtbishop.look4sat.utility.navigateSafe
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
@FlowPreview
@AndroidEntryPoint
class EntriesFragment : Fragment(R.layout.fragment_entries) {
class EntriesFragment : Fragment(R.layout.fragment_entries), EntriesAdapter.EntriesClickListener {
private val viewModel: EntriesViewModel by viewModels()
private val filePicker =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { viewModel.updateSatDataFromFile(uri) }
uri?.let { viewModel.importSatDataFromFile(uri) }
}
private var binding: FragmentEntriesBinding? = null
private var entriesAdapter: EntriesAdapter? = null
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentEntriesBinding.bind(view)
@@ -59,24 +57,28 @@ class EntriesFragment : Fragment(R.layout.fragment_entries) {
}
private fun setupComponents() {
entriesAdapter = EntriesAdapter()
entriesAdapter = EntriesAdapter().apply {
setEntriesClickListener(this@EntriesFragment)
}
binding?.apply {
entriesRecycler.apply {
setHasFixedSize(true)
adapter = entriesAdapter
layoutManager = LinearLayoutManager(requireContext())
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
addItemDecoration(RecyclerDivider(R.drawable.rec_divider_light))
}
importWeb.setOnClickListener { showImportFromWebDialog() }
importFile.setOnClickListener { filePicker.launch("*/*") }
selectAll.setOnClickListener { entriesAdapter?.selectAllItems() }
entriesFab.setOnClickListener { navigateToPasses() }
searchBar.setOnQueryTextListener(entriesAdapter)
searchBar.addTextChangedListener {
entriesAdapter?.filterItems(it.toString())
binding?.entriesRecycler?.scrollToPosition(0)
}
searchBar.clearFocus()
}
}
@ExperimentalCoroutinesApi
private fun setupObservers() {
viewModel.satData.observe(viewLifecycleOwner, { result ->
when (result) {
@@ -84,7 +86,7 @@ class EntriesFragment : Fragment(R.layout.fragment_entries) {
if (result.data.isEmpty()) {
setEmpty()
} else {
entriesAdapter?.setItems(result.data)
entriesAdapter?.submitAllItems(result.data)
setLoaded()
}
}
@@ -94,13 +96,6 @@ class EntriesFragment : Fragment(R.layout.fragment_entries) {
is Result.Error -> {
val errorMsg = getString(R.string.entries_update_error)
Snackbar.make(requireView(), errorMsg, Snackbar.LENGTH_SHORT).show()
entriesAdapter?.let { adapter ->
if (adapter.getItems().isEmpty()) {
setEmpty()
} else {
setLoaded()
}
}
}
}
})
@@ -108,7 +103,7 @@ class EntriesFragment : Fragment(R.layout.fragment_entries) {
private fun observeSourcesResult() {
getNavResult<List<String>>(R.id.nav_entries, "sources") { result ->
viewModel.updateSatDataFromSources(result.map { TleSource(it) })
viewModel.importSatDataFromSources(result.map { TleSource(it) })
}
}
@@ -140,15 +135,12 @@ class EntriesFragment : Fragment(R.layout.fragment_entries) {
findNavController().navigate(R.id.nav_dialog_sources)
}
private fun navigateToPasses() {
binding?.searchBar?.clearFocus()
entriesAdapter?.let {
val satItems = it.getItems()
if (satItems.isNotEmpty()) {
viewModel.updateItemsSelection(satItems)
requireView().findNavController().navigateSafe(R.id.action_entries_to_passes)
}
}
override fun onItemClick(catNum: Int, isSelected: Boolean) {
viewModel.updateItemSelection(catNum, isSelected)
}
override fun onSelectAllClick(catNums: List<Int>, isSelected: Boolean) {
viewModel.updateEntriesSelection(catNums, isSelected)
}
override fun onDestroyView() {
@@ -45,26 +45,26 @@ class EntriesViewModel @Inject constructor(
) : ViewModel() {
private val satDataState = MutableSharedFlow<Result<List<SatItem>>>(replay = 0)
private val satDataFlow = satelliteRepo.satDataFlow.map { Result.Success(it) }
private val satDataFlow = satelliteRepo.getAllSatItems().map { Result.Success(it) }
val satData = flowOf(satDataState, satDataFlow).flattenMerge().asLiveData()
fun updateSatDataFromFile(uri: Uri) {
fun importSatDataFromFile(uri: Uri) {
viewModelScope.launch {
satDataState.emit(Result.InProgress)
try {
satelliteRepo.updateSatDataFromFile(uri)
satelliteRepo.importSatDataFromFile(uri)
} catch (exception: Exception) {
satDataState.emit(Result.Error(exception))
}
}
}
fun updateSatDataFromSources(sources: List<TleSource> = prefsRepo.loadTleSources()) {
fun importSatDataFromSources(sources: List<TleSource> = prefsRepo.loadTleSources()) {
viewModelScope.launch {
satDataState.emit(Result.InProgress)
val updateMillis = measureTimeMillis {
try {
satelliteRepo.updateSatDataFromWeb(sources)
satelliteRepo.importSatDataFromWeb(sources)
prefsRepo.saveTleSources(sources)
} catch (exception: Exception) {
satDataState.emit(Result.Error(exception))
@@ -74,10 +74,16 @@ class EntriesViewModel @Inject constructor(
}
}
fun updateItemsSelection(items: List<SatItem>) {
fun updateEntriesSelection(items: List<Int>, isSelected: Boolean) {
viewModelScope.launch {
val selectedEntries = items.filter { it.isSelected }.map { it.catNum }
satelliteRepo.updateEntriesSelection(selectedEntries)
// val selectedEntries = items.filter { it.isSelected }.map { it.catNum }
// satelliteRepo.updateEntriesSelection(selectedEntries, isSelected)
}
}
fun updateItemSelection(catNum: Int, isSelected: Boolean) {
viewModelScope.launch {
satelliteRepo.updateItemSelection(catNum, isSelected)
}
}
}
+6 -12
View File
@@ -51,13 +51,15 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<SearchView
<EditText
android:id="@+id/search_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginStart="20dp"
android:hint="@string/entries_search_hint"
android:iconifiedByDefault="false"
android:queryHint="@string/entries_search_hint"
android:importantForAutofill="no"
android:inputType="text"
app:layout_constraintEnd_toStartOf="@+id/select_all"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/entries_bar" />
@@ -97,6 +99,7 @@
android:layout_height="wrap_content"
android:text="@string/entries_no_data"
android:textSize="20sp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -104,14 +107,5 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/entries_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center"
android:layout_margin="@dimen/fab_padding"
android:contentDescription="@string/placeholder"
android:src="@drawable/ic_arrow" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>