DEV Community

陈杨
陈杨

Posted on

Hong Mong 5 Development Treasure Case Sharing Quick Access Riding Experience

HarmonyOS Treasure Case In-Depth: Code Implementation of a "Smooth" Shared Bike Riding Experience 🚲💻

Hello everyone! Last time I shared that awesome HarmonyOS shared bike experience case, and many friends commented that they wanted to see the code details. No problem! Let's dive into the code and see how those "smooth" experiences (scan-to-unlock, real-time status window, route planning) are actually implemented. Official docs are sometimes like treasure maps, but code is the real gold!

Core Goal Recap: Use HarmonyOS <font style="color:rgba(0, 0, 0, 0.4);background-color:rgb(252, 252, 252);">Scan Kit</font> (scan-to-unlock), <font style="color:rgba(0, 0, 0, 0.4);background-color:rgb(252, 252, 252);">Map Kit</font> (find bike navigation), <font style="color:rgba(0, 0, 0, 0.4);background-color:rgb(252, 252, 252);">Live View Kit</font> (live status window) to make the scan -> unlock -> ride -> return -> pay process extremely simple, real-time, and seamless.

🎯 Module 1: Scan-to-Unlock Page (Scan Kit)

Goal: Users can scan a code anywhere and jump directly to the unlock confirmation page for that bike, skipping the steps of opening the app and finding the entry.

Key Code Explanation (TypeScript/ArkTS)

// 1. Import key modules
import scanBarcode from '@ohos.abilityAccessCtrl'; // Core Scan Kit module
import { router } from '@kit.ArkUI'; // Page routing module
import { BusinessError } from '@kit.BasicServicesKit'; // Error handling

// 2. Scan utility class (ScanUtil.ts)
export class ScanUtil {
  public static scan(obj: Object): void {
    // 3. Configure scan options: support all code types (ALL) and 1D codes (ONE_D_CODE), allow multi-code recognition, allow selecting images from album
    let options: scanBarcode.ScanOptions = {
      scanTypes: [scanBarcode.ScanType.ALL, scanBarcode.ScanType.ONE_D_CODE],
      enableMultiMode: true,
      enableAlbum: true
    };

    try {
      // 4. Start scanning and wait for result (async Promise)
      scanBarcode.startScanForResult(getContext(obj), options)
        .then((result: scanBarcode.ScanResult) => {
          console.info('Scan result:', JSON.stringify(result));

          // 5. Key logic: determine scan type (assume CyclingConstants.SCAN_TYPE represents bike code)
          if (result.scanType === CyclingConstants.SCAN_TYPE) {
            // 6. Set app state: waiting to unlock (AppStorage is HarmonyOS state management)
            AppStorage.setOrCreate(CyclingConstants.CYCLING_STATUS, CyclingStatus.WAITING_UNLOCK);

            // 7. Core navigation! Directly route to unlock confirmation page 'pages/ConfirmUnlock'
            router.pushUrl({ url: 'pages/ConfirmUnlock' });
            // Usually, the scanned data (such as bike ID) is passed to the ConfirmUnlock page via params
          }
        })
        .catch((error: BusinessError) => {
          console.error('Scan error:', JSON.stringify(error));
          // Handle error: prompt user, retry, etc.
        });
    } catch (error) {
      console.error('Failed to start scan:', JSON.stringify(error));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Code Analysis & Key Points:

  1. Permission Request (**<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">module.json5</font>**): Camera permission is required for scanning! Must be declared in the config file:
"requestPermissions": [
  {
    "name": "ohos.permission.CAMERA",
    "reason": "For scanning shared bike QR codes", // Reason shown to user
    "usedScene": {
      "abilities": ["EntryAbility"], // Which Ability to request in
      "when": "always" // When to use
    }
  }
],
Enter fullscreen mode Exit fullscreen mode
  1. **<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">ScanOptions</font>** Flexible configuration:
    • <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">scanTypes</font>: Specify code types to recognize, very flexible.
    • <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">enableMultiMode</font>: Whether to scan multiple codes at once (usually not needed for shared bikes, turn off for faster scan).
    • <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">enableAlbum</font>: Allow selecting QR code images from album (important! Users may scan from screenshots).
  2. **<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">startScanForResult</font>****: This is the core API to start scanning, returns a <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">Promise</font>. Handle success in <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">.then()</font>, handle failure in <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">.catch()</font>.
  3. Result Handling (**<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">result</font>**):
    • <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">result.scanType</font>: Type of code recognized (QR code? Barcode?).
    • <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">result.value</font>: Data string from scan (usually contains bike unique ID, unlock command, etc.). This example is simplified; in real business, you would parse **<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">result.value</font>** to get bike info!
  4. State Management (**<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">AppStorage</font>**): HarmonyOS provides app-level state management. Here, set <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">CYCLING_STATUS = WAITING_UNLOCK</font> to tell the app "user scanned a code, waiting for unlock confirmation." This state will be used by the unlock page.
  5. **<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">router.pushUrl</font>**: The key to "direct access"! Directly navigate to the unlock confirmation page <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">pages/ConfirmUnlock</font>. The user instantly jumps from the scan page to the unlock button, skipping all intermediate steps. Usually, bike ID and other info are passed via <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">params</font>: <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">router.pushUrl({ url: 'pages/ConfirmUnlock', params: { bikeId: parsedBikeId } })</font>.

When to Call: On your home page (<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">Index</font>), shared bike feature page (<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">BikePage</font>), or even a desktop widget (<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">Card</font>) button click event, call <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">ScanUtil.scan(this)</font> to trigger scanning.


🗺️ Module 2: Smart Bike Finding & Walking Navigation (Map Kit)

Goal: On the "Find Bike" page, display user location, bike location, and draw walking route.

Key Code Explanation (Map Initialization, Location, Route Planning & Drawing)

// 1. Import key modules
import { MapComponent, mapCommon, map, navi } from '@kit.MapKit'; // Core MapKit components
import geoLocationManager from '@ohos.geoLocationManager'; // Location management
import abilityAccessCtrl from '@ohos.abilityAccessCtrl'; // Permission request
import { BusinessError } from '@kit.BasicServicesKit';

// 2. In FindBikePage.ets
@Entry
@Component
struct FindBikePage {
  // ... other state variables ...
  private mapController?: map.MapComponentController; // Map controller
  private mapPolyline?: map.MapPolyline; // Line object for drawing route
  private myPosition: mapCommon.LatLng = { latitude: 0, longitude: 0 }; // User location

  aboutToAppear(): void {
    // 3. Initialize map callback
    this.callback = async (err, mapController) => {
      if (!err) {
        this.mapController = mapController;
        this.mapController.on('mapLoad', async () => {
          // 4. Check and request location permissions
          const hasPerm = await this.checkLocationPermissions();
          if (hasPerm) {
            this.enableMyLocation(); // Enable location and get location
          }
        });
      }
    };
  }

  // 5. Check location permissions
  private async checkLocationPermissions(): Promise<boolean> {
    const atManager = abilityAccessCtrl.createAtManager();
    try {
      const permissions = [
        'ohos.permission.LOCATION',
        'ohos.permission.APPROXIMATELY_LOCATION'
      ];
      const grantStatus = await atManager.checkAccessToken(
        abilityAccessCtrl.AccessTokenID.BASE,
        permissions
      );
      return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
    } catch (error) {
      console.error('Error checking permissions', error);
      return false;
    }
  }

  // 6. Request location permissions
  private requestPermissions(): void {
    const atManager = abilityAccessCtrl.createAtManager();
    atManager.requestPermissionsFromUser(
      getContext(this) as common.UIAbilityContext,
      ['ohos.permission.LOCATION', 'ohos.permission.APPROXIMATELY_LOCATION']
    ).then(() => {
      this.enableMyLocation(); // Permissions granted, location enabled
    }).catch((err: BusinessError) => {
      console.error('Failed to request permissions', err.code, err.message);
    });
  }

  // 7. Enable location and get current location
  private enableMyLocation(): void {
    if (!this.mapController) return;
    // 7.1 Set map to show my location
    this.mapController.setMyLocationEnabled(true);
    this.mapController.setMyLocationControlsEnabled(true); // Show location button

    // 7.2 Configure location request parameters (high accuracy, first fix)
    let requestInfo: geoLocationManager.CurrentLocationRequest = {
      priority: geoLocationManager.LocationRequestPriority.FIRST_FIX,
      scenario: geoLocationManager.LocationRequestScenario.NAVIGATION,
      maxAccuracy: 50 // Accuracy requirement (meters)
    };

    // 7.3 Get current location
    geoLocationManager.getCurrentLocation(requestInfo)
      .then(async (location) => {
        console.info('Location obtained:', location.latitude, location.longitude);
        // 7.4 Coordinate conversion (WGS84 -> GCJ02, commonly used in China)
        let mapPosition: mapCommon.LatLng = await map.convertCoordinate(
          mapCommon.CoordinateType.WGS84,
          mapCommon.CoordinateType.GCJ02,
          { latitude: location.latitude, longitude: location.longitude }
        );
        // 7.5 Store user location & move map view
        this.myPosition = mapPosition;
        AppStorage.setOrCreate('userLat', mapPosition.latitude);
        AppStorage.setOrCreate('userLon', mapPosition.longitude);
        let cameraUpdate = map.newCameraPosition({
          target: mapPosition,
          zoom: 16 // Zoom to a suitable level
        });
        this.mapController?.animateCamera(cameraUpdate, 1000); // 1-second animation to move to user location
      })
      .catch((err: BusinessError) => {
        console.error('Failed to get location', err.code, err.message);
      });
  }

  // 8. Listen for map clicks (user selects bike location)
  private setupMapListeners(): void {
    this.mapController?.on('mapClick', async (clickedPosition: mapCommon.LatLng) => {
      // 8.1 Clear old markers and routes
      this.mapController?.clear();
      this.mapPolyline?.remove();

      // 8.2 Add a marker at the clicked position (Marker)
      this.marker = await MapUtil.addMarker(clickedPosition, this.mapController);

      // 8.3 Key! Start walking route planning (from user location this.myPosition to clickedPosition)
      const walkingRoutes = await MapUtil.walkingRoutes(clickedPosition, this.myPosition);
      if (walkingRoutes && walkingRoutes.routes.length > 0) {
        // 8.4 Draw planned walking route
        await MapUtil.paintRoute(walkingRoutes, this.mapPolyline, this.mapController);
      }
    });
  }

  build() {
    Column() {
      // 9. Integrate map component (core UI)
      MapComponent({
        mapOptions: { ... }, // Map initial configuration (center point, zoom level, etc.)
        mapCallback: this.callback // Callback for map load completion
      })
      .onClick(() => {
        this.setupMapListeners(); // Usually set up listeners after map load
      })
      .width('100%')
      .height('100%')
    }
  }
}

// 10. Route planning utility class (MapUtil.ts)
export class MapUtil {
  // 10.1 Walking route planning
  public static async walkingRoutes(
    destination: mapCommon.LatLng,
    origin?: mapCommon.LatLng
  ): Promise<navi.RouteResult | undefined> {
    if (!origin) return undefined;
    let params: navi.RouteParams = {
      origins: [origin], // Array of starting points (only one here)
      destination: destination, // Destination
      type: navi.RouteType.WALKING, // Walking mode
      language: 'zh_CN' // Chinese result
    };
    try {
      const result = await navi.getWalkingRoutes(params); // Call Map Kit API
      console.info('Walking route planning successful', JSON.stringify(result));
      return result;
    } catch (err) {
      console.error('Failed to plan walking route', JSON.stringify(err));
      return undefined;
    }
  }

  // 10.2 Draw route on map
  public static async paintRoute(
    routeResult: navi.RouteResult,
    mapPolyline: map.MapPolyline | undefined,
    mapController?: map.MapComponentController
  ) {
    if (!mapController || !routeResult.routes[0]?.overviewPolyline) return;
    // Clear old lines
    mapPolyline?.remove();
    // Configure new line style (blue, 20 pixels wide)
    let polylineOption: mapCommon.MapPolylineOptions = {
      points: routeResult.routes[0].overviewPolyline, // Array of coordinates for route
      clickable: true,
      width: 20,
      color: 0xFF2970FF, // ARGB blue
      zIndex: 10
    };
    // Add line to map and save reference
    mapPolyline = await mapController.addPolyline(polylineOption);
    return mapPolyline;
  }

  // ... (addMarker method similar) ...
}
Enter fullscreen mode Exit fullscreen mode

Code Analysis & Key Points:

  1. Permission (**<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">module.json5</font>**): Location permission also required; must be declared:
"requestPermissions": [
  {
    "name": "ohos.permission.LOCATION",
    "reason": "For finding nearby shared bikes and navigation"
  },
  {
    "name": "ohos.permission.APPROXIMATELY_LOCATION",
    "reason": "For more accurate bike finding"
  }
],
Enter fullscreen mode Exit fullscreen mode
  1. **<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">MapComponent</font>**: UI component for the map. <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">mapCallback</font> is triggered when the maphas finished loading, at which point it's safe to get<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">mapController</font> for manipulation.
  2. Location process (**<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">enableMyLocation</font>**):
    • <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">setMyLocationEnabled(true)</font>: Allow the map to show the user's location as a blue dot.
    • <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">getCurrentLocation</font>: Getone precise location. For continuous tracking, use <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">on('locationChange')</font> listener.
    • Coordinate conversion (**<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">convertCoordinate</font>**): Very important! The device's GPS returns coordinates in WGS84 format, while domestic map services (like GCJ02) need conversion to accurately display.
  3. Route planning (**<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">getWalkingRoutes</font>**):
    • Calling <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">navi.getWalkingRoutes(params)</font> is the core. It takes starting point(<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">origins</font>), destination(<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">destination</font>), and type(<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">WALKING</font>) as input.
    • The returned <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">RouteResult</font> contains route information, where <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">overviewPolyline</font> is a compressed series of latitude and longitude points used to draw the route.
  4. Drawing route (**<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">addPolyline</font>**):
    • Using <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">mapController.addPolyline(options)</font> to draw lines.
    • <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">options.points</font> are passed in as the coordinates of the route planning (<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">overviewPolyline</font> needs to be decoded first, example code assumes <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">MapUtil.walkingRoutes</font> internally or result has been processed)。
    • Through <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">width</font>, <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">color</font> are used to customize the appearance of the route.
  5. Interactive process: User clicks on the map -> get coordinates of clicked point -> clear old data -> add new Marker -> plan and draw walking route to that Marker.

✨ Module 3: Live Status Window Display (Live View Kit)

Goal: After unlocking, display riding status/duration/cost in the status bar (capsule), notification center, and lock screen; change to pending payment after returning; end after payment.

Key Code Explanation (Creating, Updating, Destroying Live Status Window)

// 1. Import key modules
import liveViewManager, { LiveViewDataBuilder, TextLayoutBuilder, TextCapsuleBuilder, LiveNotification, LiveViewContext } from '@kit.LiveViewKit';
import { BusinessError } from '@kit.BasicServicesKit';
import wantAgent from '@ohos.app.ability.wantAgent'; // Used to define click action

// 2. Live status control class (LiveViewController.ts)
export class LiveViewController {
  private liveViewData?: liveViewManager.LiveViewData; // Current live status window data
  private liveNotification?: LiveNotification; // Live status window notification object

  // 3. Create and display live status window (called when user clicks "unlock")
  public async startLiveView(context: LiveViewContext): Promise<liveViewManager.LiveViewResult> {
    // 3.1 Build default live status window data (riding state)
    this.liveViewData = await this.buildDefaultView(context);

    // 3.2 Create LiveNotification object (associate with business type, e.g., 'RENT')
    let env: liveViewManager.LiveViewEnvironment = { id: 0, event: 'RENT' };
    this.liveNotification = LiveNotification.from(context, env);

    // 3.3 Create and display live status window!
    return await this.liveNotification.create(this.liveViewData);
  }

  // 4. Build default data for riding state live status window
  private static async buildDefaultView(context: LiveViewContext): Promise<liveViewManager.LiveViewData> {
    // 4.1 Build expanded card layout (card seen in lock screen/notification center)
    const layoutData = new TextLayoutBuilder()
      .setTitle('Riding') // Card title
      .setContent('Ridden 0 minutes') // Card content (initial 0 minutes)
      .setDescPic('bike_icon.png'); // Icon on card

    // 4.2 Build capsule (small capsule seen in status bar)
    const capsule = new TextCapsuleBuilder()
      .setIcon('bike_small.png') // Icon for capsule
      .setBackgroundColor('#FF00FF00') // Background color for capsule (green)
      .setTitle('Riding'); // Text for capsule

    // 4.3 Build click action (click to return to app's riding page)
    const wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: context.bundleName,
          abilityName: 'EntryAbility',
          parameters: { route: 'pages/RidingPage' } // Jump to riding page
        }
      ],
      operationType: wantAgent.OperationType.START_ABILITY,
      requestCode: 0
    };
    const wantAgentObj = await wantAgent.getWantAgent(wantAgentInfo);

    // 4.4 Build complete LiveViewData
    const liveViewData = new LiveViewDataBuilder()
      .setTitle('Riding') // Main title
      .setContentText(['Ridden 0 minutes']) // Array of content text (multiple lines allowed)
      .setContentColor('#FFFFFFFF') // Color of content text (white)
      .setLayoutData(layoutData) // Set card layout
      .setCapsule(capsule) // Set capsule style
      .setWant(wantAgentObj) // Set click action
      // (Optional) Configure lock screen immersive extension Ability (see later)
      .setLiveViewLockScreenAbilityName('LiveViewLockScreenExtAbility')
      .setLiveViewLockScreenPicture('bike_lock_icon.png')
      .build(); // Build completed

    return liveViewData;
  }

  // 5. Update live status window state (riding -> pending payment -> payment completed)
  public async updateLiveView(status: number, context: LiveViewContext): Promise<liveViewManager.LiveViewResult> {
    if (!this.liveViewData || !this.liveNotification) {
      console.error('Live status window not created or data is empty');
      return { code: -1 };
    }

    switch (status) {
      case CyclingStatus.RIDING: // Riding (update timer)
        // ... update the timer text of this.liveViewData (e.g., 'Ridden 5 minutes') ...
        return await this.liveNotification.update(this.liveViewData);

      case CyclingStatus.WAITING_PAYMENT: // Return successful, pending payment
        // 5.1 Update title, content, and capsule text
        this.liveViewData.primary.title = 'Pending Payment';
        this.liveViewData.primary.content = [{ text: 'Ride ended, click to pay', textColor: '#FFFFFFFF' }];
        this.liveViewData.capsule.title = 'Pending Payment';
        // 5.2 Update click action (jump to payment page)
        this.liveViewData.primary.clickAction = await this.buildWantAgent(context, 'pages/PaymentPage');
        // 5.3 Update card layout
        this.liveViewData.primary.layoutData = new TextLayoutBuilder()
          .setTitle('Pending Payment')
          .setContent('Cost: ¥2.50')
          .setDescPic('payment_icon.png');
        return await this.liveNotification.update(this.liveViewData);

      case CyclingStatus.PAYMENT_COMPLETED: // Payment completed
        // 5.4 Update to final state
        this.liveViewData.primary.title = 'Payment Successful';
        this.liveViewData.primary.content = [{ text: 'Trip completed, thank you for using', textColor: '#FFFFFFFF' }];
        this.liveViewData.capsule.title = 'Completed';
        // 5.5 Key! Stop live status window (display final state for a few seconds before disappearing)
        return await this.liveNotification.stop(this.liveViewData);

      default:
        return { code: -1 };
    }
  }

  // ... (buildWantAgent helper method) ...
}

// 6. Lock screen immersive extension Ability for live status window (LiveViewLockScreenExtAbility.ets)
import { LiveViewLockScreenExtensionAbility, UIExtensionContentSession } from '@kit.LiveViewKit';
import hilog from '@ohos.hilog';

export default class LiveViewLockScreenExtAbility extends LiveViewLockScreenExtensionAbility {
  onSessionCreate(want: Want, session: UIExtensionContentSession) {
    hilog.info(0x0000, 'LiveViewLock', 'Lock screen extension Ability session created');
    // 6.1 Load custom lock screen live status window UI page
    session.loadContent('pages/LiveViewLockScreenPage'); // This page you design with ArkUI yourself!
  }
  // ... (other lifecycle methods onForeground, onBackground, onDestroy) ...
}
Enter fullscreen mode Exit fullscreen mode

Code Analysis & Key Points:

  1. **<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">LiveViewDataBuilder</font>**: Core tool for building live status window data. It defines:
    • Main information (**<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">primary</font>**): Title, content text/color, click action (<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">WantAgent</font>), card layout (<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">LayoutData</font>), lock screen extension ability name/parameters/image.
    • Capsule state (**<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">capsule</font>**): Icon and background color for status bar, text.
    • Other: Duration (<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">keepTime</font>) and whether to persist.
  2. State management: Live status window content is not static! <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">updateLiveView</font> method updates different parts of <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">liveViewData</font> based on business status (<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">RIDING</font>, <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">WAITING_PAYMENT</font>, <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">PAYMENT_COMPLETED</font>) dynamically <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">liveViewData</font> , then call <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">update()</font> or <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">stop()</font> to refresh the interface.
  3. **<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">WantAgent</font>**: The key to user interaction! Defines what happens when user clicks on live status window (capsule or card). The most common action is to return to a specific page of the app (like riding page, payment page).<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">wantAgent</font> module is used to build this intent.
  4. **<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">LiveNotification</font>**: Manages the lifecycle of live status window (<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">create</font>, <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">update</font>, <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">stop</font>). <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">.from(context, env)</font> associates live status window with specific business environment (<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">env</font>).
  5. Immersive lock screen live status window (advanced):
    • In <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">LiveViewDataBuilder</font> configuration <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">setLiveViewLockScreenAbilityName</font> and <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">setLiveViewLockScreenPicture</font>.
    • Implement an Ability that inherits from <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">LiveViewLockScreenExtensionAbility</font> .
    • In <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">onSessionCreate</font> method, use <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">session.loadContent('your custom UI page path')</font> to load the custom lock screen card interface you designed with ArkUI. This allows you to display more information than the default template (e.g., map thumbnail, detailed cost breakdown).
    • Declare extension Ability (**<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">module.json5</font>**):
"extensionAbilities": [
  {
    "name": "LiveViewLockScreenExtAbility",
    "type": "liveViewLockScreen", // Type must be liveViewLockScreen
    "srcEntry": "./ets/entryability/LiveViewLockScreenExtAbility.ets",
    "exported": true // Allow system access
  }
],
Enter fullscreen mode Exit fullscreen mode
  1. Service activation: Before using live status window capabilities, you need to activate them in the <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">AppGallery Connect</font> backend for your app Live View Kit` service benefits.

📌 Summary and Reflection

Putting these three core code blocks together forms the skeleton of the "smooth" riding experience:

  1. **<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">ScanUtil.scan()</font>** is called -> scan successful -> <font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">router.pushUrl</font> direct access to unlock page.
  2. User clicks "unlock" -> call **<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(252, 252, 252);">LiveViewController.startLiveView()</font>** to create live status window (display riding state).
  3. Riding

Top comments (0)