Introduction
With over 90% of internet traffic now coming from mobile devices, there is a huge pressure on companies to make their customer-facing applications suitable for rendering on smart phones and tablets. A-team is involved with a number of customers who are looking for ways to adapt their existing Oracle ADF applications and make them "mobile-enabled" . This article is the second in a series of articles that describe what we learned from the experiences with these customers both from a technical and a non-technical perspective. The first article "Understanding the Options" provides insight in the technology choices you need to make and the implications of those choices. It also contains a lot of links with additional information. This second article provides tips and techniques to optimize rendering of ADF Faces applications on mobile devices.Main Article
We will take an existing ADF application (version 12.1.2 or 11.1.1.7) that runs fine on desktop browsers. We then run the same application both on an iPad and an Android smart phone. We will discuss the changes we need to make to run the same application with the same web pages successfully on a desktop browser, tablet and smart phone. Please refer to the first article in this series for a discussion on the desirability of this "adaptive design" approach. In this article, we assume you have valid reasons to choose this approach, and are looking for technical guidance in getting the job done. Testing the Sample Application on Mobile Devices The application is built using the UIShell approach: the application consists of a single .jsf page (or .jspx page when using JDeveloper 11.1.1.x) and uses a dynamic ADF region to switch the page content based on the menu tab that is selected. For details on this best-practice implementation see the article Core ADF11: UIShell with Menu Driving a Dynamic Region. We use the Skyros skin introduced in JDeveloper 11.1.1.7 as this skin minimizes the usage of images in favor of CSS3 shadows and gradients, making it perform better than older skins, in particular on mobile devices using a small bandwidth network. If you want to use another skin in your desktop browser, you can make the skin dynamic as well, by using an EL expression in trinidad-config.xml. Here are two screen shots to show you how the sample application looks when run within a desktop browser.

Testing on iPad
If we run the same application on an iPad, the employees table page looks like this

Testing on Smart Phone
When we run the same application on an Android phone (HTC Desire X), this is what we get

Setting the Initial Scaling
By default, mobile web browsers will try to fit the whole page within the available width, which means the browser will zoom out until the page fits. To fix this we need to add the viewport meta tag. See articles Don't Forget the Viewport Meta Tag and Using the viewport meta tag to control layout on mobile browsers for more information. To add this meta tag in our sample application, we add the following code snippet to our UIShell page:<f:view > <af:document title="ADF Faces Mobile Demo" id="d1"> <f:facet name="metaContainer"> <af:group id="g1"> <meta name="viewport" content="width=device-width, initial-scale=1"/> </af:group> </f:facet>If your application consists of multiple .jspx or .jsf pages, you obviously need to add the meta tag to every page. Now, let's refresh the application in our smart phone browser. Below you can see the result for both portrait and landscape orientation.




Obtaining Browser Agent Info
The ADF Faces API provides various methods to get information about the browser agent. Two agent properties are useful in the context of this article, all other documented properties either return the same value for all devices, or return the string "unknown". The table below shows the two properties and their values for the three test devices used for this article.Property Name | Expression | Desktop Value | iPad Value | HTC Value |
---|---|---|---|---|
Platform Name | #{requestContext.agent.platformName} | windows | iPhone | android |
Touch Screen | #{requestContext.agent.capabilities['touchScreen']} | none | multiple | single |
RequestContext context = RequestContext.getCurrentInstance(); Agent agent = context.getAgent(); String platformName = agent.getPlatformName(); String touchScreen = (String) agent.getCapabilities().get("touchScreen");While this information helps, it does not tell us which kind of device is used. Both an iPad and an iPhone will return "iphone" as platform name, and both will return "multiple" as value for the touchScreen property. Since there are so may different sizes for mobile phones and tablets, we really need the actual browser window width and height to be able to adapt the layout properly. This can be done using the combination of JavaScript, and an ADF Faces clientListener and serverListener, that together ensure the browser dimensions are stored in a session-scoped "AgentInfo" bean. This is the code that needs to be added to the UIShell page:
<af:resource type="javascript"> function setWindowSize(){ var h = document.documentElement.clientHeight, w = document.documentElement.clientWidth; comp = AdfPage.PAGE.findComponentByAbsoluteId('d1'); AdfCustomEvent.queue(comp, "setScreenSize",{'screenWidth':w,'screenHeight':h}, true);} </af:resource> <af:clientListener method="setWindowSize" type="load"/> <af:serverListener type="setScreenSize" method="#{agentInfo.setWindowSize}"/>Note that in a real application, you would store this JavaScript method in a separate library. The serverListener ensures that the setWindowSize method in the AgentInfoBean is called which has has the following signature:
public void setWindowSize(ClientEvent clientEvent) { if (getScreenWidth()==0) { Map<String, Object> map = clientEvent.getParameters(); Double width = (Double) map.get("screenWidth"); Double height = (Double) map.get("screenHeight"); setScreenHeight(height); setScreenWidth(width); JsfUtils.redirectToSelf(); } }Note the call to the redirect-to-self utility method at the end, this is needed because the above code is executed AFTER the page is rendered, which means the screen size info cannot be used on initial page rendering. Now, to prevent this costly redirect-to-self you can have the end user start the application with just specifying the web context root. In the index.html that will be automatically launched, you can forward to the UIShell page, passing in the screen width and height as request parameters. Here is an example index.html page using this technique:
<!DOCTYPE HTML> <html> <head> <script> var height = document.documentElement.clientHeight; // window.innerHeight does not work on IE8 and FF ; var width = document.documentElement.clientWidth; //window.innerWidth does not work on IE8 and FF ; window.location.href = "faces/UIShell?screenHeight=" + height + "&screenWidth=" + width; </script> </head> <body></body> </html>In the AgentInfoBean a method annotated with the PostConstruct will then read the request parameters and store them in member variables inside the bean:
@PostConstruct /** * Check if request contains screenWidth, screenHeight params. If so, store * the values in corresponding properties */ public void init() { String width = (String) JsfUtils.getRequestParam(SCREEN_WIDTH_KEY); String height = (String) JsfUtils.getRequestParam(SCREEN_HEIGHT_KEY); if (width!=null) { screenWidth = new Double(width); } if (height!=null) { screenHeight = new Double(height); } }With this code in place, the user gets "rewarded" with faster initial loading when he accesses the application through index.html. The setWindowSize method should stay as a fallback scenario when the end user launched the application using a URL that included the UIShell target. With the two agent properties and screen width and height available, we can add a number of convenience methods to our AgentInfoBean that we can use in EL expressions to dynamically adapt the layout based on the mobile device:
public boolean isIOS() { return "iphone".equals(getAgent().getPlatformName()); } public boolean isAndroid() { return "android".equals(getAgent().getPlatformName()); } public boolean isLandscape() { return getScreenWidth() > getScreenHeight(); } public boolean isTouchScreen() { return !"none".equals(getAgent().getCapabilities().get("touchScreen")); } /** * Return true when device has touch screen and either screen width or height is < 400px. * We check either screen width or height depending on the device orientation. * 400px is pretty arbitrary number, change it as desired */ public boolean isSmartPhone() { double size = isLandscape() ? getScreenHeight() : getScreenWidth(); return isTouchScreen() && getScreenWidth()!=0 && (size <=400); } public boolean isTablet() { return isTouchScreen() && !isSmartPhone(); }Note that when the user changes the device orientation while using the ADF Faces application, this does not affect the values returned by the above methods. If you want to support a change in device orientation while running the application, additional JavaScript is needed to detect such an orientation change and call some resize method on the AgentInfoBean with the new width and height values. If you want to detect whether the iOS device is having a Retina screen you can extend the above code and also pass in the value of window.devicePixelRatio. When this value is 2, it is a Retina device. See this article on devicePixelratio for more information.
Adapting the Layout to Enhance Mobile Rendering
With the information about device orientation and screen width and height available, you can now start to determine how you want to adapt the layout based on the viewport size of the mobile device. As you probably want to support a wider range of mobile phones and tablets, than you have at your disposal for actual testing, this overview of viewport sizes for mobiles and tablets might come in handy. Now, let's see how we can adapt the layout of our application to make it more usable for smart phones and small and (small) tablets.Using Dynamic Page Templates
The first thing we want to do is use a different page template when rendering on smaller devices. We will use the isSmartPhone convenience method from the AgentInfoBean to dynamically set the page template:<af:pageTemplate viewId="#{agentInfo.smartPhone ? '/common/pageTemplates/PhonePageTemplate.jspx' : '/common/pageTemplates/TabsMenuPageTemplate.jspx'}" id="pt"> <f:facet name="pageContent"> <af:region value="#{bindings.mainRegion.regionModel}" id="mr"/> </f:facet> <f:attribute name="menuModel" value="#{menuModel}"/> </af:pageTemplate>The PhonePageTemplate.jspx is a basic page template with a simple page header that holds the application title, and display the menu navigation pane as a drop down list instead of tabs:
<af:panelStretchLayout id="pt_pgl1" topHeight="32px"> <f:facet name="top"> <af:panelGridLayout id="pt_gPbl" styleClass="AFBrandingBar"> <af:gridRow id="pt_rh1" height="auto" marginTop="4px" marginBottom="4px"> <af:gridCell id="pt_bt" width="auto" valign="middle" marginStart="4px"> <af:outputText value="#{attrs.brandingTitle}" styleClass="AFBrandingBarTitle" id="pt_ot1"/> </af:gridCell> <af:gridCell id="pt_flexSpaceHead" width="100%"/> <af:gridCell id="mgie" width="auto" valign="middle" marginStart="4px"> <af:navigationPane id="Menu1" var="menuItem" partialTriggers="Item1" value="#{attrs.menuModel}" hint="choice"> <f:facet name="nodeStamp"> <af:commandNavigationItem id="Item1" textAndAccessKey="#{menuItem.label}" actionListener="#{pageFlowScope.pendingChangesBean.handle}" action="#{menuItem.doAction}" rendered="#{menuItem.rendered}"/> </f:facet> </af:navigationPane> </af:gridCell> </af:gridRow> </af:panelGridLayout> </f:facet> <f:facet name="center"> <af:facetRef facetName="pageContent"/> </f:facet> </af:panelStretchLayout>
You might wonder why we still use a panelStretchLayout in this template as stretch layouts are automatically converted to flowing layouts on touch devices. Well, this automatic conversion is exactly the reason we need to preserve the stretching in the page template, so ADF Faces will correctly compute the fixed table height based on the available vertical space. If we would enclose the pageContent facetRef in a panelGroupLayout, and then navigate to the employees table page, about 8 rows would be visible, while there is room for about 15 rows.
The application now looks like this on a HTC phone:

Traversing and Changing the UIComponent Tree at Runtime
To traverse and change the UI Component tree at runtime, we will use a JSF phase listener in combination with the VisitTree Callback API. In JSF 1.2 (JDeveloper 11.1.1.x) the VisitTree API was included in the Trinidad library, which is used as foundation for ADF Faces RC. In JSF 2.0 and beyond (JDeveloper 11.1.2.x and 12c) the VisitTree implementation in trinidad has become part of the JSF standard. So, depending on the JDeveloper version you are using, your imports will change, but the code can largely stay the same, as you can see in the two samples applications you can download at the bottom of this post. We start with a Java class that implements the VisitCallback interface:public class MobileRenderingVisitCallback implements VisitCallback { public VisitContext createVisitContext() { return VisitContext.createVisitContext(FacesContext.getCurrentInstance()); } public VisitResult visit(VisitContext context, UIComponent target) { AgentInfoBean agentInfo = AgentInfoBean.getInstance(); if (!agentInfo.isTouchScreen()) { // No touch device, do nothing and stop tree traversal return VisitResult.COMPLETE; } // TODO: some mobile rendering enhancements return VisitResult.ACCEPT; } }For more information on using the VisitTree API see the article Efficient component tree traversal in JSF. Next, we will create a JSF phase listener that uses this class just before render response phase:
public class MobilePhaseListener implements PhaseListener { public MobilePhaseListener() { super(); } public void afterPhase(PhaseEvent phaseEvent) { } public void beforePhase(PhaseEvent phaseEvent) { if (phaseEvent.getPhaseId() == PhaseId.RENDER_RESPONSE) { MobileRenderingVisitCallback cb = new MobileRenderingVisitCallback(); UIXComponent.visitTree(cb.createVisitContext(), JsfUtils.getViewRoot(), cb); } } public PhaseId getPhaseId() { return PhaseId.ANY_PHASE; } }And finally, we register this class as a managed bean in adfc-config.xml:
<managed-bean id="__18"> <managed-bean-name id="__19">MobilePhaseListener</managed-bean-name> <managed-bean-class id="__21">oracle.ateam.demo.ui.application.MobilePhaseListener</managed-bean-class> <managed-bean-scope id="__20">request</managed-bean-scope> </managed-bean>and reference the bean in the beforePhase property of the f:view element of the UIShell page:
<f:view beforePhase="#{MobilePhaseListener.beforePhase}"> <af:document title="ADF Faces Mobile Demo" id="d1">
You might be tempted to register the JSF phase listener in faces-config.xml as this is faster and does not require a managed bean definition. However that will not work in this case because you cannot access the UI component tree on initial page loading when the phase listener is defined in faces-config.xml!
With this generic code in place, we can now start adding code to our MobileRenderingVisitCallback class to fix various rendering issues.Changing the PanelFormLayout to One Column
To avoid horizontal swiping in multi-column form layouts, we need to set the maxColumns property of the panelFormLayout components to 1 in all our pages and page fragments. With the above generic UI component tree traversal in place, this is pretty straightforward. We add the following method to the MobileRenderingVisitCallback classprivate void processPanelFormLayout(UIComponent target) { if (target instanceof RichPanelFormLayout) { RichPanelFormLayout pfl = (RichPanelFormLayout)target; pfl.setMaxColumns(1); } }and call this method from the visit method
public VisitResult visit(VisitContext context, UIComponent target) { AgentInfoBean agentInfo = AgentInfoBean.getInstance(); if (!agentInfo.isTouchScreen()) { return VisitResult.COMPLETE; } if (agentInfo.isSmartPhone()) { processPanelFormLayout(target); } return VisitResult.ACCEPT; }With this code in place, the form layouts start to look much better on smart phones, as you can see below.

Fixing Table Pagination
In JDeveloper 11.1.1.7 the pagination size is controlled by the table fetch size property, in JDeveloper 12.1.2. it is controlled by the autoHeightRows property. If this property is not set you will not see pagination controls. You should make sure that the number of rows visible on the screen matches the value of the fetchSize and autoHeightRows properties. If the pagination size is larger than the number of rows visible ,it can cause a confusing user experience. For example, when fetchSize/autoHeightRows is set to 25, and only 18 rows are visible, then navigating to the next "table page" will show rows 26 to 44. Rows 19 to 25 remained invisible. The end user could have visited these rows by using a swipe-up action, but this is counter-intuitive as pagination controls are added to navigate through the rows. Similar to changing the number of columns in a panelFormLayout, we can change the table fetch size and autoHeightRows property in the MobileRenderingVisitCallback class:public VisitResult visit(VisitContext context, UIComponent target) { AgentInfoBean agentInfo = AgentInfoBean.getInstance(); if (!agentInfo.isTouchScreen()) { return VisitResult.COMPLETE; } if (agentInfo.isSmartPhone()) { processPanelFormLayout(target); } processTable(target, agentInfo); return VisitResult.ACCEPT; }The processTable method looks like this:
private void processTable(UIComponent target, AgentInfoBean agentInfo) { if (target instanceof RichTable) { RichTable table = (RichTable) target; // set fetch size and autoHeightRows so we get proper pagination controls int tabletRows = agentInfo.isLandscape() ? 21 : 35; int rows = agentInfo.isSmartPhone() ? 16 : tabletRows; table.setFetchSize(rows); table.setAutoHeightRows(rows); } }With this code in place, the pagination controls on the employees table show up nicely on the iPad:


Hiding the Surrounding PanelCollection
Many of the standard features in a panelCollection are less useful on a mobile phone. For example, the ability to show/hide columns, attach/detach the table, or freeze columns isn't something we expect a mobile phone user to do. So, we can save some real estate and display more rows in the table by hiding the panelCollection chrome. This is easily done by adding the following code inside the processTable method:if (agentInfo.isSmartPhone() && table.getParent() instanceof RichPanelCollection) { RichPanelCollection pc = (RichPanelCollection)table.getParent(); HashSet<String> featuresOff = new HashSet<String>(); featuresOff.add("viewMenu"); featuresOff.add("formatMenu"); featuresOff.add("wrap"); featuresOff.add("detach"); featuresOff.add("freeze"); featuresOff.add("statusBar"); pc.setFeaturesOff(featuresOff); }With this code in place, we can increase the fetch size to 19, resulting in a page like this:

Handling Wide Tables
Most tables initially designed for desktop browsers will be wider than the available width on mobile phones. In the table shown above, a user can swipe to the left with two fingers simultaneously to see the remaining columns. While this works it is not an intuitive touch gesture. There are a couple of ways to address this issue of "overflow" columns:- SImply hide the additional columns, and show that information in a detail screen after the user selected a row.
- Use the concept of "detail disclosure" by defining a detailStamp facetof the af:table component and move the remaining items inside a panelFormLayout in the detailStamp facet.
- Replace the table component with a listView component, which is more suited for mobile devices and tablets.
private void moveTableItemsToDetailStamp(RichTable table) { if (table.getFacets().get("detailStamp") != null) { return; } RichPanelGroupLayout form = new RichPanelGroupLayout(); RichSpacer spacer = new RichSpacer(); spacer.setWidth("5px"); spacer.setHeight("5px"); form.getFacets().put("separator", spacer); table.getFacets().put("detailStamp", form); List<UIComponent> children = table.getChildren(); int counter = 0; for (UIComponent kid : children) { if (kid instanceof RichColumn) { counter++; RichColumn column = (RichColumn)kid; if (counter > 3) { column.getChildren().get(0).setParent(form); form.getChildren().add(column.getChildren().get(0)); column.setRendered(false); } } } }With this code in place, the employees table has an additional detail disclosure icon at the beginning that can be used to expand the row:

Using HTML5 Input Types
A number of new input types have been added to HTML 5. Using these input types has the following advantages:- Device-native rendering of the input element. For example, a date input type will render with iOS or Android native date picker
- Context-sensitive virtual keyboard. For example, an email input type will show the @-character, a number input type will only show numeric values on the virtual keyboard
- Declarative validation. For example, a number input type will raise an error when entering a non-numeric value
<af:inputText label="#{bindings.HireDate.hints.label}" required="#{bindings.HireDate.hints.mandatory}" usage="#{'date'}" value="#{bindings.HireDate.inputValue}" id="it9"> <af:convertDateTime pattern="yyyy-MM-dd" id="cdt22" type="date" /> </af:inputText>Note the pattern used to convert the date, this format is the 'wire' format for the date input type as prescribed by the HTML5 standard. If you don't add an af:convertDateTime tag with this wire date format as value for the pattern attribute, then the field will not show any data. The way the date input field will be rendered is browser-specific. On the desktop, only Google Chrome currently displays a nice calendar widget to pick a date using the user's locale, Internet Explorer and Firefox display a simple input field, using the wire format as display date format. Chrome on android devices, and Safari on iOS nicely display a native date picker. Since most desktop browsers do not show a calendar widget, you probably want to stick with the standard ADF Faces calendar that you get for free when using an af:inputDate component on desktop browsers. So, to leverage the native date picker on iOS and android, you need to conditionally render either the af:inputDate component or the af:inputText component with usage property set to 'date'. Here is a code snippet that implements this approach:
<af:inputDate value="#{bindings.HireDate.inputValue}" label="#{bindings.HireDate.hints.label}" required="#{bindings.HireDate.hints.mandatory}" columns="#{bindings.HireDate.hints.displayWidth}" shortDesc="#{bindings.HireDate.hints.tooltip}" rendered="#{!agentInfo.touchScreen}" id="id1"> <f:validator binding="#{bindings.HireDate.validator}"/> <af:convertDateTime pattern="#{bindings.HireDate.format}"/> </af:inputDate> <af:inputText label="#{bindings.HireDate.hints.label}" required="#{bindings.HireDate.hints.mandatory}" value="#{bindings.HireDate.inputValue}" usage="#{'date'}" rendered="#{agentInfo.touchScreen}" id="it9"> <f:validator binding="#{bindings.HireDate.validator}"/> <af:convertDateTime pattern="yyyy-MM-dd" id="cdt22" type="date"/> </af:inputText>To avoid the tedious work of adding this af:inputText to each and every page with a date item, we can add the HTML5 date support in a generic fashion using the JSF phase listener and VisitTree API as explained in the section 'Traversing and Changing the UIComponent Tree at Runtime'. Here is a sample method that can be added to the MobileRenderingVisitCallback class to achieve this:
private void processDate(UIComponent target) { if (target instanceof RichInputDate) { RichInputDate date = (RichInputDate) target; int index = date.getParent().getChildren().indexOf(date); if (!date.isRendered()) { // html5 date already added or not needed return; } RichInputText html5Date = new RichInputText(); html5Date.setUsage("date"); html5Date.setValueExpression("rendered", date.getValueExpression("rendered")); html5Date.setValueExpression("required", date.getValueExpression("required")); html5Date.setValueExpression("disabled", date.getValueExpression("disabled")); html5Date.setValueExpression("readOnly", date.getValueExpression("readOnly")); html5Date.setValueExpression("label", date.getValueExpression("label")); html5Date.setValueExpression("value", date.getValueExpression("value")); // we need to use the converter from original date item, creating new DateTimeConverter instance // causes java.lang.ClassCastException for some unknown reason: // Value "2005-06-24" is not of type java.util.Date, it is class oracle.jbo.domain.Date DateTimeConverter conv = (DateTimeConverter) date.getConverter(); date.setConverter(null); conv.setPattern("yyyy-MM-dd"); html5Date.setConverter(conv); date.getParent().getChildren().add(index + 1, html5Date); date.setRendered(false); } }Using the HTML date input type on an iPad shows the iOS-native date picker (dutch language):


private void processNumberField(UIComponent target) { if (target instanceof RichInputText) { RichInputText field = (RichInputText) target; Converter converter = field.getConverter(); if (converter instanceof NumberConverter) { field.setUsage("number"); } } }This code will cause a numeric keyboard to show up on the iPad when tapping in the ManagerId field:

Using Client-Side Responsive Design Techniques
This articles described server-side adaptive design techniques. You can use these techniques in combination with client-side responsive design techniques using CSS media queries as desired. Here are some links to good articles on using CSS media queries in ADF Faces:- Responsive Design for your ADF Faces Web Applications by ADF product manager Shay Shmeltzer
- Adaptive ADF/WebCenter template for the iPad by Maiko Rocha from Oracle Product Development.
Conclusion
With some simple JavaScript we can gather additional screen sizing information from the browser agent. This information can then be used to implement server-side adaptive design techniques to enhance mobile rendering without modifying each and every page or page fragment. This is a fast and cost-effective way to go mobile with ADF. However, as explained in the first article of this series Understanding the Options, you might want to consider more significant redesign, as well as the use of ADF Mobile to truly optimize the mobile user experience. You can download the sample application for both JDeveloper 11.1.1.7 and JDeveloper 12.1.2.All content listed on this page is the property of Oracle Corp. Redistribution now allowed without written permission