App instantiated twice on startup

Hi,

My app implements a background task, glance and widget.

When the simulator starts in glance mode, my AppBase derivation is initiated twice. The first is stopped immediately, the second than persists.

Here is the log of the function calls. I don't use onStart, and I have also excluded the entries made by the background instance:

29.3.2025 10:13:12: EvccApp: initialize
29.3.2025 10:13:12: EvccApp: getGlanceView
29.3.2025 10:13:12: EvccApp: onStop
29.3.2025 10:13:13: EvccApp: initialize
29.3.2025 10:13:13: EvccApp: getGlanceView

What could be the reason for the class to be instantiated twice? Is this a simulator thing?

This only happens for the glance. When I change the Glance Launch Mode in the simulator to start right into the widget, the class is instantiated only once.

  • 1st is the background app, 2nd is the foreground app

  • The two instances seem both glance instances, because getGlanceView is called. The background is separate and I have not included that debug output. With background output it looks like that:


    Background: 29.3.2025 13:32:24: EvccApp: initialize
    Background: 29.3.2025 13:32:24: EvccApp: onStop
    29.3.2025 13:32:24: EvccApp: initialize
    29.3.2025 13:32:24: EvccApp: getGlanceView
    29.3.2025 13:32:24: EvccApp: onStop
    29.3.2025 13:32:25: EvccApp: initialize
    29.3.2025 13:32:25: EvccApp: getGlanceView

  • Not sure why but in the sim I see getGlanceView being called twice in the main app when it starts.  It could be something specific to the sim, as I've not tried on a real device, but it has zero impact on my apps with glances.  It's never called in the background service, and it shouldn't be.

  • Not sure why but in the sim I see getGlanceView being called twice in the main app when it starts.

    Yes, that is what I am seeing. I also do not see any negative impact, was just wondering what it is about.

  • Just a complete guess, but I wonder if the first call is to see the app has a glance view before it's added to the glance loop, and the second time is when it's actually in the glance loop and runs.

  • Let's say this is the intended behavior, I still don't necessarily agree that there will never be any effect. I mean what would you call 'any effect"? That you ser something in ERA?

    Maybe the only effect is that if the behavior was documented then some of the things we do now in onStart or getGlanceView could be moved to let's say the Glance view.onLayout (assuming that it is only called once, after the 2nd time getGlanceView is called) 

    But at least glanceView could be created once and saved in a variable and only returned (instead of recreated) the 2nd time

  • Just a complete guess, but I wonder if the first call is to see the app has a glance view before it's added to the glance loop, and the second time is when it's actually in the glance loop and runs.

    It might be something like this, at least in the sim.

    In the sim, if getGlanceView() returns null:

    - for CIQ 3 devices (which support both full-screen widgets and glances), the app works like a full-screen widget (regardless of the simulator's Glance Launch Mode setting)

    - for CIQ 4+ devices (no full-screen widgets), the app works like a regular device app with no glance

    This is despite the fact that:

    - the getGlanceView() docs say that the app name should be used as glance content when null is returned

    - on a real CIQ 3 device, a default glance view is returned with app name as content, just like the doc says, and just like what happens when getGlanceView() is not present (and when the app type is widget)

    - on a real CIQ 4+ device (at least fr955), a broken default glance view is shown which has no content (just the launcher icon)

    So yeah, it could be that the sim make an extra call to getGlanceView() just to determine the "mode" that the app should run in.

    Let's say this is the intended behavior, I still don't necessarily agree that there will never be any effect. I mean what would you call 'any effect"? That you ser something in ERA?

    Maybe the only effect is that if the behavior was documented then some of the things we do now in onStart or getGlanceView could be moved to let's say the Glance view.onLayout (assuming that it is only called once, after the 2nd time getGlanceView is called) 

    Yeah exactly. if this also happens on a real device, then a dev might want to avoid loading resources in the glance view's constructor, for example. (To be fair the docs recommend doing that kind of thing in onShow or onLayout)

    I don't know if it would necessarily happen on a real device (although I haven't checked), since obviously a real device doesn't behave as outlined above. For one thing, the individual widgets on a CIQ 3+ device don't determine whether they are displayed in full-screen or glance mode, but that is completely determined by a user setting. For another, if you install a device app with glance in the glance loop, the watch already knows that app has a glance (that's why it''s in the glance loop in the first place), so there's no need to do another "extra check" every time the glance loop is initialized or something like that.

  • I put a println wrapper which has the milliseconds since app start at the beginning of each function. There is a counter incremented each time onUpdate is called:

    Background:        0.000, app initialize
    Background:        0.000, app onStart
    Background:        0.016, app getServiceDelegate
    Background:        0.016, app onStop
           0.000, app initialize
           0.000, app onStart
           0.016, app getInitialView
           0.328, watch initialize
           0.391, watch onLayout
           0.500, watch onShow
           0.500, watch onUpdate 1
           0.500, watch onUpdate 2
    Background:        0.000, app initialize
    Background:        0.016, app onStart
    Background:        0.016, app getServiceDelegate
           1.156, watch onUpdate 3
    Background:        0.531, app onStop
           1.938, watch onUpdate 4
           2.953, watch onUpdate 5
           3.953, watch onUpdate 6
           4.953, watch onUpdate 7
           5.953, watch onUpdate 8
           6.969, watch onUpdate 9
           7.953, watch onUpdate 10
           8.953, watch onUpdate 11
           9.938, watch onUpdate 12
          10.953, watch onUpdate 13
          11.938, watch onUpdate 14
          12.938, watch onUpdate 15
          13.969, watch onUpdate 16
          14.953, watch onUpdate 17
          15.969, watch onUpdate 18
          16.969, watch onUpdate 19
          17.938, watch onUpdate 20
          18.953, watch onUpdate 21
          19.953, watch onUpdate 22
          20.328, app onStop
    

    I see something similar in that onUpdate is called twice at start. I figured out that to get it to update without crashing I had to set the timer to call requestUpdate on the second call to onUpdate. This doesn't happen with a watchface. This happens on both an FR235 (CIQ 1) and F7X (CIQ 5). Seems like a bug, unless someone can give a logical explanation why this behavior would be necessary and desirable.

  • I figured out that to get it to update without crashing I had to set the timer to call requestUpdate on the second call to onUpdate.

    You're kinda burying the lede here and/or assuming that we know exactly what you're talking about.

    Clearly you are setting a timer in onUpdate() which calls requestUpdate() when it expires, which seems normal enough.

    What isn't clear is why your code would necessarily crash just because onUpdate() is called twice when the app starts, unless it has some built-in (and unnecessary) assumption that onUpdate() would only be called once.

    "I had to set the timer to call requestUpdate on the second call to onUpdate"

    I also think it's a bad idea to assume that onUpdate() will be called twice, because then your code will break if it's only called once.

    Regardless of whether this behaviour is a bug or not, and regardless of whether we like it or not, I don't think it's a good idea to make any unnecessary assumptions about the number of times onUpdate() is initially called. How do we know that behaviour won't change in the future?

    In fact, if you truly believe this is a bug, you would definitely not want to assume that onUpdate() is called twice, because then if and when Garmin fixes that bug, your code will absolutely stop working.

    TL;DR if it were me, I would write my code so the number of times onUpdate() is initially called is irrelevant to the proper functioning of my code. (And I don't think it's that hard to do, I've done it in the past and I'm sure many others have done so as well.)

    e.g.

    import Toybox.Graphics;
    import Toybox.WatchUi;
    import Toybox.Timer;
    
    class TestWidgetXView extends WatchUi.View {
        var timer as Timer.Timer;
        const timerInterval = 1000; // ms
        function initialize() {
            View.initialize();
            timer = new Timer.Timer();
        }
    
        function onUpdate(dc as Dc) as Void {
            View.onUpdate(dc);
            //indeed onUpdate() is called twice at app startup, in the simulator for fr935
            System.println("onUpdate: " + System.getTimer());
    
            // It's no problem if onUpdate() is called multiple times, as a subsequent call to timer.start()
            // will cancel the previous call
            timer.start(method(:onTimerExpiry), timerInterval, false /* do not repeat */);
        }
    
        function onTimerExpiry() as Void {
            System.println("onTimerExpiry: " + System.getTimer());
            WatchUi.requestUpdate();
        }
    }