Best Practice for Loading Strings, Fonts, and Settings in Watch Face with Background and Typecheck

I have a watch face with a background process. In the View class, I have several class-level constants for loading custom fonts and strings (to use in the foreground). I also have a couple of color variables. The type checker doesn't like any of these because they are "not available in all function scopes" (i.e., background). But because they're at the class level, I cannot bypass type checking with (:typecheck(disableBackgroundCheck) like I could with a function.

Is there any way I can use class-level constants and variables like this in an app with a background process and avoid type checking issues?

class MyWatchFaceView extends Ui.WatchFace {

	var foregroundColor as Gfx.ColorType = Gfx.COLOR_WHITE;
	var backgroundColor as Gfx.ColorType = Gfx.COLOR_BLACK;  
	
	const fontSmall = Ui.loadResource(Rez.Fonts.small);
	const fontLarge = Ui.loadResource(Rez.Fonts.large);

	const weekdays = [
		Ui.loadResource(Rez.Strings.Day0),
		Ui.loadResource(Rez.Strings.Day1),
		Ui.loadResource(Rez.Strings.Day2),
		Ui.loadResource(Rez.Strings.Day3),
		Ui.loadResource(Rez.Strings.Day4),
		Ui.loadResource(Rez.Strings.Day5),
		Ui.loadResource(Rez.Strings.Day6)
	];

	const months = [
		Ui.loadResource(Rez.Strings.Month00),
		Ui.loadResource(Rez.Strings.Month01),
		Ui.loadResource(Rez.Strings.Month02),
		Ui.loadResource(Rez.Strings.Month03),
		Ui.loadResource(Rez.Strings.Month04),
		Ui.loadResource(Rez.Strings.Month05),
		Ui.loadResource(Rez.Strings.Month06),
		Ui.loadResource(Rez.Strings.Month07),
		Ui.loadResource(Rez.Strings.Month08),
		Ui.loadResource(Rez.Strings.Month09),
		Ui.loadResource(Rez.Strings.Month10),
		Ui.loadResource(Rez.Strings.Month11)
	];

    (:typecheck(disableBackgroundCheck))
	function initialize() {
        WatchFace.initialize();
   	}
   	
   	...
}

For a separate but related question, in the App file, I have a global variable for a custom class for app settings (e.g., "gSettings"). If I try to initialize it at the global level by creating a new class instance, I get an illegal error in the debug console. So for now I'm living with the "untyped" warning.

Is it possible to have a global variable assigned to a custom class and avoid type checking issues?

  • 1. Literal constants like numbers, enums, colors are anyway optimized out at compile time, so there's no problem 'having" them "also" in the background

    2. No point of having a view class in the background process, as it's in the background only, nobody can see the view...

    3. Here comes a more serious issue: you shouldn't call UI.loadResource in a "global" scope and loading a class is a global thing. Call load resource at earliest in initialize or maybe only in on show (but then it can't be a const of course, and will have to declare it as nullable) I would just change the const to var and load them in the constructor.

    4. For the error in the app: shouldn't be any type check related issue, I'm not sure if it's related. Without neither the code nor the error it's hard to guess. My guess is that it's again things that you do too early in that class that cause the illegal error

  • Sorry for the dumb questions:

    1) Why is the code in your *view* class being hit with "not available in all function scopes" errors?

    I would expect getIniitialView() in your app class to have this problem when it tries to create a new MyWatchFaceView instance, but you should be able to use the disableBackgroundCheck annotation on that function.

    Have you actually added the background annotation to any of the members of MyWatchFaceView? This would have the effect of putting the entire view class in the background.

    2) I'm not sure if I understand this statement (and the rationale for adding typecheck(disableBackground) to the view's initialize function.

    The type checker doesn't like any of these because they are "not available in all function scopes" (i.e., background). But because they're at the class level, I cannot bypass type checking with (:typecheck(disableBackgroundCheck) like I could with a function.

    But if X is not available in all function scopes (i.e. background) then the solution is to either put X in the background or add (:typecheck(disableBackgroundCheck) to the function that refers to X, not to add (:typecheck(disableBackgroundCheck) to X itself.

    Taking a look at this code:

    > var foregroundColor as Gfx.ColorType = Gfx.COLOR_WHITE;

    Is the *actual* problem here that Gfx and COLOR_WHITE are not available in all function scopes (not foregroundColor). If this is the case, again I would have to guess that it's because at least one member of the view class has the background annotation and therefore the whole view class is in the background.

    If this is the case:

    - You can actually make these errors go away if you annotate the entire view class with (:typecheck(disableBackgroundCheck)), but I don't think this is the right thing to do, because the view class shouldn't be in the background in the first place (it's a waste of limited memory). Using that annotation would only sweep the problem under the rug by eliminating the associated warnings/errors.

    - The correct fix is to ensure the view class isn't in the background (make sure that no member of the view class has the background annotation). If you need a function or variable to be accessible to the background, then it should be extracted to a separate class.

  • Thank you both. I apologize for the confusion around why the View class was using the background. The answer is -- no, it's actually not using the background. I just put the annotation there one time when I was troubleshooting something and forgot that it was there. Later when I started type checking I didn't question the background flags. I'm glad you did! I took it out and problem is solved. Thank you.

    For the global variable question, initializing the custom class doesn't involve anything fancy. I put the class definition is in a separate Util module, and the class variables are initialized as common types. Then I use a function to pull the app settings via Properties.getValue (e.g., use function in onUpdate and use settings info wherever it's needed).

    module Util {
    
    	class MySettings {
    
    		var myBooleanSetting as Boolean = false;
    		var myNumericSetting as Numeric = 0;
    		var myFloatSetting as Float = -122.47873606543394;
    		var myNumberSetting as Number = 0;
    		var myStringSetting as String = "abc123";
    	}
    
    	function getMySettings() as MySettings {
    
    		var mySettings = new MySettings();
    
    		mySettings.myBooleanSetting = Properties.getValue("MyBooleanSetting") as Boolean;
    		mySettings.myNumericSetting = Properties.getValue("MyNumericSetting") as Numeric;
    		mySettings.myFloatSetting = Properties.getValue("MyFloatSetting") as Float;
    		mySettings.myNumberSetting = Properties.getValue("MyNumberSetting") as Number;
    		mySettings.myStringSetting = Properties.getValue("MyStringSetting") as String;
    		
    		return mySettings;
    	}
    }

    In the App file, I tried initializing the global variable gSettings as follows, but I got an illegal access error in the debug console (failed invoking symbol):

    import Toybox.Application;
    ...
    
    var gSettings as MySettings = new MySettings();  // illegal access error in debug console
    
    class MyWatchFaceApp extends Application.AppBase {
    
    	function initialize() {
            AppBase.initialize();
        }
    ...
    }

    Error shown in debug console:

    Error: Illegal Access (Out of Bounds)
    Details: Failed invoking <symbol>

  • In the code where you have gSettings, how are you able to call new MySettings() without using the qualifying module name. i.e.
    var gSettings = new Util.MySettings();

    If I try code similar to yours (with MySettings in the Util module, but gSettings outside of it), I get a compile error:

    Cannot find symbol ':MySettings' on type 'self'.

    If I disable the type checker, the code builds, but crashes at runtime (with a different error than what you're seeing):

    Error: Symbol Not Found Error
    Details: Could not find symbol 'MySettings'

    If I instead call new Util.MySettings(), then my code runs without crashing.

  • IMHO the same thing again: you're not supposed to use Properties.getValue before you called AppBase.initialuze() so move the creation of MySettings into the app's initialuze

  • 3. Here comes a more serious issue: you shouldn't call UI.loadResource in a "global" scope and loading a class is a global thing. Call load resource at earliest in initialize or maybe only in on show (but then it can't be a const of course, and will have to declare it as nullable) I would just change the const to var and load them in the constructor.

    While I agree this isn't the best code, by no means is loadResource being called in a "'global' scope". The correct term here (wrt to classes) would be "static context" -- imo when the wrong word is used, it doesn't really help to add quotes around it. But even that term would be wrong, since none of those member variables are static.

    To be clear, since fontSmall, fontLarge, weeks and months are all non-static member variables, they will absolutely be initialized when the class is instantiated, and not before that. How else would you expect it to work, especially consdering multiple instances of a class can exist? Every instance of a class has to have a its own copy of the classes' non-static member variables, so it wouldn't make sense for them to be initialized before an instance is created.

    You can see this very easily with the following test code, in a datafield app:

    function getInitialView() as [Views] or [Views, InputDelegates] {
       System.println("Before new TestDFView");
       return [ new testDFView() ];
    }

    https://pastebin.com/UthtGbeg

    NOTE: in the above code, change Util.Help to Help. Sorry!

    Output with line following [!!!] uncommented:

    Before new TestDFView
    count = 0: TestView initialized nonStaticMember
    count = 1: TestView uninitializedMemberVariable @ initialize()
    count = 2: TestClass initialized staticMember
    TestClass.staticMember2 = 5
    onLayout (I added a println to onLayout and didn't bother to include it in the code)
    count = 3: TestView initialized staticMember

    Output with line following [!!!] commented:

    Before new TestDFView
    count = 0: TestView initialized nonStaticMember
    count = 1: TestView uninitializedMemberVariable @ initialize()
    onLayout (I added a println to onLayout and didn't bother to include it in the code)
    count = 2: TestView initialized staticMember

    This demonstrates that:

    - as expected, the non-static variables in TestClass are never initialized at all, because the class isn't instantiated

    - non-static variables for a class instance are initialized before initialize() is called, as expected

    - somewhat surprisingly, static variables are initialized "on demand". If a class is never referenced (even statically), then its static members aren't initialized. For a referenced class, if a static member isn't referenced, it seemingly will be initialized as late as possible. (I didn't show it in the above code, but if you reference TestView.staticMember earlier, then it will be initialized earlier.)

    Sorry for not creating a more generic example with classes that are initialized from a truly global scope - this led to circular dependency errors.

  • When you have the background permission, your appBase is used both when the main app runs, but also each time the background runs  Some is only used when the main app runs, like getInitialView, some only when the background runs, like getServiceDelegate, some when either runs, like initialize, onStart, onStop.  On top of that globals aren't really global between the main app and the background.  Each have their own copy.

    To make it interesting, with the background permission, onAppInstall/onAppUpdate can be called when the main app starts, but in the background.

    So, defining your global as

    var gSettings = new MySettings();

    means it happens much more than you expect (when the background runs)

    What you may try is having

    var gSettings=null;

    as the global, and then in getInitialView, have

    gSettings = new MySettings();

    That way MySettings only comes into play when the main app starts.

    The error you see is often due to an attempt to use resources in the background that aren't scoped for the background, and there is an attempt to access them in the background say for onAppUpdate and you crash right away.

    See https://developer.garmin.com/connect-iq/core-topics/resources/#resources and Resource Scopes

  • Anyway, OP's loadResource code as written is effectively no different than initializing all of those variables in initialize(). I do agree it would be better to call loadResource in initialize() (the constructor) as opposed to using "inline field initialization" , if only for reasons of clarity and predictability [e.g. you know what order the code in initialize() will run, but can you be sure in what order member variables will be initialized? Ofc order probably doesn't really matter here, and let's face it, they'll probably be initialized in the same order that the member variables are declared]

  • The error you see is often due to

    As per the rest of this thread, in this specific case, the error was caused by accidentally putting the View class in the background (by annotating one of its members with :background.)

  • IMHO the same thing again: you're not supposed to use Properties.getValue before you called AppBase.initialuze() so move the creation of MySettings into the app's initialuze

    Properties.getValue() isn't called when MySettings is instantiated, it's called when getMySettings() is called (which happens independently of instantiating MySettings.).

    I'm not saying I think the current implementation is optimal, but it doesn't have some of the problems that you think it does.