This tutorial is for building a SceneGraph Developer Extensions (SGDEX) based channel. Developers who want to build their first Roku channel, or are interested in moving their existing channel to RSG, benefit from this guide.

SGDEX includes the following views:

  • Grid
  • Details
  • Video with endcard view
  • Category list
  • Entitlement

Combining the views listed above, the developer can create their channel without a deep knowledge of Roku SceneGraph components.

Creating a project

Using an IDE, create a new project or use an existing project

Make sure the app manifest contains:

ui_resolutions=hd

All views are developed in HD resolution and autoscaled to FHD and SD. Make sure to develop the views for the app in HD resolution as well.

Required files

File structure

project/
    components/
        SGDEX/
        "your RSG components"
    source/
        SGDEX.brs
        main.brs
    manifest

 

In the source folder, create a file main.brs and populate it with the following:

Contents of main.brs
sub GetSceneName()
    return "MainScene"
end sub

The code above creates the scene and is the first step in building an app.

Here, "MainScene" is the name of the scene.

Developing a channel

To develop a SGDEX channel, create a Scene that extends from BaseScene.

Scene

The XML file contains the following:

<?xml version="1.0" encoding="UTF-8"?>

<component name="MainScene" extends="BaseScene" >

    <script type="text/brightscript" uri="pkg:/components/MainScene.brs" />

    <script type="text/brightscript" uri="pkg:/components/DetailsScreenLogic.brs" />

    <script type="text/brightscript" uri="pkg:/components/VideoPlayerLogic.brs" />

</component>

 

In /components/MainScene.brs, add:

sub Show(args as Object)

    'This function is called when the view is ready to show your content

end sub

This function passes params from main.brs and decides what view is displayed.

You can display a home screen(view), or create a deep linking screen(view) here by passing the proper params.

 

The scene has a ComponentController interface field that is used to show views and control flows.

ComponentController

A componentController is the component that controls all views.

Interface

Fields

FieldDescription
currentScreenThe view that is currently shown
shouldCloseLastScreenOnBackIndicates if the last screen in the stack should be closed before the channel exits


Manipulate this screen if the channel needs to implement deep linking or, a confirm exit dialog when the user presses back on the first screen.

Function interface

FunctionUse
showUsed to add a new view to the stack

Showing the first view

To show the first view, the developer needs to create the view, add content to it, and then display it.

Example

In /components/MainScene.brs, add the following to indicate the show(args) function:

sub Show(args as Object) 
    
	m.grid = CreateObject("roSGNode", "GridView")   
	m.grid.ObserveField("rowItemSelected", "OnGridItemSelected")    
	
	'Setup the UI of the view     
	
	m.grid.SetFields({         
		style: "standard"         
		posterShape: "16x9"      
	})

    'This is the root content that describes how to populate rest of the rows

    content = CreateObject("roSGNode", "ContentNode")

    content.AddFields({
        HandlerConfigGrid: {
           name: "CGRoot"
           fields : { param : "123" }
        }
    })
    m.grid.content = content

    'Triggers a job to show the view

    m.top.ComponentController.CallFunc("show", {
        view: m.grid
    })
end sub

The above code creates a simple grid view and displays it.

 

All views that extend Component have the following interfaces:

View UI setup

FieldDescription
styleThe style to be used, see the documentation for each view
posterShapeThe shape of the poster to be used in this view
contentView's content
overhangCongifure the overhang node to customize each view of your channel. For example, some views can have options or another logo or title

View visibility handling

  
wasClosedTriggered when the current view is closed. Use this when reading any value from a closed view. For example, itemFocused to set proper focus on the previous screen
saveState Triggered when a new view is opened after the current view. It is useful when data needs to be saved before another view is opened. For example, pause audio or video when a new view is opened
wasShownTriggered when the current view is opened for the first time or restored after the top view was closed
closeUse this field to manually close view. For example, during a registration flow, all registration views are closed after successful login

 

If the developer does not have content for the grid, use HandlerConfigGrid that describes how to populate rows for grid view:

   content = CreateObject("roSGNode", "ContentNode")

    content.AddFields({
        HandlerConfigGrid: {
            name: "CGRoot"
        }
    })
    m.grid.content = content

Content Getters

A content getter is a component responsible for populating content for views.

To load certain data for a view, use content getters.

To add root Content Getter, add it to the content of the created view:

m.grid = CreateObject("roSGNode", "GridView")

content = CreateObject("roSGNode", "ContentNode")

    content.AddFields({
        HandlerConfigGrid: {
            name: "CGRoot"
            fields : { param: "123" }
        }
    })

    m.grid.content = content

 

Each SGDEX view has an Associative Array (AA) Content Getter field and contains the following: 

name [required] - Project Content Getter component name

fields [optional] - Developer interface fields to be populated

Default Content Getter

Interfaces

Content getter provides a predefined list of interfaces:

FieldDescription
contentContentto be modified by the content getter. This content can be the view's content field or a child of it when the content for the child needs to be loaded (Example, a row in the grid or an item in the details or video view)
handlerConfig

Config added by the developer. Use handlerConfig to read the value of the config or restore it to content if needed

Note: Content getter removes processed Configs. If data needs to be reloaded each time content is shown, restore the config for the content in the proper Content Getter

offsetIndicates which offset is in use. Use offset for horizontal lazy loading of the grid row
pageSizeIndicates the page size configured by the developer. Used for horizontal lazy loading of the grid row

Implementing Content Getter

To implement a Content Getter, create a component and extend it from ContentHandler.

Example

<?xml version="1.0" encoding="UTF-8"?>

<component name="CGRoot" extends="ContentHandler" xsi:noNamespaceSchemaLocation="https://devtools.web.roku.com/schema/RokuSceneGraph.xsd">

    <script type="text/brightscript" uri="pkg:/components/content/CGRoot.brs" />

</component>

 

Content Getter implements only one required function GetContent() that does not return anything:

sub GetContent()

    'This is only a sample. Usually the feed is retrieved from an url using roUrlTransfer

    feed = ReadAsciiFile("pkg:/components/content/feed.json")

    if feed.Len() > 0
        json = ParseJson(feed)
        if json <> invalid AND json.rows <> invalid AND json.rows.Count() > 0
            rootChildren = []

            for each row in json.rows
                if row.items <> invalid
                    children = []

                    for childIndex = 0 to 3

                        for each item in row.items
                            itemNode = CreateObject("roSGNode", "ContentNode")
                            itemNode.SetFields(item)
                            children.Push(itemNode)
                        end for
                    end for

                    rowNode = CreateObject("roSGNode", "ContentNode")
                    rowNode.SetFields({ title: row.title })
                    rowNode.AppendChildren(children)

                    rootChildren.Push(rowNode)

                end if
            end for

            m.top.content.AppendChildren(rootChildren)

        end if
    end if
end sub


Contents of the JSON file

{
    "rows": [{
        "title": "ROW 1",

        "items": [{
                "hdPosterUrl": "poster_url"
            },{
                "hdPosterUrl": "poster_url"
            },{
                "hdPosterUrl": "poster_url"
            },{
                "hdPosterUrl": "poster_url"
        }]
    },{
        "title": "ROW 2",
        "items": [{
            "hdPosterUrl": "poster_url"
        },{
            "hdPosterUrl": "poster_url"
        },{
            "hdPosterUrl": "poster_url"
        },{
            "hdPosterUrl": "poster_url"
        }]
    }]
}

 

Note: According to SceneGraph best practices, it is suggested to use

m.top.content.AppendChildren(rootChildren)

As this removes multiple rendezvous between the render thread and the task node.

Opening next view

To open a new view for a certain action, use the same mechanism to create and populate the view.

For example, to open details screen upon selection on a grid, use:

Assuming the following has been executed,

m.grid.ObserveField("rowItemSelected", "OnGridItemSelected")

 

Implement the function OnGridItemSelected:

sub OnGridItemSelected(event as Object)

    grid = event.GetRoSGNode()
    selectedIndex = event.getdata()
    rowContent = grid.content.getChild(selectedIndex[0])

    detailsScreen = ShowDetailsScreen(rowContent, selectedIndex[1])
    detailsScreen.ObserveField("wasClosed", "OnDetailsWasClosed")
end sub

DetailsView

In /components/DetailsScreenLogic.brs

function ShowDetailsScreen(content, index)

    details = CreateObject("roSGNode", "DetailsView")

    details.content = content
    details.jumpToItem = index

    details.ObserveField("currentItem", "OnDetailsContentSet")

    details.ObserveField("buttonSelected", "OnButtonSelected")

    'Triggers a job to show the view

    m.top.ComponentController.callFunc("show", {

        view: details
    })
    return details
end function

The above code creates new details view and passes grid row as the content for details. It also uses jumpToItem to set starting index.

 

If the developer does not want to pass a list of items but only one item, they can use the snippet below:

details.content = content.getChild(index)

'Tells details screen that only one item should be visible

details.isContentList = false

isContentList – Informs details view that this is a proper item and can be rendered such that no extra items are loaded


Details view interfaces

Details view provides several interfaces:

buttons type = "node"

Buttons content node

Buttons support same content meta-data fields as Label list which sets the title as well as a small icon for each button.

FieldTypeDescription
TITLE StringThe label for the list item
HDLISTITEMICONURLUriThe image file for the icon displayed to the left of the list item label when the list item is not focused


Field description

FieldTypeDefaultDescription
isContentListBooleanTrue

Tells details view how your content is structured

  • If set to true it will take children of content to display on the screen
  • If set to false it will take content and display it on the screen
allowWrapContentBooleanTrue

Defines the logic of showing content when pressing left on the first item or pressing right on the last item

If set to true, it starts playback from first item (when pressing right) or the last item (when pressing left)  

itemFocusedInteger0Indicates the item currently in focus
jumpToItemInteger0Manually focus on the desired item
buttonFocused Integer0Tells what button is focused
buttonSelectedInteger0Is set when the button is selected by a user
jumpToButtonInteger0Interface for setting the focused button
currentItemNode-

Currently displayed item

This item is set when the Content Getter finishes loading extra meta-data  

Getting extra metadata for details view

Sometimes when setting content to details view you are still pending some info to be loaded to properly show this item.

To resolve this issue you should use content getter for details screen.

function ShowDetailsScreen(content)

    details = CreateObject("roSGNode", "DetailsView")

    for each child in content.getChildren(-1, 0)

        'Tells details view which content getter is responsible for getting the content

        child.HandlerConfigDetails = {
            name: "GetDetailsContentConfig"
        }

    end for

    details.content = content

    'this will trigger job to show this screen

    m.top.ComponentController.callFunc("show", {
        view: details
    })
    return details
end function

Opening non-SGDEX view

SGDEX is not limited to use only SGDEX views; the channel can show its own view and observe its fields.

To open a non-SGDEX view, create, and populate interface fields, set observers and call:

    m.top.ComponentController.callFunc("show", {
        view: yourViewNode
    })

This hides the current view (if any) and displays the non-SGDEX view.

Note: Component controller set's focus on your view, so your view should implement proper focus handling.


Example

<?xml version="1.0" encoding="UTF-8"?>

<component name="CustomView" extends="Group" xsi:noNamespaceSchemaLocation="https://devtools.web.roku.com/schema/RokuSceneGraph.xsd">
    <script type = "text/brightscript" uri="pkg:/components/customView.brs"/ >
    <children>
        <Group id="container">
            <Button id="btn" text="Push Me"/>
        </Group>
    </children>
</component>

 

In /components/customView.brs, add:

function init() as void

        m.btn = m.top.findNode("btn")
        m.top.observeField("focusedChild", "OnChildFocused")

    end function

    sub OnChildFocused()

        if m.top.isInFocusChain() and not m.btn.hasFocus() then
            m.btn.setFocus(true)
        end if
    end sub

Whenever the view receives focus, it should be checked if it's in the focus chain and the node unfocused.

 

Focus handling is important as component controller sets focus to a non-SGDEX view in two cases:

  • The view is just shown
  • The view is restored after top view was closed

Component Controller is responsible for closing this view when the back button is pressed.

If the view needs to be closed manually, a new field called "close" should be added to the view. 

By setting yourView.close = true, the developer can close the current view and the previous view is opened.