Developers don't always pay enough attention to structuring the logic of data processing in a mobile application. As a result, developers often face challenges when introducing changes and releasing application updates. In this article I'll talk about my own approach to structuring data processing, including data retrieval, storing and caching. This topic is relevant to the development of all mobile applications, particularly those whose primary purpose is to receive and present data, as well as sending data to the server. Essentially, this applies to any application that provides users with any type of frequently updated information, whether its news, weather, travel info, online magazines, etc.
When it comes to app architecture, the process of data retrieval from the server shouldn't be associated with data presentation or with the logic of data storing. Instead, each component should be responsible only for its own process and provide other components with a communication method. To ensure high quality of the app and to speed up the development process, it is best to follow the classic three-tier architecture.
Tier 1. Data
This layer includes work with the database. Also, it may take care of communication with the server, which in most cases acts as data storage, accessed via an Internet connection.
Tier 2. Business logic
In this layer, data is processed for display. Here, necessary requests are sent to the server or a local storage, data is classified into groups, etc.
Tier 3. Data presentation
This layer is in charge of data output to the user, as well as receiving data from the user. In this level, any work with such components as Activities, Fragments, Adapters, Views, etc. is carried out.
Now, let's examine several components of such application.
In order to work with data we will need to use special classes — JavaBeans. Using JavaBeans we can transmit information between different components of the application. These classes are intended only for information storage, they should not contain any logic for working with the data.
Communication with the server
All communication with the server should occur within one component. The component responsible for server communication should contain methods for receiving data from server (or sending data to the server). The output will consist of only domain knowledge objects or primitive types such as String, int, boolean and others.
For better component encapsulation we shouldn't directly associate domain knowledge with server's API. For example, when using popular GSON library to parse JSON objects, it's best to create separate set of classes to parse responses from server. In addition, it's a good idea to organize conversion of received objects into domain objects.
Even if parsing is implemented by hand, these intermediate classes can really make this task easier. Especially if structure of server responses doesn't comply with internal structure of domain knowledge objects. This approach makes it easier for us to handle any changes in server API, and to organize a more useful structure of domain knowledge. Among disadvantages of this approach is growth of load on garbage collector process that will have to delete all unused objects.
Also we shouldn't forget about processing server errors. This can be special error messages with codes in server's API, and typical HTTP error codes like HTTP 500 – internal server error. To process these errors it's a good idea to use standard mechanisms of Java language called Exceptions.
Local data storage
There are several ways to store data in Android application:
- by using SharedPreferences class
- in SQLite database
- in internal memory of device, i.e. on hard disk.
SharedPreferences class is best for storing individual data, such as user account. Also SharedPreferences can be used to store lists of primitives, for example a list of ID objects, separated by commas. Despite the fact that requests to SharedPrefereneces are input-output (I/O) operations that could potentially be performed for a long time, in practice they can still be implemented in UI flow.
Internal SQLite database is good for storing lists of objects. But the methods of working with it on Android are rather inconvenient. That's why it's better to turn to additional library ORMlite for storing complex objects.
Similar to the case of server communication, logic of working with locally stored data should be hidden within corresponding component. The only thing other parts of application should 'know' is what method to use for obtaining necessary data (domain knowledge objects) or for carrying out a certain operation with them.
Mobile devices don't always have a stable Internet connection, so data caching is often necessary. No user likes to be faced with the inconvenience of not being able to obtain necessary information in the absence of an internet connection. Besides, caching is used to increase the speed at which the application is working. For example, it makes sense to turn to caching if the same data is displayed on multiple pages or frequently used when displaying content. A typical example is image caching, retrieved from the server.
Working with images is often a challenge for mobile application developers. Well thought-out organization of image uploading and caching allows to speed up application work. But this issue is quite complicated. To overcome this challenge, we recommend using third-party libraries or implementing one of our own solutions, developed here at Azoft. We have already used this solution when developing our recent product, HopHop mobile app.
For caching you may choose any local storing option, including data storing in operative memory of application. In this case, data storing is implemented by using static fields in special language class or singleton-class. Another useful way of caching is saving server responses on disk as separate files.
Working with data
In most cases, receiving data from a source (such as a server, database or device's internal memory) takes a lot of time. To avoid blocking the user interface, this operation should be carried out in the separate flow. Besides, there can be different errors occurring when data is received. For example, disk read errors, Internet connection errors, or errors in server response. In order to process these errors and output them to the user there should be a centralized location.
To solve these issues, I recommend using an abstract class inherited from AsyncTask. Error processing can take place within this class.
Furthermore, for each operation with data we create its own class, inherited from ErrorHandlingTask. Here it will be enough to determine method doBackgroundTask, where errors are processed by parent class.
AsyncTask is most commonly used when data needs to be presented in a particular screen. If an operation with data isn't associated with any particular screen, it is better to take its implementation to separate service. For example, embedded class IntentService manages such problems well since it helps carry out different operations of service in a separate flow.
In case the result of an operation processed in such service must be presented in user interface (for example, progress of uploading a file to the server) it is better to use such useful tool as broadcast. This mechanism allows to subscribe to certain events that occur in other parts of application, and respond to them adequately.
Broadcast mechanism is also suitable for processing certain server errors. For example, during a request to the server we receive an error “User is not logged in”, in which case it is necessary to closed all opened displays and ask the user to enter account credentials. In this situation we can send a specialized broadcast and all active displays of the application will be automatically closed.
In this article I wanted to outline following tips for Android application development:
- Pay particular attention to the overall application architecture
- Try to separate the application into different components
- The logic of application's functionality should be hidden within corresponding components
- Only the methods necessary for communication between components should be available outside these components.
Based on our experience, this approach helps simplify the process of application development, especially in the later stages. In addition, this approach simplifies the development on any subsequent updates and further application support.