# network-prefetch-demo
**Repository Path**: noskovii/network-prefetch-demo
## Basic Information
- **Project Name**: network-prefetch-demo
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 3
- **Created**: 2024-05-15
- **Last Updated**: 2024-05-15
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
ArkUI PoC demo app for images prefetching
## Prerequisites
- HarmonyOS latest (3.0.0.22 and higher - built after April 12, 2024)
- DevEco 5.0.1.100
- SDK: HarmonyOS NEXT Developer Beta1 (build number B.0.13)
## Project structure
1. [lib_prefetch](./lib_prefetch) to be used by app developers. Here is introduced BasicScrollingPrefetcher which is responsible for dynamic content downloading.
2. [Demo app](./examples/prefetch_example) showing lib_prefetch usage. All app is single List, components of which prefetched in advanced. Prefetch size selected dynamically so that we achieve fast download speed without downloading too much images.
## How to build and run Demo app
1. Download DevEco
2. Install SDK
3. Update hvigor (click on solution)

4. Add signing configs (open File -> Project Structure -> Signing Configs. Enable "Support HarmonyOS" and "Automatically generate signature" and press "Sign In")
5. Sync project
6. Build and run project
## Solution description
### 1. Preamble
To improve scrolling experience and minimize number of white blocks and their on-screen time, currently the only mechanism provided by AkrUI API is `cachedCount` parameter, which can be applied to ``, `` and `` components. This parameter sets the number of items pre-loaded and pre-rendered outside of the screen area, like in the code snippet below:
```typescript
List() {
LazyForEach(this.dataSource, () => { ListItem() { } })
}
.cachedCount(20)
```
To further improve user experience and provide app developer with additional abilities to prefetch data items the approach developed by RRI OS Networking team can be used. There are two interfaces introduced [IPrefetcher.ets](./lib_prefetch/src/main/ets/IPrefetcher.ets) and [IDataSourcePrefetching.ets](./lib_prefetch/src/main/ets/IDataSourcePrefetching.ets). The prefetcher logic proposed is implemented in [BasicScrollingPrefetcher.ets](./lib_prefetch/src/main/ets/BasicScrollingPrefetcher.ets). According to the measurements performed [BasicScrollingPrefetcher.ets](./lib_prefetch/src/main/ets/BasicScrollingPrefetcher.ets) can give a noticeable gain in scrolling experience and CPU savings in comparison with conventional `cachedCount` parametrization.
### 2. Used entities and interfaces
The responsibility to prefetch data items from network before they get appeared on the screen is divided into two parts. First, it is supposed that dataSource is capable of downloading required data by itself and hence can serve as a data cache, storing and managing data items, like images, either in memory or in file system. For that purpose app developer should implement `IDataSourcePrefetching` interface, in particular its `prefetch()` and `cancel()` methods:
```typescript
interface IDataSourcePrefetching extends IDataSource {
prefetch(index: number): Promise; // prefetches the data item that matches the specified index
cancel(index: number): Promise; // cancels prefething / downloading requests for the specified data item
};
```
It is highly recommended to use **'rcp'** high-performance HTTP engine from **'@hms.collaboration.rcp'** to prefetch data via HTTP(S) protocols. So the implementation could look like:
```typescript
import rcp from '@hms.collaboration.rcp';
...
class DataSourcePrefetchingRCP implements IDataSourcePrefetching {
private data: Array;
private requestsInFlight: HashMap;
private requestsCancelled: HashMap;
private session: rcp.Session;
...
public async prefetch(index: number) {
const item = this.data[index];
if (item.cachedImagePath === undefined || this.requestsInFlight.hasKey(index)) {
return Promise.reject(); // already in prefetching
}
if(item.cachedImagePath !== null) {
return Promise.resolve(); // already prefetched
}
const request = new rcp.Request(item.imageUrl, 'GET');
this.requestsInFlight.set(index, request);
try {
const response = await this.session.fetch(request);
if (response.statusCode !== 200) {
handleError();
return Promise.reject();
}
const path = `file://${this.cachePath}/${item.imageId}.jpg`;
item.cachedImagePath = undefined;
return (async () => {
let file = await fs.open(path, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
await fs.write(file.fd, response.body);
await fs.close(file);
item.cachedImagePath = path;
})().catch(handleError);
}
catch { ... }
...
}
...
public async cancel(index: number) {
if (this.requestsInFlight.hasKey(index) && !this.requestsCancelled.hasKey(index)) {
const request = this.requestsInFlight.get(index);
this.requestsCancelled.set(index, request);
this.session.cancel(request);
}
}
...
}
```
Full implementation of `DataSourcePrefetchingRCP` class can be found in [DataSourcePrefetchingRCP.ets](./examples/prefetch_example/src/main/ets/viewmodel/DataSourcePrefetchingRCP.ets) file.
In the approach described it is implied that dataSource should not return data item's URLs in any way, but instead it should return file path to the cached data when it has been successfully prefetched or null value otherwise. In the example above it means that `item.cachedImagePath` should be provided to `` component instead of providing `item.imageUrl`:
```typescript
@Component
struct ListItemComponent {
@ObjectLink private item: dataItem;
build() {
Row() {
Image(this.item.cachedImagePath ?? $r('app.media.default'))
}
}
}
```
```typescript
@Observed
class dataItem {
...
imageUrl: string;
cachedImagePath: ResourceStr | null | undefined = null;
...
}
```
Please note `@Observed` and `@ObjectLink` decorators used for data item to trigger UI update when `cachedImagePath` gets filled in by the dataSource.
It is _not_ supposed to provide app developer with _any_ kind of dataSource implementation, since it is application specific and is highly coupled with app business domain, but instead publish `DataSourcePrefetchingRCP` implementation in documentation as an example and best practice for building high performance scrolling experience for ``, `` and `` components.
The second part of the solution described is implementation of `IPrefetcher` interface which is supposed to make conscious decisions about what data items to prefetch and what prefetching requests to cancel based on the realtime changes of visible on-screen area due to scrolling events and variety of prefetching response times caused by changing networking conditions. In other words, that is `IPrefetcher` implementation which drives dataSource prefetch and cancellation execution.
```typescript
export interface IPrefetcher {
setDataSource(ds: IDataSourcePrefetching): void; // sets dataSource instance satisfying the requirements above
visibleAreaChanged(minVisible: number, maxVisible: number): void; // updates visible area boundaries
};
```
As part of the solution it is supposed that ArkUI will be shipped with `BasicScrollingPrefetcher` implementation developed by RRI OS Networking team and which is generic enough to outperform any static `cachedCount` value, so that it can be re-used by app developers in similar scenarios. The internals of `BasicScrollingPrefetcher` and the details of how it works can be found in accompanying presentation **"White blocks problem solution"** on request.
Full implementation of `BasicScrollingPrefetcher` class can be found in [BasicScrollingPrefetcher.ets](./lib_prefetch/src/main/ets/BasicScrollingPrefetcher.ets) file.
### 3. Usage in App
Current implementation does not imply any extensions of ArkUI API as app developer may use `onScrollIndex()` callback to inform `IPrefetcher` implementation about visible area changes as it is shown in the example below:
```typescript
@Component
struct MainComponent {
...
dataSource = new DataSourcePrefetchingRCP();
prefetcher = new BasicScrollingPrefetcher();
this.prefetcher.setDataSource(this.dataSource);
build() {
Column() {
List() {
LazyForEach(this.dataSource, (item: dataItem) => {
ListItem() {
ListItemComponent( { item: item } )
}
}, (item: dataItem) => item.itemId.toString())
}
.cachedCount(5)
.onScrollIndex((start, end) => {
this.prefetcher.visibleAreaChanged(start, end);
})
}
}
}
```
However, passing visible area boundaries around seems excessive and it could make sense to implement such notification right within `LazyForEach` or for the containers like ``, `` and ``, but such ArkUI API extensions should be discussed separately...