Designing for Nomad (Android)

I’ve been playing around with the Nomad Client for a while now. Most demos you find are built for  iOS devices. However, I’m (un)fortunate to have an Android phone.
Based on my experiences, I rebuilt Theo Heselmans’ Wine Tastings App to guide you through specific issues I encountered while developing for Nomad Android.
Please have a look at this project before reading further. You need to know the design of the Wine Tasting App (Wine App in short) to understand most of my examples.
This article is not for Android only. Some of the issues I encountered happen also on an iOS device, so this blog might help Apple lovers as well.

I know this is a long post. Keep on reading until the end, because there are a lot of examples and best practices I want to share with you.

@Platform

Using @Platform([Specific]), you get information on the device you are running on. The info you get on an iOS device is different from an Android device (and a Windows device for that matter).
When you run @Platform([specific]) on iOS, you get something like:
“iOS” : “13.1” : “iPad” : “iPad11,4” on an iPad
or “iOS” : “13.1” : “iPhone” : “iPhone10,6” on an iPhone
For an Android Phone, I get
“Android” : “10” : “phone” : “359,678”
I have no Android Tablet available for testing, but i assume that it would be something like
“Android” : “10” : “tablet” : “359,678”. Please correct me if I’m wrong.
On Windows devices, @Platform will again return something different.

Unfortunately, you get no information on the size of your device, nor if you use it in Portrait or Landscape mode. This would be interesting information and hopefully HCL will add it in further releases.
For the Wine App, 2 different UI are defined, one for phone and one for other (bigger) devices. Using @Platform can help to decide the device type. For example, to decide the subform to use on the ‘Tasting’ form, this code is used:

small := "_Small";
large:= "";
Platform := @Platform([Specific]);
suffix := @If(
Platform = "phone";small;
Platform = "iPhone";small;
large);

@If(@Environment("wtPreview") = "Yes":"";"SubPreview";"SubEdit")+suffix

I use similar code throughout the application to do device specific actions.

Form development

The Floating Form

One of the most annoying things in Nomad is the ‘floating form’. When you scroll up and down in a form and accidentally move your finger a bit sideways, the form starts scrolling horizontally and it is very hard to get it centered again.

The form scrolls horizontally while it shouldn’t.

Luckily I found a solution for that, but it takes a few steps.
When you create a form or subform, the first thing to do is to set the margin to 2cm (0.788”) instead of the default. This will make sure the left edge of the form is exactly at the left side of the screen on your mobile device.

Reset the default settings

Then create a single row, single column table that fits with margins.

You can set the border to 0 to make it nicer on the screen. Make sure you do not set any row/column spacing.

I suggest you create an inner table, again with 1 row and 1 column and add a small row and column spacing to this table. This is to avoid that your form content is glued to the edge of your screen.

Now it is time to start adding content to your form. It is important not to set any fixed widths or at least use a width that will certainly fit on a screen. Be careful with a table containing multiple columns with images or a radio/checkbox where you put multiple options on one row.
Note that testing on one device does not guarantee it will work on all devices. Android phones come in a lot of sizes and every device has its own maximum width.

If all goes well, you should have a form that does not scroll sideways. If you played around with complex form design and tables, you know that it can be frustrating. Designing for smartphones does not make it easier. So make sure you test regularly to avoid that you have to start over again.

Dialog actions

The dialog lists on an iPhone show really nice and integrated in the OS. But on Android, the dialog list is a bit clumsy. The selection part is shown in a list on the left of the dialog and the OK and Cancel buttons are shown on the right. Also, the list items are rather small. It is hard to select an item, especially when there is a lot of text in it.

The selection area is small.

Therefor, I created my own dialog box, using a little bit of lotusscript and a form. It can handle single and multi selection. My dialog looks like this:

Unfortunately, it is not possible to change the font style of the list boxes I used but at least, the selection area is wider and you can add a custom title as a bonus.

I also hid the entry helper button on each dialog list. Since I use a custom dialog, I don’t need the drop down arrow and I don’t like it that the button is floating on the right side of the data and thus it’s place depends on the amount of data in the field. I prefer all my selection buttons at the same width. You can still use the default dialog by double tapping on the field.

Display entry helper button is unchecked.

Field Labels

In the Wine App, the labels were next to the fields. Although this saves space in height, some fields became too small to show the data on one line on my device. Therefor I put the labels above the fields. This gives more room for the data, but requires more vertical scrolling to see everything.

Inspired by Material Design’s filled text fields, I only set the bottom border of the cell containing a fields instead of using a filled table cell. Both options are equal, it’s up to your preference. Note that in the design, I always use the actual field in edit mode (in an underlined cell) and a display field in read mode (non-underlined cell). In designer, it looks like this:

In read mode, the form looks like this:

And this is edit mode

Fonts

For now, the Nomad Client on my Android device does not support (true type) fonts. So don’t bother changing your font to Tahoma, Verdana, Arial or any other font. It will look different on your device anyway. Stick with the ‘Default Sans Serif’ or ‘Default User Interface’ font and you will be fine. Other font settings (bold, underline, italic, size) work fine.
Furthermore, make sure you use a large enough font size (I suggest 12 instead of the default 10).

Views

Categorization and columns

In a lot of (older) Notes applications, you see views containing a lot of data. Sometimes they even look like a spreadsheet. This is not useful on mobile devices. Try to avoid that users have to scroll horizontally. Sometimes it is necessary, but note that you cannot lock the first columns like you can in Excel, so you will loose context.

I’m sure this view won’t fit on your mobile device

As Theo already did in the Wine Tastings, avoid categorized views. Although seasoned Notes users are accustomed to it, it works kind of clumsy on a smartphone. Using embedded views showing a single category works better. Furthermore, the white space you get from the indentation is lost space for other data.

The multi-line row

You can put data on multiple lines in a rows by using multi-value columns with a New Line as separator. This allows you to have more data on the small screen. As a bonus, a row becomes larger, which makes it easier to click.

Name, Region and Vintage Year are shown in one row.

To avoid confusion, it is important that every value (name, region or year) does not wrap to the next row. Therefore I limit each line (= value) to a number of characters, depending on the width of your column. This all depends on your data set, of course. If a lot of rows start with the same characters, it can become difficult to see differences.

Note that there is still a bug in Nomad Android that makes it a little harder using this technique. If the combined data of values fit on one row, Nomad will force them to one row, regardless of the newline for multiple values. This is no issue on iOS as far as I know.

The data in the red rows are cropped to one line

I add a number of blanks as filler to add to the field values to fix this.
Hopefully this will be solved in one of the next releases, but for now, you can avoid it using this snippet:

maxlength := 30;
vals := name : region : @Text(Year);

repeat := @Repeat(" ";maxlength; maxlength);
@For(i:=1;i<=@Elements(vals);i:=i+1;
	tmp := vals[i] + repeat;
	val :=  @If(@Length(vals[i])>maxLength;@Left(vals[i];maxLength) + "...";@Left(tmp;maxLength));
     result := @If(result = ""; val;result:val)
);
result

Using previews

Using previews in your application is another way to show more info on a specific row. I changed the preview of the Wine App for two reasons:

First, I wanted to avoid people having to scroll to see data. On my device, the large flag prevented me to see all the data in the preview.

Secondly, I made the preview more compact to show the same data with less height. On smaller devices, previews, headers, category selectors and so on can take so much space that the actual view data lacks space.

The preview shows a lot of info without taking too much place.

Icons

As you can see in some screenshots above, I changed the slider bar in the original Wine App to smiley icons. It’s not that I don’t like the slider, but it was to wide to see the rating without having to scroll.
The icons I chose are personal taste, but the main idea is to use icons that use less width.

Icons can help you to put more info on your screen. Use small icons that don’t use a lot of width. If you use the multi-line technique as described above, you even use icons that are a bit larger.

Example of larger icons with
multi-line technique.

The FAB (Floating Action Button)

In Domino/Notes 10 and higher, you can use the FAB icon instead of an actual button. Although this might be handy in some applications, I chose not to use it here.
The reason I didn’t use it, is not because I don’t like the icon (which I don’t), but mainly because it still acts a little buggy on Android devices. The button does not stay at the bottom of the view on my device. Sometimes it is shown over the preview or in the middle of the view.

The FAB is kind of jumpy.

Of course, these are teething problems which will hopefully be solved in a next release.

Another reason not to use the FAB button is because it is not (yet) available on forms and I like form and view actions to be consistent.

Device width

Keep in mind that each device has its own width. This can become annoying in combination with the fixed size of your columns. I would suggest to extend the last column to window width and optimize for smaller devices. But this might not be ideal for your application. In general, try to avoid that people have to scroll sideways to see data.

Also note that a larger device does not necessarily mean that you can see more. It also depends on the pixel density (pixels per inch) on each device.

Frames and navigation

The header bar containing the window tabs in Nomad is very small and it becomes too crowded really fast.

To avoid the opening of a new tab on every click, some redesign is necessary.

@SetTargetFrame is your friend

You can use this by using @SetTargetFrame(<framename>) to set a target frame before opening another frameset, page or view. This way, it will open in the frame instead of a creating a new tab.

Redesigning navigation

If you click on a navigation image of the Home Page in the Wine App, a new tab is opened with the view you selected. You can go back to the home page by closing the view tab or clicking on the Home tab in Notes.
Of course, when you open the view in the same frameset, there is only one tab in Notes and you have to create another way of navigating back to the home page.

I left the original behavior for the desktop/tablet version. For the phone version, I redesigned the navigation to use one single frameset to use the homepage and views.

Main navigation

HomeFS is the frameset opened when launching the application. It contains only one frame.
Using a computed value for the frame content, you can open a different page, depending on the device you work on.

Instead of opening the page ‘HomeSmall’ for phone/iPhone, another frameset (HomeFS_Small) is opened within HomeFS. This is done using the @Platform snippet I mentioned above.

HomeFS_Small is a frameset containing two frames. One frame on top named ‘Header’ containing a page ‘Home_Small_Header’ and one main frame called ‘MainSmall’ containing the page ‘Home_Small’.

HomeFS_Small inner frameset

The frame ‘MainSmall’ is where the magic happens, Pages and other framesets will be targeted in here using @SetTargetFrame.

In the ‘Home_Small’ page, the different tiles all have similar formula’s opening a frameset for that specific view; after targeting ‘MainSmall’

@Environment("wtTitle";"Wines by Vintage");
@SetTargetFrame("MainSmall");
@Command([OpenFrameset];"YearVintage_Small")

‘Home_Small_Header’ is a page containing only the action bar. The action bar contains a left aligned Home action, showing the Wine App icon. This action opens ‘Home_Small’ in ‘MainSmall’, similar to the code above. Right aligned, there is an action ‘Menu’ containing two sub-actions; one to create a new tasting and one to exit the application.

Updated Home Page

Vintage Year Navigation

Because of the changes I made, the Year selector on the ‘WinesVintage Header_Small’ form didn’t work anymore. The NotesUIWorkspace.reloadwindow() in the Exiting event of the field caused the complete window – thus the HomeFS frameset – to refresh; resulting in showing the homepage again instead of the updated view. The goal was to only reload or reopen the form containing the embedded view, to show the rows for the newly selected category.

You cannot open a form in a frame using formula or lotusscript. So I created a frameset (YearVintage_Small_Inner) containing only the form loading the embedded view. By reopening the frameset, the form (creating a new document) is loaded, containing the embedded view showing the newly selected category.

Sub Exiting(Source As Field)
	Dim session As New notessession
	Dim ws As New NotesUIWorkspace
	Dim doc As NotesDocument
	Set doc=ws.CurrentDocument.Document
	'Setting the year as session variable in Notes INI
	Call session.setenvironmentvar("wtYear",doc.Year(0))
	'Refreshing this document to update counter	
	Call ws.CurrentDocument.Refresh
	'Setting the target frame (in YearVintage_Small frameset)
	Call ws.SetTargetFrame("Main")
	'Reopening the inner frame causing the form with embedded view to reload
	Call ws.OpenFrameSet("YearVintage_Small_Inner")
End Sub

I also updated the arrow navigators using @setTargetFrame to refresh the Main frame on the inner frameset.

Continent/Country Navigation

The ‘Continents_Small’ frameset is removed. Instead I open a form ‘World_Small’ in the ‘MainSmall’ frame (‘HomeFS_Small’ frameset) to show the map.
The continent/country navigation was one one form, containing both category selectors and the embedded view. Although this works fine and is an easier approach than working with different frames and reloading them as was done for the Vintage Year navigation, I updated the navigation to make it similar to the Vintage Year approach.
The main reason for this is to show a document preview below the view.

I deleted the continent field, because both continent and country fields did not fit well on my screen.
Instead I used a Map Icon to go back to the World Map to select another continent.

Next steps

The tasting form works better now on an Android Phone. However, opening a Tasting document does not open in the same frame. It opens a new tab for each document.
Another issue with the Tasting document is that you have to scroll back to the top when you switch sections (Info, Notes, Label, …). I haven’t found a way to provide a ‘go back to top button’ yet.

Final notes

An important thing to keep in mind when designing an application for Nomad mobile devices is that there is a wide variation in screen resolutions and sizes.
Test your UI on many devices (Android, iPhone and other) to make sure it looks good on all of them.
Screenshots in this article are made with a Nokia 8.1.

Note that every design change does not show immediately on your Android device. To see the result of a design change, you’ll have to close all tabs of the application in Nomad and reopen the app by clicking on the workspace icon.

Some changes I made to Theo’s original application are personal taste and this does not detract from the work already put in the application. I always tried to explain my choices, but feel free to discuss when you think otherwise.

Congratulations, you reached the final paragraph of the article. Thanks for staying with me until the end. As you can see, you can create beautiful, modern looking apps from a Notes database, working on Windows (desktop), iOS and Android. Happy coding…

Download Wine App Android

Rest calls and JSON Parsing in LotusScript

This is my first technical article on this blog. To reach a broader audience in the Notes/Domino community (and to avoid weird translations), I decided to write technical articles in English. Other experiences and opinions on the Notes/Domino world will stay in Dutch.

That being said, I wanted to discuss the new functions in LotusScript to do HTTP Requests and parsing the json response to use it in a Notes application.

You can use the new NotesHTTPRequest class to do the api request. This class allows you to do get, put and post requests. You can set headers using the setHeaderField method.
By default, the returned response can be either a byte array or a string (depending on the service called). To avoid this, set the (undocumented) preferString property to true. This will enforce the response to be a string.

Dim webRequest As NotesHTTPRequest
Set webRequest = session.createhttprequest()

'Get the response Dim response As String
webrequest.preferstrings = True response = webrequest.Get(<URL>)
'Throw error if response status is not OK If InStr(webRequest.Responsecode, "200 OK") = 0 Then 'Return Status is not OK Error 1000, "Request returned response code " + webRequest.responseCode End If

Note that the URL that you pass to the request has to be encoded. Most browsers encode the url after you typed it in the addressbar. But the NotesHTTPRequest does not. Therefor, make sure you encode the url before using it for the request. Here’s a small example:

Dim url as string
url = "https://mysite.com/get?name=John Doe"

'Using this url in the get request won't work
response = webrequest.get(baseurl + parameters)

'By encoding the url, it will
response = webrequest.get(urlEncode(url))

%REM
I use this function to encode url parts
It will encode special characters, but won't touch & or =, 
so you can pass the full url in one go
Kuddos to 
http://dominonotes.blogspot.com/2008/11/lotusscript-equivalents-for-urlencode.html
%END REM
Function urlEncode(s As String) As String
    If Len(s) = 0 Then Exit Function

    Dim tmp As String
    Dim c As String
    Dim i As Integer

    For i = 1 To Len(s)
        c = Mid(s, i, 1)
        If (Asc(c) >= 65 And Asc(c) <= 90) _
            Or (Asc(c) >= 97 And Asc(c) <= 122) _
            Or (Asc(c) >= 48 And Asc(c) <= 58) _
            Or Asc(c) = 38 _
            Or (Asc(c) >= 45 And Asc(c) <= 47) _
            Or Asc(c) = 58 Or Asc(c) = 61 _
            Or Asc(c) = 63 Or Asc(c) = 126 Then
            tmp = tmp + c
        Else
            tmp = tmp + "%" + Hex(Asc(c))
        End If
    Next i
    urlEncode = tmp
End Function

You can now parse the response using the NotesJSONNavigator class. However, this still has some caveats. The new class doesn’t behave well on some special characters in the json you pass (like CR, LF, °, ü, …) and you’ll get an error trying to parse this. According to HCL, there might still be an issue with the character encoding while processing the json data.

However, there is a workaround to avoid this by forcing the data to UTF-8. When you save the response to a file and then read it using a notesstream with UTF-8 encoding, you will be able to parse the data (Thanks to Dave Cohen for the tip)

'write the requests response to a file
Dim fileNum As Integer
fileNum% = FreeFile()
Dim filepath As String
filepath = "c:\temp\" + Format(Now, "yyyy-mm-dd-hhnnss") + ".json"
Open filepath For Output As fileNum%
Print #filenum, response
Close filenum

'Read the file using a NotesStream
Dim inbuf As NotesStream
Dim json As variant
Set inbuf = session.Createstream()
If Not(inbuf.Open(filepath, "UTF-8")) Then
	Error 1001, "Unable to open JSON file (" + filepath + ")"
End If
json = inbuf.Read()
Call inbuf.Close()

'remove temporary file
Kill filepath

'Create the Json Navigator to parse the data
Dim jsnav As NotesJSONNavigator
If IsArray(json) Then
	Set jsnav = session.CreateJSONNavigator(json)
Else
	Error 1002, "JSON is nothing"
End If

Once you have the NotesJSONNavigator object, you can loop through the different elements. Here’s some sample code to do so.

Dim parsed As string
Dim el As notesjsonelement
Set el = jsnav.Getfirstelement()

While Not el Is Nothing
	parsed = parsed + parseelement(el, "")
	Set el = jsnav.Getnextelement()		
Wend

MsgBox parsed


Function parseElement(el As notesjsonelement, parent As string)
	Dim returnValue As String
	Dim sName As String
	sName = parent + el.name
	Select Case el.type
		Case 1: 'Object
			Dim ob As notesjsonobject
			Set ob = el.value
			returnValue = returnValue + parseObject(ob, parent + el.name)
		Case 2: 'Array
			Dim ar As NOTESJSONARRAY
			Set ar = el.value
			returnValue = returnValue + parseArray(ar, parent + el.name)
		Case 3: 'String
			returnValue = returnValue +  sName + " - '" & el.Value & "'" + Chr$(10)
		Case 4: 'Number
			returnValue = returnValue +  sName + " - " & el.value + Chr$(10)
		Case 5: 'Boolean
			Dim b As String
			If el.value Then 
				returnValue = returnValue +  sName + " - true" + Chr$(10)
			Else
				returnValue = returnValue +  sName + " - false" + Chr$(10)
			End If
		Case 64: 'empty
			returnValue = returnValue +  sName + " - <EMPTY>" + Chr$(10)
		Case Else: 'Unknown
			returnValue = returnValue +  sName + " Unknown type " & el.type + Chr$(10)
	End Select
	parseElement = returnValue
End Function

Function parseObject(ob As notesjsonobject, parent As String) As string
	Dim el As notesjsonelement
	Dim sName As String
	Dim returnValue As string
	Set el = ob.Getfirstelement()
	While Not el Is Nothing
		returnValue = returnValue + parseElement(el, parent + ".")
		Set el = ob.Getnextelement()
	Wend
	parseObject = returnValue
End Function

Function parseArray(ar As NOTESJSONARRAY, parent As String)
	Dim el As notesjsonelement
	Dim sName As String
	Dim returnValue As String
	
	
	Set el = ar.Getfirstelement()
	Dim counter As Integer
	While Not el Is Nothing
		returnValue = returnValue + parseElement(el, parent + "." & counter)
		Set el = ar.Getnextelement()
		counter = counter + 1
	Wend
	parseArray = returnValue
End Function

Another way to get a json element is to get it by name or by pointer. A
JSON Pointer defines a string syntax for identifying a specific value within a JavaScript Object Notation (JSON) document.

'By name
Set el = jsnav.Getelementbyname("latitude")

'By pointer
Set el = jsnav.getElementbypointer("/daily/data/0")

I created a demo application that allows youto retrieve a weather forecast for any place on earth. I used the Google Maps API to retrieve address information and the Dark Sky API to get a forecast. You’ll need a key for both api to be able to use the app. You can download the demo here.