(7)Kotlin/Js For Harmony——ArkTs 开发架构

本章我们讨论如何基于上一章节介绍的工具,实现鸿蒙上的响应式架构。

我们基于基于MVVM 架构,主要讨论VM。

Flow + Transformer

ViewModel的核心思想是flow + transformer flow 是响应式数据的基础,transformer 负责数据转换。

举个栗子:

typescript 复制代码
class BaseViewModel {
	protected readonly viewModelScope = new Scope()
}


interface MyModel {
	nameColor: string
	name: string
}

class MyModelTransformer{
	
	async transform(name: string, isLogin: boolean): MyModel {
		return {
			name: name,
			nameColor: isLogin ? Color.Black : Color.Grey
		}

	}

}

class MyViewModel extends BaseViewModel {
	private _nameFlow = mutableStateFlow('')
	private _isLoginFlow = mutableStateFlow(false)

	readonly modelFlow: Flow<MyModel>

	private transfomer = new MyModelTransformer()

	constructor() {
		super()

		this.modelFlow = combine2(this._nameFlow, this._isLoginFlow, 
			async (name: string, isLogin: string) => { return this.transfomer.transform(name, isLogin)}
		)
		.stateIn(this.viewModelScope, { name : '', nameColor: ''})
	}

	updateIsLogin(isLogin: boolean) {
		this._isLoginFlow.value = isLogin
	}

	updateName(name: string) {
		this._nameFlow.value = name
	}

}


@ComponentV2
class MyComponent {
	private scope = new Scope()
	private viewModel = new MyViewModel()

	@Local name: string = ''
	@Local nameColor: string = ''

	aboutToAppear(){
		this.scope.launch(async (abortSignal) => {
			await this.viewModel.modelFlow.collect(async(myModel) => {
				this.name = myModel.name
				this.nameColor = myModel.nameColor
			})
		})

	}


	build(){

		Text(this.name).fontColor(this.nameColor)
		.onClick(()=>{
			this.viewModel.updateName(`name${util.generateRandomUUID()}`)
		})

	}

}

上面这个小demo比较清晰的展示了响应式架构的样子。 每次调用viewModel.updateName都会触发_nameFlow发射新值,进而触发transformer.transform,生成一个新的MyModel, UI 层监听modelFlow,去相应的更新数据。

用flow的优势是可以非常方便的流式处理数据,中间提供了多种操作符。 将数据转换抽象成transformer的优势是可以方便的做单元测试,因此我们需要保证transformer是无状态的,即确定的输入对应确定的输出。

UiModel Mapping

上述例子比较简单,如果我们是一个复杂的列表,列表的每一个Item 都有他自己的状态,那这些状态该怎样维护? 如果用Compose的话,remember 天然的可以做这个事情,但是ArkUI中没有类似的操作符,该如何做呢?

为了不丢状态,我们需要自己维护数据到UiModel的映射,"同一个数据",永远映射到同一个UiModel对象,这样我们在UI上就可以方便的使用对应的UiModel 里,而不怕Component 划出屏幕销毁后状态丢失。 注意,"同一个数据"我这里使用了引号,如何理解"同一个"呢?不同场景有不同的含义,比如一个消息列表,那可以用消息id 来判断是不是"同一个数据",同一个消息id,永远都使用同一个UiModel。

保证这样的映射关系还有一个很重要的好处:在List中,ListItem 绑定的是固定的UiModel,因此数据更新时无需每次都重建ListItem。

在List 内使用,即配合LazyForEach使用时,keyGenerator 的返回值应该是UiModel的内存地址,保证同一个UiModel对象,不会重新创建ListItem。 但是我们是无法直接访问到js 对象的内存地址的,js 中也没有提供对象hashCode的方法。为了解决这个问题,我们可以给每个对象加一个自增的Id,这里叫union_id,于是UiModel会变成这样:

typescript 复制代码
class MyUiModel {
	private static UNION_ID = 0
	readonly unionId = `MyUiModel_${MyUiModel.UNION_ID++}`

}

这样,我们可以用unionId 来指代对象的唯一性,所以LazyForEach 可以这样写:

scss 复制代码
List(){
	
	LazyForEach(this.dataSource, (item: MyUiModel, index)=> {
	   ListItem(){
	   	  // UI 代码
	   }
	}, (item: MyUiModel, index) => item.unionId)

}

为了维护UiModel 的稳定映射,现在提供一个基础工具类 ModelStore, 作为UiModel 的通用管理工具:

typescript 复制代码
export class ModelStore<T> {
  readonly map = new Map<string, T>()

  getOrCreate(id: string, factory: (id: string) => T): T {
    if (this.map.has(id)) {
      return this.map.get(id)!
    } else {
      const model = factory(id)
      this.map.set(id, model)
      return model
    }
  }

  tryGet(id: string): Nullable<T> {
    return this.map.get(id)
  }

  clear() {
    this.map.forEach(model => {
      const clearFunc = (model as IClearable).clear
      if (typeof clearFunc === 'function') {
        (model as IClearable).clear()
      }
    })
    this.map.clear()
  }
}

使用的时候非常简单:

typescript 复制代码
class MyUiModel {
	private static UNION_ID = 0
	readonly unionId = `MyUiModel_${MyUiModel.UNION_ID++}`
	readonly id: string

	name = new DistinctState('')

	constructor(id: string) {
    	super()
    	this.id = id
  	}

	static newInstance(id: string) : MyUiModel{
		return new MyUiModel(id)
	}

	static makeId(data: Data): string {
		return `MyUiModel_${data.id}`
	}

	update(data: Data) {
		this.name.update(data.name)
	}

}

// 使用


const modelStore = new ModelStore()

//假设这是数据
//interface Data {
//	id: stirng
//	name: string
//}

const data: Data = {id: '1', name : 'Sean'} 

const myUiModel = this.modelStore.getOrCreate(MyUiModel.makeId(data.id), MyUiModel.newInstance)
myUiModel.update(data)

最终版本

将上述所有东西组合起来,就实现了我们的最终版本啦,来看一下整体的架构:

typescript 复制代码
//假设这是数据
//interface Data {
//	id: stirng
//	name: string
//}

class BaseViewModel {
	protected readonly viewModelScope = new Scope()
}


class MyUiModel {
	private static UNION_ID = 0
	readonly unionId = `MyUiModel_${MyUiModel.UNION_ID++}`
	readonly id: string

	readonly name = new DistinctState('')
	readonly nameColor = new DistinctState('')

	constructor(id: string) {
    	super()
    	this.id = id
  	}

	static newInstance(id: string) : MyUiModel{
		return new MyUiModel(id)
	}

	static makeId(data: Data): string {
		return `MyUiModel_${data.id}`
	}

	update(name: string, nameColor: string) {
		this.name.update(name)
		this.nameColor.update(nameColor)
	}

}

class MyModelTransformer{
	async transform(dataList: Data[], isLogin: boolean, modelStore: ModelStore<MyUiModel>): MyModel {
		const result: MyUiModel[] = []
		for (const data of dataList) {
			const uiModel = modelStore.getOrCreate(MyUiModel.makeId(data), MyUiModel.newInstance)

			uiModel.update(data.name, isLogin ? Color.Black : Color.Grey)
			result.push(uiModel)

		}
		return result

	}

}

class MyViewModel extends BaseViewModel {
	private _dataListFlow = mutableStateFlow<Data[]>([])
	private _isLoginFlow = mutableStateFlow(false)

	readonly modelFlow: Flow<MyUiModel[]>

	private transfomer = new MyModelTransformer()
	private modelStore = new ModelStore<MyUiModel>()

	constructor() {
		super()

		this.modelFlow = combine2(this._dataListFlow, this._isLoginFlow, 
			async (dataList: Data[], isLogin: string) => { return this.transfomer.transform(
			dataList, isLogin, this.modelStore)}
		)
		.stateIn(this.viewModelScope, [])
	}

	updateIsLogin(isLogin: boolean) {
		this._isLoginFlow.value = isLogin
	}

	updateData(dataList: Data[]) {
		this._dataListFlow.value = dataList
	}

}


// UI 
@ComponentV2
class MyComponent {
	private scope = new Scope()
	private viewModel = new MyViewModel()

	private dataSource = new MyUiModelDataSource()

	aboutToAppear(){
		this.scope.launch(async (abortSignal) => {
			await this.viewModel.modelFlow.collect(async(myModeList) => {
				this.dataSource.submit(myModeList)
			})
		})

	}


	build(){
		List(){
			LazyForEach(this.dataSource, (item: MyUiModel, index)=> {
	   			ListItem(){
	   	 		 	Text(this.name.state).fontColor(this.nameColor.state)
	   			}
			}, (item: MyUiModel, index) => item.unionId)
		}
	}

}

这套架构还可以继续优化,比如,将MyUiModel 对应的Ui 样式也放到UiModel 内,这样数据描述与Ui描述就会内聚到一个类中, MyComponent 相当于一个壳,没有任何Ui细节,优化后的MyUiModel如下:

typescript 复制代码
export interface CellBuildContext<T> {
	data: T
	context: CellBuildContext // 这里放给UI用的context信息
} 

class MyUiModel {
	private static UNION_ID = 0
	readonly unionId = `MyUiModel_${MyUiModel.UNION_ID++}`
	readonly id: string

	readonly name = new DistinctState('')
	readonly nameColor = new DistinctState('')

 	readonly cellBuilder = wrapBuilder(Builder)


	constructor(id: string) {
    	super()
    	this.id = id
  	}

	static newInstance(id: string) : MyUiModel{
		return new MyUiModel(id)
	}

	static makeId(data: Data): string {
		return `MyUiModel_${data.id}`
	}

	update(name: string, nameColor: string) {
		this.name.update(name)
		this.nameColor.update(nameColor)
	}

}

@Builder
function Builder(ctx: CellBuildContext<MyUiModel>) {
	Text(ctx.data.name.state).fontColor(ctx.data.nameColor.state)	
}

UI 会变成这样:

typescript 复制代码
@ComponentV2
class MyComponent {
	private scope = new Scope()
	private viewModel = new MyViewModel()

	private dataSource = new MyUiModelDataSource()

	aboutToAppear(){
		this.scope.launch(async (abortSignal) => {
			await this.viewModel.modelFlow.collect(async(myModeList) => {
				this.dataSource.submit(myModeList)
			})
		})

	}


	build(){
		List(){
			LazyForEach(this.dataSource, (item: MyUiModel, index)=> {
	   			ListItem(){
	   	 		 	item.cellBuilder.builder({
	   	 		 		data: item
	   	 		 		context: {} as CellBuildContext // 这里放给UI用的context信息
	   	 		 	})
	   			}
			}, (item: MyUiModel, index) => item.unionId)
		}
	}

}

关于「ArkTs 开发架构」的介绍就告一段落了,如果大家在使用过程中有任何问题,欢迎留言讨论。 Android工程师的kmp(kotlin/js) for harmony开发指南 这一系列文章旨在系统性的提供一套完整的Kotlin/Js For Harmony的解决方案。后续系列文章会介绍如何复用ViewModel,序列化卡顿优化,鸿蒙开发套件,架构设计思路等等,欢迎关注!

相关推荐
盐焗西兰花2 小时前
鸿蒙学习实战之路:Dialog 组件封装最佳实践
学习·华为·harmonyos
大雷神2 小时前
HarmonyOS中高德地图第一篇:高德地图SDK集成与初始化
harmonyos
大雷神2 小时前
HarmonyOS中开发高德地图第五篇:定位蓝点功能
harmonyos
汉堡黄•᷄ࡇ•᷅2 小时前
鸿蒙开发:案例集合List:ListItem拖拽(交换位置,过渡动画)(性能篇)
华为·harmonyos·鸿蒙·鸿蒙系统
HONG````3 小时前
鸿蒙应用动态文件读取全指南:从JSON、XML到文本文件的解析实战
华为·harmonyos
Mr_Hu4044 小时前
鸿蒙开发学习笔记-生命周期小记
笔记·学习·harmonyos·鸿蒙
汉堡黄4 小时前
鸿蒙开发:案例集合List:ListItem拖拽(交换位置,过渡动画)(性能篇)
harmonyos
食品一少年6 小时前
开源鸿蒙 PC · Termony 自验证环境搭建与外部 HNP 集成实践(DAY4-10)(2)
华为·harmonyos
waeng_luo6 小时前
[鸿蒙2025领航者闯关] 鸿蒙应用中如何管理组件状态?
前端·harmonyos·鸿蒙·鸿蒙2025领航者闯关·鸿蒙6实战·开发者年度总结