前言
作者:賈葉照
在萬物互聯時代,人均持有裝置量不斷攀升,裝置和場景的多樣性,使應用開發變得更加複雜、應用入口更加豐富。在此背景下,應用提供方和使用者迫切需要一種新的服務提供方式,使應用開發更簡單、服務(如聽音樂、打車等)的擷取和使用更便捷。為此,HarmonyOS提供了特定功能的免安裝的應用(即原子化服務)。
原子化服務是HarmonyOS提供的一種面向未來的服務提供方式,是有獨立入口的(使用者可通過點選方式直接觸發)、免安裝的(無需顯式安裝,由系統程式架構背景安裝後即可使用)、可為使用者提供一個或多個便捷服務的使用者應用程式形态。例如:某傳統方式的需要安裝的購物應用A,在按照原子化服務理念調整設計後,成為由“商品浏覽”“購物車”“支付”等多個便捷服務組成的、可以免安裝的購物原子化服務A*。
原子化服務特征
1.随處可及
原子化服務通過以下方式被發現并使用:
- 服務發現:原子化服務可在服務中心發現并使用。
- 智能推薦:原子化服務可以基于合适場景被主動推薦給使用者使用;使用者可在服務中心和小藝建議中發現系統推薦的服務。
2.服務直達
服務直達展現在以下兩個方面:
- 原子化服務支援免安裝使用。
- 服務卡片:支援使用者無需打開原子化服務便可擷取服務内重要資訊的展示和動态變化,如天氣、關鍵事務備忘、熱點新聞清單。
3.跨裝置
跨裝置展現在以下幾個方面:
- 原子化服務支援運作在1+8+N裝置上,如手機、平闆等裝置。
- 支援跨裝置分享:例如接入華為分享後,使用者可分享原子化服務給好友,好友确認後打開分享的服務。
- 支援跨端遷移:例如手機上未完成的郵件,遷移到平闆繼續編輯。
- 支援多端協同:例如手機用作文檔翻頁和批注,配合智慧屏顯示完成分布式辦公;手機作為搖桿,與智慧屏配合玩遊戲。
初始化原子化服務
差別于傳統的HarmonyOS應用,在通過DevEco Studio工程向導建立原子化服務時,Project Type應選擇為Service,同時勾選Show in Service Center,如圖:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsQTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5SMxETM5QzN3cDOkZjY2kzMyYzXxQzMyQTMyIzLcNDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjL0M3Lc9CX6MHc0RHaiojIsJye.png)
原子化服務的項目結構
如下圖:
上述目錄中,widget包下的代碼即為服務卡片的相關的代碼。其中:
- controller包下的FormController是服務卡片控制器的接口。
- controller包下的FormControllerManager是服務卡片控制器的管理器。
-
widget包下的WidgetImpl是FormController的預設實作類。
此時,會同步建立一個2x2的預設服務卡片模闆form_image_with_information_widget_2_2.xml,同時還會建立該卡片對應的快照圖form_image_with_information_widget_default_image_2.png。
FormController
FormController定義了卡片的控制器接口,代碼如下:
/**
* The api set for form controller.
*/
public abstract class FormController {
/**
* Context of ability
*/
protected final Context context;
/**
* The name of current form service widget
*/
protected final String formName;
...
/**
* Get the destination ability slice to route
*
* @param intent intent of current page slice
* @return the destination ability slice name to route
*/
public abstract Class<? extends AbilitySlice> getRoutePageSlice(Intent intent);
上述接口由代碼生成器直接生成,基本上在開發過程中無須修改,直接使用即可。
FormControllerManager
FormControllerManager是服務卡片控制器的管理器,代碼如下:
/**
* Form controller manager.
*/
public class FormControllerManager {
private static final HiLogLabel TAG = new HiLogLabel(HiLog.DEBUG, 0x0, FormControllerManager.class.getName());
private static final String PACKAGE_PATH = "com.example.weatherservicecard.widget";
private static final String SHARED_SP_NAME = "form_info_sp.xml";
private static final String FORM_NAME = "formName";
private static final String DIMENSION = "dimension";
private static FormControllerManager managerInstance = null;
private final HashMap<Long, FormController> controllerHashMap = new HashMap<>();
private final Context context;
private final Preferences preferences;
/**
* Constructor with context.
*
* @param context instance of Context.
*/
private FormControllerManager(Context context) {
this.context = context;
DatabaseHelper databaseHelper = new DatabaseHelper(this.context.getApplicationContext());
preferences = databaseHelper.getPreferences(SHARED_SP_NAME);
}
/**
* Singleton mode.
*
* @param context instance of Context.
* @return FormControllerManager instance.
*/
public static FormControllerManager getInstance(Context context) {
if (managerInstance == null) {
synchronized (FormControllerManager.class) {
if (managerInstance == null) {
managerInstance = new FormControllerManager(context);
}
}
}
return managerInstance;
}
...
private String getClassNameByFormName(String formName) {
String[] strings = formName.split("_");
StringBuilder result = new StringBuilder();
for (String string : strings) {
result.append(string);
}
char[] charResult = result.toString().toCharArray();
charResult[0] = (charResult[0] >= 'a' && charResult[0] <= 'z') ? (char) (charResult[0] - 32) : charResult[0];
return String.copyValueOf(charResult) + "Impl";
}
}
上述類由代碼生成器直接生成,基本上在開發過程中無須修改,直接使用即可。
WidgetImpl
WidgetImpl是FormController的預設實作類,代碼如下:
public class WidgetImpl extends FormController {
private static final HiLogLabel TAG = new HiLogLabel(HiLog.DEBUG, 0x0, WidgetImpl.class.getName());
private static final int DEFAULT_DIMENSION_2X2 = 2;
private static final MapInteger, Integer RESOURCE_ID_MAP = new HashMap();
static {
RESOURCE_ID_MAP.put(DEFAULT_DIMENSION_2X2, ResourceTable.Layout_form_image_with_information_widget_2_2);
}
public WidgetImpl(Context context, String formName, Integer dimension) {
super(context, formName, dimension);
}
...
@Override
public Class extends AbilitySlice getRoutePageSlice(Intent intent) {
HiLog.info(TAG, get the default page to route when you click card.);
return null;
}
}
上述WidgetImpl類可以根據實際開發需要進行修改,比如涉及資料的更新或者事件的監聽,隻需要重寫上述的updateFormData或者onTriggerFormEvent方法即可。
MainAbility
MainAbility是首頁面,代碼如下:
public class MainAbility extends Ability {
public static final int DEFAULT_DIMENSION_2X2 = 2;
private static final int INVALID_FORM_ID = -1;
private static final HiLogLabel TAG = new HiLogLabel(HiLog.DEBUG, 0x0, MainAbility.class.getName());
private String topWidgetSlice;
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setMainRoute(MainAbilitySlice.class.getName());
if (intentFromWidget(intent)) {
topWidgetSlice = getRoutePageSlice(intent);
if (topWidgetSlice != null) {
setMainRoute(topWidgetSlice);
}
}
stopAbility(intent);
}
@Override
protected ProviderFormInfo onCreateForm(Intent intent) {
HiLog.info(TAG, "onCreateForm");
long formId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, INVALID_FORM_ID);
String formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
int dimension = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, DEFAULT_DIMENSION_2X2);
HiLog.info(TAG, "onCreateForm: formId=" + formId + ",formName=" + formName);
FormControllerManager formControllerManager = FormControllerManager.getInstance(this);
FormController formController = formControllerManager.getController(formId);
formController = (formController == null) ? formControllerManager.createFormController(formId,
formName, dimension) : formController;
if (formController == null) {
HiLog.error(TAG, "Get null controller. formId: " + formId + ", formName: " + formName);
return null;
}
return formController.bindFormData();
}
@Override
protected void onUpdateForm(long formId) {
HiLog.info(TAG, "onUpdateForm");
super.onUpdateForm(formId);
FormControllerManager formControllerManager = FormControllerManager.getInstance(this);
FormController formController = formControllerManager.getController(formId);
formController.updateFormData(formId);
}
@Override
protected void onDeleteForm(long formId) {
HiLog.info(TAG, "onDeleteForm: formId=" + formId);
super.onDeleteForm(formId);
FormControllerManager formControllerManager = FormControllerManager.getInstance(this);
formControllerManager.deleteFormController(formId);
}
@Override
protected void onTriggerFormEvent(long formId, String message) {
HiLog.info(TAG, "onTriggerFormEvent: " + message);
super.onTriggerFormEvent(formId, message);
FormControllerManager formControllerManager = FormControllerManager.getInstance(this);
FormController formController = formControllerManager.getController(formId);
formController.onTriggerFormEvent(formId, message);
}
@Override
public void onNewIntent(Intent intent) {
if (intentFromWidget(intent)) { // Only response to it when starting from a service widget.
String newWidgetSlice = getRoutePageSlice(intent);
if (topWidgetSlice == null || !topWidgetSlice.equals(newWidgetSlice)) {
topWidgetSlice = newWidgetSlice;
restart();
}
}
}
private boolean intentFromWidget(Intent intent) {
long formId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, INVALID_FORM_ID);
return formId != INVALID_FORM_ID;
}
private String getRoutePageSlice(Intent intent) {
long formId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, INVALID_FORM_ID);
if (formId == INVALID_FORM_ID) {
return null;
}
FormControllerManager formControllerManager = FormControllerManager.getInstance(this);
FormController formController = formControllerManager.getController(formId);
if (formController == null) {
return null;
}
Class<? extends AbilitySlice> clazz = formController.getRoutePageSlice(intent);
if (clazz == null) {
return null;
}
return clazz.getName();
}
}
卡片服務也是在該MainAbility類中進行管理和路由的。
修改form_image_with_information_widget_2_2.xml
form_image_with_information_widget_2_2.xml是原子化服務卡片的布局,對其進行個性化的修改,代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<DependentLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:background_element="#FFFFFFFF"
ohos:remote="true">
<Image
ohos:height="match_parent"
ohos:width="126vp"
ohos:horizontal_center="true"
ohos:image_src="$media:weather"
ohos:scale_mode="zoom_start"
ohos:top_margin="17vp"/>
<DirectionalLayout
ohos:height="match_content"
ohos:width="126vp"
ohos:align_parent_bottom="true"
ohos:bottom_margin="12vp"
ohos:horizontal_center="true"
ohos:orientation="vertical">
<Text
ohos:height="match_content"
ohos:width="match_parent"
ohos:text="天氣預報"
ohos:text_color="#E5000000"
ohos:text_size="16fp"
ohos:text_weight="500"
ohos:truncation_mode="ellipsis_at_end"/>
<Text
ohos:height="match_content"
ohos:width="match_parent"
ohos:text="随時掌握天氣情況"
ohos:text_color="#99000000"
ohos:text_size="12fp"
ohos:text_weight="400"
ohos:top_margin="2vp"
ohos:truncation_mode="ellipsis_at_end"/>
</DirectionalLayout>
</DependentLayout>
預覽原子化服務卡片,如下圖所示:
修改ability_main.xml
ability_main是整個項目的布局,修改ability_main.xml,代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:alignment="center"
ohos:orientation="vertical">
<Image
ohos:height="match_content"
ohos:width="match_content"
ohos:image_src="$media:weather"
/>
<Text
ohos:height="match_content"
ohos:width="match_parent"
ohos:text="天氣預報"
ohos:text_size="30fp"
/>
<Text
ohos:height="match_content"
ohos:width="match_parent"
ohos:text="随時掌握天氣情況"
ohos:text_size="20fp"
/>
</DirectionalLayout>
預覽效果,如圖所示:
config.json
config.json是整個項目的配置檔案,代碼如下:
{
"app": {
...
}
},
"deviceConfig": {},
"module": {
....
"abilities": [
{
...
"orientation": "unspecified",
"name": "com.example.weatherservicecard.MainAbility",
"icon": "$media:icon",
"description": "$string:mainability_description",
"formsEnabled": true,
"label": "$string:entry_MainAbility",
"type": "page",
"forms": [
{
"landscapeLayouts": [
"$layout:form_image_with_information_widget_2_2"
],
"isDefault": true,
"scheduledUpdateTime": "10:30",
"defaultDimension": "2*2",
"name": "widget",
"description": "This is a service widget",
"colorMode": "auto",
"type": "Java",
"supportDimensions": [
"2*2"
],
"portraitLayouts": [
"$layout:form_image_with_information_widget_2_2"
],
"updateEnabled": true,
"updateDuration": 1
}
],
"launchType": "standard"
}
]
}
}
注意:這裡的label标簽是便捷服務隊使用者顯示的名稱,必須配置,且應以資源索引的方式配置,以支援多語言,不同的HAP包的label要唯一,以免造成使用者看到多個同名服務而無法區分。此外label的命名應知名見義。
{
string [
{
"name": "entry_MainAbility",
"value": "WeatherServiceCard"
},
...
]
}
搜尋原子化服務
安裝完原子化服務後,就可以在服務中心通過的名稱搜尋到該原子服務,如下圖所示,可以通過長按卡片将服務添加到桌面和我的收藏,或者直接點選卡片來打開原子化服務。
運作原子化服務
點選卡片來打開原子化服務,就能夠運作該原子服務,效果如下圖所示:
更多原創内容請關注:中軟國際 HarmonyOS 技術團隊
入門到精通、技巧到案例,系統化分享HarmonyOS開發技術,歡迎投稿和訂閱,讓我們一起攜手前行共建鴻蒙生态。
想了解更多關于鴻蒙的内容,請通路:
51CTO和華為官方合作共建的鴻蒙技術社群
https://ost.51cto.com/#bkwz