DEV Community

fan
fan

Posted on • Edited on

HarmonyOS Form Kit Card Development Practical Guide

HarmonyOS Form Kit Card Development Practical Guide

Project Background

During the development of HarmonyOS applications, developers often need to provide users with convenient information display and direct service access. Form Kit (Card Development Framework) offers a framework and APIs for embedding application information into system entry points such as the home screen and lock screen. It enables developers to extract important or frequently used information and operations from the application into service cards. By adding these cards to the home screen, users can enjoy quick access to information and services without opening the app.

1. Fundamentals

1.1 Form Kit Concept

Form Kit is a card development framework provided by HarmonyOS that supports embedding application information into system entry points like the home screen and lock screen. Cards are lightweight UI components that display key information and provide quick actions without requiring the user to open the application.

1.2 Card Use Cases

  • Supported Device Types: Phones, tablets, PCs/2-in-1s, smart screens, smartwatches, and car infotainment systems.
  • Supported Application Types: Both applications and meta-services support card development.
  • Supported Locations: Users can add and use cards on the home screen, lock screen, and other system applications.

1.3 Steps to Use a Card

  1. Long-press an app icon on the home screen to open the action menu.
  2. Tap the "Card" option to enter the card management page and preview cards.
  3. Tap "Add to Home Screen" to see and interact with the new card on the home screen.

2. Development Background

The project aims to implement a complete card feature that supports information display, direct service access, and periodic updates. This card is designed to display important information that users care about and provide quick action entries to enhance the user experience.

3. Technical Implementation

3.1 Service Card Architecture

// Card architecture component relationships
interface CardArchitecture {
  // Card consumer: Host application that displays card content
  cardConsumer: {
    // System applications like home screen and lock screen
    hostApplication: string;
    // Controls where the card is displayed in the host
    displayControl: string;
  };

  // Card provider: Application or meta-service that provides the card
  cardProvider: {
    // The actual implementer of card functionality
    implementation: string;
    // Responsibilities: designing the card UI, updating data, handling interactions
    responsibilities: string[];
  };

  // Card management service: System service that manages card information
  cardManagementService: {
    // Acts as a bridge between card providers and consumers
    bridge: string;
    // Provides capabilities like querying, adding, and deleting cards for consumers
    consumerCapabilities: string[];
    // Notifies providers about card events like addition, deletion, refresh, and clicks
    providerCapabilities: string[];
  };
}
Enter fullscreen mode Exit fullscreen mode

3.2 Development Mode Selection

// Application runtime mode selection
enum ApplicationModel {
  STAGE = 'Stage',  // Recommended Stage model
  FA = 'FA'         // Traditional FA model
}

// UI development paradigm selection
enum UIDevelopmentParadigm {
  ARKTS = 'ArkTS',  // Declarative paradigm using ArkTS for card development
  JS = 'JS'         // Web-like paradigm using JS for card development
}

// Card type comparison
interface CardTypeComparison {
  jsCard: {
    developmentParadigm: 'Web-like paradigm';
    componentCapability: 'Supported';
    layoutCapability: 'Supported';
    eventCapability: 'Supported';
    customAnimation: 'Not supported';
    customDrawing: 'Not supported';
    logicCodeExecution: 'Not supported';
  };

  arktsCard: {
    developmentParadigm: 'Declarative paradigm';
    componentCapability: 'Supported';
    layoutCapability: 'Supported';
    eventCapability: 'Supported';
    customAnimation: 'Supported';
    customDrawing: 'Supported';
    logicCodeExecution: 'Supported';
  };
}
Enter fullscreen mode Exit fullscreen mode

3.3 ArkTS Card Implementation Principle

// ArkTS card implementation principle
interface ArkTSCardImplementation {
  // Card consumer process
  cardConsumerProcess: {
    description: 'Host process that displays card content, e.g., home screen process';
    responsibilities: [
      'Controls where the card is displayed in the host',
      'Handles user interaction events',
      'Manages card lifecycle'
    ];
  };

  // Card rendering service process
  cardRenderingServiceProcess: {
    description: 'System process that loads and renders card UIs';
    features: [
      'All cards are rendered in the same process',
      'Different app cards are isolated by virtual machines',
      'Runs card page code from widgets.abc files',
      'Sends rendered data to the card consumer'
    ];
  };

  // Card management service process
  cardManagementServiceProcess: {
    description: 'System SA service that manages card lifecycle';
    responsibilities: [
      'Manages formProvider interface capabilities',
      'Provides card object management and usage',
      'Handles periodic card refresh capabilities'
    ];
  };

  // Card provider process
  cardProviderProcess: {
    description: 'Application process that provides the card';
    components: [
      'Main process where the app UIAbility runs',
      'Separate FormExtensionAbility process for the card'
    ];
    isolation: 'The two processes are memory-isolated but share the same file sandbox';
  };
}
Enter fullscreen mode Exit fullscreen mode

3.4 Card Type Implementation

// Dynamic card implementation
export class DynamicCard {
  // Dynamic card event handling
  async handleCardAction(actionType: 'router' | 'message' | 'call', params: any) {
    switch (actionType) {
      case 'router':
        // Navigate to a specific UIAbility
        await this.handleRouterAction(params);
        break;
      case 'message':
        // Launch FormExtensionAbility and notify via onFormEvent callback
        await this.handleMessageAction(params);
        break;
      case 'call':
        // Launch a specific UIAbility in the background
        await this.handleCallAction(params);
        break;
    }
  }

  // Routing action handling
  private async handleRouterAction(params: any) {
    try {
      // For non-system apps, only navigation within the same app is supported
      await router.pushUrl({
        url: params.url,
        params: params.params
      });
    } catch (error) {
      console.error('Routing failed:', error);
    }
  }

  // Message event handling
  private async handleMessageAction(params: any) {
    try {
      // Send message event via postCardAction
      await postCardAction({
        action: 'message',
        params: params
      });
    } catch (error) {
      console.error('Message event handling failed:', error);
    }
  }

  // Background call handling
  private async handleCallAction(params: any) {
    try {
      // Launch a specific UIAbility in the background
      await postCardAction({
        action: 'call',
        params: params
      });
    } catch (error) {
      console.error('Background call failed:', error);
    }
  }
}

// Static card implementation
export class StaticCard {
  // Static card interaction handling
  async handleFormLinkAction(actionType: 'router' | 'message' | 'call', params: any) {
    // Static cards only support navigating to a specific UIAbility via FormLink
    if (actionType === 'router') {
      await this.handleRouterAction(params);
    }
  }

  private async handleRouterAction(params: any) {
    try {
      // Static card routing
      await router.pushUrl({
        url: params.url,
        params: params.params
      });
    } catch (error) {
      console.error('Static card routing failed:', error);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

3.5 Card Lifecycle Management

import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@kit.FormKit';
import { Configuration, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG: string = 'EntryFormAbility';
const DOMAIN_NUMBER: number = 0xFF00;

export default class EntryFormAbility extends FormExtensionAbility {
  // Triggered when a card is created
  onAddForm(want: Want): formBindingData.FormBindingData {
    hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onAddForm');
    hilog.info(DOMAIN_NUMBER, TAG, want.parameters?.[formInfo.FormParam.NAME_KEY] as string);

    // Return initial card data
    let obj: Record<string, string> = {
      'title': 'Welcome to the Card',
      'detail': 'Click for more information',
      'updateTime': new Date().toLocaleString()
    };
    let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj);
    return formData;
  }

  // Triggered when a temporary card becomes a normal card
  onCastToNormalForm(formId: string): void {
    hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onCastToNormalForm');
    // Handle temporary to normal card conversion logic
  }

  // Triggered when a card is updated
  onUpdateForm(formId: string): void {
    hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onUpdateForm');

    // Update card data
    let obj: Record<string, string> = {
      'title': 'Card Updated',
      'detail': 'Data refreshed',
      'updateTime': new Date().toLocaleString()
    };
    let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(obj);

    // Notify card update
    formProvider.updateForm(formId, formData).catch((error: BusinessError) => {
      hilog.error(DOMAIN_NUMBER, TAG, '[EntryFormAbility] updateForm, error:' + JSON.stringify(error));
    });
  }

  // Triggered when card visibility changes
  onChangeFormVisibility(newStatus: Record<string, number>): void {
    hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onChangeFormVisibility');
    // Handle card visibility change logic
  }

  // Called when a card event is triggered
  onFormEvent(formId: string, message: string): void {
    hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onFormEvent');

    try {
      const eventData = JSON.parse(message);

      // Handle different business logic based on event type
      switch (eventData.action) {
        case 'refresh':
          this.handleRefreshEvent(formId, eventData);
          break;
        case 'navigate':
          this.handleNavigateEvent(eventData);
          break;
        case 'update':
          this.handleUpdateEvent(formId, eventData);
          break;
        default:
          hilog.warn(DOMAIN_NUMBER, TAG, 'Unknown event type: ' + eventData.action);
      }
    } catch (error) {
      hilog.error(DOMAIN_NUMBER, TAG, 'Event handling failed: ' + error);
    }
  }

  // Triggered when a card is deleted
  onRemoveForm(formId: string): void {
    hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onRemoveForm');
    // Delete card instance data
    this.cleanupFormData(formId);
  }

  // Triggered when system configuration is updated
  onConfigurationUpdate(config: Configuration) {
    hilog.info(DOMAIN_NUMBER, TAG, '[EntryFormAbility] onConfigurationUpdate:' + JSON.stringify(config));
    // Handle system configuration updates
  }

  // Triggered when querying card status
  onAcquireFormState(want: Want) {
    // Return current card state
    return formInfo.FormState.READY;
  }

  // Handle refresh event
  private async handleRefreshEvent(formId: string, eventData: any) {
    try {
      // Get latest data
      const latestData = await this.fetchLatestData();

      // Update card
      let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(latestData);
      await formProvider.updateForm(formId, formData);

      hilog.info(DOMAIN_NUMBER, TAG, 'Card refresh successful');
    } catch (error) {
      hilog.error(DOMAIN_NUMBER, TAG, 'Card refresh failed: ' + error);
    }
  }

  // Handle navigation event
  private async handleNavigateEvent(eventData: any) {
    try {
      // Navigate to specified page
      await router.pushUrl({
        url: eventData.url,
        params: eventData.params
      });
    } catch (error) {
      hilog.error(DOMAIN_NUMBER, TAG, 'Navigation failed: ' + error);
    }
  }

  // Handle update event
  private async handleUpdateEvent(formId: string, eventData: any) {
    try {
      // Update card data
      let formData: formBindingData.FormBindingData = formBindingData.createFormBindingData(eventData.data);
      await formProvider.updateForm(formId, formData);
    } catch (error) {
      hilog.error(DOMAIN_NUMBER, TAG, 'Update event handling failed: ' + error);
    }
  }

  // Fetch latest data
  private async fetchLatestData(): Promise<Record<string, any>> {
    // Simulate fetching latest data
    return {
      'title': 'Data Updated',
      'detail': 'Latest Information',
      'updateTime': new Date().toLocaleString(),
      'data': Math.random().toString(36).substring(7)
    };
  }

  // Clean up card data
  private cleanupFormData(formId: string) {
    // Delete previously persisted card instance data
    hilog.info(DOMAIN_NUMBER, TAG, 'Cleaning up card data: ' + formId);
  }
}
Enter fullscreen mode Exit fullscreen mode

3.6 Card UI Implementation

// Dynamic card UI implementation
@Entry
@Component
struct DynamicWidgetCard {
  @State title: string = 'Dynamic Card'
  @State detail: string = 'Click for details'
  @State updateTime: string = ''
  @State data: string = ''

  aboutToAppear() {
    // Initialize data
    this.loadCardData();
  }

  build() {
    Column() {
      // Card title
      Text(this.title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .margin({ bottom: 8 })

      // Card content
      Text(this.detail)
        .fontSize(14)
        .fontColor('#666666')
        .margin({ bottom: 8 })

      // Data display
      if (this.data) {
        Text(`Data: ${this.data}`)
          .fontSize(12)
          .fontColor('#999999')
          .margin({ bottom: 8 })
      }

      // Update time
      Text(this.updateTime)
        .fontSize(10)
        .fontColor('#CCCCCC')

      // Action buttons
      Row() {
        Button('Refresh')
          .fontSize(12)
          .backgroundColor('#007AFF')
          .fontColor(Color.White)
          .borderRadius(4)
          .onClick(() => {
            this.handleRefresh();
          })

        Button('Details')
          .fontSize(12)
          .backgroundColor('#34C759')
          .fontColor(Color.White)
          .borderRadius(4)
          .margin({ left: 8 })
          .onClick(() => {
            this.handleNavigate();
          })
      }
      .margin({ top: 8 })
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
  }

  // Load card data
  private loadCardData() {
    // Get information from card data
    const cardData = getContext(this)?.cardData;
    if (cardData) {
      this.title = cardData.title || this.title;
      this.detail = cardData.detail || this.detail;
      this.updateTime = cardData.updateTime || this.updateTime;
      this.data = cardData.data || this.data;
    }
  }

  // Handle refresh event
  private handleRefresh() {
    try {
      // Send refresh event
      postCardAction({
        action: 'message',
        params: {
          action: 'refresh',
          timestamp: Date.now()
        }
      });
    } catch (error) {
      console.error('Failed to send refresh event:', error);
    }
  }

  // Handle navigation event
  private handleNavigate() {
    try {
      // Send navigation event
      postCardAction({
        action: 'router',
        params: {
          url: 'pages/DetailPage',
          params: {
            title: this.title,
            data: this.data
          }
        }
      });
    } catch (error) {
      console.error('Failed to send navigation event:', error);
    }
  }
}

// Static card UI implementation
@Entry
@Component
struct StaticWidgetCard {
  @State title: string = 'Static Card'
  @State detail: string = 'Click for details'

  aboutToAppear() {
    // Initialize data
    this.loadCardData();
  }

  build() {
    Column() {
      // Card title
      Text(this.title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .margin({ bottom: 8 })

      // Card content
      Text(this.detail)
        .fontSize(14)
        .fontColor('#666666')
        .margin({ bottom: 8 })

      // Static link
      FormLink({
        action: 'router',
        params: {
          url: 'pages/DetailPage',
          params: {
            title: this.title
          }
        }
      }) {
        Button('View Details')
          .fontSize(12)
          .backgroundColor('#007AFF')
          .fontColor(Color.White)
          .borderRadius(4)
      }
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
  }

  // Load card data
  private loadCardData() {
    // Get information from card data
    const cardData = getContext(this)?.cardData;
    if (cardData) {
      this.title = cardData.title || this.title;
      this.detail = cardData.detail || this.detail;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

3.7 Card Configuration File

{
  "forms": [
    {
      "name": "dynamic_widget",
      "displayName": "$string:dynamic_widget_display_name",
      "description": "$string:dynamic_widget_desc",
      "src": "./ets/widget/pages/DynamicWidgetCard.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "renderingMode": "fullColor",
      "isDefault": true,
      "updateEnabled": true,
      "scheduledUpdateTime": "10:30",
      "updateDuration": 1,
      "defaultDimension": "2*2",
      "supportDimensions": [
        "2*2",
        "4*4"
      ],
      "formConfigAbility": "ability://EntryAbility",
      "isDynamic": true,
      "metadata": []
    },
    {
      "name": "static_widget",
      "displayName": "$string:static_widget_display_name",
      "description": "$string:static_widget_desc",
      "src": "./ets/widget/pages/StaticWidgetCard.ets",
      "uiSyntax": "arkts",
      "window": {
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "renderingMode": "fullColor",
      "isDefault": false,
      "updateEnabled": false,
      "defaultDimension": "2*2",
      "supportDimensions": [
        "2*2"
      ],
      "formConfigAbility": "ability://EntryAbility",
      "isDynamic": false,
      "metadata": []
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

4. Technical Challenges and Solutions

4.1 Issues Encountered During Development

  1. Card Lifecycle Management Issues

    • Issue: The FormExtensionAbility process cannot stay in the background and will be terminated after 10 seconds of inactivity.
    • Solution: Use the main application to handle long-running tasks and notify the card to refresh via updateForm upon completion.
    • Improvement: Implement task queuing and state management mechanisms.
  2. Card Data Update Issues

    • Issue: Card data updates are not timely, leading to poor user experience.
    • Solution: Implement both scheduled updates and event-driven data update mechanisms.
    • Improvement: Use caching and incremental updates to optimize performance.
  3. Card Interaction Event Handling Issues

    • Issue: Dynamic and static cards have different event handling methods.
    • Solution: Provide a unified event handling interface and choose different handling methods based on card type.
    • Improvement: Implement event distribution and routing mechanisms.
  4. Card Performance Optimization Issues

    • Issue: Card rendering performance affects overall system performance.
    • Solution: Use card rendering service isolation and optimize rendering logic.
    • Improvement: Implement virtualization and lazy loading mechanisms.

4.2 Optimization Suggestions

  1. Feature Optimization

    • Support card data caching
    • Add card configuration interface
    • Implement card template system
    • Support card data synchronization
    • Add card analytics
    • Implement card recommendation algorithms
    • Support card personalization
    • Add card sharing functionality
  2. Performance Optimization

    • Optimize card rendering performance
    • Implement card data preloading
    • Use card rendering service isolation
    • Optimize memory usage
    • Implement card update strategies
    • Optimize network requests
    • Compress resource files
    • Adopt lazy loading techniques
  3. User Experience Optimization

    • Add card animation effects
    • Support card theme switching
    • Implement card interaction feedback
    • Add card usage guidance
    • Support card quick actions
    • Implement card intelligent recommendations
    • Add card usage statistics
    • Support multiple languages

5. Development Considerations

5.1 Card Design

  • Design card layouts reasonably to avoid information overload
  • Use appropriate colors and fonts to ensure readability
  • Consider adaptation for different device sizes
  • Design clear interaction feedback

5.2 Performance Considerations

  • Avoid time-consuming operations in cards
  • Use scheduled updates reasonably to avoid frequent refreshes
  • Release unnecessary resources promptly
  • Optimize card rendering performance

5.3 User Experience

  • Provide clear operation feedback
  • Support card personalization configuration
  • Implement smooth animation effects
  • Ensure card information accuracy

5.4 Compatibility

  • Support different device types
  • Be compatible with different system versions
  • Adapt to different screen sizes
  • Support dark mode

6. Best Practices

6.1 Code Organization

  • Encapsulate card logic independently
  • Use TypeScript type definitions
  • Implement unified error handling
  • Add necessary logging

6.2 Card Design

  • Follow design guidelines
  • Use consistent visual styles
  • Provide clear information hierarchy
  • Consider accessibility

6.3 Testing Recommendations

  • Test various card scenarios
  • Verify lifecycle management
  • Test performance
  • Verify user experience

7. Frequently Asked Questions

7.1 Card Not Displaying

  • Check if card configuration is correct
  • Confirm if FormExtensionAbility is functioning properly
  • Verify if card data is correct
  • Check system permission settings

7.2 Card Update Failure

  • Check network connection status
  • Confirm if data source is normal
  • Verify if update logic is correct
  • Check error log information

7.3 Card Interaction Issues

  • Check event handling logic
  • Confirm if routing configuration is correct
  • Verify permission settings
  • Test on different device types

8. Project Summary

This Form Kit card development project has implemented the following core features:

  • Support for both dynamic and static cards
  • Complete lifecycle management
  • Flexible data update mechanisms
  • Rich interaction event handling
  • Optimized performance

Although various technical challenges were encountered during development, continuous optimization led to a stable and reliable card system. While some details still need further refinement, the basic functionality meets most usage scenarios.

9. References

Top comments (0)