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
- Long-press an app icon on the home screen to open the action menu.
- Tap the "Card" option to enter the card management page and preview cards.
- 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[];
};
}
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';
};
}
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';
};
}
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);
}
}
}
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);
}
}
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;
}
}
}
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": []
}
]
}
4. Technical Challenges and Solutions
4.1 Issues Encountered During Development
-
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.
-
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.
-
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.
-
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
-
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
-
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
-
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.
Top comments (0)