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?

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

    Note: The fact that the class members involved with loading resources (fontSmall, weekdays, etc.) are marked as const in OP's code only means that their value can't change; it doesn't make place them in a single static context as the static keyword would.

    The example will work the same way if you change var to const.

  • Well using the module name would make sense! :) When I use the module name, it clears the error right up; the type checker is happy, and there is no error in the debug console:

    var gSettings as MySettings = new Util.MySettings();

    Regarding how the code was still able to compile earlier -- Perhaps it is because I have a background service delegate like jim_m_58 said?

    (Apologies for not quoting your previous post; for some reason I do not see the option to quote.)

  • Jim_m_58, I was previously creating the variable at the global level by simply declaring it:

    var gSettings;

    But this triggers a warning from the type checker (level 2), because it's untyped.

    Initializing with the module name solves both problems (type checker and console error), and it doesn't seem to increase memory usage at all compared to declaring the variable without initializing it, at least in the simulator:

    var gSettings as MySettings = Util.MySettings();

  • FlowState, for the class-level constants and variables, I previously did wait until onLayout() to call loadResource, and at the class level, I simply declared all of them as variables:

    class MyWatchFaceView extends Ui.WatchFace {
    
    	var foregroundColor, backgroundColor;
    	var fontSmall, fontLarge;
    	var weekdays, months;
    
    	function initialize() {
            WatchFace.initialize();
       	}
    	
    	function onLayout(dc) {
    		
    		foregroundColor = Gfx.COLOR_WHITE;
    		backgroundColor = Gfx.COLOR_BLACK;
    		
    		fontSmall = Ui.loadResource(Rez.Fonts.small);
    		fontLarge = Ui.loadResource(Rez.Fonts.large);
    		
    		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)
    		];
    		
    		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)
    		];
        }
        
        function onUpdate(dc) {
        	...
    	}
    	...
    }

    But this triggers warnings/errors from the type checker, because they are untyped when declared.

    So I am now doing this instead:

    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)
    	];
    
    	function initialize() {
            WatchFace.initialize();
       	}
    	
    	function onLayout(dc) {
        }
        
        function onUpdate(dc) {
        	...
        }
        ...
    }

    The type checker is happy, and I'm still loading everything only once (I switched to constants because I never change these, and I could be lazy and not specify types).

    With this being "not the best code" ;) , how else would you recommend initializing these variables/constants while still making them available at the class level (accessible by multiple functions without having to load resources multiple times) and while also satisfying the type checker?

  • The type checker is happy, and I'm still loading everything only once (I switched to constants because I never change these, and I could be lazy and not specify types).

    With this being "not the best code" ;) , how else would you recommend initializing these variables/constants while still making them available at the class level (accessible by multiple functions without having to load resources multiple times) and while also satisfying the type checker?

    Yeah it was my understanding that you did this to make the type checker happy (i.e. avoiding warnings about uninitialized variables and avoiding the need to declare these variables as nullable, which leads to a lot of unnecessary code down the line).

    I should've expanded on the recommendation to move this type of initialization to initialize() as opposed to doing it "inline". From the type checker's POV, member variables can be initialized either "inline" or in the class's initialize() method (constructor) - either way avoids warnings/errors about uninitialized variables.

    However, as you pointed out, the way you have done it (as const with an inline initializer) does save you from having to specify types, as the type checker can infer the type in this case. And it's not possible to initialize const members in initialize(), only var members.

    With this being "not the best code" ;

    I actually think it's fine, but as you found, it might cause some confusion when others are looking at it. (I personally think it should be understood that non-static member variables are only initialized when a class is instantiated tho.)

    "not the best code" was probably way too harsh. It's just slightly unconventional for some who may be used to putting all "non-trivial" initializers in the constructor.

    And you do lose some flexibility, such as the ability to unload resources / load them on demand, which doesn't seem to be a need in this particular case. Then again, if you had that flexibility, the member variables would have to be nullable, which is something you apparently wanted to avoid in the first place (and understandably so).

    So here's another way to do load your resources while still preserving type safety, avoiding warnings/errors about uninitialized variables, and avoiding making your resource variables nullable:

    // no need to make this type nullable as long as it's initialized either here
    // or in
    class MyWatchFaceView extends Ui.WatchFace {
    //...
        var fontSmall as WatchUi.FontResource;

        function initialize() {
           // ...
           fontSmall = Ui.loadResource(Rez.Fonts.small);
           // ...
        }

        //...
    }

    Here you actually have the advantage of being able to specify a more specific type (FontResource) than would've be inferred by the type checker (Resource).

    But yeah, I think your existing code is fine (what you're doing now is certainly not a grave error afaict).

    I do agree that it's best practice to avoid global variables when possible (although they can be convenient). jim_m_58 made a great point about how global variables are always including in both the background or foreground, so the use of global variables in an app with a background process can have significant memory implications, in some cases. (Ofc the background process typically has less available memory than the foreground app).

  • No worries, I'm all for doing things the conventional way :)  Thanks for the example. However, the type checker still complains with a warning/error in initialize(). Here's the warning I get for informative type checking:

    Assigned value type 'PolyType<$.Toybox.Graphics.BitmapReference or $.Toybox.Graphics.FontReference or $.Toybox.Lang.Array or $.Toybox.Lang.Dictionary or $.Toybox.Lang.String or $.Toybox.WatchUi.AnimationResource or $.Toybox.WatchUi.BitmapResource or $.Toybox.WatchUi.FontResource>' to member '$.MyWatchFaceView.fontSmall' of non-poly type '$.Toybox.WatchUi.FontResource'.

  • Never mind, the warning can be avoided by assigning the type again in initialize(). I assume it needs this reassignment because loadResource can return any of the types it listed in the warning. Thanks again! I think I'm good now.

    Re: global variable -- In my case, I use a couple of settings in the background, so I want to be able to call gSettings there. But you all make a good point on memory and I may try to rework it.

    To summarize the solutions (so I can mark this single post as the answer):

    1. The class-level variable issue was solved by declaring the variable with type at class level and then assigning the value in initialize(). If you're using loadResource or something else with poly output, you'll need to reassign the type in initialize(). Note that this approach requires using variables and not constants.

    2. The global variable issue with the custom class was solved by simply referencing the module when creating the new class instance. Note: it is generally not advised to use global variables if you can avoid it because they use more memory.

  • You're right, I confused 2 classes in the thread

  • Nice summary. Couple of clarifications:

    1. The class-level variable issue was solved by declaring the variable with type at class level and then assigning the value in initialize(). If you're using loadResource or something else with poly output, you'll need to reassign the type in initialize().

    If fontSmall was instead declared as Resource, the cast in initialize() would be unnecessary. I chose to use FontResource as I felt it might be more useful for your purposes.

    In general type declarations (e.g. "var fontSmall as FontResource") make your program more type safe, but type casts (e.g. "fontSmall = loadResource(...) as FontResource") make your program less type safe. A type declaration constrains what types a variable is allowed to be, but a type cast bypasses the compiler's determination of a value's type (so one could easily type something invalid like 'fontSmall = "foo" as FontResource', which would bypass the type checker, possibly leading to a crash at runtime). (Ofc real cases involving an invalid type cast would be a lot more subtle.)

    With that in mind, I wish Garmin hadn't used the same syntax ("as") for both type declarations and type casts. Contrast with Typescript, which uses different syntax for each, so it's very easy to distinguish them at a glance.

    e.g.

    Monkey C:

    var x as String; // type declaration
    var y = z as String; // type cast

    TypeScript

    var x: string; // type declaration
    var y = z as string; // type cast

    In Monkey C, if "as" appears after the *name* in a const/variable declaration or function parameter, that's a type declaration. If "as" appears after a *value* (including the name of a variable after it's been declared), that's a type cast. Ofc it's unambiguous to the compiler, but it's arguably not inconceivable for a human to get mixed up.

    e.g. The following 2 lines of code (at the global or class scope) have very different meanings and outcomes:

    var x as String = 5; // type mismatch error (5 is not a String)

    var y = 5 as String; // no type mismatch error, but you will get a warning/error about y being an untyped variable at certain type check levels

    Having said all that:

    - type casts are actually necessary in some cases, when using the CIQ API (hopefully the list of cases will approach 0 one day)

    - they can arguably be excused as long as you "know what you're doing"

    it is generally not advised to use global variables if you can avoid it because they use more memory.

    In CIQ, the "wasting memory issue" applies to when you need a global variable for the foreground and not the background, but the global variable is going to exist in both contexts anyway. This is where, as jim_m_58 pointed out, you may want to at least avoid initializing the global variable in the background (but ofc that only makes your app more complex and brings out the same issue where you have to make a variable nullable when you really didn't want to.)

    In general, global variables are considered bad practice for many reasons (such as making your program harder to understand and more unpredictable): [https://wiki.c2.com/?GlobalVariablesAreBad]

    (Ofc, some of the replacements for global variables, such as using a singleton class or passing an object all over your program may have similar or additional issues.)

    The biggest argument in favour of global variables is convenience / simplicity.

    You're right, I confused 2 classes in the thread

    Gotcha, sorry I assumed that you don't understand how non-static class members work haha. Should've given you the benefit of the doubt there.

    I do think that in CIQ / Monkey C at least, code which calls loadResource from a literally global scope would probably cause a crash or a compile-time error. (So it would probably be hard to get very far at all with that sort of error in your code.)

    The original version of my example tried to call System.println() from a global scope (using the same helper class), but it always resulted in a circular reference error at either compile time or runtime. I'm guessing that most (or all?) of the CIQ API functions can't be called globally (i.e. before the app is initialized).

  • Thanks! Very helpful insights Slight smile