switchToView/onUpdate weirdness

I recently ran into a really nefarious bug; eventually I was able to track it down to an interaction between `switchToView` and `onUpdate`. I'm wondering if someone can help me understand this behavior. Here's the scenario:

You have two views, A and B. You present a menu on A with a menu option that switches to B.

After you switch to view B, I expected view A to stop receiving `onUpdate` calls.

However, that doesn't seem to be the case:

ViewA.onUpdate
Switched to B
ViewA.onUpdate
ViewB.onUpdate


I'm guessing the menu somehow calls `onUpdate` one more time on its view (not the current view which was changed) after it returns. But I'm not sure, and definitely not sure why.

Code to replicate this is here:

using Toybox.Application;
using Toybox.Graphics as Gfx;
using Toybox.WatchUi as Ui;
using Toybox.System;

class WidgetTestApp extends Application.AppBase {
function getInitialView() {
return [ new ViewA(), new ViewADelegate() ];
}
}

class ViewA extends Ui.View {
function onUpdate(dc) {
System.println("ViewA.onUpdate");
Ui.View.onUpdate(dc);
dc.setColor(Gfx.COLOR_WHITE, Gfx.COLOR_BLACK);
dc.drawText(50, 50, Gfx.FONT_MEDIUM, "A", Gfx.TEXT_JUSTIFY_LEFT);
}
}


class ViewAMenuInputDelegate extends Ui.MenuInputDelegate {
function onMenuItem(item) {
Ui.popView(Ui.SLIDE_IMMEDIATE);
Ui.switchToView(new ViewB(), new ViewBDelegate(), Ui.SLIDE_IMMEDIATE);
System.println("Switched to B");
}
}


class ViewADelegate extends Ui.BehaviorDelegate {
function onMenu() {
var menu = new Ui.Menu();
menu.addItem("Switch To B", :switchToB);
Ui.pushView(menu, new ViewAMenuInputDelegate(), Ui.SLIDE_UP);
return true;
}
}

class ViewB extends Ui.View {
function onUpdate(dc) {
System.println("ViewB.onUpdate");
Ui.View.onUpdate(dc);
dc.setColor(Gfx.COLOR_WHITE, Gfx.COLOR_BLACK);
dc.drawText(50, 50, Gfx.FONT_MEDIUM, "B", Gfx.TEXT_JUSTIFY_LEFT);
}
}

class ViewBDelegate extends Ui.BehaviorDelegate {
function onBack() {
return false;
}
}


Is this expected behavior?

Thanks!
  • Former Member
    Former Member over 8 years ago
    This is working as expected. All views receive an update when being removed, primarily to guarantee they are fully rendered, and ready for screen transitions. This is more an artifact of of something the native system needs than something that is important to ConnectIQ, but it is there nonetheless.

    Another important thing to understand is that view changes do not occur when the VM is executing. pushView(), popView(), and switchToView() are queued by their respective functions, and occur once the method that called them completes and the VM exits.
  • Thanks for the explanation.

    I think the fundamental problem is I'm implementing the state-machine as a series of view transitions, so if views are not instantaneously representative of the actual state, then I hit bugs. Given what you've said, that's a bad idea.

    Better would be to maintain an explicit `STATE` variable and then make sure the current view matches what `STATE` dictates (not the other way around).
  • Former Member
    Former Member over 8 years ago
    Yeah, tying a state machine transition to a push/pop/switchto would probably not be a good idea. You could look at the timing of the onShow/onHide calls for those views, which should be more directly tied to when the view changes. Ultimately though, the best track is what you have suggested with the state machine controlling your views instead of vice-versa.
  • Unfortunately, since the view is the only thing that knows when it is being shown/hidden, the view has to notify the state. It would be nice if the delegate was given the equivalent of onShow and onHide callbacks to avoid the need for this communication. Of course an application can do this...

    using Toybox.WatchUi as Ui;
    using Toybox.Application as App;
    using Toybox.Timer as Timer;
    using Toybox.Graphics as Gfx;
    using Toybox.Math as Math;
    using Toybox.System as Sys;

    class XState extends Ui.BehaviorDelegate
    {
    function initialize() {
    BehaviorDelegate.initialize();
    }

    function onEnter() {
    }

    function onLeave() {
    }
    }

    class XView extends Ui.View
    {
    hidden var _M_state;

    function initialize(state) {
    View.initialize();
    _M_state = state;
    }

    function onShow() {
    _M_state.onEnter();
    }

    function onHide() {
    _M_state.onLeave();
    }
    }

    class XLightOffView extends XView
    {
    function initialize(state) {
    XView.initialize(state);
    }

    function onUpdate(dc) {
    dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_BLACK);
    dc.clear();
    }
    }

    class XLightOnView extends XView
    {
    function initialize(state) {
    XView.initialize(state);
    }

    function onUpdate(dc) {
    dc.setColor(Gfx.COLOR_WHITE, Gfx.COLOR_WHITE);
    dc.clear();
    }
    }

    class XLightOffState extends XState
    {
    function initialize() {
    XState.initialize();
    }

    function onSelect() {
    var state = new XLightOnState();
    state.switchToView(Ui.SLIDE_UP);

    return true;
    }

    function switchToView(transition) {
    Ui.switchToView(new XLightOffView(self), self, transition);
    }

    function getInitialView() {
    return [ new XLightOffView(self), self ];
    }
    }

    class XLightOnState extends XState
    {
    function initialize() {
    XState.initialize();
    }

    hidden var _M_timer;

    function onEnter() {
    _M_timer = new Timer.Timer();
    _M_timer.start(self.method(:onTimer), 5000, false);
    }

    function onLeave() {
    _M_timer.stop();
    _M_timer = null;
    }

    function onTimer() {
    var state = new XLightOffState();
    state.switchToView(Ui.SLIDE_DOWN);
    }

    function onSelect() {
    var state = new XLightOffState();
    state.switchToView(Ui.SLIDE_UP);

    return true;
    }

    function onBack() {
    var state = new XLightOffState();
    state.switchToView(Ui.SLIDE_DOWN);

    return true;
    }

    function getInitialView() {
    return [ new XLightOnView(self), self ];
    }

    function switchToView(transition) {
    Ui.switchToView(new XLightOnView(self), self, transition);
    }
    }


    class XApp extends App.AppBase
    {
    function initialize() {
    AppBase.initialize();
    }

    function getInitialView() {
    var state;
    if (Math.rand() & 1) {
    state = new XLightOffState();
    }
    else {
    state = new XLightOnState();
    }

    return state.getInitialView();
    }
    }


    .. but it would be great if this wasn't necessary.

    Travis
  • Yeah I think I'm going to end up doing something similar.

    Actually, it's been a weird evolution here:

    1) Using view-based state machine out of simplicity (and ignorance)
    2) See a bunch of weird view related bug, implement an explicit state-machine which fixes a lots of these bugs, but not really sure why
    3) Rip out the explicit state machine because it's inelegant and complex in favor of simple view based transitions
    4) See all of these bugs reappear (this is where I am currently)
    5) Add back explicit state machine, but this time understand *why* it has to be this way (hopefully enlightenment, sort-of)

    I think this is definitely an area where the APIs could be improved and certainly the docs as well.