Mobile apps bring a completely new mindset for the development model. On the one hand, users have very high expectations regarding their experience with mobile apps. On the other hand, device limitations together with network restrictions can have a major impact on that experience. No matter the circumstances or limitations, users expect mobile apps to work like a charm!
If you want to develop a mobile app that offers your users the best mobile experience, there are some key rules that you should always keep in mind:
- Focus on mobile use cases to guide your development
- Choose the best use cases for a great mobile experience. Start small and iterate. Use less and optimized content. Show what's more important first. Use common mobile patterns such as the ones provided by the OutSystems UI framework.
- Keep heavy logic on the server side
- Keep client-side information as simple as possible. Execute complex data processing and calculations on the server side, using the minimum number of server requests as possible. Cache your results on the client side.
- Test your app in real world scenarios
- Understand how your mobile app will be used and thoroughly test it before deploying to production. Assure that, device limitations, network restrictions, or offline periods will not prevent users having a great experience.
Focusing on these ground rules, this article contains a set of best practices that will help you avoid common pitfalls which would impact user experience and the performance of your OutSystems mobile app. If you haven't done so already, you should learn how to develop high-quality mobile apps by following the Becoming a Mobile Developer online training first.
We'll be updating and adding new content to this article along the way!
A great UI experience increases the user perception of a fast and fluid mobile app. This section presents a set of best practices to improve the UI experience of your OutSystems mobile app.
Discover and Use OutSystems Components
Developing common mobile patterns from scratch takes a huge effort and feels like reinventing the wheel.
Use the OutSystems UI framework or other OutSystems Forge components, which provide you with a large set of common mobile patterns. Remember to keep them up to date to benefit from the latest improvements.
Design an Empty State for Content Being Fetched
When a user navigates to a screen, the static content is usually rendered first, while the dynamic content takes more time since it is fetched asynchronously. This results in a poor UI experience, where an apparently incomplete screen is displayed with content moving around while the dynamic data is being rendered.
Improve the user experience by designing and displaying an empty state image while the dynamic content is being fetched. A couple of good examples are Facebook or LinkedIn.
This strategy is valid for all dynamic content in the screen such as blocks, cards or list items. When the empty state turns into the fetched content you may experience some flickering. To avoid flickering when the empty state turns into the fetched content, choose one image that assures a smooth transition, such as a blurred gray line or a spinner.
Prioritize Screen Content Rendering
By default, OutSystems mobile apps fetch screen data without a specific priority. If your screen has content that is more relevant and should be displayed first but you are not prioritizing its rendering, it may lead to a bad user experience. For example, displaying an advertising banner before the main information of the screen.
Delay the rendering of the secondary content so that the main content is rendered first. To do this:
Place the secondary content in a Block inside the True branch of an If widget. The Block must enclose all the logic to fetch the secondary content so that data fetching of the secondary content only runs when the Block is rendered.
On the False branch of the If widget, place an empty state to avoid content from moving around when the secondary content is fetched.
Set the If condition to a variable holding False by default.
In the On Render event of the screen, add logic to set the variable to True so that the secondary content starts to render.
Set the Width and Height of Image Widgets
If you do not set the width and height of an Image widget, the user can get a flickering effect while the final image is being downloaded. For example, not setting the image height might cause the total height of the screen to change until the image loads completely since the widget height will be changing from 0px to the height of the final image.
Set the width and height of the Image widget to the expected size of the final image.
Handling data or implementing logic specifically for mobile scenarios can have a significant impact on a mobile app performance. Follow the best practices in this section to develop performant mobile apps. Also check the other sections for best practices that, although not directly related to performance, can contribute to improving it.
Fix the Performance Warnings
OutSystems automatically detects potential performance issues while you are developing your mobile app in Development Environment. Not paying attention to them may result in a bad performance of the application.
Check the performance warnings displayed in the TrueChange™ tab and fix them.
Design a Lightweight Local Storage
Keeping a large amount of data in the device's local storage, for example in offline or cache scenarios, might become a performance killer in low-end devices.
Use the following techniques to design a lightweight local storage:
Design the local storage data model using only the attributes that you need, instead of all attributes of the corresponding server entity. Consider denormalizing the local data model to avoid complex queries like having multiple JOINS.
Identify the best data synchronization patterns for your use case and define exactly when to run them.
Keep only the entity records you need for your use case in the local storage, instead of all records. For example, get only the open tasks for a given time period, instead of all tasks.
Consider synchronizing data in small chunks, starting with the most relevant data first (e.g. synchronize only text information first; photos and other images can be synchronized later).
Keep the Splash Screen Simple and Fast
When your mobile app starts, it displays a splash screen while internal operations are running. Adding heavy or lengthy operations to the splash screen will increase the time that the users must wait to use the app. Additionally, if the splash screen has a complex UI, the users may see a blank screen before the splash screen renders.
To keep the splash screen simple and fast to load you should avoid:
- Requests to the server
- Heavy logic
- Complex UI, namely too many Blocks
For example, consider the following possible approaches:
Make server requests preferably in an event handler run at a later stage, like On Ready. If you really need to obtain server data in the app initialization stage, minimize the number of server calls (e.g. by obtaining several pieces of data using a single server action designed for the loading stage).
Do not perform lengthy operations when the mobile app starts. For example, if you must synchronize data that is used in the first screen of the app, keep the data transfer to a minimum and postpone the transfer of secondary data.
Tip: To provide an amazing user experience, we also recommend that you customize the native splash screen.
Optimize Fetching Server Data for a Screen
Using the On After Fetch of server aggregates to execute other aggregates in sequence will generate several server requests and slow down the application.
Create a Data Action containing aggregates in the correct order. This action will run in the server in a single request and return all data.
Optimize the Loading of Lists
Lists involve fetching and rendering multiple records at the same time. If not done carefully, the experience can become cumbersome, especially on low-end devices or with bad network connectivity.
To get the most out of lists and provide a good experience, follow these rules:
- Fetch data on demand
- Fetch records as you need them instead of all at once. Start with a minimum set, for example, 10 records. As the user scrolls down, use the On Scroll Ending event to fetch the next set of records — for example, the following 10 records.
- To understand how this works, scaffold a list on a screen and this mechanism is provided by default.
- Keep list items simple
- Avoid expanding content in list items
- Do not design list items with content that can be expanded, such as a description that is trimmed and has a 'Show All...' link. This will impact the behavior of the list while rendering. Use Silk UI patterns such as SplitScreen or MasterDetail instead.
- Fine tune how lists fetch data on demand
- Adjust the number of records that are initially loaded, the increment when scrolling down and the scroll threshold to trigger the On Scroll Ending event. It will provide a better user experience when using lists by avoiding visual glitches and slow list scrolls.
- The values to use depend on the size of the records:
- The initial number of fetched items should ensure a balance between a fast data fetch and a sensible amount of scrolling until a request for more data occurs;
- The incremental number of fetched items triggered by scrolling should generally be similar to the initial amount of fetched items, but you may need to tune it according to the usage of your app. If users will frequently use the list to search for entries, your app should be prepared to fetch data faster and load more items at a time;
- The scroll threshold that triggers fetching new items is the distance in pixels before the scroll hits the end of the list and should be set to 2000 pixels. If you need to tune this threshold to improve the usability of your application, you can add the attribute
infinite-scroll-thresholdto the list widget with a new integer value in pixels.
The next picture shows examples of values to use in different situations. You should start with these as initial guidelines and then test and adjust to your specific case.
- Provide visual feedback while fetching data on demand
- Users need a clear indication that the list has more items to show and that it is actually doing something. An animated image should be enough to provide this message to the user. Even so, as a performance target, keep in mind that during typical application usage the users should never hit a loading wall at the end of a list.
Optimize the File Size of Images
Using big image files in your mobile app can increase the download time (or even block the download) and lead to poor performance, especially in older devices or in low connectivity scenarios. They also consume storage space and might be too big for the screens of mobile devices, even high-DPI models.
Reduce image size (in bytes) and adapt its dimensions (height/width) considering your target user's experience — follow Google's Image Optimization recommendations. Also, consider delaying the fetch of images to a later stage. Refer to the best practice to Prioritize Screen Content Rendering.
- The most common components for implementing a mobile app are provided in OutSystems UI and in the OutSystems Forge. You can probably find the ones you need for your scenario and customize them accordingly.
alert() in client actions.
Avoid Manipulating the DOM
Avoid Using Global Objects
OutSystems uses single-page applications to optimize the runtime execution of mobile apps. As such, the window object isn't cleared between navigations and global objects are not destroyed, increasing memory usage in the long run.
Avoid using objects that get attached to the window object, like global variables, cache objects, or listeners. Do the following instead:
Use browser features like localStorage and sessionStorage objects.
Change Widgets Style using OutSystems Low-code
Use the Style Classes property of widgets to change the CSS classes applied to them. In this property you can use expressions that are evaluated at runtime to change the styles dynamically.
Use OutSystems low-code and style classes to run animations on the device's Graphics Processing Unit (GPU) and remove load from the CPU. For example, in Making Magic with WebSockets and CSS3, cards are animated using CSS.
Learn more about how to properly animate elements in your mobile apps in Smooth as Butter: Achieving 60 FPS Animations with CSS3 and FLIP Your 60 FPS Animations, FLIP 'Em Good.
window.requestAnimationFrame() method to run the animation using the GPU.
Understanding bugs in your mobile apps can sometimes be a hard task, but there are a few techniques that facilitate that troubleshooting process. Also following the development guidelines presented below allows you to more easily replicate and fix a problem in your mobile app.
Define Fallbacks for Your Native Plugins
When you are troubleshooting your app in a desktop browser, the native plugins will not be available. This might prevent you from troubleshooting issues detected in real mobile devices.
Include sensible fallbacks in your apps to account for when the plugins are missing or do not work. This helps ensuring that apps can be tested using desktop browsers. You can do so by wrapping your plugin calls in client actions that return an error (or mock data for testing purposes instead) when a given plugin is not available.
In the following example, the AddToContacts action of the Contacts native plugin was wrapped in a client action called AddToContacts_Safe that first validates if the plugin is available:
Having these fallbacks in place also helps avoiding issues in the apps running in devices that do not support a given plugin.
Create a Simple Client-Side Logging System
Client-side issues can be hard to troubleshoot, especially when they seem to occur in a single user's device in production. If you cannot replicate the issues, it will be much difficult to find and fix them.
OutSystems provides a way for you to log information in a mobile app. However, you can also create your client-side logging system that stores log entries in local storage. Then provide a way for the user to upload these logs in case of errors. This will allow you to analyze what might have caused an issue in that specific user's device.