DEV Community

HarmonyOS
HarmonyOS

Posted on

🌤️Weather Forecast Application Utilizing Deferred Background Tasks by HarmonyOS Next

Read the original article:🌤️Weather Forecast Application Utilizing Deferred Background Tasks by HarmonyOS Next

🚩Introduction

Hello Everyone! In this article, we will create a simple weather forecast application that demonstrates the use of deferred tasks in HarmonyOS Next. Let me give you an overview of what the app will do. Essentially, it will display the weather forecast for the day and will periodically request updated forecast data to remain current with any changes.

If you're not familiar with background tasks and would like to learn more about them, you can refer to my previous article. However, let me briefly explain what a deferred task is. A deferred task is a type of background task in HarmonyOS Next that allows apps to run non-urgent tasks in the background, such as syncing emails or uploading logs. The system schedules deferred tasks based on specific conditions, like battery level and network availability.

🤔Trying to Find the Best Approach

As mentioned in the title, we are creating a weather forecast app. While it is possible to develop it without background tasks, let's consider some potential scenarios:

  1. The app can request information whenever it is opened. However, if this approach is used, it could lead to situations where the server becomes overloaded, potentially causing crashes or long wait times during startup.
  2. Users may want to access the app while offline. In this case, if the device is not connected to the internet, it cannot retrieve the latest data, leaving users with outdated information.
  3. If a user opens the app after a short break of just one minute, the app will request new data again, leading to unnecessary use of time and resources, and in the case of wearable devices, the resource usage is so important🪫.

To address these issues, we can utilize deferred tasks. First, the system can check the device's network and battery status to determine the best times to trigger work and get updates. This way, the app can always provide updated information. If the device is offline, it will display the most recent data obtained while connected to the internet. Additionally, the system tries to run deferred tasks based on the frequency of app usage, ensuring that resources are not wasted. A good strategy would be to save the timestamp of the last update for each request. If the last update was more than three hours ago and the network is available, the app should request at startup. Otherwise, it should continue using the most recent data available.

🚀Implementation

First, let's look at the app's structure. We have the component WeatherPage. We have WeatherService to make requests to fetch the latest updates. We have abilities by default. We have model classes WeatherModels. We have an Index to show component pages in there. We have TaskManager to manage deferred tasks that we create. We have a ViewModel that serves to view, and maybe the most important one is that we have a file named WeatherWorker, which runs in the background when the system is called.

Here is the basic UI of our application. Since the article about creating and managing deferred tasks, I won’t go through the creation of this process, but if you are curious about it, you can look at it from here.

Let’s dive into the Deferred Tasks part.

To use deferred tasks, you need to create a class that extends WorkSchedulerExtensionAbility. This acts like a lightweight background worker. The system triggers it when certain conditions are met, like being on Wi-Fi or charging.

In your app, you schedule the task using workScheduler.startWork(), and when triggered, the system calls onWorkStart() so you can run your logic, such as syncing weather data. When finished (or if time runs out), onWorkStop() is called automatically.

In the figure below, you can understand the logic.

You can look below to see how it applies to our scenario. OnWorkStart is called when the system decides to run our background tasks by the device situation at that time. When onWorkStart is run, we have a maximum of 2 minutes to do our task in the background. In this example, we request updates on the forecast. After 2 minutes, if we don’t cancel the task manually, the system forcibly closes the task. When the task is closed, OnWorkStop is called. In here, we can log some reports, or if we had opened the database, we can close it, or for our example, we can save the time we fetched the forecast to preferences.

import WorkSchedulerExtensionAbility from '@ohos.WorkSchedulerExtensionAbility';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { WeatherService } from '../data/service/WeatherService';
import { workScheduler } from '@kit.BackgroundTasksKit';

const DOMAIN = 0xFFFF;

export default class WeatherWorker extends WorkSchedulerExtensionAbility {
  async onWorkStart(workInfo: workScheduler.WorkInfo): Promise<void> {
    hilog.info(DOMAIN, 'WeatherWorker', 'Deferred task started');
    const forecast = await WeatherService.fetchForecast();
    // For example save forecast here to app data to show on app start.
    hilog.info(DOMAIN, 'WeatherWorker', `Fetched ${forecast.length} forecast entries`);
  }

  onWorkStop(workInfo: workScheduler.WorkInfo): void {
    hilog.info(DOMAIN, 'WeatherWorker', 'Deferred task stopped');
    // At here we can save the time we fetched forecast to the preferences as we said.
  }
}
Enter fullscreen mode Exit fullscreen mode

After we created the WorkSchedulerExtensionAbility, we need to add it to the extensionAbilities part in module.json5 file as below:

"extensionAbilities": [
      {
        "name": "EntryBackupAbility",
        "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
        "type": "backup",
        "exported": false,
        "metadata": [
          {
            "name": "ohos.extension.backup",
            "resource": "$profile:backup_config"
          }
        ],
      },
      {
        "srcEntry": "./ets/weatherworker/WeatherWorker.ets",
        "name": "WeatherWorker",
        "type": "workScheduler",
        "label": "$string:app_name"
      }
    ]
Enter fullscreen mode Exit fullscreen mode

Since we created the part, what we will do in the background when our task is given the opportunity to run, let’s create a TaskManager class to set, query, and remove deferred tasks.

In this context, we create workInfo. While creating workInfo, we can specify the conditions under which we want our task to run. For example, in our application, we might want the task to trigger only when there is a Wi-Fi connection, regardless of the battery status.

Here are the conditions we can set. You can look further from here.

Below you can view our class to do it. The important things on here are:

  • Setting the true bundle name
  • Setting the ability name to the ability you created as an extension (WeatherWorker in our case)
  • Also, if we want, we can set repeatCycleTime, isRepeat, and repeatCount to make it repeat
import { workScheduler } from '@kit.BackgroundTasksKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const DOMAIN = 0xFF00;
const TAG = 'WeatherTaskManager';

export class WeatherTaskManager {
    static readonly workId = 1;
    static readonly bundleName = 'com.dtse.simple_weather';
    static readonly abilityName = 'WeatherWorker';

    static startWeatherSyncTask(): void {
        const workInfo: workScheduler.WorkInfo = {
            workId: WeatherTaskManager.workId,
            networkType: workScheduler.NetworkType.NETWORK_TYPE_WIFI,
            bundleName: WeatherTaskManager.bundleName,
            abilityName: WeatherTaskManager.abilityName,
        };

        try {
            workScheduler.startWork(workInfo);
            hilog.info(DOMAIN, TAG, '%{public}s', '✅ startWork success');
        } catch (error) {
            hilog.error(DOMAIN, TAG, '❌ startWork failed. Code: %{public}d, Msg: %{public}s',
                (error as BusinessError).code,
                (error as BusinessError).message);
        }
    }

    static stopWeatherSyncTask(): void {
        const workInfo: workScheduler.WorkInfo = {
            workId: WeatherTaskManager.workId,
            networkType: workScheduler.NetworkType.NETWORK_TYPE_WIFI,
            bundleName: WeatherTaskManager.bundleName,
            abilityName: WeatherTaskManager.abilityName,
        };

        try {
            workScheduler.stopWork(workInfo);
            hilog.info(DOMAIN, TAG, '%{public}s', '🛑 stopWork success');
        } catch (error) {
            hilog.error(DOMAIN, TAG, '❌ stopWork failed. Code: %{public}d, Msg: %{public}s',
                (error as BusinessError).code,
                (error as BusinessError).message);
        }
    }

    static queryTasks(): void {
        workScheduler.obtainAllWorks().then((runningWorks) => {
            hilog.info(DOMAIN, TAG, '✅ Deferred tasks running: %{public}d', runningWorks.length);
            runningWorks.forEach((task) => {
                hilog.info(DOMAIN, TAG, '➡️ WorkId: %{public}d, abilityName: %{public}s',
                    task.workId,
                    task.abilityName);
            });
        }).catch((error: BusinessError) => {
            hilog.error(DOMAIN, TAG, '❌ Failed to query tasks. Code: %{public}d, Msg: %{public}s',
                error.code,
                error.message);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Lastly, we need to set when we want to create our background task. In our application, we want to set our deferred task when our app is in the background. Since onBackground is called when our application goes into the background, we will create the deferred task there.

⚠️In our example, we create a new task for everytime the app goes into the background, but in real real-life example, before the app creates a new instance of the same task, it should query if the task already exists. We created already a queryTasks function for it in TaskManager class.
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { WeatherTaskManager } from '../task/TaskManager';

const DOMAIN = 0x0000;

export default class EntryAbility extends UIAbility {
    onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
        this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
        hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
        WeatherTaskManager.queryTasks()
    }

    onDestroy(): void {
        hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
    }

    onWindowStageCreate(windowStage: window.WindowStage): void {
        // Main window is created, set main page for this ability
        hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

        windowStage.loadContent('pages/Index', (err) => {
            if (err) {
                hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
                return;
            }
            hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
        });
    }

    onWindowStageDestroy(): void {
        // Main window is destroyed, release UI related resources
        hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
    }

    onForeground(): void {
        // Ability has brought to foreground
        hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
    }

    onBackground(): void {
        hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
        WeatherTaskManager.startWeatherSyncTask();
        // Ability has back to background
        WeatherTaskManager.queryTasks()
    }
}
Enter fullscreen mode Exit fullscreen mode

After that is done, we are ready. We can debug the process from the logs:

As you see, on onCreate we query the deferred tasks, and we see there are no deferred tasks. When the app goes into the background, we set our task to run when the conditions are met by the system.

Here we can see that our task is taken by the system:

And when our conditions are met, and the system triggers our task to run, we will see our log as below.

The system intelligently decides the trigger/execution frequency of the task by looking apps usage by the user. Below, you can see the policy of the system.

🧩 Conclusion

In this article, we examined how to implement deferred tasks in HarmonyOS Next by developing a weather forecast application that keeps its data updated even when the app runs in the background. We discussed how to configure WorkInfo with specific system conditions, create a WorkSchedulerExtensionAbility, and correctly trigger it using WorkScheduler. Deferred tasks are essential for building efficient and battery-friendly applications, particularly on wearable devices with limited system resources. By offloading non-critical work to a system-managed queue, you not only enhance the user experience but also consider system health and power consumption. Whether you’re developing a fitness app that syncs data periodically or a news app that pulls updates occasionally, mastering deferred tasks allows you to gain more control over background behavior in HarmonyOS Next.

🔗References

Deferred Task (ArkTS)-Background Tasks Kit-Application Framework - HUAWEI Developers

@ohos.WorkSchedulerExtensionAbility (Deferred Task Scheduling Callbacks)-ArkTS APIs-Background Tasks Kit-Application Framework - HUAWEI Developers

Ability Kit-Application Framework - HUAWEI Developers

Written by Muaz Kartal

Top comments (0)