本篇Codelab是基于ArkTS的声明式开发范式实现的样例,主要介绍了数据请求和touch事件的使用。包含以下功能:
1.数据请求。
2.列表下拉刷新。
3.列表上拉加载。
官方代码链接:[https://gitee.com/harmonyos/codelabs/tree/master/NewsDataArkTS](Codelabs: 分享知识与见解,一起探索HarmonyOS的独特魅力。 - Gitee.com)
网络数据请求需要申请权限:ohos.permission.INTERNET。
需要搭建服务端环境,参照使用说明NewsDataArkTS,参照文档打开DevEco Studio中Terminal进行配置,默认预览器可以打开预览,不需要开模拟器(重点是电脑内存扛不住)。
一个Tab对应多个TabContent,每个TabContent会有一个页签标题,因此会有一个页签的数组。
import { CommonConstant as Const } from '../common/constant/CommonConstant';
export class NewsViewModel{
getDefaultTypeList(): NewsTypeBean[]{
return Const.TabBars_DEFAULT_NEWS_TYPES
}
}
export default new NewsViewModel()
export class NewsTypeBean{
id: number = 0
name:ResourceStr = ''
}
在index.ets中使用该数组
@Component
export default struct TabBar {
@State tabBarArray:NewsTypeBean[] = NewsViewModel.getDefaultTypeList()
}
通过查看API,我们大致知道怎么使用了(模板代码,不需要记忆)
@Component
export default struct TabBar {
@State tabBarArray:NewsTypeBean[] = NewsViewModel.getDefaultTypeList()
@State currentIndex: number = 0;
@State currentPage: number = 1;
build() {
Tabs(){
ForEach(this.tabBarArray,(tabsItem:NewsTypeBean,index:number)=>{
TabContent(){
Column(){
Text('11')
}
}.tabBar(this.TabBuilder(tabsItem.id))
},(item:NewsTypeBean)=>JSON.stringify(item))
}
.barHeight(Const.TabBars_BAR_HEIGHT)
.barMode(BarMode.Scrollable)
.barWidth(Const.TabBars_BAR_WIDTH)
.onChange((index: number) => {
this.currentIndex = index;
this.currentPage = 1;
})
.vertical(false)
}
@Builder TabBuilder(index:number){
Column(){
Text(this.tabBarArray[index].name)
.height(Const.FULL_HEIGHT)
.padding({ left: Const.TabBars_HORIZONTAL_PADDING, right: Const.TabBars_HORIZONTAL_PADDING })
.fontSize(this.currentIndex === index ? Const.TabBars_SELECT_TEXT_FONT_SIZE : Const.TabBars_UN_SELECT_TEXT_FONT_SIZE)
.fontWeight(this.currentIndex === index ? Const.TabBars_SELECT_TEXT_FONT_WEIGHT : Const.TabBars_UN_SELECT_TEXT_FONT_WEIGHT)
.fontColor('#182431')
}
}
}
至此,Tab功能已实现。
新建NewsList.ets,将currentIndex传递至子组件
import NewsViewModel,{ NewsTypeBean} from '../viewModel/NewsViewModel'
import { CommonConstant as Const } from '../common/constant/CommonConstant';
import NewsList from '../view/NewsList'
@Component
export default struct TabBar {
build() {
...
TabContent(){
Column(){
NewsList({currentIndex:$currentIndex})
}
...
NewsList中的代码如下
@Component
export default struct NewsList {
@Link currentIndex:number
build() {
Column() {
Text(this.currentIndex.toString())
.fontColor(Color.Red)
}
}
}
至此,我们发现TabContent中的内容与页面索引保持一致了。
module.json5中别忘了加上网络权限
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
export function httpRequestGet(url: string): Promise {
let httpRequest = http.createHttp();
// 发送数据请求
let responseResult = httpRequest.request(url, {
method: http.RequestMethod.GET,
readTimeout: Const.HTTP_READ_TIMEOUT,
header: {
'Content-Type': ContentType.JSON
},
connectTimeout: Const.HTTP_READ_TIMEOUT,
extraData: {}
});
let serverData: ResponseResult = new ResponseResult();
// Processes the data and returns.
// 处理数据,并返回
return responseResult.then((value: http.HttpResponse) => {
if (value.responseCode === Const.HTTP_CODE_200) {
// Obtains the returned data.
// 处理数据,并返回
let result = `${value.result}`;
let resultJson: ResponseResult = JSON.parse(result);
if (resultJson.code === Const.SERVER_CODE_SUCCESS) {
serverData.data = resultJson.data;
}
serverData.code = resultJson.code;
serverData.msg = resultJson.msg;
} else {
serverData.msg = `${'网络错误'}&${value.responseCode}`;
}
return serverData;
}).catch(() => {
serverData.msg = '网络错误';
return serverData;
})
}
import prompt from '@ohos.prompt';
import { CommonConstant as Const } from '../common/constant/CommonConstant';
import { httpRequestGet } from '../common/utils/HttpUtil';
import Logger from '../common/utils/Logger';
export class NewsViewModel {
....
// 获取服务端新闻数据列表
getNewsList(currentPage: number, pageSize: number, path: string): Promise {
return new Promise(async (resolve: Function, reject: Function) => {
let url = `${Const.SERVER}/${path}`;
url += '?currentPage=' + currentPage + '&pageSize=' + pageSize;
httpRequestGet(url).then((data: ResponseResult) => {
if (data.code === Const.SERVER_CODE_SUCCESS) {
resolve(data.data);
} else {
Logger.error('getNewsList failed', JSON.stringify(data));
reject(Const.TabBars_DEFAULT_NEWS_TYPES)
}
})
.catch((err: Error) => {
Logger.error('getNewsList failed', JSON.stringify(err));
reject(Const.TabBars_DEFAULT_NEWS_TYPES)
});
});
}
}
...
export class ResponseResult {
code: string
msg: ResourceStr
data: string | Object | ArrayBuffer
constructor() {
this.code = '';
this.msg = '';
this.data = '';
}
}
export class NewsData {
title: string = ''
content: string = ''
imagesUrl: Array = [];
source: string = ''
}
export class NewsFile {
id: number = 0
url: string = ''
type: number = 0
newId: number = 0
}
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是
resolve
和reject
。它们是两个函数,由JavaScript引擎提供,不用自己部署。
调用页面的生命周期aboutToAppear()方法,方法内再调用changeCategory()方法
aboutToAppear() {
this.changeCategory()
}
changeCategory() {
NewsViewModel.getNewsList(this.currentPage, this.pageSize, Const.GET_NEWS_LIST)
.then((data: NewsData[]) => {
this.newsData = data;
})
}
import NewsViewModel, { NewsData } from '../viewModel/NewsViewModel'
import { CommonConstant as Const, PageState } from '../common/constant/CommonConstant';
@Component
export default struct NewsList {
@Link currentIndex: number
currentPage: number = 1;
pageSize: number = Const.PAGE_SIZE;
@State newsData: Array = [];
changeCategory() {
NewsViewModel.getNewsList(this.currentPage, this.pageSize, Const.GET_NEWS_LIST)
.then((data: NewsData[]) => {
this.newsData = data;
})
}
aboutToAppear() {
this.changeCategory()
}
build() {
Column() {
List() {
ForEach(this.newsData, (item: NewsData) => {
ListItem() {
Text(item.title)
.width('100%')
.fontColor(Color.Red)
}
.height(256)
.width('100%')
.backgroundColor(Color.White)
.margin({ top: 12 })
.borderRadius(Const.NewsListConstant_ITEM_BORDER_RADIUS)
}, (item: NewsData, index?: number) => JSON.stringify(item) + index)
}
.width('93.3%')
.height('100%')
.backgroundColor('#f1f3f5')
}
}
}
案例效果分析:此时我们发现,只有首次进入的时候有“哈哈哈”的提示,此后切换页面就不会再提示,因此无法检测到页面的切换。那么,我们该怎么实现呢?
答:用==@watch==
@Component
export default struct NewsList {
@Watch('changeCategory')@Link currentIndex: number
...
}
当currentIndex改变时,会时刻调用changeCategory()方法。因此,可以做到,当页面进行切换时,可以获取当前index界面的数据。
此后在进行页面切换时,都会事实调用changeCategory()。
ps:根据页面的不同,可以拼接不同的参数,案例都是调用同一个接口,因此数据是一样的。实际工作中,可以把index拼到链接中传递给后端,让后端返回不同的参数,从而显示不同的界面。
实际工作中,肯定有==“请求成功”、”请求失败“、”数据加载中“、”下拉刷新“、”上拉加载“==等各种场景。
参照案例,新建NewsModel.ets
export default class NewsModel {
newsData: Array = [];
currentPage: number = 1;
pageSize: number = Const.PAGE_SIZE;
pullDownRefreshText: Resource = $r('app.string.pull_down_refresh_text');
pullDownRefreshImage: Resource = $r('app.media.ic_pull_down_refresh');
pullDownRefreshHeight: number = Const.CUSTOM_LAYOUT_HEIGHT;
isVisiblePullDown: boolean = false;
pullUpLoadText: Resource = $r('app.string.pull_up_load_text');
pullUpLoadImage: Resource = $r('app.media.ic_pull_up_load');
pullUpLoadHeight: number = Const.CUSTOM_LAYOUT_HEIGHT;
isVisiblePullUpLoad: boolean = false;
offsetY: number = 0;
pageState: number = PageState.Loading;
hasMore: boolean = true;
startIndex = 0;
endIndex = 0;
downY = 0;
lastMoveY = 0;
isRefreshing: boolean = false;
isCanRefresh = false;
isPullRefreshOperation = false;
isLoading: boolean = false;
isCanLoadMore: boolean = false;
}
import NewsViewModel, { NewsData } from '../viewModel/NewsViewModel'
import { CommonConstant as Const, PageState } from '../common/constant/CommonConstant';
import prompt from '@ohos.prompt';
import NewsModel from '../viewModel/NewsModel';
@Component
export default struct NewsList {
@Watch('changeCategory')@Link currentIndex: number
@State newsModel: NewsModel = new NewsModel()
...//报错的地方自行修改,加上.newsModel即可
}
先定义pageState为PageState.Loading,当请求成功时,设置成PageState.Success,请求失败时,设置成PageState.Fail
先完善NewsList.ets中changeCategory代码
changeCategory() {
NewsViewModel.getNewsList(this.newsModel.currentPage, this.newsModel.pageSize, Const.GET_NEWS_LIST)
.then((data: NewsData[]) => {
this.newsModel.pageState = PageState.Success;
this.newsModel.newsData = data;
})
.catch((err:string | Resource)=>{
this.newsModel.pageState = PageState.Fail
})
}
根据请求状态码,显示不同的界面,因newsModel对象中的属性被@state修饰,因此视图是可以刷新的
Column() {
if (this.newsModel.pageState === PageState.Success) {
...
} else if (this.newsModel.pageState === PageState.Loading) {
...
} else {
...
}
}
NewsList.ets完整代码如下:
import NewsViewModel, { NewsData } from '../viewModel/NewsViewModel'
import { CommonConstant as Const, PageState } from '../common/constant/CommonConstant';
import prompt from '@ohos.prompt';
import NewsModel from '../viewModel/NewsModel';
import promptAction from '@ohos.promptAction';
import Logger from '../common/utils/Logger';
@Component
export default struct NewsList {
@Watch('changeCategory')@Link currentIndex: number
@State newsModel: NewsModel = new NewsModel()
changeCategory() {
NewsViewModel.getNewsList(this.newsModel.currentPage, this.newsModel.pageSize, Const.GET_NEWS_LIST)
.then((data: NewsData[]) => {
this.newsModel.pageState = PageState.Success;
this.newsModel.newsData = data;
})
.catch((err:string | Resource)=>{
this.newsModel.pageState = PageState.Fail
})
}
aboutToAppear() {
this.changeCategory()
}
build() {
Column() {
if (this.newsModel.pageState === PageState.Success) {
//请求成功
} else if (this.newsModel.pageState === PageState.Loading) {
//加载中
Text('数据加载中')
} else {
//请求失败
Text('请求失败')
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
import NewsViewModel, { CustomRefreshLoadLayoutClass, NewsData } from '../viewModel/NewsViewModel'
import { CommonConstant as Const, PageState } from '../common/constant/CommonConstant';
import prompt from '@ohos.prompt';
import NewsModel from '../viewModel/NewsModel';
import promptAction from '@ohos.promptAction';
import Logger from '../common/utils/Logger';
import { CustomRefreshLoadLayout } from './CustomRefreshLoadLayout';
@Component
export default struct NewsList {
@Watch('changeCategory') @Link currentIndex: number
@State newsModel: NewsModel = new NewsModel()
changeCategory() {
NewsViewModel.getNewsList(this.newsModel.currentPage, this.newsModel.pageSize, Const.GET_NEWS_LIST)
.then((data: NewsData[]) => {
this.newsModel.pageState = PageState.Success;
this.newsModel.newsData = data;
})
.catch(() => {
this.newsModel.pageState = PageState.Fail
})
}
aboutToAppear() {
this.changeCategory()
}
build() {
Column() {
if (this.newsModel.pageState === PageState.Success) {
} else if (this.newsModel.pageState === PageState.Loading) {
} else {
//请求失败
this.FailLayout()
}
}
.width('100%')
.height('100%')
.backgroundColor(Color.White)
.justifyContent(FlexAlign.Center)
}
@Builder FailLayout() {
Image($r('app.media.none'))
.height(Const.NewsListConstant_NONE_IMAGE_SIZE)
.width(Const.NewsListConstant_NONE_IMAGE_SIZE)
Text('网络加载失败')
.opacity(Const.NewsListConstant_NONE_TEXT_opacity)
.fontSize(Const.NewsListConstant_NONE_TEXT_size)
.fontColor('#182431')
.margin({ top: Const.NewsListConstant_NONE_TEXT_margin })
}
}
先定义CustomRefreshLoadLayout.ets,画出界面,以及参数传递。
(1)在NewsViewModel.ets中添加如下代码
@Observed
export class CustomRefreshLoadLayoutClass{
isVisible:boolean
imageSrc:Resource
textValue:string
heightValue:number
constructor(isVisible: boolean, imageSrc: Resource, textValue: string, heightValue: number) {
this.isVisible = isVisible;
this.imageSrc = imageSrc;
this.textValue = textValue;
this.heightValue = heightValue;
}
}
(2)在CustomRefreshLoadLayout.ets中添加如下代码
import { CustomRefreshLoadLayoutClass } from '../viewModel/NewsViewModel'
@Component
export struct CustomRefreshLoadLayout {
@ObjectLink customRefreshLoadClass:CustomRefreshLoadLayoutClass
build() {
Row() {
Image(this.customRefreshLoadClass.imageSrc)
.width(18)
.height(18)
Text(this.customRefreshLoadClass.textValue)
.margin({left:7})
.fontSize(17)
// .textAlign(TextAlign.Center)
}
.clip(true)
.width('100%')
.justifyContent(FlexAlign.Center)
.height(this.customRefreshLoadClass.heightValue)
}
}
ps:聪明的你已经发现了,这里用到了==@Observed和@ObjectLink==
总结:数组的元素是对象,当对象的属性发生修改时,不能触发视图的重新渲染。
@Builder LoadLayout() {
CustomRefreshLoadLayout({
customRefreshLoadClass: new CustomRefreshLoadLayoutClass(true, $r('app.media.ic_pull_up_load'), '加载中', this.newsModel.pullDownRefreshHeight)
})
}
我们发现,后端环境正常时,数据加载正常;后端服务没起时,数据加载异常。
完整代码如下:
import NewsViewModel, { CustomRefreshLoadLayoutClass, NewsData } from '../viewModel/NewsViewModel'
import { CommonConstant as Const, PageState } from '../common/constant/CommonConstant';
import NewsModel from '../viewModel/NewsModel';
import { CustomRefreshLoadLayout } from './CustomRefreshLoadLayout';
@Component
export default struct NewsList {
@Watch('changeCategory') @Link currentIndex: number
@State newsModel: NewsModel = new NewsModel()
changeCategory() {
NewsViewModel.getNewsList(this.newsModel.currentPage, this.newsModel.pageSize, Const.GET_NEWS_LIST)
.then((data: NewsData[]) => {
this.newsModel.pageState = PageState.Success;
this.newsModel.newsData = data;
})
.catch(() => {
this.newsModel.pageState = PageState.Fail
})
}
aboutToAppear() {
this.changeCategory()
}
build() {
Column() {
if (this.newsModel.pageState === PageState.Success) {
//请求成功
} else if (this.newsModel.pageState === PageState.Loading) {
//加载中
this.LoadLayout()
} else {
//请求失败
this.FailLayout()
}
}
.width('100%')
.height('100%')
.backgroundColor(Color.White)
.justifyContent(FlexAlign.Center)
}
@Builder ListLayout() {
List() {
ForEach(this.newsModel.newsData, (item: NewsData) => {
ListItem() {
Text(item.title)
.width('100%')
.fontColor(Color.Red)
}
.height(256)
.width('100%')
.backgroundColor(Color.White)
.margin({ top: 12 })
.borderRadius(Const.NewsListConstant_ITEM_BORDER_RADIUS)
}, (item: NewsData, index?: number) => JSON.stringify(item) + index)
}
.width('93.3%')
.height('100%')
.backgroundColor('#f1f3f5')
}
@Builder LoadLayout() {
CustomRefreshLoadLayout({
customRefreshLoadClass: new CustomRefreshLoadLayoutClass(true, $r('app.media.ic_pull_up_load'), '加载中', this.newsModel.pullDownRefreshHeight)
})
}
@Builder FailLayout() {
Image($r('app.media.none'))
.height(Const.NewsListConstant_NONE_IMAGE_SIZE)
.width(Const.NewsListConstant_NONE_IMAGE_SIZE)
Text('网络加载失败')
.opacity(Const.NewsListConstant_NONE_TEXT_opacity)
.fontSize(Const.NewsListConstant_NONE_TEXT_size)
.fontColor('#182431')
.margin({ top: Const.NewsListConstant_NONE_TEXT_margin })
}
}
3.完善listItem界面
新建NewsItem.ets
import { NewsData, NewsFile } from '../viewModel/NewsViewModel'
import { CommonConstant, CommonConstant as Const } from '../common/constant/CommonConstant';
@Component
export struct NewsItem {
newsData:NewsData = new NewsData()
build() {
Column() {
Row(){
Image($r('app.media.news'))
.width('11.9%')
.height(20)
.objectFit(ImageFit.Fill)
Text(this.newsData.title)
.fontSize(20)
.fontColor('#000000')
.width('78.6%')
.maxLines(1)
.margin({ left: '2.4%' })
.textOverflow({ overflow: TextOverflow.Ellipsis })
.fontWeight(FontWeight.Regular)
}
.alignItems(VerticalAlign.Center)
.height(22)
Text(this.newsData.content)
.fontSize(14)
.fontColor('#000000')
.width('93%')
.height('16.8%')
.maxLines(2)
.margin({ top:'3.5%' })
.textOverflow({ overflow: TextOverflow.Ellipsis })
.fontWeight(FontWeight.Regular)
Grid(){
ForEach(this.newsData.imagesUrl,(itemImg:NewsFile)=>{
GridItem(){
Image(Const.SERVER+itemImg.url)
.objectFit(ImageFit.Cover)
.borderRadius(8)
}
},(itemImg:NewsFile,index?:number)=>JSON.stringify(itemImg)+index)
}
.columnsTemplate('1fr'.repeat(this.newsData.imagesUrl.length))
.columnsGap(5)
.rowsTemplate('1fr')
.width('93%')
.height('31.5%')
.margin({left:'3.5%',right:'3.5%',top:'5%'})
Text(this.newsData.source)
.fontSize(Const.NewsSource_FONT_SIZE)
.fontColor('#FF989898')
.height(Const.NewsSource_HEIGHT)
.width(Const.NewsSource_WIDTH)
.maxLines(Const.NewsSource_MAX_LINES)
.margin({ left: Const.NewsSource_MARGIN_LEFT, top: Const.NewsSource_MARGIN_TOP })
.textOverflow({ overflow: TextOverflow.None })
}
}
}
import NewsViewModel, { CustomRefreshLoadLayoutClass, NewsData } from '../viewModel/NewsViewModel'
import { CommonConstant as Const, PageState } from '../common/constant/CommonConstant';
import NewsModel from '../viewModel/NewsModel';
import { CustomRefreshLoadLayout } from './CustomRefreshLoadLayout';
import { NewsItem } from './NewsItem';
@Component
export default struct NewsList {
...
@Builder ListLayout() {
List() {
ForEach(this.newsModel.newsData, (item: NewsData) => {
ListItem() {
NewsItem({newsData:item})
}
...
}
}
...
}
...
}
哇,看到界面,神清气爽~~~
import NewsViewModel, { CustomRefreshLoadLayoutClass, NewsData } from '../viewModel/NewsViewModel'
import { CommonConstant as Const, PageState } from '../common/constant/CommonConstant';
import NewsModel from '../viewModel/NewsModel';
import { CustomRefreshLoadLayout } from './CustomRefreshLoadLayout';
import { NewsItem } from './NewsItem';
import promptAction from '@ohos.promptAction';
import Logger from '../common/utils/Logger';
import { LoadMoreLayout } from './LoadMoreLayout';
import { NoMoreLayout } from './NoMoreLayout';
import { listTouchEvent } from '../common/utils/PullDownRefresh';
import prompt from '@ohos.prompt';
import RefreshLayout from './RefreshLayout';
@Component
export default struct NewsList {
@Watch('changeCategory') @Link currentIndex: number
@State newsModel: NewsModel = new NewsModel()
changeCategory() {
NewsViewModel.getNewsList(this.newsModel.currentPage, this.newsModel.pageSize, Const.GET_NEWS_LIST)
.then((data: NewsData[]) => {
this.newsModel.pageState = PageState.Success;
if (data.length === this.newsModel.pageSize) {
this.newsModel.currentPage++
this.newsModel.hasMore = true
}else {
this.newsModel.hasMore = false
}
this.newsModel.newsData = data;
})
.catch(() => {
this.newsModel.pageState = PageState.Fail
})
}
build() {
Column() {
if (this.newsModel.pageState === PageState.Success) {
this.ListLayout()
} else if (this.newsModel.pageState === PageState.Loading) {
this.LoadLayout()
} else {
this.FailLayout()
}
}
...
.onTouch((event: TouchEvent | undefined) => {
if (event) {
if (this.newsModel.pageState === PageState.Success) {
listTouchEvent(this.newsModel, event);
}
}
})
}
@Builder ListLayout() {
List() {
ListItem() {
RefreshLayout({
refreshLayoutClass: new CustomRefreshLoadLayoutClass(this.newsModel.isVisiblePullDown, this.newsModel.pullDownRefreshImage,
this.newsModel.pullDownRefreshText, this.newsModel.pullDownRefreshHeight)
})
}
...
ListItem(){
if (this.newsModel.hasMore){
LoadMoreLayout({
loadMoreLayoutClass:new CustomRefreshLoadLayoutClass(this.newsModel.isVisiblePullUpLoad,
this.newsModel.pullUpLoadImage,this.newsModel.pullUpLoadText,this.newsModel.pullUpLoadHeight)
})
}else {
NoMoreLayout()
}
}
}
.width('93.3%')
.height('100%')
.backgroundColor('#f1f3f5')
.margin({ left: Const.NewsListConstant_LIST_MARGIN_LEFT, right: Const.NewsListConstant_LIST_MARGIN_RIGHT })
.divider({
color: '#e2e2e2',
strokeWidth: Const.NewsListConstant_LIST_DIVIDER_STROKE_WIDTH,
endMargin: Const.NewsListConstant_LIST_MARGIN_RIGHT
})
// Remove the rebound effect.
.edgeEffect(EdgeEffect.None)
.offset({ x: 0, y: `${this.newsModel.offsetY}px` })
.onScrollIndex((start: number, end: number) => {
// Listen to the first index of the current list.
this.newsModel.startIndex = start;
this.newsModel.endIndex = end;
})
}
}
以下文件可直接CV官网PullDownRefresh.ets、PullUpLoadMore.ets、LoadMoreLayout.ets、NoMoreLayout.ets、RefreshLayout.ets
效果如下:
至此,所有功能已全部完成。
代码链接:https://gitee.com/runitwolf/NewsDataArkTS
typora笔记链接:https://gitee.com/runitwolf/NewsDataArkTS/blob/master/NewsDataArkTS.md
ps: 软件有点小bug,不知道是开发软件的问题还是代码的问题,点击NewsModel的isCanLoadMore,提示No usages found in All Places Press Ctrl+Alt+F7 again to search in ‘Project Files’,但是我明明有地方使用啊,没找到解决方案。