(This is a reproduction of a post I wrote for my consulting company. The original can be found here).
This is the second part of our 4 part React Native (RN) evaluation. In this post, we delve in software metrics comparing the RN and Android applications.
There are many metrics that could be used, and whichever metric you care about (all or some), always remember that you only get a partial picture of the nature of the code. As they say “The map is not the territory”, so take this data with a grain of salt.
Other parts of this analysis can be found here:
- Part 1: Introduction to our test approach
- Part 2: Code Analysis (this post)
- Part 3: Comparison between RN and Android ecosystems (coming soon).
- Part 4: Conclusion (coming soon).
Note that this is not a “paid” advertisement, we just like the tool.
The metrics that we use are:
- Lines of code
- Cognitive complexity.
The choice of these metrics is debatable as the LOCs will have an impact on maintainability and complexity. Same thing can be said between the relationships between maintainability and complexity. That being said, it is our opinion that we still gain by the analysis of these.
React Native modularization
The RN application is split in modules. This is not something we initially planned as it makes our analysis more complex. The reason for this modularization is explained in a later section. For the moment, keep in mind that the RN application is composed of 5 modules (4 libraries and the main app). The following diagram describes this:
The top level module (reacticity-montreal) is the application itself and is not offered as open source software (OSS). The other modules are available as OSS and can found on both GitHub and NPM.
- urbanoe-model: Urbanoe domain model types.
- urbanoe-common: Formatting rules for the domain model.
- urbanoe-communications: Redux actions and reducers to communicate with the platform.
- urbanoe-mobile-ui: User interface components.
- reacticity-montreal: Application per se.
Lines of Code
As stated, Urbanoe Mobile is one project. Here’s a summary of its analysis:
The application is composed of 33KLOCs. Most of it is Java but there’s also quite a bit of XML files. In any case, this is a substantial amount of code. Though, it’s far from being an outlier when it comes to Android projects. We have worked on a number of Android apps that had more code than this.
(With regards to the code smells, a large portion of those are due to deviations from the standard Java coding standard, which we adopted late. None of the deviations are important).
As to the RN modules and application we have:
The whole application stands at roughly 5KLOCs. This was quite a surprise considering that with these lines of code we have an app that works on both Android AND and iOS.
Obviously this does not tell the whole story as the Android app implements a lot more features than the RN app. We need to normalize this data to compare apples to apples.
The normalization was done as follows. Each package of the Android app was reviewed to see where it fit (or not) in the RN application. Practically, each package was categorized as either mobile-ui, communication, common, model, app or “not replicated”.
We debated as to the coarseness of this categorization. Class or method categorization were also valid choices. There are 80 packages while there are over over 800 classes and close to 3000 methods. Categorizing using packages is more efficient in our eyes. Since our code has good cohesion, we would gain little to do with a lower coarseness. In other words we won’t have functionality that is badly categorized (but may get some level of non replicated code making its way in the categorization).
Doing this categorization exercise yields the following results:
Let’s dive in those numbers and justify them (or not):
In the Android app, the model is defined using POJOs with accessors and some domain specific methods. In the RN app, we use Flow to define our types. Furthermore, all model instances are stored in Redux and made immutable (so no accessor is required). Here’s a comparison of the code: Android and RN. As you can see, there are no accessors, no domain methods. The “+” sign is used indicate immutability.
The “urbanoe-common” code is a grab bag of formatting code used throughout the application. It contains a lot of presently unused code and for this reason, we consider this module to be an outlier.
This module will probably be re-written if the RN project is kept. It has the lowest cohesion in the system.
This is one of the most interesting parts of our analysis. The 6KLOCs Android communication system was replaced by 800 LOCs of Redux and Redux Thunk. We lost a little bit in terms of performance as the Android system is multithreaded but this difference in performance is not noticeable practically. We also gain through the use Redux middlewares which offers us time travelling and data persistency for free.
For the Android app, maintainability is as follows:
For the RN app, we have the following:
Though not shown in the preceding diagrams, the worst Android code (ratings C, D and E) can be divided as:
- the login/logout functionality (C rating)
- the communication mechanism (D and E rating)
- some of the complex screens (C rating)
The login/logout maintainability can be discounted as this functionality is not implemented in the RN application.
With regards to the communication mechanism, again we believe that the complexity difference can be explained through the use of Redux. Redux code is easier to maintain and test. It is our impression, looking at the Redux code, that complexity of the RN communication package will increase linearly for the coming months.
As to the complex screens, they exist in both RN and Android, so we can’t truly explain the difference through the complexity of the screens themselves. From the analysis of the code, we believe that the difference here can be explained through:
- the Android asynchronous event handling used by Activities and Fragments (which is more complex and error prone than the equivalent in React/Redux).
- the split between rendering code and “connected code” promoted in React/Redux. It makes for a larger number of components but of much greater simplicity.
Cyclomatic complexity is not a good measure of overall complexity. It does a good job in determining the number of test cases a method should have but it’s pretty useless to determine how understandable a given method is. Furthermore, it becomes totally useless on a class or project basis as the modularity of your code will actually worsen your score.
We will use cognitive complexity instead. It does a better job of capturing code clarity and elegance. You can find an explanation of cognitive complexity here.
Sadly, there are no fancy graphs in this section. What we’ll do is characterize the cognitive complexity for each module and for the worst 10% of the code (as complex code is usually not spread out equally throughout a codebase).
For the Android code, we have:
The total complexity value is 1815. It’s quite interesting to note that we have a score of score of 1252 for the worse 10% of the code. Basically this means that 10% of the codebase accounts for 70% of the complexity.
For the React Native code, we have:
The RN codebase is far less complex than the Android codebase. The average complexity is 0.75 while 10% complexity is around 6. In other words, the RN code is more understandable than the Android code. That being said in the RN case too, 10% of the codebase accounts for close to 70% of the complexity. All of this complexity is in the UI. In other words, when writing a standard RN app, you can expect your UI code to be where complexity will be found (though it will be lower than what you have in Android).
We don’t know if all of the elements that go in the calculation of the cognitive complexity are exponential in nature but since some of them definitively are, we can safely say that cognitive complexity is exponential (the only question is about its “sloping”… ).