Skip to content
This repository has been archived by the owner on May 27, 2021. It is now read-only.

Tips and Tricks

MJ edited this page Dec 17, 2017 · 1 revision

This is a simple list of tips that might help you tackle common issues related to LML.

Refresh your projects when using snapshot versions

To make sure that you use the latest snapshot version, run Gradle tasks with --refresh-dependencies property. If you encounter any inconsistencies between Gradle run task and your application run by a regular IDE, try using gradle clean cleanEclipse eclipse --refresh-dependencies or gradle clean cleanIdea idea --refresh-dependencies and then refreshing the projects in your IDE of choice.

Prefer i18n bundles over plain text

It's much easier to translate your application or replace all occurrences of a certain text when you use i18n bundles rather than plain strings. It's even easier when you don't have to deal with clunky Java I18NBundle API and you can just proceed a string with @ to extract it from the bundle. Say hello to your favorite search engine, look up LibGDX i18n, create myBundle.properties file, pass I18NBundle to LmlParser using [Vis]Lml.parser().i18nBundle(I18NBundle.createBundle(Gdx.files.internal("myBundle"))); and enjoy your localized application. gdx-autumn-mvc-simple example project has more than one language version, you can check it out in action. gdx-lml-vis-websocket example also uses i18n (and pure gdx-lml-vis rather than gdx-autumn-mvc - one of my other libraries).

Use @LmlAction on every method available in LML templates

When you register an ActionContainer, all its methods are automatically available in LML templates. You can just use their names in LML templates and it will simply work. For the most part. You should still choose to annotate every method expected to use in LML templates with @LmlAction for the following reasons:

  • documentation: a method with this annotation is clearly meant to be used from within LML. Your IDE would probably complain about unused private method, but as soon as you annotate it, it suddenly becomes cool to have one of these in your code;
  • refactoring: Java method name can change. You might want to refactor your code some day or simply obfuscate it with some software. @LmlAction ensures that your templates won't break - thanks to passed method alias, change of method name won't affect LML templates;
  • performance: annotated method look-up will be faster. This is especially relevant on GWT platform.

Use DTD for content assist

If you can manage to go all the way through with XML awkward syntax - sacrificing optional quotations, non-named attributes in macros and whatnot - you can gain the DTD schema validation goodness. There's a whole section about DTD. In short, you can pass your fully constructed LmlParser instance to one of Dtd static methods to generate your personalized DTD schema. If you're wondering about your custom macros, tags and attributes: don't. Unless you use some really unusual code and the DTD generator is unable to create some of your complex, custom actors, all your extra data will be included in DTD file. Just make sure to use lmlParser.setStrict(false) to make sure that the DTD generator ignores some of the non-relevant errors.

Exploit your IDE for DTD (sorta-)comments

Some IDEs (like IntelliJ) allow to CTRL + click on tags and attributes and redirect you to the exact line of DTD file that defines them. If you generated your DTD file with comments included, every tag and attribute will be proceeded with corresponding Java class name that you can quickly find in your IDE. LML Java code is heavily documented, and Javadocs include some tag usage examples as well. This tries to make up for lack of proper XML comments.

Prefer custom macros over imports

It's tempting to import an external LML template. For example, you might want to create something like this at some point:

header.lml

        <table>
          <!-- {content} is provided by the importer. -->
          <label>{content}</label>
          <textButton growX="true">@thisIsJustAnExample</textButton>
        </table>

actualView.lml

        <table>
          <:import path="header.lml" replace="content">@myTitle</:import>
          <!-- Rest of view -->
        </table>

You can pass the {content} attribute, so you're fine, right? Wrong. First of all, as soon as you move your header template, actualView.lml will break. In addition to that, if you want to add more arguments to the imported template, it will not be as easy to read. It can be done, but it's not that obvious either. (For the record: you can use another {argument} in header.lml and then use one of argument assignment macros before importing, like <:assign key="argument" value="something"/>. It should work. But I shouldn't confuse you with stuff like that.)

Instead, define a single file with your macros:

myMacros.lml

        <:macro header content vertical="false">
          <!-- {vertical} is an optional attribute that defaults to false -->
          <table oneColumn="{vertical}">
            <!-- {content} is provided by the importer. -->
            <label>{content}</label>
            <textButton growX="true">@thisIsJustAnExample</textButton>
          </table>
        </:macro>

actualView.lml

        <table>
          <!-- vertical attribute is optional - if you don't pass it, it will
               become "false". -->
          <:header vertical="true">@myTitle</:header>
          <!-- Rest of view -->
        </table>

Loading myMacros in Java - execute before parsing actualView:

        // Processing global LML macros, available in all views:
        lmlParser.parseTemplate(Gdx.files.internal("myMacros.lml"));

As you can see above, {content} is still replaced by the text between tags (in this case, @myTitle), because content was used as second :macro attribute. Instead of importing an external file - and having to load it each time - we defined a new macro once and now we can use it everywhere. You don't even need to remember the replace="content" attribute, as :header macro automatically replaces the argument you pointed during its creation (in this case: {content}.

It's also very easy to define new custom attributes, even with some pre-defined default values. Hell, you could have a macro with 20 attributes and as long as most (or all) of them have a default value, and your LML templates using it might still be readable. Macros might need a few more lines of code to create, but they're still worth it.

In case you're wondering, custom macros defined in LML templates are "permanently" added to the LmlParser instance when they are parsed. You do not have to include the same macro again and again in every template - load one template with your global macros once and then you can use them in every other template that you parse later. See gdx-lml-vis-websocket example project for global macro file usage in action.

Dtd generator will detect your custom macros and generate DTD files with their tags and attributes, so you don't have to worry about content assist - just remember to actually parse the macros file before generating DTD.

To sum up, import macros are good for prototyping or when you need to load and include an external file (for example: a user-defined template or a file stored on a server). To build view abstractions that are easy to extend and customize, use macros.

Customize your syntax if you really have to

Both gdx-lml and gdx-lml-vis register a LOT of tags and even MORE attributes with MANY aliases. In fact, DTD schema files without your custom stuff can easily be 45 thousand lines long. You can choose which tags are registered and which aliases are chosen by overriding DefaultLmlSyntax (or VisLmlSyntax when using gdx-lml-vis) and then passing it to the LmlParser (or the parser builder). Methods to consider:

        new DefaultLmlSyntax() {
            // Normally registers all Scene2D actors with multiple aliases.
            protected void registerActorTags() {}
            // Normally registers all LML macros with multiple aliases.
            protected void registerMacroTags() {}
            // Normally calls methods that register attributes of Scene2D actors.
            protected void registerAttributes() {}
        };

For example, this syntax contains all macros, but only the table actor tag along with its attributes:

        new VisLmlSyntax() {
            @Override
            protected void registerActorTags() {
                addTagProvider(new TableLmlTagProvider(), "table");
            }
            @Override
            protected void registerAttributes() {
                registerCommonAttributes(); // Available for all actors.
                registerTableAttributes();
            }
        };

As attributes are grouped by the actor types that their support, it's very easy to cherry-pick which actors you want to use in your application. This requires some manual work, but your DTD file will become much smaller and LmlParser will use less memory at runtime (although you shouldn't worry about memory too much, unless it really becomes an issue). Look through DefaultLmlSyntax and VisLmlSyntax sources for more informations - they're both pretty well documented.

Note: LML 1.8 also brought EmptyLmlSyntax, which implements LmlSyntax interface with all the methods to set up LML syntax, but registers no tags on its own. It can be a great base class if you want to customize your syntax.

Keep global ActionContainer for actions shared across multiple views

You probably use one of LmlParser#createView methods - as long as you pass them an object (or class) that implements ActionContainer interface, their methods will be scanned using reflection and available in LML views. (BTW, AbstractLmlView already implements this interface.) But keep in mind that ActionContainers can be registered before you parse any templates, which allows you have "global" actions. API usage:

        Lml.parser()
           // Will be created automatically using MyActionContainer no-arg constructor:
           .actions("myContainer", MyActionContainer.class)
           // You can also construct your ActionContainer manually:
           .actions("other", new OtherActionContainer())
           // A custom, non-reflection based action - can be used for performance critical input:
           .action("myAction", new ActorConsumer<String, TextButton>() {
            @Override
            public String consume(TextButton actor) {
                return actor.getName();
            })
            /* ...other parser settings... */
            .build();

If you're wondering about name collisions - first of all, if non-reflection based ActorConsumer is found for the selected action ID, it is always preferred over ActionContainer methods. If you proceed action name with actionContainerId. (the one you used during registration), method from the selected ActionConrainer will be used. This also speeds up method look up, as only 1 container has to be analyzed, but don't worry about it too much. If you don't proceed the method with container ID, the first found method matching the passed name will be returned.

So, for example:

        <!-- Will invoke ActorConsumer and set method result as the text: -->
        <textButton>$myAction</textButton>

        <!-- Will invoke MyActionContainer#myAction or any method annotated with
            @LmlAction("myAction"): -->
        <label onClick="myContainer.myAction"/>

Registering "global" ActionContainers and ActorConsumers allows you to share logic across multiple views. And remember that it's just plain old Java - nothing is stopping you from having some fields and additional helper methods in your action-related classes.

Javadocs do not bite!

I know that tutorials and code snippets are usually a more pleasant way of learning the API, but LML is a pretty well-documented project at this point. Don't be afraid to dive into Javadocs of this project. Lectures to consider:

  • LmlParserBuilder - this the class of which instance you get on Lml.parser() call. It allows you to customize your LmlParser that you'll use to parse LML templates. This class was designed to contain the most essential settings that a typical user might want to modify at some point. Going through this class alone will let you learn how to register i18n bundles, preferences, actions, LML arguments, skins or even new tags and attributes.
  • LmlParser, LmlSyntax, LmlData - these interfaces are the core of LML. LmlParser manages the actual template parsing and contains many helpful methods which will allow you to create your views. LmlSyntax contains all currently registered tags, attributes and macros. Extending one of its implementations (DefaultLmlSyntax, VisLmlSyntax) will allow you to, well, change the syntax itself: you can even use (tagsInRoundParenthesis) if you don't like <theXmlWay>. LmlData is a data container, which allows you to access and register your methods, skins, bundles, preferences etc.
  • tag classes - usually ending with -LmlTag, these classes usually explain which Actor class they actually handle and how they treat their children (or plain text between their tags).
  • attribute classes - usually ending with -LmlAttribute, these classes have a direct reference to the used part of the Scene2D or VisUI API in their docs. They are the link between your tag settings and actual Java methods.
  • macro classes - usually ending with -LmlMacroTag, these classes are arguably one of the best documented parts of LML. Javadocs usually explain their purpose and include usage examples written with default LML syntax.

When using DTD (with generated comments), you can easily find out the name of the class handling a particular tag/attribute: a comment with class name will proceed its definition. Most IDEs handle CTRL click equivalent, which will take you directly from your .lml file to the correct line in .dtd definition, so you can navigate through DTD files without significant problems.

Use LmlApplicationListener if you want LML to handle your views

What LibGDX Game is to Screen, LmlApplicationListener is the same thing to AbstractLmlView - only packed with more features. It will handle views management for you, providing customizable smooth screen transitions. It eases LmlParser building and DTD schema generation. You just have to implement your views and write their .lml files, and LmlApplicationListener will handle rendering, resizing and most other boilerplate actions for you. Your first LML application features a good usage example.

Prefer style sheets over custom macros

As a rule of thumb: if you wrap a single actor with a macro, consider if a set of the default attributes would be enough to set it up. If yes, do not use macros. Style sheets page explains why default attributes are usually more convenient.