From 0e3c62b8fcaa7368b5f773ca68d0033432930f85 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Wed, 13 Dec 2023 11:05:25 -0800 Subject: [PATCH 01/10] removed refresh action --- .../featureformsapp/data/PortalItemRepository.kt | 12 ++++++------ .../screens/browse/MapListScreen.kt | 15 ++------------- .../screens/browse/MapListViewModel.kt | 9 ++++----- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt index 14935af46..74a18fc3a 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt @@ -62,26 +62,26 @@ class PortalItemRepository( fun observe(): Flow> = portalItemsFlow /** - * Refreshes the underlying data source to fetch the latest content. [forceUpdate] when set to - * true, will clear the existing cache. + * Refreshes the underlying data source to fetch the latest content. * * This operation is suspending and will wait until the underlying data source has finished * AND the repository has finished loading the portal items. */ suspend fun refresh( portalUri: String, - connection: Portal.Connection, - forceUpdate: Boolean = false + connection: Portal.Connection ) = withContext(dispatcher) { mutex.withLock { - if (forceUpdate) deleteAllCacheEntries() portalItems.clear() // get local items val localItems = getListOfMaps().map { ItemData(it) } // get network items + Log.e("TAG", "refresh: started remote items load", ) val remoteItems = remoteDataSource.fetchItemData(portalUri, connection) + Log.e("TAG", "refresh: got remote items", ) // load the portal items and add them to cache loadAndCachePortalItems(localItems + remoteItems) + Log.e("TAG", "refresh: caching complete", ) } } @@ -113,7 +113,7 @@ class PortalItemRepository( null } else { val thumbnailUri = portalItem.thumbnail?.let { thumbnail -> - thumbnail.load() + //thumbnail.load() thumbnail.image?.bitmap?.let { bitmap -> createThumbnail( portalItem.itemId, diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt index 3a65ca8cc..15cc8aee2 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt @@ -241,7 +241,7 @@ fun AppSearchBar( username: String, modifier: Modifier = Modifier, onQueryChange: (String) -> Unit = {}, - onRefresh: (Boolean) -> Unit = {}, + onRefresh: () -> Unit = {}, onSignOut: () -> Unit = {} ) { val focusManager = LocalFocusManager.current @@ -322,23 +322,12 @@ fun AppSearchBar( enabled = !isLoading, onClick = { expanded = false - onRefresh(false) + onRefresh() }, leadingIcon = { Icon(imageVector = Icons.Default.Refresh, contentDescription = null) } ) - DropdownMenuItem( - text = { Text(text = "Clear Cache") }, - enabled = !isLoading, - onClick = { - expanded = false - onRefresh(true) - }, - leadingIcon = { - Icon(imageVector = Icons.Default.Delete, contentDescription = null) - } - ) DropdownMenuItem( text = { Text( diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListViewModel.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListViewModel.kt index 7c0642ef8..76a09b4d1 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListViewModel.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListViewModel.kt @@ -66,7 +66,7 @@ class MapListViewModel @Inject constructor( // if the data is empty, refresh it // this is used to identify first launch if (portalItemRepository.getItemCount() == 0) { - refresh(false) + refresh() } } } @@ -80,16 +80,15 @@ class MapListViewModel @Inject constructor( } /** - * Refreshes the data. [forceUpdate] clears the local cache. + * Refreshes the data. */ - fun refresh(forceUpdate: Boolean) { + fun refresh() { if (!_isLoading.value) { viewModelScope.launch { _isLoading.emit(true) portalItemRepository.refresh( portalSettings.getPortalUrl(), - portalSettings.getPortalConnection(), - forceUpdate + portalSettings.getPortalConnection() ) _isLoading.emit(false) } From edba6fadd5244b06f44a58b80c103dc9cb7a7a9f Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Wed, 13 Dec 2023 13:56:26 -0800 Subject: [PATCH 02/10] added parallel item loading --- .../data/PortalItemRepository.kt | 102 +++++++++++------- .../toolkit/featureformsapp/di/DataModule.kt | 4 +- .../screens/browse/MapListScreen.kt | 42 +++----- 3 files changed, 85 insertions(+), 63 deletions(-) diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt index 74a18fc3a..4e7ece503 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt @@ -1,7 +1,9 @@ package com.arcgismaps.toolkit.featureformsapp.data import android.graphics.Bitmap +import android.os.FileUtils import android.util.Log +import com.arcgismaps.LoadStatus import com.arcgismaps.mapping.PortalItem import com.arcgismaps.portal.Portal import com.arcgismaps.toolkit.featureformsapp.data.local.ItemCacheDao @@ -9,16 +11,20 @@ import com.arcgismaps.toolkit.featureformsapp.data.local.ItemCacheEntry import com.arcgismaps.toolkit.featureformsapp.data.local.ItemData import com.arcgismaps.toolkit.featureformsapp.data.network.ItemRemoteDataSource import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import java.io.File import java.io.FileOutputStream +import kotlin.system.measureTimeMillis data class PortalItemData( val portalItem: PortalItem, @@ -31,6 +37,7 @@ data class PortalItemData( * mechanism. */ class PortalItemRepository( + private val scope: CoroutineScope, private val dispatcher: CoroutineDispatcher, private val remoteDataSource: ItemRemoteDataSource, private val itemCacheDao: ItemCacheDao, @@ -42,6 +49,8 @@ class PortalItemRepository( // to protect shared state of portalItems private val mutex = Mutex() + private lateinit var thumbsDirPath: String + @OptIn(ExperimentalCoroutinesApi::class) private val portalItemsFlow: Flow> = itemCacheDao.observeAll().mapLatest { entries -> @@ -56,6 +65,14 @@ class PortalItemRepository( } }.flowOn(dispatcher) + init { + scope.launch(Dispatchers.IO) { + val thumbsDir = File("$filesDir/thumbs") + if (!thumbsDir.exists()) thumbsDir.mkdirs() + thumbsDirPath = thumbsDir.absolutePath + } + } + /** * Returns the list of loaded PortalItemData as a flow. */ @@ -71,17 +88,22 @@ class PortalItemRepository( portalUri: String, connection: Portal.Connection ) = withContext(dispatcher) { + deleteAll() mutex.withLock { - portalItems.clear() // get local items val localItems = getListOfMaps().map { ItemData(it) } + val remoteItems: List // get network items - Log.e("TAG", "refresh: started remote items load", ) - val remoteItems = remoteDataSource.fetchItemData(portalUri, connection) - Log.e("TAG", "refresh: got remote items", ) + val fetchTime = measureTimeMillis { + remoteItems = remoteDataSource.fetchItemData(portalUri, connection) + } + Log.e("TAG", "refresh: got remote items $fetchTime") // load the portal items and add them to cache - loadAndCachePortalItems(localItems + remoteItems) - Log.e("TAG", "refresh: caching complete", ) + val cacheTime = measureTimeMillis { + // call your function here + loadAndCachePortalItems(localItems + remoteItems) + } + Log.e("TAG", "refresh: caching complete in $cacheTime") } } @@ -102,34 +124,36 @@ class PortalItemRepository( /** * Loads the list of [items] into loaded portal items and adds them to the Cache. */ - private suspend fun loadAndCachePortalItems(items: List) { - val entries = items.mapNotNull { itemData -> - val portalItem = PortalItem(itemData.url) - // ignore if the portal items fails to load - val result = portalItem.load().onFailure { - Log.e("PortalItemRepository", "loadAndCachePortalItems: $it") + private suspend fun loadAndCachePortalItems(items: List) = withContext(dispatcher) { + val portalItems = items.map { PortalItem(it.url) } + portalItems.map { portalItem -> + launch { + portalItem.load().onFailure { + Log.e("PortalItemRepository", "loadAndCachePortalItems: $it") + } + portalItem.thumbnail?.let { thumbnail -> + thumbnail.load() + thumbnail.image?.bitmap?.let { bitmap -> + createThumbnail(portalItem.itemId, bitmap) + } + } } - if (result.isFailure) { + }.joinAll() + + val entries = portalItems.mapNotNull { portalItem -> + // ignore if the portal items fails to load + if (portalItem.loadStatus.value is LoadStatus.FailedToLoad || portalItem.loadStatus.value is LoadStatus.NotLoaded) { null } else { - val thumbnailUri = portalItem.thumbnail?.let { thumbnail -> - //thumbnail.load() - thumbnail.image?.bitmap?.let { bitmap -> - createThumbnail( - portalItem.itemId, - bitmap - ) - } - } ?: "" ItemCacheEntry( - itemData.url, + portalItem.itemId, portalItem.toJson(), - thumbnailUri, + getThumbnailUri(portalItem.itemId), portalItem.portal.url ) } } - // purge existing items and add the updated items + // add all the items into the local cache storage createCacheEntries(entries) } @@ -144,22 +168,17 @@ class PortalItemRepository( /** * Deletes all entries in the database using the [ItemCacheDao]. */ - private suspend fun deleteAllCacheEntries() = - withContext(Dispatchers.IO) { - itemCacheDao.deleteAll() - val thumbsDir = File("$filesDir/thumbs") - if (thumbsDir.exists()) thumbsDir.deleteRecursively() - } + private suspend fun deleteAllCacheEntries() = withContext(Dispatchers.IO) { + itemCacheDao.deleteAll() + } /** - * Creates a JPEG thumbnail using the [bitmap] with [name] filename in the local files + * Creates a JPEG thumbnail using the [bitmap] with [itemId].jpg filename in the local files * directory and returns the absolute path to the file. */ - private suspend fun createThumbnail(name: String, bitmap: Bitmap): String = + private suspend fun createThumbnail(itemId: String, bitmap: Bitmap): String = withContext(Dispatchers.IO) { - val thumbsDir = File("$filesDir/thumbs") - if (!thumbsDir.exists()) thumbsDir.mkdirs() - val file = File("${thumbsDir.absolutePath}/${name}.jpg") + val file = File("${thumbsDirPath}/${itemId}.jpg") file.createNewFile() FileOutputStream(file).use { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) @@ -167,6 +186,17 @@ class PortalItemRepository( file.absolutePath } + /** + * Returns the thumbnail file path for a portal item with the [itemId]. An empty string + * is returned if a thumbnail does not exist. + */ + private suspend fun getThumbnailUri(itemId: String): String = withContext(Dispatchers.IO) { + val file = File("${thumbsDirPath}/${itemId}.jpg") + return@withContext if (file.exists()) { + file.absolutePath + } else "" + } + operator fun invoke(itemId: String): PortalItem? = portalItems[itemId] } diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/di/DataModule.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/di/DataModule.kt index 2344448d0..40b9eb3ac 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/di/DataModule.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/di/DataModule.kt @@ -30,6 +30,7 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import javax.inject.Qualifier import javax.inject.Singleton @@ -67,12 +68,13 @@ class DataModule { @Singleton @PortalItemRepo internal fun providePortalItemRepository( + @ApplicationScope scope: CoroutineScope, @IoDispatcher dispatcher: CoroutineDispatcher, @ItemRemoteSource remoteDataSource: ItemRemoteDataSource, @ItemCache itemCacheDao: ItemCacheDao, @ApplicationContext context: Context ): PortalItemRepository = - PortalItemRepository(dispatcher, remoteDataSource, itemCacheDao, context.filesDir.absolutePath) + PortalItemRepository(scope, dispatcher, remoteDataSource, itemCacheDao, context.filesDir.absolutePath) @Singleton @Provides diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt index 15cc8aee2..68336d8fe 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.with -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -32,7 +31,6 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle -import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.ExitToApp import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.outlined.Close @@ -140,7 +138,7 @@ fun MapListScreen( lastModified = item.portalItem.modified?.format("MMM dd yyyy") ?: "", shareType = item.portalItem.access.encoding.uppercase(Locale.getDefault()), - thumbnailUri = item.thumbnailUri.ifEmpty { null }, + thumbnailUri = item.thumbnailUri, modifier = Modifier .fillMaxWidth() .height(100.dp) @@ -179,8 +177,8 @@ fun MapListItem( title: String, lastModified: String, shareType: String, + thumbnailUri: String, modifier: Modifier = Modifier, - thumbnailUri: String? = null, onClick: () -> Unit = {} ) { Row( @@ -190,26 +188,17 @@ fun MapListItem( ) { Spacer(modifier = Modifier.width(20.dp)) Box { - thumbnailUri?.let { - AsyncImage( - model = it, - contentDescription = null, - modifier = Modifier - .fillMaxHeight(0.8f) - .aspectRatio(16 / 9f) - .clip(RoundedCornerShape(15.dp)), - contentScale = ContentScale.Crop - ) - } // if thumbnail is empty then use the default map placeholder - ?: Image( - painter = painterResource(id = R.drawable.ic_default_map), - contentDescription = null, - modifier = Modifier - .fillMaxHeight(0.8f) - .aspectRatio(16 / 9f) - .clip(RoundedCornerShape(15.dp)), - contentScale = ContentScale.Crop - ) + AsyncImage( + model = thumbnailUri, + contentDescription = null, + modifier = Modifier + .fillMaxHeight(0.8f) + .aspectRatio(16 / 9f) + .clip(RoundedCornerShape(15.dp)), + contentScale = ContentScale.Crop, + placeholder = painterResource(id = R.drawable.ic_default_map), + error = painterResource(id = R.drawable.ic_default_map) + ) Box( modifier = Modifier .padding(5.dp) @@ -289,7 +278,7 @@ fun AppSearchBar( Icon( imageVector = Icons.Default.AccountCircle, contentDescription = null, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(35.dp) ) } MaterialTheme( @@ -387,7 +376,8 @@ fun MapListItemPreview() { title = "Water Utility", lastModified = "June 1 2023", shareType = "Public", - modifier = Modifier.size(width = 485.dp, height = 100.dp) + modifier = Modifier.size(width = 485.dp, height = 100.dp), + thumbnailUri = "" ) } From cf9f118cf3cb82cfca28db6003638395531c24c8 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Wed, 13 Dec 2023 14:39:02 -0800 Subject: [PATCH 03/10] removed redundant code --- .../data/PortalItemRepository.kt | 82 ++++++++----------- .../screens/browse/MapListScreen.kt | 2 +- 2 files changed, 33 insertions(+), 51 deletions(-) diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt index 4e7ece503..c0f72632d 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt @@ -1,7 +1,6 @@ package com.arcgismaps.toolkit.featureformsapp.data import android.graphics.Bitmap -import android.os.FileUtils import android.util.Log import com.arcgismaps.LoadStatus import com.arcgismaps.mapping.PortalItem @@ -24,11 +23,10 @@ import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import java.io.File import java.io.FileOutputStream -import kotlin.system.measureTimeMillis data class PortalItemData( val portalItem: PortalItem, - val thumbnailUri: String + var thumbnailUri: String ) /** @@ -37,7 +35,7 @@ data class PortalItemData( * mechanism. */ class PortalItemRepository( - private val scope: CoroutineScope, + scope: CoroutineScope, private val dispatcher: CoroutineDispatcher, private val remoteDataSource: ItemRemoteDataSource, private val itemCacheDao: ItemCacheDao, @@ -66,6 +64,8 @@ class PortalItemRepository( }.flowOn(dispatcher) init { + // create the thumbnails directory if it does not exist + // and save its absolute path scope.launch(Dispatchers.IO) { val thumbsDir = File("$filesDir/thumbs") if (!thumbsDir.exists()) thumbsDir.mkdirs() @@ -92,24 +92,19 @@ class PortalItemRepository( mutex.withLock { // get local items val localItems = getListOfMaps().map { ItemData(it) } - val remoteItems: List // get network items - val fetchTime = measureTimeMillis { - remoteItems = remoteDataSource.fetchItemData(portalUri, connection) - } - Log.e("TAG", "refresh: got remote items $fetchTime") + val remoteItems = remoteDataSource.fetchItemData(portalUri, connection) // load the portal items and add them to cache - val cacheTime = measureTimeMillis { - // call your function here - loadAndCachePortalItems(localItems + remoteItems) - } - Log.e("TAG", "refresh: caching complete in $cacheTime") + loadAndCachePortalItems(localItems + remoteItems) } } - suspend fun deleteAll() = withContext(dispatcher) { + /** + * Deletes all the portal items from the local cache storage. + */ + suspend fun deleteAll() = withContext(Dispatchers.IO) { mutex.withLock { - deleteAllCacheEntries() + itemCacheDao.deleteAll() portalItems.clear() } } @@ -125,53 +120,51 @@ class PortalItemRepository( * Loads the list of [items] into loaded portal items and adds them to the Cache. */ private suspend fun loadAndCachePortalItems(items: List) = withContext(dispatcher) { - val portalItems = items.map { PortalItem(it.url) } - portalItems.map { portalItem -> + // create PortalItems from the urls + val portalItemData = items.map { + PortalItemData(PortalItem(it.url), "") + } + portalItemData.map { data -> + // load each portal item and its thumbnail in a new coroutine launch { - portalItem.load().onFailure { + data.portalItem.load().onFailure { Log.e("PortalItemRepository", "loadAndCachePortalItems: $it") } - portalItem.thumbnail?.let { thumbnail -> + data.portalItem.thumbnail?.let { thumbnail -> thumbnail.load() thumbnail.image?.bitmap?.let { bitmap -> - createThumbnail(portalItem.itemId, bitmap) + data.thumbnailUri = createThumbnail(data.portalItem.itemId, bitmap) } } } + // suspend till all the portal loading jobs are complete }.joinAll() - - val entries = portalItems.mapNotNull { portalItem -> - // ignore if the portal items fails to load - if (portalItem.loadStatus.value is LoadStatus.FailedToLoad || portalItem.loadStatus.value is LoadStatus.NotLoaded) { + // create entries to be inserted into the local cache storage. + val entries = portalItemData.mapNotNull { data -> + // ignore if the portal item fails to load + if (data.portalItem.loadStatus.value is LoadStatus.FailedToLoad) { null } else { ItemCacheEntry( - portalItem.itemId, - portalItem.toJson(), - getThumbnailUri(portalItem.itemId), - portalItem.portal.url + itemId = data.portalItem.itemId, + json = data.portalItem.toJson(), + thumbnailUri = data.thumbnailUri, + portalUrl = data.portalItem.portal.url ) } } - // add all the items into the local cache storage - createCacheEntries(entries) + // insert all the items into the local cache storage + insertCacheEntries(entries) } /** * Deletes and inserts the list of [entries] using the [ItemCacheDao]. */ - private suspend fun createCacheEntries(entries: List) = + private suspend fun insertCacheEntries(entries: List) = withContext(dispatcher) { itemCacheDao.deleteAndInsert(entries) } - /** - * Deletes all entries in the database using the [ItemCacheDao]. - */ - private suspend fun deleteAllCacheEntries() = withContext(Dispatchers.IO) { - itemCacheDao.deleteAll() - } - /** * Creates a JPEG thumbnail using the [bitmap] with [itemId].jpg filename in the local files * directory and returns the absolute path to the file. @@ -186,17 +179,6 @@ class PortalItemRepository( file.absolutePath } - /** - * Returns the thumbnail file path for a portal item with the [itemId]. An empty string - * is returned if a thumbnail does not exist. - */ - private suspend fun getThumbnailUri(itemId: String): String = withContext(Dispatchers.IO) { - val file = File("${thumbsDirPath}/${itemId}.jpg") - return@withContext if (file.exists()) { - file.absolutePath - } else "" - } - operator fun invoke(itemId: String): PortalItem? = portalItems[itemId] } diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt index 68336d8fe..b8f4dd88f 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt @@ -278,7 +278,7 @@ fun AppSearchBar( Icon( imageVector = Icons.Default.AccountCircle, contentDescription = null, - modifier = Modifier.size(35.dp) + modifier = Modifier.size(30.dp) ) } MaterialTheme( From f7b9071927f45769a73a2b032e5dadd09591b27c Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Thu, 14 Dec 2023 12:46:10 -0800 Subject: [PATCH 04/10] introduced AsyncImage and removed dependency on coil --- gradle/libs.versions.toml | 2 - .../FeatureFormsApp/app/build.gradle.kts | 2 - .../data/PortalItemRepository.kt | 7 +- .../screens/browse/AsyncImage.kt | 74 +++++++++++++++++++ .../screens/browse/MapListScreen.kt | 51 ++++++++++--- 5 files changed, 116 insertions(+), 20 deletions(-) create mode 100644 microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/AsyncImage.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 97f8d40e3..84a691b02 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,6 @@ androidxMaterialIcons = "1.4.3" androidxTestExt = "1.1.2" androidxViewmodelCompose = "2.6.1" androidxWindow = "1.2.0" -coil = "2.4.0" compileSdk = "34" compose-material3 = "1.1.0" compose-navigation = "2.7.5" @@ -50,7 +49,6 @@ androidx-test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.r androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidxEspresso" } androidx-window-core = { group = "androidx.window", name = "window-core", version.ref = "androidxWindow" } androidx-window = { group = "androidx.window", name = "window", version.ref = "androidxWindow" } -coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } hilt-android-core = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } hilt-ext-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltExt" } diff --git a/microapps/FeatureFormsApp/app/build.gradle.kts b/microapps/FeatureFormsApp/app/build.gradle.kts index 7ebffcf12..35b22a6ca 100644 --- a/microapps/FeatureFormsApp/app/build.gradle.kts +++ b/microapps/FeatureFormsApp/app/build.gradle.kts @@ -76,8 +76,6 @@ dependencies { annotationProcessor(libs.room.compiler) implementation(libs.room.ext) kapt(libs.room.compiler) - // coil - implementation(libs.coil.compose) // jetpack window manager implementation(libs.androidx.window) implementation(libs.androidx.window.core) diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt index c0f72632d..9fbce98b4 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt @@ -130,12 +130,7 @@ class PortalItemRepository( data.portalItem.load().onFailure { Log.e("PortalItemRepository", "loadAndCachePortalItems: $it") } - data.portalItem.thumbnail?.let { thumbnail -> - thumbnail.load() - thumbnail.image?.bitmap?.let { bitmap -> - data.thumbnailUri = createThumbnail(data.portalItem.itemId, bitmap) - } - } + data.portalItem.thumbnail?.load() } // suspend till all the portal loading jobs are complete }.joinAll() diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/AsyncImage.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/AsyncImage.kt new file mode 100644 index 000000000..39b1e718e --- /dev/null +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/AsyncImage.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2023 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.featureformsapp.screens.browse + +import android.graphics.drawable.BitmapDrawable +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import com.arcgismaps.portal.LoadableImage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart.UNDISPATCHED +import kotlinx.coroutines.launch + +@Composable +fun AsyncImage( + imageLoader: ImageLoader, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Fit +) { + val painter = imageLoader.image.value + Image( + painter = painter, + contentDescription = null, + modifier = modifier, + contentScale = contentScale + ) +} + +class ImageLoader( + private val loadable: LoadableImage, + scope: CoroutineScope, + placeholder: Painter, +) { + private val _image: MutableState = mutableStateOf(placeholder) + val image: State = _image + + init { + scope.launch(start = UNDISPATCHED) { + load() + } + } + + private suspend fun load() { + loadable.load().onSuccess { + loadable.image?.let { + _image.value = it.toPainter() + } + } + } +} + +fun BitmapDrawable.toPainter() = BitmapPainter(bitmap.asImageBitmap()) + diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt index b8f4dd88f..962b814dd 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.with +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -50,6 +51,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -57,6 +59,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource @@ -65,7 +68,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import coil.compose.AsyncImage +import com.arcgismaps.portal.LoadableImage import com.arcgismaps.toolkit.featureformsapp.AnimatedLoading import com.arcgismaps.toolkit.featureformsapp.R import java.time.Instant @@ -125,6 +128,7 @@ fun MapListScreen( } false -> if (uiState.data.isNotEmpty()) { + val itemThumbnailPlaceholder = painterResource(id = R.drawable.ic_default_map) LazyColumn( modifier = Modifier.fillMaxSize(), state = lazyListState, @@ -138,7 +142,8 @@ fun MapListScreen( lastModified = item.portalItem.modified?.format("MMM dd yyyy") ?: "", shareType = item.portalItem.access.encoding.uppercase(Locale.getDefault()), - thumbnailUri = item.thumbnailUri, + thumbnail = item.portalItem.thumbnail, + placeholder = itemThumbnailPlaceholder, modifier = Modifier .fillMaxWidth() .height(100.dp) @@ -177,7 +182,8 @@ fun MapListItem( title: String, lastModified: String, shareType: String, - thumbnailUri: String, + thumbnail: LoadableImage?, + placeholder: Painter, modifier: Modifier = Modifier, onClick: () -> Unit = {} ) { @@ -188,16 +194,14 @@ fun MapListItem( ) { Spacer(modifier = Modifier.width(20.dp)) Box { - AsyncImage( - model = thumbnailUri, - contentDescription = null, + MapListItemThumbnail( + loadableImage = thumbnail, + placeholder = placeholder, modifier = Modifier .fillMaxHeight(0.8f) .aspectRatio(16 / 9f) .clip(RoundedCornerShape(15.dp)), - contentScale = ContentScale.Crop, - placeholder = painterResource(id = R.drawable.ic_default_map), - error = painterResource(id = R.drawable.ic_default_map) + contentScale = ContentScale.Crop ) Box( modifier = Modifier @@ -223,6 +227,32 @@ fun MapListItem( } } +@Composable +fun MapListItemThumbnail( + loadableImage: LoadableImage?, + placeholder: Painter, + modifier: Modifier, + contentScale: ContentScale +) { + val scope = rememberCoroutineScope() + loadableImage?.let { + AsyncImage( + imageLoader = ImageLoader( + loadable = it, + scope = scope, + placeholder = placeholder, + ), + modifier = modifier, + contentScale = contentScale + ) + } ?: Image( + painter = placeholder, + contentDescription = null, + modifier = modifier, + contentScale = contentScale + ) +} + @Composable fun AppSearchBar( query: String, @@ -377,7 +407,8 @@ fun MapListItemPreview() { lastModified = "June 1 2023", shareType = "Public", modifier = Modifier.size(width = 485.dp, height = 100.dp), - thumbnailUri = "" + thumbnail = null, + placeholder = painterResource(id = R.drawable.ic_default_map) ) } From c4206dd4113c958e0dac6b9ff398830c1f4d91a4 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Thu, 14 Dec 2023 13:31:51 -0800 Subject: [PATCH 05/10] updated asyncimage params --- .../featureformsapp/screens/browse/AsyncImage.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/AsyncImage.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/AsyncImage.kt index 39b1e718e..4319662aa 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/AsyncImage.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/AsyncImage.kt @@ -22,7 +22,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.DefaultAlpha import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter @@ -36,14 +39,20 @@ import kotlinx.coroutines.launch fun AsyncImage( imageLoader: ImageLoader, modifier: Modifier = Modifier, - contentScale: ContentScale = ContentScale.Fit + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null ) { val painter = imageLoader.image.value Image( painter = painter, contentDescription = null, modifier = modifier, - contentScale = contentScale + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter ) } From cc07602b6c09426995e1d91691999e0e479dba52 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Thu, 14 Dec 2023 14:04:45 -0800 Subject: [PATCH 06/10] removed thumbnail loading from portal item repository and from the database --- .../data/PortalItemRepository.kt | 66 +++++-------------- .../data/local/ItemCacheDao.kt | 3 +- .../screens/browse/MapListScreen.kt | 25 +++---- .../screens/browse/MapListViewModel.kt | 8 +-- 4 files changed, 29 insertions(+), 73 deletions(-) diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt index 9fbce98b4..0d9ddc1fe 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt @@ -1,6 +1,5 @@ package com.arcgismaps.toolkit.featureformsapp.data -import android.graphics.Bitmap import android.util.Log import com.arcgismaps.LoadStatus import com.arcgismaps.mapping.PortalItem @@ -21,13 +20,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileOutputStream - -data class PortalItemData( - val portalItem: PortalItem, - var thumbnailUri: String -) /** * A repository to map the data source items into loaded PortalItems. This is the primary repository @@ -47,36 +39,25 @@ class PortalItemRepository( // to protect shared state of portalItems private val mutex = Mutex() - private lateinit var thumbsDirPath: String - @OptIn(ExperimentalCoroutinesApi::class) - private val portalItemsFlow: Flow> = + private val portalItemsFlow: Flow> = itemCacheDao.observeAll().mapLatest { entries -> // map the cache entries into loaded portal items entries.mapNotNull { entry -> val portal = Portal(entry.portalUrl) val portalItem = PortalItem.fromJsonOrNull(entry.json, portal) portalItem?.let { - portalItems[portalItem.itemId] = portalItem - PortalItemData(portalItem, entry.thumbnailUri) + portalItem.also { + portalItems[portalItem.itemId] = it + } } } }.flowOn(dispatcher) - init { - // create the thumbnails directory if it does not exist - // and save its absolute path - scope.launch(Dispatchers.IO) { - val thumbsDir = File("$filesDir/thumbs") - if (!thumbsDir.exists()) thumbsDir.mkdirs() - thumbsDirPath = thumbsDir.absolutePath - } - } - /** * Returns the list of loaded PortalItemData as a flow. */ - fun observe(): Flow> = portalItemsFlow + fun observe(): Flow> = portalItemsFlow /** * Refreshes the underlying data source to fetch the latest content. @@ -121,30 +102,29 @@ class PortalItemRepository( */ private suspend fun loadAndCachePortalItems(items: List) = withContext(dispatcher) { // create PortalItems from the urls - val portalItemData = items.map { - PortalItemData(PortalItem(it.url), "") + val portalItems = items.map { + PortalItem(it.url) } - portalItemData.map { data -> + portalItems.map { item -> // load each portal item and its thumbnail in a new coroutine launch { - data.portalItem.load().onFailure { + item.load().onFailure { Log.e("PortalItemRepository", "loadAndCachePortalItems: $it") } - data.portalItem.thumbnail?.load() + item.thumbnail?.load() } // suspend till all the portal loading jobs are complete }.joinAll() // create entries to be inserted into the local cache storage. - val entries = portalItemData.mapNotNull { data -> - // ignore if the portal item fails to load - if (data.portalItem.loadStatus.value is LoadStatus.FailedToLoad) { + val entries = portalItems.mapNotNull { item -> + // ignore if the portal item failed to load + if (item.loadStatus.value is LoadStatus.FailedToLoad) { null } else { ItemCacheEntry( - itemId = data.portalItem.itemId, - json = data.portalItem.toJson(), - thumbnailUri = data.thumbnailUri, - portalUrl = data.portalItem.portal.url + itemId = item.itemId, + json = item.toJson(), + portalUrl = item.portal.url ) } } @@ -160,20 +140,6 @@ class PortalItemRepository( itemCacheDao.deleteAndInsert(entries) } - /** - * Creates a JPEG thumbnail using the [bitmap] with [itemId].jpg filename in the local files - * directory and returns the absolute path to the file. - */ - private suspend fun createThumbnail(itemId: String, bitmap: Bitmap): String = - withContext(Dispatchers.IO) { - val file = File("${thumbsDirPath}/${itemId}.jpg") - file.createNewFile() - FileOutputStream(file).use { - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) - } - file.absolutePath - } - operator fun invoke(itemId: String): PortalItem? = portalItems[itemId] } diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/local/ItemCacheDao.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/local/ItemCacheDao.kt index b8bb9b7a0..6287151dc 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/local/ItemCacheDao.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/local/ItemCacheDao.kt @@ -18,7 +18,6 @@ import kotlinx.coroutines.flow.Flow data class ItemCacheEntry( @PrimaryKey val itemId: String, val json: String, - val thumbnailUri: String, val portalUrl: String ) @@ -83,7 +82,7 @@ interface ItemCacheDao { /** * The room database that contains the ItemCacheEntry table. */ -@Database(entities = [ItemCacheEntry::class], version = 1, exportSchema = false) +@Database(entities = [ItemCacheEntry::class], version = 2, exportSchema = false) abstract class ItemCacheDatabase : RoomDatabase() { abstract fun itemCacheDao() : ItemCacheDao } diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt index 962b814dd..240204b9e 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt @@ -1,11 +1,6 @@ package com.arcgismaps.toolkit.featureformsapp.screens.browse -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.with +import androidx.compose.animation.Crossfade import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -80,7 +75,6 @@ import java.util.Locale * Displays a list of PortalItems using the [mapListViewModel]. Provides a callback [onItemClick] * when an item is tapped. */ -@OptIn(ExperimentalAnimationApi::class) @Composable fun MapListScreen( modifier: Modifier = Modifier, @@ -109,12 +103,9 @@ fun MapListScreen( ) // use a cross fade animation to show a loading indicator when the data is loading // and transition to the list of portalItems once loaded - AnimatedContent( + Crossfade( targetState = uiState.isLoading, modifier = Modifier.padding(top = 88.dp), - transitionSpec = { - fadeIn(animationSpec = tween(1000)) with fadeOut() - }, label = "list fade" ) { state -> when (state) { @@ -138,17 +129,17 @@ fun MapListScreen( uiState.data ) { item -> MapListItem( - title = item.portalItem.title, - lastModified = item.portalItem.modified?.format("MMM dd yyyy") + title = item.title, + lastModified = item.modified?.format("MMM dd yyyy") ?: "", - shareType = item.portalItem.access.encoding.uppercase(Locale.getDefault()), - thumbnail = item.portalItem.thumbnail, + shareType = item.access.encoding.uppercase(Locale.getDefault()), + thumbnail = item.thumbnail, placeholder = itemThumbnailPlaceholder, modifier = Modifier .fillMaxWidth() .height(100.dp) ) { - onItemClick(item.portalItem.itemId) + onItemClick(item.itemId) } } } @@ -243,7 +234,7 @@ fun MapListItemThumbnail( placeholder = placeholder, ), modifier = modifier, - contentScale = contentScale + contentScale =contentScale ) } ?: Image( painter = placeholder, diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListViewModel.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListViewModel.kt index 76a09b4d1..d7e1e6192 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListViewModel.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListViewModel.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.arcgismaps.ArcGISEnvironment -import com.arcgismaps.toolkit.featureformsapp.data.PortalItemData +import com.arcgismaps.mapping.PortalItem import com.arcgismaps.toolkit.featureformsapp.data.PortalItemRepository import com.arcgismaps.toolkit.featureformsapp.data.PortalSettings import com.arcgismaps.toolkit.featureformsapp.navigation.NavigationRoute @@ -22,7 +22,7 @@ import javax.inject.Inject data class MapListUIState( val isLoading: Boolean, val searchText: String, - val data: List + val data: List ) /** @@ -51,8 +51,8 @@ class MapListViewModel @Inject constructor( ) { isLoading, searchText, portalItemData -> val data = portalItemData.filter { searchText.isEmpty() - || it.portalItem.title.uppercase().contains(searchText.uppercase()) - || it.portalItem.itemId.contains(searchText) + || it.title.uppercase().contains(searchText.uppercase()) + || it.itemId.contains(searchText) } MapListUIState(isLoading, searchText, data) }.stateIn( From eb21aa10eeae3929be01047d5e978c921d75fdc6 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Thu, 14 Dec 2023 14:10:56 -0800 Subject: [PATCH 07/10] added comments --- .../featureformsapp/screens/browse/AsyncImage.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/AsyncImage.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/AsyncImage.kt index 4319662aa..e990eec48 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/AsyncImage.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/AsyncImage.kt @@ -35,6 +35,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart.UNDISPATCHED import kotlinx.coroutines.launch +/** + * Loads an image asynchronously using the [ImageLoader]. + */ @Composable fun AsyncImage( imageLoader: ImageLoader, @@ -56,6 +59,14 @@ fun AsyncImage( ) } +/** + * A model to asynchronously load the image from a [LoadableImage]. Once the loading is complete + * the loaded image is presented via [image] State. + * + * @param loadable the [LoadableImage] to load. + * @param scope the CoroutineScope to run the loading job on. + * @param placeholder the placeholder image to show until the loading is complete. + */ class ImageLoader( private val loadable: LoadableImage, scope: CoroutineScope, @@ -73,11 +84,8 @@ class ImageLoader( private suspend fun load() { loadable.load().onSuccess { loadable.image?.let { - _image.value = it.toPainter() + _image.value = BitmapPainter(it.bitmap.asImageBitmap()) } } } } - -fun BitmapDrawable.toPainter() = BitmapPainter(bitmap.asImageBitmap()) - From 9c0cce6edba203cb2458e2d6052882129f6fafea Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Thu, 14 Dec 2023 14:12:37 -0800 Subject: [PATCH 08/10] removed coroutinescope dependency for portal item repo --- .../toolkit/featureformsapp/data/PortalItemRepository.kt | 2 -- .../com/arcgismaps/toolkit/featureformsapp/di/DataModule.kt | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt index 0d9ddc1fe..ddb840417 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt @@ -9,7 +9,6 @@ import com.arcgismaps.toolkit.featureformsapp.data.local.ItemCacheEntry import com.arcgismaps.toolkit.featureformsapp.data.local.ItemData import com.arcgismaps.toolkit.featureformsapp.data.network.ItemRemoteDataSource import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -27,7 +26,6 @@ import kotlinx.coroutines.withContext * mechanism. */ class PortalItemRepository( - scope: CoroutineScope, private val dispatcher: CoroutineDispatcher, private val remoteDataSource: ItemRemoteDataSource, private val itemCacheDao: ItemCacheDao, diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/di/DataModule.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/di/DataModule.kt index 40b9eb3ac..2344448d0 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/di/DataModule.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/di/DataModule.kt @@ -30,7 +30,6 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope import javax.inject.Qualifier import javax.inject.Singleton @@ -68,13 +67,12 @@ class DataModule { @Singleton @PortalItemRepo internal fun providePortalItemRepository( - @ApplicationScope scope: CoroutineScope, @IoDispatcher dispatcher: CoroutineDispatcher, @ItemRemoteSource remoteDataSource: ItemRemoteDataSource, @ItemCache itemCacheDao: ItemCacheDao, @ApplicationContext context: Context ): PortalItemRepository = - PortalItemRepository(scope, dispatcher, remoteDataSource, itemCacheDao, context.filesDir.absolutePath) + PortalItemRepository(dispatcher, remoteDataSource, itemCacheDao, context.filesDir.absolutePath) @Singleton @Provides From 90756841e71dee80a2699a87c20964d5f4ca66a8 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Thu, 14 Dec 2023 14:22:59 -0800 Subject: [PATCH 09/10] added remember for imageloader --- .../featureformsapp/screens/browse/MapListScreen.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt index 240204b9e..fb3d2dd74 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/screens/browse/MapListScreen.kt @@ -227,12 +227,15 @@ fun MapListItemThumbnail( ) { val scope = rememberCoroutineScope() loadableImage?.let { - AsyncImage( - imageLoader = ImageLoader( + val imageLoader = remember { + ImageLoader( loadable = it, scope = scope, placeholder = placeholder, - ), + ) + } + AsyncImage( + imageLoader = imageLoader, modifier = modifier, contentScale =contentScale ) From 9b18d291d44ec842e1fe0409074d96490c2dd128 Mon Sep 17 00:00:00 2001 From: Kaushik Meesala Date: Thu, 14 Dec 2023 14:25:38 -0800 Subject: [PATCH 10/10] Update PortalItemRepository.kt --- .../toolkit/featureformsapp/data/PortalItemRepository.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt index ddb840417..14af6a570 100644 --- a/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt +++ b/microapps/FeatureFormsApp/app/src/main/java/com/arcgismaps/toolkit/featureformsapp/data/PortalItemRepository.kt @@ -67,8 +67,10 @@ class PortalItemRepository( portalUri: String, connection: Portal.Connection ) = withContext(dispatcher) { - deleteAll() mutex.withLock { + // delete existing cache items + itemCacheDao.deleteAll() + portalItems.clear() // get local items val localItems = getListOfMaps().map { ItemData(it) } // get network items