Use the SDK

更新时间:
复制 MD 格式

Initialization

Initialize HRiverMini after the mPaaS frame is initialized.

HRiverMini.init()

To initialize the miniapp, call HRiverMini.init() in the onCreate method of the Ability.

Important

Before you use the SDK, you must initialize the mPaaS frame and set the userId for previews and on-device debugging.

For more information about mPaaS frame initialization, see Initialize mPaaS. The following is an example:

import AbilityStage from '@ohos.app.ability.AbilityStage';
import { MPFramework } from '@mpaas/framework';

export default class ModuleEntry extends AbilityStage {
  async onCreate() {
    MPFramework.create(this.context);
    
    MPFramework.instance.userId = 'MPTestCase'
  }
}

Open a miniapp

Miniapp page routing is based on Navigation. Add the following configuration to the aboutToAppear() method of the Navigation page:

aboutToAppear() {
  HRiverMini.notifyNavigationCreate(this.context, this.pageInfos)
}

Add Navigation:

@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack()
@Builder
  PageMap(name: string, navPageIntent: Map<string, Object>) {
    AppPage(name, navPageIntent);
  }

build() {
  Navigation(this.pageInfos) {
      Column() {
      ...
      }
      .height('100%')
      .width('100%')
  }.navDestination(this.PageMap);
}

To start a miniapp, use the following API:

let startParams = new Map<string, Object>() // Startup parameters
HRiverMini.startApp("2023112713520001", startParams)

To start a miniapp and navigate to a specific page:

let startParams = new Map<string, Object>()
startParams.set("page", "/page/component/view/view");
HRiverMini.startApp("2020012000000001", startParams)

Register a custom API

Call the following API before you initialize HRiverMini.init():

HRiverMini.registerExtension(()=> {
      import('@mpaas/hriverminidemo/src/main/ets/component/CustomExtension')
      import('@mpaas/hriverminidemo/src/main/ets/component/CustomExtension1')
      import('@mpaas/hriverminidemo/src/main/ets/component/CustomExtension2')
    })

The import statement is used to dynamically import the custom API's Extension. The implementation of CustomExtension is as follows:

import { ApiBaseExtension,
  BridgeCallback,
  defineJSAPIClass, ExtensionParameter,
  MyExtHubContext,
  registerJSAPI, required } from '@alipay/exthub'

@defineJSAPIClass(():ApiBaseExtension => {return new CustomExtension()})
export class CustomExtension extends ApiBaseExtension {
  /**
   * Custom API: customApi
   * @param param
   * @param context
   * @param callback
   */
  @registerJSAPI
  customApi(@required(ExtensionParameter.CallParameters) param: Record<string, Object>,
    @required(ExtensionParameter.MyExtHubContext) context: MyExtHubContext,
    @required(ExtensionParameter.BridgeCallback) callback: BridgeCallback) {
    // Read parameters from param.
    // Read the context, such as the page context, from context.
    // Use the callback to call back to the miniapp.

    // Call back specific data after a successful call.
    callback.sendSuccessResponse({
      data: 'apiSuccess'
    })
  }

  /**
   * Custom API: customApi2
   * @param param
   * @param context
   * @param callback
   */
  @registerJSAPI
  customApi2(@required(ExtensionParameter.CallParameters) param: Record<string, Object>,
    @required(ExtensionParameter.MyExtHubContext) context: MyExtHubContext,
    @required(ExtensionParameter.BridgeCallback) callback: BridgeCallback) {
    // Callback notification for a failure.
    callback.sendErrorResponse(-1, "call failed msg")
  }
}

Native Notifications Mini Program

import { EngineUtils, HRiverMiniEngine } from '@mpaas/hrivermini'

HRiverMiniEngine.sendToRender(EngineUtils.getPageFromContext(context)?.getRender() || null, 'event', {})

Scan a QR code for preview or on-device debugging

Method 1: Start the QR code scanner directly.

let startParams = new Map<string, Object>()
HRiverMini.scan(getContext(this), startParams)

Method 2: Use a different scanner and open the scan result.

let startParams = new Map<string, Object>()
HRiverMini.scanByUri(scanResult, startParams) // scanResult is the string of the scan result.

Configure the request interval for miniapp packages

mPaaS lets you configure the request interval for miniapp packages. You can configure this setting globally.

HRiverMini.setAsyncReqRate("{\"config\":{\"al\":\"3\",\"pr\":{\"4\":\"86400\",\"common\":\"864000\"},\"ur\":\"1800\",\"fpr\":{\"common\":\"3888000\"}},\"switch\":\"yes\"}")

In this example, \"ur\":\"1800\" sets the global update interval. The default value is 1800 seconds. You can change this value to set the global request interval for miniapp packages. The valid range is from 0 to 86400 seconds (0 to 24 hours). A value of 0 indicates that there is no limit on the request interval.

Startup parameters

Key

Value

Description

query

Example: a=xx&c=xx

Pass parameters to the miniapp.

page

Example: pages/twoPage/twoPage

Specify the page to open and pass parameters.

nbupdate

synctry or async. The default is async.

  • synctry: Forcibly updates the version.

  • async: Does not forcibly update the version.

disablePresetMenu

YES or NO. The default is NO.

Specifies whether to hide the capsule menu.

Set the user agent

HRiverMini.setUserAgent(ua) // Appended to the default user agent.

Update a miniapp

HRiverMini.updateApp(appId)

Switch configuration

You can use switches to configure features.

HRiverMini.setConfig(`${key}`, `${value}`)

The following table describes the keys, values, and their descriptions:

Key

Value

Description

xriver_show_share_menu

YES or NO. The default is NO.

Specifies whether to show the Share button in the miniapp menu.

Preset miniapps

  1. In the rawfile directory of your project, create the nebulaPreset and nebulapresetinfo directories. Place all built-in miniapp packages in the nebulaPreset directory. The file name for each package must be its appid. Place the customnebulapreset.json file in the nebulapresetinfo directory. This file contains the JSON configuration for the built-in miniapps. The JSON format is slightly different from other platforms. You only need to keep the content under the data object.

    The following is an example:

    image

    The content of customnebulapreset.json is as follows:

    [
    		{
    			"app_desc":"Miniapp demo",
    			"app_id":"9999999988888888",
    			"auto_install":1,
    			"extend_info":{
    				"launchParams":{
    					"enableTabBar":"NO",
    					"enableKeepAlive":"NO",
    					"enableDSL":"YES",
    					"nboffline":"sync",
    					"enableWK":"YES",
    					"page":"pages/index/index",
    					"tinyPubRes":"YES",
    					"enableJSC":"YES"
    				},
    				"usePresetPopmenu":"YES"
    			},
    			"fallback_base_url":"https://xxx.com/xxxx/nebula/fallback/",
    			"global_pack_url":"",
    			"installType":1,
    			"main_url":"/index.html#pages/index/index",
    			"name":"yttest",
    			"online":1,
    			"package_url":"https://xxx.com/xxx/nebula/9999999988888888_1.0.0.0.amr",
    			"patch":"",
    			"sub_url":"",
    			"version":"1.0.0.0",
    			"vhost":"https://9999999988888888.h5app.com"
    		},
        {
    			"app_desc":"Miniapp demo 2",
    			"app_id":"9999999988888889",
    			"auto_install":1,
    			"extend_info":{
    				"launchParams":{
    					"enableTabBar":"NO",
    					"enableKeepAlive":"NO",
    					"enableDSL":"YES",
    					"nboffline":"sync",
    					"enableWK":"YES",
    					"page":"pages/index/index",
    					"tinyPubRes":"YES",
    					"enableJSC":"YES"
    				},
    				"usePresetPopmenu":"YES"
    			},
    			"fallback_base_url":"https://xxx.com/xxxx/nebula/fallback/",
    			"global_pack_url":"",
    			"installType":1,
    			"main_url":"/index.html#pages/index/index",
    			"name":"yttest",
    			"online":1,
    			"package_url":"https://xxx.com/xxx/nebula/9999999988888889_1.0.0.0.amr",
    			"patch":"",
    			"sub_url":"",
    			"version":"1.0.0.0",
    			"vhost":"https://9999999988888889.h5app.com"
    		},
    	]
  2. After initialization, call HRiverMini.loadPresetApp.

    HRiverMini.loadPresetApp((appId: string) => {
        // A callback is triggered for each successful installation, returning the appId.
        hilog.debug(1, "MiniTag", "installPreset: " + appId)
    })

Miniapp information management

  1. Retrieve miniapp information based on the appId. The returned information includes the appId, version, and title.

    let result: ESObject = HRiverMini.getAppInfo('1122334455667788')
    if (result) {
        // result.appId: The miniapp ID.
        // result.version: The miniapp version.
        // result.title: The miniapp name.
        hilog.debug(1, "MiniTag", `getAppInfo: ${result.appId} ${result.version}`)
    }
  2. Delete miniapp information.

    HRiverMini.deleteAppInfo('1122334455667788')
  3. Delete all miniapp information.

    HRiverMini.deleteAllAppInfo()

Aspect-oriented event interception

You can inject an interception aspect through the registerPoint method of HRiverMini to listen for and intercept partial events.

/**
* Register an interception point.
* extensionName: The name of the implemented Extension.
* pointArr: An array of specific events to intercept.
**/
registerPoint(extensionName: string, pointArr: Array<string>)

Back event interception

Event name: CRV_PROTOCOL_XRiverPageBackIntercept

The implementation is as follows:

{ CRV_PROTOCOL_XRiverPageBackIntercept } from '@mpaas/xriverohos';

// PageInterceptExtension is a custom aspect implementation. For more information, see the demo. CRV_PROTOCOL_XRiverPageBackIntercept is the key for the back event.
HRiverMini.registerPoint(PageInterceptExtension.name, [CRV_PROTOCOL_XRiverPageBackIntercept])

The implementation of PageInterceptExtension is as follows:

import {
  CRV_PROTOCOL_XRiverPageBackIntercept,
  defineExtensionConstructor, Extension, ExtensionContext,
  Page,
  PageBackInterceptPoint } from '@mpaas/xriverohos';

@defineExtensionConstructor((): Extension => {
  return new PageInterceptExtension();
})

// Inherit Extension and implement PageBackInterceptPoint.
export class PageInterceptExtension extends Extension implements PageBackInterceptPoint{
  constructor() {
    super();
    // Register the corresponding method name for the CRV_PROTOCOL_XRiverPageBackIntercept interception event. The back event is fixed to interceptBackEvent.
    this.registerProtocolFunction(CRV_PROTOCOL_XRiverPageBackIntercept, 'interceptBackEvent');
  }

  // Implement the interceptBackEvent method. It returns true to intercept or false to not intercept.
  // The following method body is a demo example. Whether to intercept depends on your business needs.
  interceptBackEvent(context: ExtensionContext): boolean {
    let page = context.getCurrentNode() as Page
    if (page.isFirstPage()) {
      return true
    }

    return false
  }

}

Support for hiding the title bar

You can hide the miniapp title bar globally or based on the miniapp ID.

After initialization, add a point interception:

HRiverMini.registerAppPoint(PageInterceptExtension.name, [CRV_PROTOCOL_SESSION_sessionDidStart])

The implementation of PageInterceptExtension.ets is as follows:

import {
    CRV_PROTOCOL_SESSION_sessionDidStart,
    defineExtensionConstructor, Extension
} from '@mpaas/xriverohos';
import { CRVApp } from '@mpaas/nebulaintegration';

@defineExtensionConstructor((): Extension => {
  return new PageInterceptExtension();
})

export class PageInterceptExtension extends Extension{
  constructor() {
    super();
    this.registerProtocolFunction(CRV_PROTOCOL_SESSION_sessionDidStart, 'sessionDidStart');
  }

  sessionDidStart(app: CRVApp) {
    let originParam = app.getStartParams()
    // To configure based on the appId, you can add a conditional judgment here.
    if (originParam) {
      originParam['transparent'] = new Boolean(true)
      originParam['forceDefaultBackground'] = new Boolean(true)
    }

  }

}

Custom UI

You can customize UI elements, such as the title bar, menu bar, permission dialog box, and miniapp loading and error pages. To do this, set the MYNavigationBarAdapter implementation class during initialization.

TinyAdapterUtils.setProvider(MYNavigationBarAdapter.name, new DemoNavBarAdapter())

The implementation of DemoNavBarAdapter.ets is as follows:

import { CRVPage, MYNavigationBarAdapter } from '@mpaas/nebulaintegration';
import { TinyMenuState } from '@mpaas/xriverohos';
import { DemoMenuCustomDialog } from './DemoMenuCustomDialog';
import { DemoNavComponent } from './DemoNavComponent';

export class DemoNavBarAdapter extends MYNavigationBarAdapter {
  // Custom menu dialog box UI
  getMenuCustomDialog(): WrappedBuilder<[TinyMenuState, CustomDialogController, CRVPage]> {
    return wrapBuilder(customDialogBuilder);
  }

  // Custom title bar UI
  getNavBarComponent(): WrappedBuilder<[ESObject]> | undefined {
    return wrapBuilder(customNavComponentBuilder);
  }

  /** Custom loading and error pages
   * @param data: {appId: The miniapp ID, appInfo: The loading appInfo (see EntryInfo), loadingProgress: The loading progress, errorCode: The error code, rightButtonState: The button state data}
   * @returns
   */
  getLoadingComponent(): WrappedBuilder<[ESObject]> | undefined {
    return wrapBuilder(customLoadingComponentBuilder);
  }

  /**
   * Custom permission dialog box
   * @param component The page component
   * @param dlgData {app: The miniapp information, scope: The permission scope (for example, scope.bluetooth), icon: The miniapp logo, title: The miniapp title, desc: The dialog box content (for example, 'Use your Bluetooth')}
   * @param reject The callback method for rejection
   * @param agree The callback method for agreement
   * @returns true: Use a custom dialog box. false: Use the default dialog box.
   */
  showPermissionDialog(component: Object, dlgData: ESObject, reject: Function, agree: Function): boolean {
    AUPanelManager.showPermissionFrom(component, {
      icon: dlgData.icon,
      title: dlgData.title,
      subTitle: 'Request',
      content: dlgData.desc,
      subContent: '',
      checkbox: undefined,
      buttons: [
        {
          title: 'Deny', type: DialogButtonType.Cancel, action: (isChecked: boolean) => {
          reject()
        }
        },
        {
          title: 'Allow', type: DialogButtonType.Normal, action: () => {
          agree()
        }
        }
      ],
      info: new ObservedPermissionInfo()
    })
    return true;
  }
}

@Builder
export function customDialogBuilder(state: TinyMenuState, controller: CustomDialogController, page: CRVPage) {
  // Menu dialog box UI implementation
  DemoMenuCustomDialog({
    tinyMenuState: state,  // Menu data
    customDialogController: controller, // Custom dialog box controller
    page: page // The current miniapp page, from which you can get miniapp data
  })
}

@Builder
export function customNavComponentBuilder(data: ESObject) {
  // Title bar implementation
  DemoNavComponent({
    needHideBackButton: data.needHideBackButton, // Specifies whether to show the Back button
    navigationBarState: data.navigationBarState, // Specific data for the title bar
    page: data.page // The current miniapp page, from which you can get miniapp data
  })
}


@Builder
export function customLoadingComponentBuilder(data: ESObject) {
  DemoLoadingComponent({
    appId: data.appId,
    appInfo: data.appInfo,
    loadingProgress: data.loadingProgress,
    errorCode: data.errorCode,
    rightButtonState: data.rightButtonState
  })
}

Support for a custom title bar

The implementation of DemoNavComponent.ets is as follows:

import { AUCustomIcon, AUIcon, IconFontKey } from '@mpaas/antui'
import { CRVPage } from '@mpaas/nebulaintegration'
import { CapsuleState, FrontColor, NavigationBarState, NavigationBarUtils } from '@mpaas/xriverohos'

@Component
export struct DemoNavComponent {

  page ?: CRVPage  // Current page

  needHideBackButton: boolean = false; // Specifies whether to show the Back button

  @ObjectLink navigationBarState: NavigationBarState // Title bar data

  aboutToAppear(): void {
  }

  aboutToDisappear() {
  }

  build() {
      Row() {
        // Back button
        if(!this.needHideBackButton) {
          Button({
            type: ButtonType.Normal,
            stateEffect: true
          }) {
            AUIcon({
              icon: IconFontKey.ICONFONT_BACK,
              fontSize: 22,
              fontColor: this.navigationBarState.backButtonIconColor
            })
              .margin({
                top: 11,
                right: 2,
                bottom: 11,
                left: 0
              })
              .onAreaChange((oldValue: Area, newValue: Area): void => {
                this.navigationBarState.backButtonIconArea = newValue
              })
          }
          .visibility((this.navigationBarState.backButtonVisibility === 0) ? Visibility.Visible :
          Visibility.None)
          .backgroundColor('#00000000')
          .margin({
            top: 0,
            right: 0,
            bottom: 0,
            left: 8
          })
          .onClick((event: ClickEvent) => {
            // Back button click
            if (this.navigationBarState.backButtonOnClickListener !== undefined) {
              this.navigationBarState.backButtonOnClickListener(event)
            }
          })
          .onAreaChange((oldValue: Area, newValue: Area): void => {
            this.navigationBarState.backButtonInteractiveArea = newValue
          })
        }

        // Left close button
        Button({
          type: ButtonType.Normal,
          stateEffect: true
        }) {
          AUIcon({
            icon: IconFontKey.ICONFONT_AD_CLOSE,
            fontSize: 22,
            fontColor: this.navigationBarState.leftCloseButtonIconColor
          })
            .margin({ top: 11, right: 5, bottom: 11, left: 5 })
        }
        .visibility((this.navigationBarState.leftCloseButtonVisibility === 0) ? Visibility.Visible : Visibility.None)
        .backgroundColor('#00000000')
        .margin({ top: 0, right: 0, bottom: 0, left: 3 })
        .onClick((event) => {
          // Close button
          if (this.navigationBarState.leftCloseButtonOnClickListener !== undefined) {
            this.navigationBarState.leftCloseButtonOnClickListener(event)
          }
        })

        // Home button
        Button({
          type: ButtonType.Normal,
          stateEffect: true
        }) {
          AUCustomIcon({
            text: "\ue67d",
            fontSrc: $rawfile('tiny_iconfont.ttf'),
            fontSize: 22,
            fontColor: this.navigationBarState.homeButtonIconColor
          })
            .margin({ top: 11, right: 2, bottom: 11, left: 2 })
            .onAreaChange((oldValue: Area, newValue: Area): void => {
              this.navigationBarState.homeButtonIconArea = newValue
            })
        }
        .visibility((this.navigationBarState.homeButtonVisibility === 0) ? Visibility.Visible : Visibility.None)
        .backgroundColor('#00000000')
        .margin({ top: 0, right: 0, bottom: 0, left: 8 })
        .onClick((event) => {
          // Home button click
          if (this.navigationBarState.homeButtonOnClickListener !== undefined) {
            this.navigationBarState.homeButtonOnClickListener(event)
          }
        })
        .onAreaChange((oldValue: Area, newValue: Area): void => {
          this.navigationBarState.homeButtonInteractiveArea = newValue
        })

        // Title
        Row() {

          Column() {
            // Title text
            Text(this.navigationBarState.titleText)
              .visibility((this.navigationBarState.titleVisibility === 0) ? Visibility.Visible : Visibility.None)
              .fontSize(18)
              .fontStyle(FontStyle.Normal)
              .fontColor(this.navigationBarState.titleTextColor)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
                // .textOverflow({ overflow: TextOverflow.Clip })
              .maxLines(1)
              .onClick((clickEvent: ClickEvent): void => {
                // Title click
                if (this.navigationBarState.titleOnClickListener !== undefined) {
                  this.navigationBarState.titleOnClickListener(clickEvent)
                }
              })
          }
          .alignItems(HorizontalAlign.Start)

        }
        .width(0)
        .layoutWeight(1)
        .margin({
          top: 0,
          right: 0,
          bottom: 0,
          left: ((this.navigationBarState.backButtonVisibility === 1)
            && (this.navigationBarState.leftCloseButtonVisibility === 1)
            && (this.navigationBarState.homeButtonVisibility === 1)) ? 16 : 6
        })

        // // Placeholder
        // Blank()
        //   .layoutWeight(1)

        // Right buttons
        if (this.navigationBarState.capsuleState.visibility === 0) { // WTF: Conditional rendering here is not working...
          // Capsule style
          RightButtonComponent({
            capsuleState: this.navigationBarState.capsuleState
          })
            .animation({ duration: 300 })
        }
      }
      .visibility((this.navigationBarState.visibility === 0) ? Visibility.Visible : Visibility.None)
      .width('100%')
      .height('100%')
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.Start)
      .backgroundColor(NavigationBarUtils.alterColorWithAlpha(
        this.navigationBarState.backgroundColor,
        this.navigationBarState.backgroundAlpha))
      .hitTestBehavior(this.navigationBarState.penetrable ? HitTestMode.Transparent : HitTestMode.Default)
      .borderStyle(BorderStyle.Solid)
      .borderWidth({
        top: 0,
        right: 0,
        bottom: (this.navigationBarState.bottomLineVisibility === 0) ? '1px' : 0,
        left: 0
      })
      .borderColor(NavigationBarUtils.alterColorWithAlpha(
        this.navigationBarState.bottomLineColor,
        this.navigationBarState.bottomLineAlpha))
    }

}

@Component
export struct RightButtonComponent {

  aboutToAppear(): void {
  }

  @ObjectLink capsuleState: CapsuleState

  build() {
    // Capsule with more and close buttons
    Row() {
      // More button
      AUIcon({
        icon: this.capsuleState.moreButtonIconfont as IconFontKey,
        fontSize: 22,
        fontColor: (this.capsuleState.frontColor === FrontColor.Black) ? '#FF333333' : '#FFFFFFFF'
      })
        .visibility((this.capsuleState.moreButtonVisibility === 0) ? Visibility.Visible : Visibility.None)
        .margin({
          top: '4vp',
          right: '11vp',
          bottom: '4vp',
          left: '11vp'
        })
        .onClick((clickEvent: ClickEvent) => {
          // More menu click
          if (this.capsuleState.moreButtonOnClickListener !== undefined) {
            this.capsuleState.moreButtonOnClickListener(clickEvent)
          }
        })

      // Divider
      Row()
        .borderStyle(BorderStyle.Solid)
        .borderWidth('1px')
        .borderColor('#1A000000')
        .width('1px')
        .height('22vp')

      // Close button
      AUIcon({
        icon: this.capsuleState.closeButtonIconfont as IconFontKey,
        fontSize: 22,
        fontColor: (this.capsuleState.frontColor === FrontColor.Black) ? '#FF333333' : '#FFFFFFFF'
      })
        .visibility((this.capsuleState.closeButtonVisibility === 0) ? Visibility.Visible : Visibility.None)
        .margin({
          top: '4vp',
          right: '11vp',
          bottom: '4vp',
          left: '11vp'
        })
        .onClick((clickEvent: ClickEvent) => {
          // Close button click
          if (this.capsuleState.closeButtonOnClickListener !== undefined) {
            this.capsuleState.closeButtonOnClickListener(clickEvent)
          }
        })
    }
    .visibility((this.capsuleState.visibility === 0) ? Visibility.Visible : Visibility.Hidden)
    .borderStyle(BorderStyle.Solid)
    .borderWidth('1px')
    .borderColor('#1A000000')
    .borderRadius('10000px')
    .backgroundColor(this.capsuleState.frontColor === FrontColor.White ? '#16000000' : '#00000000')
    .margin({
      top: '9vp',
      right: '4vp',
      bottom: '9vp',
      left: 0
    })
    .onAreaChange((oldValue: Area, newValue: Area): void => {
      this.capsuleState.capsuleArea = newValue
    })

  }

}

Support for a custom menu

The implementation of DemoMenuCustomDialog.ets is as follows:

import { TinyMenuButtonState, TinyMenuState } from '@mpaas/xriverohos'
import { window } from '@kit.ArkUI'
import { AUCustomIcon } from '@mpaas/antui'
import { CRVPage } from '@mpaas/nebulaintegration'

/**
 * The tiny menu dialog.
 */
@CustomDialog
export struct DemoMenuCustomDialog {

  @ObjectLink tinyMenuState: TinyMenuState

  customDialogController?: CustomDialogController

  @State isFullScreen: boolean = false

  @State paddingBottom: Length = 0

  page?: CRVPage

  aboutToAppear(): void {
    window.getLastWindow(getContext(this)).then((win: window.Window) => {
      this.isFullScreen = win.getWindowProperties().isLayoutFullScreen
      let area = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR)
      if (area.visible && area.bottomRect.height > 0) {
        this.paddingBottom = px2vp(area.bottomRect.height)
      }
    })
  }

  build() {
    Column() {
      Row() {
        Image(this.tinyMenuState.appIconImageUrl)
          .width((this.tinyMenuState.appIconImageUrl && this.tinyMenuState.appIconImageUrl.length > 0) ? '35vp' : '2vp')
          .height('35vp')
          .borderRadius('50vp')
          .borderWidth('0px')
          .margin({
            top: '18vp',
            right: '8vp',
            left: '16vp',
            bottom: '18vp',
          })

        Text(this.tinyMenuState.appName)
          .fontSize(16)
          .fontStyle(FontStyle.Normal)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
      }
      .width('100%')
      .alignItems(VerticalAlign.Center)

      // Divider
      Row() {
        Row()
          .width('100%')
          .height('1px')
          .backgroundColor('#cccccc')
      }
      .width('100%')
      .margin({
        top: 0,
        right: '16vp',
        bottom: 0,
        left: '16vp'
      })

      // Top tiny menu buttons
      Row() {
        ForEach(
          this.tinyMenuState.tinyMenuButtonStateArrayTop,
          (tinyMenuButtonState: TinyMenuButtonState, index: number) => {
            TinyMenuButtonComponent({
              tinyMenuButtonState: tinyMenuButtonState,
              menuButtonOnClickListener: new MenuButtonOnClickListener((mid: string): void => {
                // Close this dialog first
                this.customDialogController?.close()

                // Trigger click event logic
                if (this.tinyMenuState.menuButtonOnClickListener) {
                  this.tinyMenuState.menuButtonOnClickListener(mid)
                }
              })
            })
          })
      }
      .width('100%')
      .height('95vp')

      // Bottom tiny menu buttons
      Scroll(new Scroller()) {
        Row() {
          ForEach(
            this.tinyMenuState.tinyMenuButtonStateArrayBottom,
            (tinyMenuButtonState: TinyMenuButtonState, index: number) => {
              TinyMenuButtonComponent({
                tinyMenuButtonState: tinyMenuButtonState,
                menuButtonOnClickListener: new MenuButtonOnClickListener((mid: string): void => {
                  // Close this dialog first
                  this.customDialogController?.close()

                  // Trigger click event logic
                  if (this.tinyMenuState.menuButtonOnClickListener) {
                    this.tinyMenuState.menuButtonOnClickListener(mid)
                  }
                })
              })
            })
        }
        .height('95vp')
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .align(Alignment.Start)

      Text('Cancel')
        .fontSize(18)
        .fontStyle(FontStyle.Normal)
        .fontColor('ff333333')
        .backgroundColor('#FFFFFF')
        .width('100%')
        .height('57vp')
        .textAlign(TextAlign.Center)
        .onClick((event: ClickEvent) => {
          this.customDialogController?.close()
        })

      Row()
        .width('100%')
        .height(this.isFullScreen ? this.paddingBottom : 0)
        .backgroundColor('#FFFFFF')
    }
    .width('100%')
    .backgroundColor('#fff5f4f3')
    .borderRadius({
      topLeft: '12vp',
      topRight: '12vp',
      bottomLeft: 0,
      bottomRight: 0
    })
  }

}

/**
 * Menu button in the tiny menu.
 */
@Component
struct TinyMenuButtonComponent {

  @ObjectLink tinyMenuButtonState: TinyMenuButtonState

  @ObjectLink menuButtonOnClickListener: MenuButtonOnClickListener

  build() {
    Column() {
      // Icon
      Row() {
        // Image
        Image(this.tinyMenuButtonState.image)
          .width('26vp')
          .height('26vp')
          .visibility(this.tinyMenuButtonState.image ? Visibility.Visible : Visibility.None)
          .objectFit(ImageFit.Contain)

        // Iconfont
        AUCustomIcon({
          text: this.tinyMenuButtonState.iconfont,
          fontSrc: $rawfile('tiny_iconfont.ttf'),
          fontSize: 26,
          fontColor: this.tinyMenuButtonState.iconfontColor
        })
          .visibility(this.tinyMenuButtonState.image ? Visibility.None : Visibility.Visible)
          .align(Alignment.Center)
      }
      .width('45vp')
      .height('45vp')
      .backgroundColor('#ffffff')
      .borderRadius('10vp')
      .borderStyle(BorderStyle.Solid)
      .borderWidth(0)
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.Center)

      // Title
      Text(this.tinyMenuButtonState.title)
        .fontColor('#333333')
        .fontSize('10vp')
        .margin({
          top: '2vp'
        })
        .maxLines(2)
        .ellipsisMode(EllipsisMode.END)
    }
    .width('65vp')
    .justifyContent(FlexAlign.Center)
    .onClick((clickEvent: ClickEvent): void => {
      if (this.menuButtonOnClickListener.onClickListener) {
        this.menuButtonOnClickListener.onClickListener(this.tinyMenuButtonState.mid)
      }
    })
  }

}

@Observed
class MenuButtonOnClickListener {

  public onClickListener: ((mid: string) => void) | undefined

  public constructor(onClickListener: ((mid: string) => void) | undefined) {
    this.onClickListener = onClickListener
  }

}

Support for custom loading and error pages

The following example shows the implementation of DemoLoadingComponent.ets:

import { EntryInfo, RightButtonState } from '@mpaas/xriverohos';

type AnimationStep = () => void;

@Component
export struct DemoLoadingComponent {
  @State rightButtonState: RightButtonState = new RightButtonState();
  appId?: string = '' // Miniapp ID
  @Prop appInfo?: EntryInfo; // Loading information
  @Prop loadingProgress: number = 0; // Loading progress
  @Prop errorCode: number = 0; // Error code. A non-zero value indicates an error.
  
  @State progressRotateOptions: RotateOptions = {
    angle: 0,
    centerX: '50%',
    centerY: '50%',
  }
  private rotateAnimationStep1?: AnimationStep;
  private rotateAnimationStep2?: AnimationStep;
  private rotateAnimationStep3?: AnimationStep;

  build() {
      Column() {
        Row() {
          RightButtonComponent({
            rightButtonState: this.rightButtonState
          })
            .margin({
              top: 0,
              right: 12,
              bottom: 0,
              left: 0
            })
        }.width('100%').justifyContent(FlexAlign.End)

        Row().height('20%')
        Stack() {
          Progress({ value: this.loadingProgress, total: 100, type: ProgressType.Ring })
            .width(55)
            .height(55)
            .color(this.errorCode <= 0 ? 0x1677ff : 0xdddddd)
            .backgroundColor(0xdddddd)
            .style({ strokeWidth: 1 })
            .rotate(this.progressRotateOptions)
          Image(this.appInfo?.iconUrl).alt($r('app.media.loading_page_icon')).width(40).height(40).borderRadius(20)
        }

        Row().height(18)
        Text(this.appInfo?.title).fontColor(0x333333).fontSize(18).width('100%').textAlign(TextAlign.Center)

        if (this.errorCode > 0) {
          Row().height(21)
          Text('Poor network connection').fontColor(0x333333).fontSize(20).width('100%').textAlign(TextAlign.Center)

          Row().height(10)
          Text('Please try again later').fontColor(0xaaaaaa).fontSize(14).width('100%').textAlign(TextAlign.Center)
        }
      }
      .width('100%')
  }

  aboutToAppear(): void {
    // rotate animation config
    this.rotateAnimationStep1 = this.generateAnimateStep(
      this.generateAnimateParam(() => {
        this.rotateAnimationStep2?.()
      }),
      120
    )

    this.rotateAnimationStep2 = this.generateAnimateStep(
      this.generateAnimateParam(() => {
        this.rotateAnimationStep3?.()
      }),
      240
    )

    this.rotateAnimationStep3 = this.generateAnimateStep(
      this.generateAnimateParam(() => {
        // reset rotateOptions
        this.progressRotateOptions = {
          angle: 0,
          centerX: '50%',
          centerY: '50%',
        }
        // repeat animation step
        if (this.errorCode <= 0) {
          this.rotateAnimationStep1?.()
        }
      }),
      360
    )

    setTimeout(() => {
      this.rotateAnimationStep1?.()
    }, 1000)
  }

  aboutToDisappear(): void {
  }

  private generateAnimateStep(value: AnimateParam, angle: number): AnimationStep {
    return () => {
      animateTo(value, () => {
        this.progressRotateOptions = {
          angle: angle,
          centerX: '50%',
          centerY: '50%',
        }
      })
    }
  }

  private generateAnimateParam(event: () => void): AnimateParam {
    return {
      duration: 300,
      tempo: 1,
      curve: Curve.Linear,
      iterations: 1,
      playMode: PlayMode.Normal,
      onFinish: () => {
        setTimeout(event,5);
      }
    }
  }
}

@Component
export struct RightButtonComponent {

  @ObjectLink rightButtonState: RightButtonState

  build() {
    Button({
      type: ButtonType.Normal,
      stateEffect:false
    }) {
    }
    .visibility(Visibility.Visible)
    .backgroundColor('#00000000')
    .onClick((event) => {
      if (this.rightButtonState.onClickListener !== undefined) {
        this.rightButtonState.onClickListener(event);
      }
    })
  }

}
import { AUDialogManager } from "@mpaas/antui";
import { PermissionAdapter, PermissionType } from "@mpaas/mpaas_permission_fortress";
import { common, Want } from "@kit.AbilityKit";

export class DemoPermissionAdapter extends PermissionAdapter {
  // type: The type of permission being requested. For specific values, see the subsequent part of this document. close: The callback method, which needs to be called after the dialog box is canceled. goToSettings parameter: true indicates to go to the Settings page and then dismiss the dialog box; false indicates to dismiss the dialog box directly.
  showGuideDialog(type: PermissionType, close: (goToSettings: boolean) => void): boolean {
    // Custom dialog box logic. The following is just an example.
    AUDialogManager.showNotice({
      onClose: () => {
        close(false) // Callback
      },
      title: 'Permission Request ' + type, // Title, based on business requirements
      message: '',  // Dialog box content
      buttons: [{
        title: $r('app.string.go_to_settings'),
        action: () => {

          close(true) // Callback

          let context = getContext(this) as common.UIAbilityContext;
          let want: Want = {
            bundleName: 'com.huawei.hmos.settings',
            abilityName: 'com.huawei.hmos.settings.MainAbility',
            uri: 'application_info_entry',
            parameters: { pushParams: '' }
          };
          context.startAbility(want).then((data) => {
            console.info('Go to the settings authorization page');
          })
        }
      }]
    })
    return true
  }
}

PermissionType types:

export declare enum PermissionType {
    None = 0,
    Location = 1, // Location
    Location_Background = 2,
    Location_Approximately = 3,
    Camera = 4, // Camera
    MicroPhone = 5, // Microphone
    BlueTooth = 6, // Bluetooth
    Health_Read = 7, // Health data
    Notification = 8, // Notification
    Pasteboard = 9, // Clipboard
    Calendar = 10, // Calendar
    Media = 11, // Media
    Photo_Write = 12, // Photo album write
    Photo = 13, // Photo album read
    Contacts = 14, // Address book
    Carrier = 15, // Carrier
    Vibrate = 16 // Vibrate
}

Custom permission guide dialog box

If a system permission, such as location access, is denied, a guide dialog box appears the next time the corresponding feature is called. You can customize this guide dialog box.

After startup and initialization, you can set a custom provider.

import { PermissionAdapter, PermissionAdapterUtils } from '@mpaas/mpaas_permission_fortress';
...
...


PermissionAdapterUtils.setProvider(PermissionAdapter.name, new DemoPermissionAdapter())

The implementation of DemoPermissionAdapter.ets is as follows:

import { AUDialogManager } from "@mpaas/antui";
import { PermissionAdapter, PermissionType } from "@mpaas/mpaas_permission_fortress";
import { common, Want } from "@kit.AbilityKit";

export class DemoPermissionAdapter extends PermissionAdapter {
  // type: The type of permission being requested. For specific values, see the subsequent part of this document. close: The callback method, which needs to be called after the dialog box is canceled. goToSettings parameter: true indicates to go to the Settings page and then dismiss the dialog box; false indicates to dismiss the dialog box directly.
  showGuideDialog(type: PermissionType, close: (goToSettings: boolean) => void): boolean {
    // Custom dialog box logic. The following is just an example.
    AUDialogManager.showNotice({
      onClose: () => {
        close(false) // Callback
      },
      title: 'Permission Request ' + type, // Title, based on business requirements
      message: '',  // Dialog box content
      buttons: [{
        title: $r('app.string.go_to_settings'),
        action: () => {

          close(true) // Callback

          let context = getContext(this) as common.UIAbilityContext;
          let want: Want = {
            bundleName: 'com.huawei.hmos.settings',
            abilityName: 'com.huawei.hmos.settings.MainAbility',
            uri: 'application_info_entry',
            parameters: { pushParams: '' }
          };
          context.startAbility(want).then((data) => {
            console.info('Go to the settings authorization page');
          })
        }
      }]
    })
    return true
  }
}

PermissionType types:

export declare enum PermissionType {
    None = 0,
    Location = 1, // Location
    Location_Background = 2,
    Location_Approximately = 3,
    Camera = 4, // Camera
    MicroPhone = 5, // Microphone
    BlueTooth = 6, // Bluetooth
    Health_Read = 7, // Health data
    Notification = 8, // Notification
    Pasteboard = 9, // Clipboard
    Calendar = 10, // Calendar
    Media = 11, // Media
    Photo_Write = 12, // Photo album write
    Photo = 13, // Photo album read
    Contacts = 14, // Address book
    Carrier = 15, // Carrier
    Vibrate = 16 // Vibrate
}

Support for delayed initialization

To comply with privacy policy requirements, you must support delayed initialization. For delayed initialization, you need to inject the homepage UIAbility information during the initialization process. The procedure is as follows:

  1. Create the HomeAbilityContext class.

    import { common } from '@kit.AbilityKit';
    import { window } from '@kit.ArkUI';
    
    export class HomeAbilityContext {
      private mAbilityContext?: common.UIAbilityContext;
      private mNavPathStack?: NavPathStack;
      private mWindowStage?: window.WindowStage;
    
      constructor(abilityContext?: common.UIAbilityContext) {
        this.mAbilityContext = abilityContext;
      }
    
      setWindowStage(windowStage?: window.WindowStage) {
        this.mWindowStage = windowStage;
      }
    
      getWindowStage() {
        return this.mWindowStage;
      }
    
      setNavPathStack(navPathStack?: NavPathStack) {
        this.mNavPathStack = navPathStack;
        if (this.mAbilityContext) {
          this.mAbilityContext.eventHub.emit(navPathStack ? "FRAMEWORK_EVENT_NAV_PATH_STACK_ATTACH"
            : "FRAMEWORK_EVENT_NAV_PATH_STACK_DETACH");
        }
      }
    
      getNavPathStack(): NavPathStack | undefined {
        return this.mNavPathStack;
      }
    }
  2. In the onCreate method of the entry EntryAbilityStage, register abilityLifecycle and cache TopUIAbility.

    let abilityLifecycleCallback = {
          onAbilityCreate(ability: UIAbility) {
            console.log(ability + ' onAbilityCreate.');
            setTopAbility(ability)
          },
    
          onAbilityDestroy(ability: UIAbility) {
            if (getTopAbility() === ability) {
              setTopAbility(undefined)
            }
    
          },
    
          onAbilityForeground(ability: UIAbility) {
            console.log(ability + ' onAbilityForeground.');
            setTopAbility(ability)
          },
    
          onAbilityBackground(ability: UIAbility) {
            console.log(ability + ' onAbilityBackground.');
          },
    
        } as AbilityLifecycleCallback;
        this.context.getApplicationContext().getApplicationContext().on("abilityLifecycle", abilityLifecycleCallback)
  3. After you initialize the miniapp, you must inject the homepage context before you call HRiverMini.notifyNavigationCreate.

    let applicationManager: ESObject = Framework.microApplicationContext["applicationManger"]
        if (applicationManager) {
          applicationManager["mTopAbility"] = getTopAbility() // Get the TopUIAbility cached in the second step.
          if (applicationManager["mAbilityInfoMap"]) {
            let map: Map<common.UIAbilityContext, ESObject> = applicationManager["mAbilityInfoMap"] as Map<common.UIAbilityContext, ESObject>
            let homeContext = new HomeAbilityContext(this.context)
            homeContext.setWindowStage(getWindowStage())
            map.set(this.context, homeContext)
          }
        }

Special notes for components in the current version

  • The map component is based on Huawei Map and supports the getCurrentLocation and moveToLocation APIs. You must apply for permission to use it on the official Huawei Map website.

  • To retrieve clipboard content, the app must request the ohos.permission.READ_PASTEBOARD permission. This is an Access Control List (ACL) permission.

Unsupported components and APIs in the current version

  • File APIs: Retrieve file information, retrieve file list, remove file, delete file

  • canvas2

  • Live streaming

  • Contacts

  • Hide keyboard

Map component support

Supported map component APIs

  • latitude

  • longitude

  • scale

  • markers

  • polyline

  • circles

  • polygon

  • include-points

Unsupported map component APIs

  • Advanced custom rendering for maps: The customCallout of a marker only supports type=2.

  • The style attribute only supports style rendering of type=3. Other types are not supported.

  • The onRegionChange event only supports the end type, not the start type.