Thursday, February 28, 2013

ADF Mobile : Implementing the "Pull to Refresh" Pattern

One very common pattern in Mobile Apps is the use of "pull" to refresh a list of data in an App.
For one of my projects I suggested to use this pattern and I had to figure out a way to implement this in ADF Mobile. In this post I show you a way to implement this. If you are only interested in the refresh part, you might want to skip the first 5 steps. These explain in short the webservice and the basic ADF Mobile app. From step 6 on I explain how to implement the pattern.

1. The webservice
For this example I use a simple webservice that returns a List of Locations. The webservice is a service enabled Application Module. I simply created Business Component (EO and VO) on the Locations table from the HR schema. I exposed the Locations view object as AllLocations, and added a method called getCurrentCount() on the Application Module Impl class.  This method simply invokes the getEstimatedRowCount() to find out the number of rows. Next I created a new Service Interface on the Application Module. And that is about it for the service.
This all resides in a separate application and is deployed on a remote weblogic server. The mobile app will use this webservice.

2. The Mobile App
Create a simple ADF Mobile Application. In this application I need to call out to the webservice. For that I use a webservice datacontrol. Simply invoke the "create webservice" dialog, give the webservice a name and enter the URL of the WSDL.

In the second part of the wizard, I simply select the two operations that I need.

The webservice datacontrol is now finished. Both operations are now available from the datacontrol. The datacontrol looks like this:

3. The custom model
For the application I want to use a custom object model in order to be independent of the webservice data structure. This is common practice and recommended approach. The class that I use for this is called MyLocation.


I use the following properties:

 package nl.amis.technology.mob.objects;  
 public class MyLocation {  
 private int LocationId;  
 private String StreetAddress;  
 private String PostalCode;  
 private String City;  
 private String StateProvince;  
 private String CountryId;  
   public MyLocation() {  
     super();  
   }  
 }  

Simply generate accessors for these and the class is finished.

4. Invoking the webservice
Now I create a new class that I use in my mobile application to provide the app with data. This class is called LocationInfoDC. In this class I create a list of locations that I can display in the mobile app.

 public class LocationInfoDC {  
   private static List s_locationsList;   
   private transient ProviderChangeSupport providerChangeSupport =   
               new ProviderChangeSupport(this);  
   public LocationInfoDC() {  
     super();  
     if (s_locationsList == null) {  
       s_locationsList = new ArrayList();  
       retreiveLocationInformation();  
     }  
   }  
   public void AddLocation(MyLocation l) {  
     s_locationsList.add(l);  
   }  
   public MyLocation[] getAllLocations() {  
     MyLocation locs[] = null;  
     locs = (MyLocation[])s_locationsList.toArray(new MyLocation[s_locationsList.size()]);  
     return locs;  
   }  
  ......  

For webservice invocation I use AdfmfJavaUtilities.invokeDataControlMethod(). This enables me to invoke a Datacontrol Method whithout having a page Definition available. It is a powerful mechanism.

 // This calls the DC method and gives us the Return  
    GenericType result =  (GenericType)  
       AdfmfJavaUtilities.invokeDataControlMethod("HrWsDc"  
                                          , null  
                                          ,"findAllLocations"  
                                          ,pnames, params, ptypes);  

The AdfmfJavaUtilities.invokeDataControlMethod() returns a GenericType. This response needs to be "converted" to the custom object model described in step 3.

  // The Return wraps the findAllLocations Result, so get that out of the Result  
   s_locationsList.clear();          
    for (int i = 0; i < result.getAttributeCount(); i++)   
    {  
      GenericType row = (GenericType)result.getAttribute(i);  
      MyLocation locationInformation = (MyLocation)  
             GenericTypeBeanSerializationHelper.fromGenericType(MyLocation.class, row);  
      AddLocation(locationInformation);  
    }  

Another important part is the invocation of the client page refresh. The ProviderChangeSupport class is used for sending notifications relating to collection elements, so that components update properly when a change occurs in a Java bean data control. I refresh the collection delta, using the ProviderChangeSupport class. Since the provider change is required only when you have a dynamic collection exposed by a data control wrapped bean, there are only a few types of provider change events to fire:
  • fireProviderCreate—when a new element is added to the collection
  • fireProviderDelete—when an element is removed from the collection
  • fireProviderRefresh—when multiple changes are done to the collection at one time and you decide it is better to simply ask for the client to refresh the entire collection (this should only be used in bulk operations)
I use the fireProviderRefresh, asking for the client to refresh the entire collection.

The trick that does the magic:

 providerChangeSupport.fireProviderRefresh("allLocations");  

For this class I also generate a datacontrol so I can use it on the mobile page.

5. The Mobile Page
The view for the list is a simple amx page.
Create the page and drag the AllLocations collection from the datacontrol onto the page. In the popup menu pick "ADF Mobile List View".


Actually that is all that needs to be done. Now after deploying, the app looks like this.


Now it is time to implement the "pull to refresh".

6. Implementing the pull to refresh: Calling the service
In order to refresh the list, I need to know if there are actually new records available in the database.
This information is provided by the getCurrentCount webservice operation. First I create a new pageflowscoped backing bean to hold all logic involved in this functionality. In this bean I have one property. This is used to store the latest record count. This count is used to make sure that the findAllLocations webservice is only invoked if necessary.

 public class LocationsBackingBean {  
   String count="0";  
   private transient PropertyChangeSupport propertyChangeSupport =   
           new PropertyChangeSupport(this);  
   public LocationsBackingBean() {  
   }  
   public void setCount(String count) {  
     String oldCount = this.count;  
     this.count = count;  
     propertyChangeSupport.firePropertyChange("count", oldCount, count);  
   }  
   public String getCount() {  
     return count;  
   }  


To call the webservice that returns the current record count (getCurrentCount) I also use AdfmfJavaUtilities.invokeDataControlMethod().
The returned value (= the current number of rows in the database table) is compared to the previous amount returned by this service call. If there are more rows in the database table, I want to refresh the List. To do that, I simply call out to the  "findAllLocations" service. After successful invocation, the count is updated to the new value.

   public void checkForUpdates(ActionEvent ea) {  
    ......  
   String result;  
     try {  
       // This calls the DC method and gives us the Return  
       result = (String)   
             AdfmfJavaUtilities.invokeDataControlMethod("HrWsDc"  
                                                , null  
                                                , "getCurrentCount"  
                                                , pnames, params, ptypes);  
       // After service call, compare result to current count  
       // If new records are available, refresh the list by calling the webservice via invoking     
       // the corresponding method binding   
       if (Integer.valueOf(this.count).compareTo(Integer.valueOf(result))<0){  
         AdfELContext adfELContext = AdfmfJavaUtilities.getAdfELContext();  
         MethodExpression me =   
                AdfmfJavaUtilities.getMethodExpression(  
                     "#{bindings.retreiveLocationInformation.execute}"  
                    , Object.class, new Class[]{});  
         me.invoke(adfELContext, new Object[]{});  
         // after succesfully refreshing the list, update the current number of rows  
         setCount(result);  
        }  
     } catch (AdfInvocationException e) {  
        ............    }   
       catch (Exception e2) {  
       ...............    }  
   }  

7. Implementing the pull to refresh: Configure the Listview
I want to refresh the list with Locations whenever the list is pulled down.
For this I use the amx:actionlistener component. This component allows you to declaratively invoke an action. This component is meant to be a child component of any tag that has some kind of listener attribute like actionListener, or valueChangeListener, etc. The type attribute defines which listener of the parent this actionListener should be fired for. These methods fire before the listener on the parent component fires.
In the actionListener I invoke the checkForUpdates method that was described earlier.

The component has two attributes: type and binding. Type is set to swipeDown and in the binding attribute I use the checkForUpdates method that is implemented in the locationsbean.

  <amx:actionListener type="swipeDown"   
                  binding="#{pageFlowScope.locationsBackingBean.checkForUpdates}" />  

The complete code for this simple Listview now looks like this:
   <amx:listView var="row" value="#{bindings.allLocations.collectionModel}"  
          fetchSize="#{bindings.allLocations.rangeSize}"   
          id="lv1">  
    <amx:listItem id="li1">  
    <amx:actionListener type="swipeDown"   
                     binding="#{pageFlowScope.locationsBackingBean.checkForUpdates}" />  
     <amx:tableLayout width="100%" id="tl1">  
      <amx:rowLayout id="rl1">  
       <amx:cellFormat width="10px" id="cf3"/>  
       <amx:cellFormat width="60%" height="43px" id="cf1">  
        <amx:outputText value="#{row.streetAddress}" id="ot2" inlineStyle="font-size:x-small;"/>  
       </amx:cellFormat>  
       <amx:cellFormat width="10px" id="cf2"/>  
       <amx:cellFormat width="40%" halign="end" id="cf4">  
        <amx:outputText value="#{row.city}" styleClass="adfmf-listItem-highlightText" id="ot3"  
                inlineStyle="font-size:small;"/>  
       </amx:cellFormat>  
      </amx:rowLayout>  
     </amx:tableLayout>  
    </amx:listItem>  

8. The result
After deploying the app it is time to test the behavior. After starting the app and the list shows some Locations.

Now add 1 or 2 Locations to the database and commit these changes.



Swipe the list down, release  and see what happens.......

The new Locations are displayed.

9. Some Final Notes
This example (and actually the pattern) works for lists that show rather static data (no update or deletes). Every "pull down" gesture checks the server for new rows. That is, it checks if there are more rows then during the last call, and if so, the new rows are returned and added to the list. So whenever there are deleted or changed rows, this is not reflected in this sample. I could have implemented a service only returning the new Locations and add those to the list, but for this example I decided to simply fetch all Locations and rebuild the list. One other improvement is to only invoke the webservices if the "pull down" is invoked on the first row in the list.

4 comments:

Anonymous said...

Can we have application for download

Narendra Kumar said...

Wow. Thanks for the post.
The growth in use of mobile devices has led to an explosion in the development of mobile apps.
iPhone & Android Application Development

RAM PRADEEP KILLADI said...

Thanks a ton for the post.I am looking for a use case where the "invokeDataControlMethod" takes in complex type paramaters like 'An array of objects as input".It would be great if you can throw some light on it.

RAM PRADEEP KILLADI said...

Thanks a ton for the post.I am looking for a use case where invokeDataControlMethod can take complex type parameters i.e., an array of objects ,a list etc.
Please throw some light on such cases.