Skip to main content

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.

Comments

Anonymous said…
Can we have application for download
Unknown 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
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.
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.
Unknown said…
Hi I am Trying to implement the pull to Refresh Pattern in ADF mobile ,
I created a Web service Datacontrol
and Displays the data in amx page but I am unable to find what is
{bindings.retreiveLocationInformation.execute} and Where I have to Write

providerChangeSupport.fireProviderRefresh("allLocations");

could you please attach a sample application for pull to refresh pattern

Regards,
UmaShankar.

Popular posts from this blog

ADF 12.1.3 : Implementing Default Table Filter Values

In one of my projects I ran into a requirement where the end user needs to be presented with default values in the table filters. This sounds like it is a common requirement, which is easy to implement. However it proved to be not so common, as it is not in the documentation nor are there any Blogpost to be found that talk about this feature. In this blogpost I describe how to implement this. The Use Case Explained Users of the application would typically enter today's date in a table filter in order to get all data that is valid for today. They do this each and every time. In order to facilitate them I want to have the table filter pre-filled with today's date (at the moment of writing July 31st 2015). So whenever the page is displayed, it should display 'today' in the table filter and execute the query accordingly. The problem is to get the value in the filter without the user typing it. Lets first take a look at how the ADF Search and Filters are implemented by...

ADF 11g Quicky 3 : Adding Error, Info and Warning messages

How can we add a message programatically ? Last week I got this question for the second time in a months time. I decided to write a short blogpost on how this works. Adding messages is very easy, you just need to know how it works. You can add a message to your faces context by creating a new FacesMessage. Set the severity (ERROR, WARNING, INFO or FATAL ), set the message text, and if nessecary a message detail. The fragment below shows the code for an ERROR message. 1: public void setMessagesErr(ActionEvent actionEvent) { 2: String msg = "This is a message"; 3: AdfFacesContext adfFacesContext = null; 4: adfFacesContext = AdfFacesContext.getCurrentInstance(); 5: FacesContext ctx = FacesContext.getCurrentInstance(); 6: FacesMessage fm = 7: new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, ""); 8: ctx.addMessage(null, fm); 9: } I created a simple page with a couple of buttons to show the result of setting the message. When the but...

ADF 12c : Using Jasper Reports en JasperSoft Studio 6.1; What Libraries do you need?

Over the last couple of years, or better in the last decade I have implemented several reporting solutions with Jasper Reports in ADF. I did that in ADF 10g, ADF 11.1.1.x, ADF 11.1.2.x and ADF 12.1.x I also used several version of Jasper Reports. There is a whole lot of documentation, blogposts and presentations available. So when today I got a request from one of my customers to make a setup for the implementation of Jasper Reports 6.1 in ADF 12.1.3 I did not expect any problems. Boy was I wrong. Here is the Story With all the knowledge from the past, I decided to follow the known steps. 1) Download iReport Designer, 2) Build a report in iReport 3) Create an ADF application 4) Add the necessary libraries to use the report 5) Call the report from a button via a Managed Bean Step 1 In the past I used iReport designer to build the reports. When you go to the download site of iReport designer you now see an interesting message. So I took this serious and decided not to u...