App Upgrade: Developing a Fast-Loading App with a Native Feel for Both Android and iOS

Let’s talk about your project

Let's connect
Callstack Cosmos
Callstack Cosmos

In a few words: It’s been amazing. We think they are great developers and we learned a lot from them. They communicate efficiently with the stakeholders and provide helpful feedback and suggestions. Even though they are external contributors, we consider them as colleagues and part of the team.

Robin Cussol
-
Frontend developer at Kiwi.com

In brief

We optimized the client’s mobile app performance and introduced a custom tool to measure performance regressions. We set up performance tests and documentation to enable a seamless transition of knowledge with the in-house team of developers.

Client Info
Entain is one of the world’s largest sports betting and gaming entertainment groups.
Industry
Commercial gaming
Company type
Enterprise
Region
Australia, Asia & Oceania
Tech Info
Performance optimization project with the best practices, approaches & tech solutions
Technologies
React Native | Native
Platforms
iOS | Android
Stack
Redux | Tanstack | Reassure | GraphQL

Performance Boost

87.5%

In 87.5% of the screens, the number of rerenders decreased or remained the same, which affected the acceleration of the initial performance.

React Universe

In brief

The kiwi.com app helps organize your air travel in a simple and safe way. It offers the best prices for your airline tickets and guarantees to cover missed connections. The client had a mobile app for both platforms - iOS and Android - with totally separated codebases.

React Native integration

We identified several problems; the major ones were as follows:

Loading React Native screens was slow

The user opened the app and when it was time to click a button to navigate to a React Native screen, the user had to wait for the JS to load. If the user navigated back and clicked again, they had to wait one more time.

Navigation

The app had problems integrating the native side of things with react-navigation. On Android the major problem was the back button interaction. On iOS, the swipe back gesture.

Packaging and distribution

  • Kiwi had a repository with the JS code that was being released to npmjs
  • A different repository was creating a library for Android consuming the npm package
  • A different repository was creating an iOS framework consuming the npm package
  • The problems with this approach were that the responsibilities were spread among 3 repositories and 3 owners.

The main problem here was keeping the React Native version in sync as we needed to update it in the 3 different repositories at the same time.
The client was also interested in having CodePush.

Reusing Native code

The client wanted to be able to reuse some existing Native code in React Native without having to re-implement it.

Kiwi app screens

Techniques to adopt

We decided to go for a brownfield approach and apply the following techniques for the following issues:

  • Loading React Native was slow
    Init the bridge in advance and only one time (share/reuse it)
  • Navigation
    Init the bridge in advance and only one time (share/reuse it)
  • Packaging and distribution
    Move everything to a single repo, automate releases (maven on Android) and generate a .framework on iOS. Add Codepush
  • Reusing Native code
    Write native modules that can use the dependency injection pattern
Within these native applications, some screens were presented as a WebView. The goal of our Kiwi project was to switch those views into React Native using a single codebase. In other words, it was all about integrating React Native into two fully native production apps.

Solution: the singleton pattern for Android and iOS

Kiwi app screens

Loading React Native screens

The solution for both platforms is written in a different way but the idea is the same: Using the singleton pattern.

iOS
We initialize the RCTBridge when the app starts (not when the user navigates to a React Native screen) and we create a singleton to share it whenever it is needed.


Android
We override the ReactNativeHost class and we configure anything related to our ReactInstanceManager there. In the Application level, that is, a class extending android.app.Application, we store there our reactNativeHost instance to be reused and shared. This way, it does not rely on a Context that can be destroyed (like if we were initiating the ReactInstanceManager in some Activity). We also make use of createReactContextInBackground().

Kiwi app screens

Navigation

iOS
On iOS, we needed to disable the native swipe back gesture if we were in a nested react-navigation screen (so it could be handled in the JavaScript side). On the other hand, if we were in the first React Native screen (not nested), the gesture needed to be enabled. Therefore, we created a native module so we could enable/disable native gesture from React Native. For such, we used the interactivePopGestureRecognizer.

Moreover, in order to go back from a React Native screen to a native one, we expose a method in our native module to close the current view controller (which is basically calling popViewControllerAnimated.

Android
The Android Activity that hosts the React Native View needs to implement DefaultHardwareBackBtnHandler from the react package. This is explained in detail in Integration with Existing Apps documentation.


However, we should also be able to navigate back from a React Native screen to a native one using any kind of button (like the back arrow on the Toolbar). For that, we wrote a Native Module for React Native to be able to go back to native whenever is necessary. This is as simple as calling the .finish() method of an Activity.

Packaging and distribution

The following would be triggered by GitlabCI in every merge to master so it was fully automatic.

iOS
Here, we had a script that would package everything into a .framework. These were the requirements from the iOS Native team, so they didn’t have to use CocoaPods and build React. We uploaded that .framework into Github Releases and the native team just had to copy and paste it into their project.

Moreover, in order to go back from a React Native screen to a native one, we expose a method in our native module to close the current view controller (which is basically calling popViewControllerAnimated.

Implementing code on iOS diagram

Android
On Android there were more steps:

For every native dependency, build the library and deploy it into a private maven repository For every new version of React Native, build it and deploy it to a private maven repository We wrote generic classes and the code responsible for the bridge, navigation, etc. and we made a library out of it, that was published to maven too. Here we were also building the JS code and attach it as an asset The native app was just consuming the library without having a direct dependency to react-native.

We also implemented CodePush so GitlabCI was able to release a new version whenever conditions matched.

Implementing code on Android diagram

Faster for the users and more developer friendly

  • React Native screens load without delay
  • Navigation works as expected
  • Native code can be reused in React Native
  • Everything in one repository and React Native version in sync.
  • CodePush is working which means not waiting for the App Store anymore when there are JavaScript changes

In short: Delivering a brownfield solution for both platforms which keeps a natural native feeling without having to rely on a WebView. Plus it can work offline!

appstore
playstore

The challenges we’ve solved so far

Related services

No items found.

get in touch

Fill in the form and tell us a little bit about your enquiry. We’ll get back to you promptly to discuss your requirements.

Get in touch
This information will be used only to contact you. For details, check our Privacy Policy.