Read the original article:Emitter UI refresh failure issue
Problem Description
When using the emitter, subscription and unsubscription are encapsulated inside a class and called through new instances. After subscribing, when the emitter event is triggered and parameters change, the UI does not refresh as expected.
Background Knowledge
-
Emitter: Event handling mechanism in HarmonyOS for intra-thread or inter-process communication.
- Subscription: Register callbacks to listen for events.
- Publishing: Trigger events to notify subscribers.
- Unsubscription: Stop listening to free resources.
-
Use cases: Useful for event handling across threads or when UI needs to react to state changes.
-
State management in ArkTS: Properties must be wrapped by state decorators (
@State,@ObservedV2,@Trace) or reassigned after constructor execution to trigger UI updates.
Troubleshooting Process
- Checked whether the
thiscontext inside arrow functions is correctly bound. - Verified whether state variables are being tracked by the ArkTS reactive system.
- Tested different subscription styles (
emitter.on(event, callback)vs inline arrow). - Observed that
number1andnumber2were updated, but since they weren’t properly proxied by state decorators, UI didn’t refresh.
Analysis Conclusion
- Arrow functions assigned during constructor execution capture raw values before state proxying by
@Provide. - This results in updates happening on plain objects, bypassing state management → UI does not refresh.
Solution
Option 1: Re-assign the arrow function after constructor execution
Define a method to reassign the arrow function and call it in aboutToAppear.
import { emitter } from '@kit.BasicServicesKit'
export class BaseA {
static MESSAGE_ONE = "1"
static MESSAGE_TWO = "2"
number1: number = 0
number2: number = 0
private callback1 = () => {
this.number1 += 1
}
public registerCallback1() {
this.callback1 = () => {
this.number1 += 1
}
}
register1() {
emitter.on(BaseA.MESSAGE_ONE, this.callback1)
}
register2() {
emitter.on(BaseA.MESSAGE_TWO, () => this.callback2())
}
private callback2() {
this.number2 += 1
}
}
@Entry
@Component
struct EmitterTestPage {
@Provide viewModel: BaseA = new BaseA()
aboutToAppear(): void {
this.viewModel.registerCallback1()
}
build() {
Column({ space: 10 }) {
Text("callback1: " + this.viewModel.number1.toString())
.fontSize(18).fontColor('#f00').margin({ top: 20 })
Text("callback2: " + this.viewModel.number2.toString())
.fontSize(18).fontColor('#0f0').margin({ top: 20 })
Button('Send message, update directly').onClick(() => {
this.viewModel.number1 += 1
this.viewModel.number2 += 1
})
Button('Subscribe in ViewModel (method 1)').onClick(() => {
this.viewModel.register1()
})
Button('Subscribe in ViewModel (method 2)').onClick(() => {
this.viewModel.register2()
})
Button('Send message, update MessageOne').onClick(() => {
emitter.emit(BaseA.MESSAGE_ONE)
})
Button('Send message, update MessageTwo').onClick(() => {
emitter.emit(BaseA.MESSAGE_TWO)
})
}.margin({ top: 20 })
}
}
Option 2: Use @ObservedV2 with @trace
Wrap properties with @Trace to ensure UI refreshes when values change.
import { emitter } from '@kit.BasicServicesKit'
@ObservedV2
export class BaseA {
static MESSAGE_ONE = "1"
static MESSAGE_TWO = "2"
@Trace number1: number = 0
@Trace number2: number = 0
private callback1 = () => {
this.number1 += 1
}
register1() {
emitter.on(BaseA.MESSAGE_ONE, this.callback1)
}
register2() {
emitter.on(BaseA.MESSAGE_TWO, () => this.callback2())
}
private callback2() {
this.number2 += 1
}
}
@Entry
@Component
struct EmitterTestPage {
viewModel: BaseA = new BaseA()
build() {
Column({ space: 10 }) {
Text("callback1: " + this.viewModel.number1.toString())
.fontSize(18).fontColor('#f00').margin({ top: 20 })
Text("callback2: " + this.viewModel.number2.toString())
.fontSize(18).fontColor('#0f0').margin({ top: 20 })
Button('Send message, update directly').onClick(() => {
this.viewModel.number1 += 1
this.viewModel.number2 += 1
})
Button('Subscribe in ViewModel (method 1)').onClick(() => {
this.viewModel.register1()
})
Button('Subscribe in ViewModel (method 2)').onClick(() => {
this.viewModel.register2()
})
Button('Send message, update MessageOne').onClick(() => {
emitter.emit(BaseA.MESSAGE_ONE)
})
Button('Send message, update MessageTwo').onClick(() => {
emitter.emit(BaseA.MESSAGE_TWO)
})
}.margin({ top: 20 })
}
}
Verification Result
- Both solutions ensure that state changes in
number1andnumber2trigger UI refresh. - Option 1 works by reassigning the callback after constructor execution.
- Option 2 works by directly marking fields as reactive using
@ObservedV2and@Trace.
Written by Muhammet Cagri Yilmaz
