One HarmonyOS app can have only one or more Ability instances. Abilities are classified into two types: Feature Ability (FA) and Particle Ability (PA). Each type has their respective templates for different capabilities. This codelab will use Page and Service abilities for development. A Page ability refers to an FA using the Page template, which is the only template FAs can use. A Service ability refers to a PA using the Service template, which is used to run background tasks. In this codelab, we will build a simple distributed network-supported news reading client using common HarmonyOS components such as ListContainer and Image, and the OS's cross-device FA startup capability, in addition to Page and Service abilities.

Final Effect Preview

We will eventually build a simple HarmonyOS news app. The app consists of two levels of pages: home page and details page. Both pages display various HarmonyOS components. The implementation logic of the details page also shows how to call particular APIs to start an FA across devices. In this codelab, we are going to implementing the following components and functions:

  1. ListContainer objects for the top menu and news list
  2. Text box and image of each piece of news
  3. Page layout and redirection
  4. Device discovery and cross-device FA startup

The procedure is as follows:

You can use either of the following devices to run the codelab:

This codelab helps you understand the core code only. For complete code, refer to 12 References to download the source code. Before we start, let's have a look at the code structure of the entire project.

First, let's use the ListContainer component to add some news categories on the top for our app. For more information about ListContainer, see HarmonyOS Common Java Components.
Declare the ListContainer component in the layout file. In this example, we will put the following code in the resources\base\layout\news_list_layout.xml file:

<ListContainer ohos:id="$+id:selector_list" ohos:width="match_parent" ohos:height="40vp" ohos:orientation="horizontal" />

In addition, in the NewsListAbilitySlice.java file, set the selectorListContainer variable in the initView() method for component association.

selectorListContainer = (ListContainer) findComponentById(ResourceTable.Id_selector_list);

Adding a Listener

The news items displayed on the app screen change accordingly when a user switches between different news categories. Therefore, we also need to implement the initListener() method as follows in NewsListAbilitySlice.java to add a listener for the ListContainer:

selectorListContainer.setItemClickedListener( (listContainer, component, position, id) -> { // Set the zoom-in effect after the component gets focus. setCategorizationFocus(false); selectText = (Text) component.findComponentById(ResourceTable.Id_news_type_text); setCategorizationFocus(true); newsDataList.clear(); for (NewsInfo mTotalNewsData : totalNewsDataList) { if (selectText.getText().equals(mTotalNewsData.getType()) || id == 0) { newsDataList.add(mTotalNewsData); } } updateListView(); });

Declaring NewsTypeProvider

Save the news data in two JSON files in the resources/rawfile directory beforehand so that our app will use data faster and easier. What's more, declare some data providers to facilitate data acquisition and transmission. For example, a code snippet of the NewsTypeProvider object for obtaining news categories is defined as follows:

@Override public Component getComponent(int position, Component component, ComponentContainer componentContainer) { ViewHolder viewHolder; Component temp = component; if (temp == null) { temp = LayoutScatter.getInstance(context).parse(ResourceTable.Layout_item_news_type_layout, null, false); // Bind all child components to the list item instance through ViewHolder. viewHolder = new ViewHolder(); viewHolder.title = (Text) temp.findComponentById(ResourceTable.Id_news_type_text); component.setTag(viewHolder); } else { viewHolder = (ViewHolder) temp.getTag(); } viewHolder.title.setText(mNewsTypeList.get(position).getName()); return temp; }

Use initData to parse the news data stored in the JSON files and transfer the data to a data provider. The implementation of the initData() method in NewsListAbilitySlice.java is as follows:

private void initData() { Gson gson = new Gson(); List<NewsType> newsTypeList = gson.fromJson( CommonUtils.getStringFromJsonPath(this, "entry/resources/rawfile/news_type_datas.json"), new TypeToken<List<NewsType>>(){ }.getType()); newsTypeProvider = new NewsTypeProvider(newsTypeList, this); }

Adding a Transition Effect

Put the following code in the setCategorizationFocus() method to implement a zoom-in effect when a user selects the title of a particular news category:

private void setCategorizationFocus(boolean isFocus) { if (selectText == null) { return; } if (isFocus) { selectText.setTextColor( new Color(CommonUtils.getColor(NewsListAbilitySlice.this, ResourceTable.Color_news_type_text_on))); selectText.setScaleX(FOCUS_TEXT_SIZE); selectText.setScaleY(FOCUS_TEXT_SIZE); } else { selectText.setTextColor( new Color(CommonUtils.getColor(NewsListAbilitySlice.this, ResourceTable.Color_news_type_text_off))); selectText.setScaleX(UNFOCUSED_TEXT_SIZE); selectText.setScaleY(UNFOCUSED_TEXT_SIZE); }

The following figure shows the zoom-in effect implemented on the selected news category.

Layout of Items in the News List

The layout of the home page consists of the top bar displaying news categories and a list presenting news items. The news list as a whole is a ListContainer component. First, let's see how the ListContainer should be defined in the new_list_layout.xml file:

<ListContainer ohos:id="$+id:news_container" ohos:width="match_parent" ohos:height="match_parent"/>

The news list consists of multiple news items. Each item consists of a title and an image. The following example shows how an item is defined in the item_news_layout.xml layout:

<DirectionalLayout ohos:height="109.5vp" ohos:width="match_parent" ohos:orientation="horizontal" ohos:padding="10vp"> <Text ohos:id="$+id:item_news_title" ohos:height="match_content" ohos:width="0vp" ohos:max_text_lines="3" ohos:multiple_lines="true" ohos:right_padding="20vp" ohos:text_size="18vp" ohos:weight="3"/> <Image ohos:id="$+id:item_news_image" ohos:height="match_parent" ohos:width="0vp" ohos:scale_mode="stretch" ohos:weight="2"/> </DirectionalLayout>

Declaring NewsListProvider

Similar to the news categories component on the top bar, the title and image in each news item are also passed through a provider. The NewsListProvider.java file contains the following code:

@Override public Component getComponent(int position, Component component, ComponentContainer componentContainer) { ViewHolder viewHolder; Component temp = component; if (temp == null) { component = LayoutScatter.getInstance(context).parse(ResourceTable.Layout_item_news_layout, null, false); // Bind all child components to the list item instance through ViewHolder. viewHolder = new ViewHolder(); viewHolder.title = (Text) temp.findComponentById(ResourceTable.Id_item_news_title); viewHolder.image = (Image) temp.findComponentById(ResourceTable.Id_item_news_image); temp.setTag(viewHolder); } else { viewHolder = (ViewHolder) temp.getTag(); } viewHolder.title.setText(newsInfoList.get(position).getTitle()); viewHolder.image.setPixelMap(CommonUtils.getPixelMapFromPath(context, newsInfoList.get(position).getImgUrl())); return temp; }

Similarly, news data is parsed and assigned to newsListProvider via initData. Therefore, add the following code to the initData() method in NewsListAbilitySlice:

totalNewsDataList = gson.fromJson( CommonUtils.getStringFromJsonPath(this, "entry/resources/rawfile/news_datas.json"), new TypeToken<List<NewsInfo>>(){ }.getType()); newsDataList = new ArrayList<>(); newsDataList.addAll(totalNewsDataList); newsListProvider = new NewsListProvider(newsDataList, this);

Now, we have loaded and parsed the data. Let's move on to adding click events for the items.

Adding a Listener

When a user taps a particular news item on the home page, the app will navigate to the news details page. To achieve this, implement the initListener() method as follows in NewsListAbilitySlice.java to add a click listener for news items:

newsListContainer.setItemClickedListener( (listContainer, component, position, id) -> { Intent intent = new Intent(); Operation operation = new Intent.OperationBuilder() .withBundleName(getBundleName()) .withAbilityName(NewsAbility.class.getName()) .withAction("action.detail") .build(); intent.setOperation(operation); intent.setParam(NewsDetailAbilitySlice.INTENT_TITLE, newsDataList.get(position).getTitle()); intent.setParam(NewsDetailAbilitySlice.INTENT_READ, newsDataList.get(position).getReads()); intent.setParam(NewsDetailAbilitySlice.INTENT_LIKE, newsDataList.get(position).getLikes()); intent.setParam(NewsDetailAbilitySlice.INTENT_CONTENT, newsDataList.get(position).getContent()); intent.setParam(NewsDetailAbilitySlice.INTENT_IMAGE, newsDataList.get(position).getImgUrl()); startAbility(intent); });

The startAbility() method is the key method for page navigation. The intent parameter specifies important parameters such as the bundle name, ability name, and the title and image URL on the details page.

The layout of the news details page is a bit more complex than that of the home page. The details page as a whole is held in a DependentLayout component, which contains a DirectionalLayout, ScrollView, and other components. The entire page is divided into the top, middle, and bottom panes. The layout of the details page is implemented in the resources\base\layout\new_detail_layout.xml file.

Top Pane

The top pane is presented using the DirectionalLayout and Text components. The DirectionalLayout is used to hold the NewsDemo string on the left, and the Text components hold the reads and likes strings on the right. The layout effect and code for implementing the layout are as follows.

<DirectionalLayout ohos:width="match_parent" ohos:height="match_content" ohos:alignment="vertical_center" ohos:orientation="horizontal"> <Text ohos:id="$+id:title_icon" ohos:width="match_content" ohos:height="match_content" ohos:weight="1" ohos:text="NewsDemo" ohos:text_size="20fp"/> <Text ohos:id="$+id:read_num" ohos:width="match_content" ohos:height="match_content" ohos:text_size="10fp" ohos:right_margin="10vp"/> <Text ohos:id="$+id:like_num" ohos:width="match_content" ohos:height="match_content" ohos:text_size="10fp"/> </DirectionalLayout>

Middle Pane

The middle pane of the page consists of a Text component holding the news title, an Image component presenting the thumbnail, and another Text component holding the news content. The layout effect and code for implementing the layout are as follows.

<Text ohos:id="$+id:title_text" ohos:width="match_parent" ohos:height="match_content" ohos:text_size="18fp" ohos:max_text_lines="4" ohos:multiple_lines="true" ohos:text_color="#000000" ohos:top_margin="10vp"/> <Image ohos:id="$+id:image_content" ohos:width="match_parent" ohos:scale_mode="stretch" ohos:height="300vp" ohos:top_margin="10vp"/> <Text ohos:id="$+id:title_content" ohos:width="match_parent" ohos:height="match_content" ohos:multiple_lines="true" ohos:text_color="#708090" ohos:text_size="16vp" ohos:text_alignment="center_horizontal" ohos:top_margin="5vp"/>

Bottom Pane

The DirectionalLayout, TextField, and Image components are used to hold the comment writing text box and several buttons in the bottom pane. The layout effect and code for implementing the layout are as follows.

<DirectionalLayout ohos:id="$+id:bottom_layout" ohos:align_parent_bottom="true" ohos:width="match_parent" ohos:height="50vp" ohos:orientation="horizontal" ohos:background_element="#ffffff" ohos:alignment="vertical_center" ohos:left_padding="20vp" ohos:right_padding="20vp" > <TextField ohos:id="$+id:text_file" ohos:width="160vp" ohos:height="30vp" ohos:left_padding="5vp" ohos:right_padding="10vp" ohos:text_alignment="vertical_center" ohos:text_size="15vp" ohos:hint="Enter a comment." ohos:background_element="$graphic:corner_bg_comment"/> <Image ohos:id="$+id:button1" ohos:width="20vp" ohos:height="20vp" ohos:image_src="$media:message_icon" ohos:scale_mode="stretch" ohos:left_margin="20vp"/> <Image ohos:id="$+id:button2" ohos:width="20vp" ohos:height="20vp" ohos:image_src="$media:collect_icon" ohos:scale_mode="stretch" ohos:left_margin="20vp"/> <Image ohos:id="$+id:button3" ohos:width="20vp" ohos:height="20vp" ohos:image_src="$media:like_icon" ohos:scale_mode="stretch" ohos:left_margin="20vp"/> <Image ohos:id="$+id:button4" ohos:width="20vp" ohos:height="20vp" ohos:image_src="$media:share_icon" ohos:scale_mode="stretch" ohos:left_margin="20vp"/> </DirectionalLayout>

Receiving Data from the NewsListAbilitySlice Page

We have described how important parameters, such as the news title and image URL, are stored in 5 Adding News Items on the Home Page. Now, Let's see how to retrieve these parameters on the details page. The code for the onStart() method in NewsDetailAbilitySlice.java is as follows:

public void onStart(Intent intent) { super.onStart(intent); super.setUIContent(ResourceTable.Layout_news_detail_layout); reads = intent.getStringParam(INTENT_READ); likes = intent.getStringParam(INTENT_LIKE); title = intent.getStringParam(INTENT_TITLE); content = intent.getStringParam(INTENT_CONTENT); image = intent.getStringParam(INTENT_IMAGE); }

The parameters previously stored in the intent are now retrieved one by one in onStart().

Initializing the Layout and Components

In addition to declaring the layout and components in the XML layout file, we must add the initView() method to onStart() in the NewsDetailAbilitySlice.java file to initialize them. The sample code is as follows:

private void initView() { parentLayout = (DependentLayout) findComponentById(ResourceTable.Id_parent_layout); commentFocus = (TextField) findComponentById(ResourceTable.Id_text_file); iconShared = (Image) findComponentById(ResourceTable.Id_button4); Text newsRead = (Text) findComponentById(ResourceTable.Id_read_num); Text newsLike = (Text) findComponentById(ResourceTable.Id_like_num); Text newsTitle = (Text) findComponentById(ResourceTable.Id_title_text); Text newsContent = (Text) findComponentById(ResourceTable.Id_title_content); Image newsImage = (Image) findComponentById(ResourceTable.Id_image_content); newsRead.setText("reads: " + reads); newsLike.setText("likes: " + likes); newsTitle.setText("Original title: " + title); newsContent.setText(content); newsImage.setPixelMap(CommonUtils.getPixelMapFromPath(this, image)); }

Adding a Listener

The device discovery process will be triggered when a user taps the share button on the lower right corner of the details page, and all candidate devices will be displayed for the user to select. To implement this functionality, we need to add two listeners by calling initListener() to the onStart() method in NewsDetailAbilitySlice.java. The implementation code of initListener() is as follows:

private void initListener() { parentLayout.setTouchEventListener( (component, touchEvent) -> { if (commentFocus.hasFocus()) { commentFocus.clearFocus(); } return true; }); iconShared.setClickedListener( component -> { initDevices(); showDeviceList(); }); }

The listener set in parentLayout is used to listen for whether the touch focus is on the pop-up device list dialog, and the listener set in iconShared is used to listen for whether the share button is tapped.

In the previous section, we mentioned that the device discovery process will be triggered when a user taps the share button. In this section, we are going to learn how devices are discovered upon the touch event.
There are two methods in initListener() for discovering devices: initDevices() and showDeviceList(). The initDevices() method discovers available devices and stores the discovered devices in a List object. The implementation code of initDevices() is as follows:

private void initDevices() { if (devices.size() > 0) { devices.clear(); } List<DeviceInfo> deviceInfos = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE); devices.addAll(deviceInfos); }

The showDeviceList() method, on the other hand, displays the discovered devices in a dialog so that the user can select one for cross-device transitions. Add the following code to the showDeviceList() method in NewsDetailAbilitySlice.java:

private void showDeviceList() { // Set the device list dialog. dialog = new CommonDialog(NewsDetailAbilitySlice.this); dialog.setAutoClosable(true); dialog.setTitleText("HarmonyOS devices"); dialog.setSize(DIALOG_SIZE_WIDTH, DIALOG_SIZE_HEIGHT); ListContainer devicesListContainer = new ListContainer(getContext()); DevicesListAdapter devicesListProvider = new DevicesListProvider(devices, this); devicesListContainer.setItemProvider(devicesListAdapter); devicesListContainer.setItemClickedListener( (listContainer, component, position, id) -> { dialog.destroy() // Start the specified FA on a remote device. startAbilityFA(devices.get(position).getDeviceId()); }); devicesListAdapter.notifyDataChanged(); dialog.setContentCustomComponent(devicesListContainer); dialog.show(); }

When a user selects a particular device, the specified FA will be started on the target device. The content of the started FA on the target device is the same as that of the FA on the local device.

Connecting to a Service Ability

In this section, we'll talk about how multi-device collaboration is implemented. Similar to the news item list, the discovered device list is also displayed through a ListContainer, and its layout is declared in an XML file with certain variables defined. In order to start the specified FA on the particular device, we have added a listener for each device. To start the specified FA, add the startAlibityFA() method to the showDeviceList() method defined in NewsDetailAbilitySlice.java. The code is as follows:

private void startAbilityFA(StringdevicesId) { Intent intent = new Intent(); Operation operation = new Intent.OperationBuilder() .withDeviceId(devicesId) .withBundleName(getBundleName()) .withAbilityName(SharedService.class.getName()) // This flag is used to enable cross-device startup for distributed scheduling. .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) .build(); intent.setOperation(operation); boolean connectFlag = // Connect to the remote Service ability. connectAbility( intent, new IAbilityConnection() { @Override public void onAbilityConnectDone( ElementName elementName, IRemoteObject iRemoteObject, int i) { INewsDemoIDL sharedManager = NewsDemoIDLStub.asInterface(iRemoteObject); try { sharedManager.tranShare(title, reads, likes, content, image); } catch (RemoteException e) { LogUtil.i(TAG, "connect successful,but have remote exception"); } } @Override public void onAbilityDisconnectDone(ElementName elementName, int i) { disconnectAbility(this); } }); DialogUtil.toast( this, connectFlag ? "Sharing succeeded!" : "Sharing failed. Please try again later.", WAIT_TIME); }

In the preceding code snippet, parameters such as bundleName, abilityName, and devicesId are set for the intent, and the connectAbility method is used to connect to the specified Service ability on the remote device. After the connection is successful, the tranShare method is called in onAbilityConnectDone to pass the required data to the remote device.

Defining the Remote Service Ability

The local device connects to the remote Service ability via connectAbility. To define the remote Service ability, we need to add the tranShare() method to the SharedService.java file. The sample code is as follows:

public void tranShare(String title, String reads, String likes, String content, String image) { Intent intent = new Intent(); Operation operation = new Intent.OperationBuilder() .withBundleName(getBundleName()) .withAbilityName(NewsAbility.class.getName()) .withAction("action.detail") .build(); intent.setOperation(operation); intent.setParam(NewsDetailAbilitySlice.INTENT_TITLE, title); intent.setParam(NewsDetailAbilitySlice.INTENT_READ, reads); intent.setParam(NewsDetailAbilitySlice.INTENT_LIKE, likes); intent.setParam(NewsDetailAbilitySlice.INTENT_CONTENT, content); intent.setParam(NewsDetailAbilitySlice.INTENT_IMAGE, image); startAbility(intent);

In this way, the specified FA is started using thestartAbilityy method, and the parameters carried in the intent are passed to the target device.
That's all the details for this codelab.

In this codelab, we have introduced how to add a home page and details page for a news app. On the home page, users can switch between different categories of news through the top news category bar, and the news items displayed below change accordingly. When a user taps a particular news item, the news details page is displayed. On the news details page, users can swipe up and down to view the news content and tap the share button to share the particular page to another device. Figure 10-1 and Figure 10-2 show the overall effect of the simple news app we have built in this codelab.
Switching to the details page

Starting the specified FA on another device

Well done. You have completed this codelab and learned:

Source code on Gitee

Source code on GitHub

Code copied