Loading resources asynchronously

Resources such as layers, maps, portal items, tasks, and others, commonly rely on remote services or datasets on disk to initialize their state. The nature of accessing such data requires the resources to initialize their state asynchronously. The loadable design pattern unifies the behavior that different resources use to load metadata asynchronously, and resources that adopt this pattern are referred to as "loadable." The pattern also provides a mechanism to retry if previous attempts to load failed so that you can properly handle and recover from exceptional circumstances such as network outages or service interruption. Loadable resources appropriately handle concurrent and repeated requests to load in order to accommodate the common practice of sharing the same resource instance among various parts of an application, and also permit cancellation so that you can cancel loading a resource, for example, if the service is slow to respond. Finally, loadable resources provide information about their initialization status through explicit states that can be inspected and monitored.

Classes that conform to the loadable pattern implement the Loadable interface.

Load status

The Loadable.loadStatus property returns the state of the loadable resource. The LoadStatus can be one of four states:

  • NotLoaded—the resource has not been asked to load its metadata and its state isn't properly initialized yet.
  • Loading—the resource is in the process of loading its metadata asynchronously.
  • FailedToLoad—the resource failed to load its metadata (for example, due to network outage, or the operation was cancelled, and so on.) The error encountered is accessible through the FailedToLoad.error property.
  • Loaded—the resource successfully loaded its metadata and its state is properly initialized.

The following state transitions represent the stages that a loadable resource goes through.

Loadable flow diagram

The Loadable.loadStatus property is a StateFlow which your code can collect to monitor loadable resources, display progress, and take action when the state changes.

Results of attempting to load metadata

An attempt to load the metadata for a resource has the following results:

  • The Loadable.loadStatus property is updated with a new load status. The property is a StateFlow that emits the new load status, and your code can collect the load status changes.

  • A function that attempts to load the metadata returns a Kotlin Result<Unit>. When the call toload() or retryLoad() returns, the value property of the loadStatus state flow will reflect the success or failure of the load process. The loadStatus.value property may be queried without calling collect. Using this fact, the programmer can write less burdensome sequential code in a launched CoroutineScope to execute the asynchronous loading of metadata.

  • Kotlin also defines Result extension functions onSuccess() and onFailure(), with each taking an action parameter that is a function type. You can call these extension functions and supply a trailing lambda for the action. In that case, your onSuccess() lambda will be executed if the loading function succeeded in loading the metadata, and your onFailure() lambda will be executed if the loading function failed to load the metadata.

Loading

A resource starts loading its metadata asynchronously when load() is invoked either through an explicit call or when another object requires that the resource is loaded. For example, ArcGISMap.load() will be invoked by MapView in preparation for displaying a map. At that time, if the current load status is NotLoaded, it changes to Loading. When the asynchronous operation completes, the Loadable.loadStatus is updated. If the operation completes successfully, the updated load status is Loaded, which means the resource has finished loading its metadata and is now properly initialized. If the operation encounters an error, the updated load status is FailedToLoad, which has an error property. Additionally, the appropriate load().onSuccess() or load().onFailure() lambda, if you supplied them, will be executed.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    LaunchedEffect(Unit) {
        // Collect the load status, which will change as your app executes.
        launch {
            map.loadStatus.collect { newValue ->
                // This lambda is executed when the map's load status changes. The newValue
                // is the new load status.
                // Note that `FailedToLoad` has an `error` property.
                Log.e("TAG", "Map's load status is ${newValue}")
            }
        }

        // When the asynchronous function load() completes,
        // the new load status will be either Loaded or FailedToLoad.
        map.load()
        if (map.loadStatus.value is LoadStatus.Loaded) {
            // At this point, you can be sure the map is loaded.
        } else {
            val newLoadStatus = map.loadStatus.value as LoadStatus.FailedToLoad
            // The error is retrievable from newLoadStatus.error
        }
    }

    MapView(
        modifier = Modifier.fillMaxSize(),
        arcGISMap = map
    )

Many times, the same resource instance is shared by different parts of the application. For example, a legend component and a table of contents component may have access to the same layer, and they both may want to access the layer's properties to populate their UI. Or the same portal instance may be shared across the application to display the user's items and groups in different parts of the application.

In order to accommodate this type of application development pattern, you can check the load status of any resource at any point in your app. The load() function can be called concurrently and repeatedly, but only one attempt is ever made to load the metadata. This makes it safe for you to liberally invoke load() on a loadable resource, without having to check if the resource is already loaded or not, and without worrying that it will make unnecessary network requests every time.

Calling load() has different results based on the load status at the time the call is made.

If the current load status is Loaded, the load status is not updated and the load().onSuccess lambda, if you defined one, is executed.

If the current load status is FailedToLoad, the load status is not updated and the load().onFailure lambda, if you defined one, is executed. In other words, a new attempt to load the resource metadata does not occur. You should use retryLoad() instead; for details, see Retry loading.

If a load operation is already in progress (Loading state) when load() is called, the call simply piggy-backs on the outstanding operation. The term "piggy-back" here means that the load() call will not result in a new attempt to load the metadata. Instead, the in-progress loading operation continues until completion, which updates load status to either Loaded or FailedToLoad. Additionally, the appropriate load().onSuccess() or load().onFailure() lambda, if you supplied them, will be executed.

Calling Loadable.load()

Status beforeTransition to Loading?Completion of load()
emits new status?
load().onSuccess or .onFailure
lambda executed
NotLoadedYes- If load succeeded: Loaded
- If load failed: FailedToLoad
- If load succeeded: onSuccess {}
- If load failed: OnFailure {}
LoadedNoNoonSuccess {}
FailedToLoadNoNoonFailure {}
LoadingNo. load() piggy-backs on the
in-progress loading operation
NoAfter in-progress loading operation completes—
- If status is now Loaded: onSuccess {}
- If status is now FailedToLoad: onFailure{}

Retry loading

A resource retries to load its metadata when retryLoad() is invoked, but only if it previously failed to load (FailedToLoad state) or wasn't loaded to begin with (NotLoaded state). The resource transitions to the Loading state and makes a new attempt to fetch its metadata. When the loading operation completes, the load status is updated to Loaded or FailedToLoad. Additionally, the appropriate load().onSuccess() or load().onFailure() lambda, if you supplied them, will be executed.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
        map.load()
        // When the asynchronous function load() completes,
        // the new load status will be either Loaded or FailedToLoad.
        if (map.loadStatus.value is LoadStatus.FailedToLoad) {
            val newLoadStatus = map.loadStatus.value as LoadStatus.FailedToLoad

            // The error is retrievable from newLoadStatus.error.
            // If the error is fixable, do it here. Then try again to load the metadata.
            map.retryLoad().onFailure{ error ->
                // The error parameter is the Throwable raised when the second attempt to load failed.
                return@onFailure
            }
        }
        // At this point in the code path, you can be sure the map is loaded.

If the resource has already fetched its metadata and initialized its state (Loaded state), retryLoad() does not attempt to load the metadata again. The function does not update the load status and the retryLoad().onSuccess() lambda, if you defined one, will be executed.

If an asynchronous operation is already in progress (Loading state) when retryLoad() is called, it simply piggy-backs on the outstanding operation. The in-progress loading operation continues until completion, which updates load status to either Loaded or FailedToLoad. Additionally, the appropriate load().onSuccess() or load().onFailure() lambda, if you supplied them, will be executed.

The main use case for this method is if the loadable failed to load previously, for example, due to network outage or service interruption. It is not meant to refresh the metadata for an already loaded resource which should instead be accomplished by creating a new instance of the loadable resource.

Calling Loadable.retryLoad()

Status beforeTransition to Loading?Completion of retryLoad()
emits new status?
retryLoad().onSuccess or .onFailure
lambda executed
NotLoaded
or
FailedToLoad
Yes- If retry succeeded: Loaded
- If retry failed: FailedToLoad
- If retry succeeded: onSuccess {}
- If retry failed: OnFailure {}
LoadedNoNoonSuccess {}
LoadingNo. retryLoad() piggy-backs on
the in-progress loading operation
NoAfter in-progress loading operation completes—
- If status is now Loaded: onSuccess {}
- If status is now FailedToLoad: onFailure{}

Cancel loading

A resource cancels any outstanding asynchronous operation to load its metadata when cancelLoad() is invoked and it transitions from the Loading to FailedToLoad state. The FailedToLoad object has an error property that reflects that the operation was cancelled.

This method should be used carefully because all enqueued callbacks for that resource instance will get invoked with an error stating that the operation was cancelled. Thus, one component in the application can cancel the load initiated by other components when sharing the same resource instance.

The cancelLoad() method does nothing if the resource is not in Loading state.

Conveniences

Cascading load dependencies

It is common for a loadable resource to depend on loading other loadable resources to properly initialize its state. For example, a portal item cannot finish loading until its parent portal finishes loading. A feature layer cannot be loaded until its feature service table is first loaded. This situation is referred to as a load dependency.

Loadable operations invoked on any resource transparently cascade through its dependency graph. This helps simplify using loadable resources and puts the responsibility on the resource to correctly establish and manage its load dependencies.

Cascading behavior leads to concise code. Loading the map causes the portal item to begin loading, which in turn initiates loading its portal. You do not have to load each one of them explicitly.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
    // Create the portal item found in the ArcGIS Online portal
    val portalItem = PortalItem(
        portal = Portal.arcGISOnline(connection = Portal.Connection.Anonymous),
        itemId = "acc027394bc84c2fb04d1ed317aac674"
    )

    // Create the map.
    val map = remember { ArcGISMap(item = portalItem) }

    LaunchedEffect(Unit) {
        // When the asynchronous function load() completes, the new load status will be
        // either Loaded or FailedToLoad.
        map.load()
        if (map.loadStatus.value is LoadStatus.FailedToLoad) {
            // The error is retrievable from newLoadStatus.error.
            return@LaunchedEffect
        }
        // At this point, you know metadata for the portal, portal item, and map has all been loaded.
    }

It is possible that dependencies may fail to load. Some dependencies might be critical without which the initiating resource cannot load successfully, for example, a portal item's dependency on its portal. If a failure is encountered while loading such a dependency, that error would bubble up to the resource that initiated the load cycle, which would also fail to load. Furthermore, retrying to load that resource would automatically retry loading the failed dependency. Other load dependencies may be incidental, such as a map's dependency on one of its operational layers, and the resource may be able to load successfully even if one of its dependency fails to load.

Overriding state before initialization

A loadable resource's state is not properly initialized until it has finished loading. Accessing properties of the resource before the resource is loaded could return null or uninitialized values that might change when the resource finishes loading. Therefore, it is always advisable to wait until the resource has finished loading before accessing its properties. However, many times, especially while prototyping, you may want to change a property value before the resource is loaded without regard for its proper initialized value. For instance, you may want to change the scale visibility of a layer or the initial viewpoint of a map. To simplify this workflow without requiring that the resource first be loaded, loadable resources permit overriding their properties before the resource has finished loading and the overridden value will take precedence over the value specified in the resource's metadata.

You can override the min/max scale properties of a layer without having to load it first.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
    // Create the map.
    val map = remember { ArcGISMap(BasemapStyle.ArcGISImageryStandard) }

    // Create an ArcGIS map image layer and set min/max scale before loading the layer
    val layer = ArcGISMapImageLayer(url = "https://sampleserver5.arcgisonline.com/arcgis/rest/services/Census/MapServer")
    layer.minScale = 10_000_000.0
    layer.maxScale = 100_000.0

    map.operationalLayers.add(layer)

    LaunchedEffect(Unit) {
        // Load the layer.
        layer.load()

        // When the asynchronous function load() completes, the new load status will be
        // either Loaded or FailedToLoad.
        if (layer.loadStatus.value is LoadStatus.FailedToLoad) {
            // The error is retrievable from newLoadStatus.error.
            return@LaunchedEffect
        }
        // At this point, you know metadata for the layer has been loaded.
        // The layer.minScale and layer.maxScale variables override those property values
        // as defined in the map server's layer.
    }

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.