赞
踩
本篇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<ResponseResult> { 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<NewsData[]> { 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<NewsFile> = []; 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<NewsData> = []; 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<NewsData> = []; 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’,但是我明明有地方使用啊,没找到解决方案。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。