In the realm of mobile app development, Flutter and NativeScript are two powerful frameworks that enable developers to build cross-platform applications. Each has its own unique features, strengths, and weaknesses. In this article, I aim to show the main differences between Flutter and NativeScript that can help you choose the right tool for your next project.
When we started our journey with Lapka, our one-stop shop for happy pet parents, we initially chose NativeScript. Several reasons influenced our decision:
Thus, it was a natural choice to use a familiar tool to build our new app. Although we considered Flutter, at that time, it lacked some essential tools and plugins. After a few years with NativeScript, we decided to make a significant switch and completely rewrite the entire app using Flutter. In this article, I’ll share insights into why we switched and what we learned during our journey.
We will cover several topics:
NativeScript uses JavaScript/TypeScript coupled with Angular, Vue, Svelte, or Vanilla JS, making it easier for experienced web developers to transition to mobile development. JavaScript is widely known, and many developers are already familiar with it, which reduces the learning curve. TypeScript adds optional static types to JavaScript, which can help with code quality and maintainability. NativeScript allows you to build your app using a familiar stack if you are already working with Vue, leveraging Vue's vast ecosystem. And if you use Svelte or Angular, they've got you covered.
On the other hand, you have Dart, which is used for Flutter development. Sure, many will say it is very similar to JS, but realistically, you will need to learn one more language with a very specific use case. And while Dart is a really easy language to pick up, you still will need to learn a lot of new stuff about how to approach common tasks, like state management, structuring components, and overall app architecture. It's doable, but it can be a huge stopping factor for many web developers.
It's a really tricky topic to talk about because of how different the approaches are. For a very high-level overview — in NativeScript, all your business logic and UI declarations are done in the JS level, which communicates with underlying native APIs and components. So basically, you describe in JS what you want from the native part. And to be precise, from native UI components — describing how to style and position everything. So all UI possibilities are based on native platforms with all possible differences between iOS and Android and also some limitations. But this way, you have access to any of the native capabilities right from the JS part.
And what's better — in NativeScript, you can call native code directly, in contrast with React Native, for example, where you need to create a special bridge to use native functions. So in Objective-C, you will write something like this to get the device battery level:
float batteryLevel = [[UIDevice currentDevice] batteryLevel];
And in NS all you need is to call same function from JS code like this:
const batteryLevel = UIDevice.currentDevice.batteryLevel
There is little performance overhead in this approach, usually noticeable when parsing and updating large amounts of data. Like when you tap something, and there is a noticeable delay in response. But usually, you can overcome some issues with smart state animations and careful planning of how you manage background tasks to keep the main thread running. A great benefit here is that your app has a native feel, and it's hard to tell that everything is running by JS.
In contrast, Flutter has a very different approach to the same problems. I think that it can be compared to canvas in web development. So basically, your app is a blank canvas (pun intended) targeted to smooth 60 fps, while all the heavy lifting is done by Flutter itself — all positioning, styling, composing, etc. This way, you have more granular control over everything we see on the screen, as well as one of the main benefits of the framework — seamless representation of your app on any platform. This way, you can implement truly unique animations not only for layout items but for whole screens.
But this approach comes with a noticeable tradeoff, as Flutter needs to mimic all the native components, and in some cases, an experienced user can easily find some distinctions. In my experience, most of them come from animations and how they feel when compared with native iOS (usually I work with an iOS-first approach). But based on experience again, most of the time, you can overcome this by putting a little bit more time and effort into this space.
Little tip — try spring animation curves, usually with 400-500ms duration for your animations, and you will be surprised how well this mimics native iOS feel.
Also, in some articles from the past, you can find complaints about shader junk and overall poor Flutter performance on iOS. The good thing is that in 2024, this is no longer the problem. The new (relatively speaking) and shiny Impeller engine solves most of the old problems. But you still need to keep in mind that the framework is not that forgiving about how you architect your app. You can easily create a situation where some heavy widget tries to refresh at 60fps for no good reason, and as a result, limit overall app performance. These issues can be solved by a deep understanding of what's going on. So, please, invest some time into learning the basics of the framework's architecture. I remember how useful it was and how much better my apps became.
Also, I want to mention that whichever you choose, you need to be careful with plugins. Sure, there are tons of great and useful plugins for NativeScript and Flutter, but you always need to pay attention not only to what the plugin does but also to how it is implemented. A poorly written plugin can easily add hours of debug adventures to solve performance issues.
NativeScript provides a solid development experience, but it can be hit or miss with some tools and plugins (more on that later). For example, the hot reload feature is available, but it might not be as seamless as desired in all scenarios. In my experience, I always disabled HMR because of totally unpredictable results. Thanks to the app's rebuild time with NativeScript, it was pretty great. And while NativeScript has a great and helpful dedicated community, it falls short in terms of the plugins ecosystem. Yes, there are tons of plugins, but when you try to use something, the chances that plugins will be incompatible with your app are really high. I spent days forking some needed plugins and fixing issues inside them to make them compatible with our app. And the worst part is, there is another major version of the framework with some breaking changes under the hood. Hence, you need to update all of your bespoke plugins again. I think we never updated our app to NativeScript 8 because of such problems, and at this point, ditched NativeScript development completely.
In the shiny Flutter world, the framework excels in DX, particularly with its hot reload feature that works out of the box. I mean, it really works. I was shocked by this when I picked it up for the first time. Whatever you do, it just works in a fraction of the time. This feature alone was a huge point to never look back, as it significantly speeds up the development process and makes debugging much more efficient.
More than that, Flutter's community is huge and helpful. You can easily find tutorials and useful plugins for common things. I've never struggled to find a plugin with the native capabilities that I need for a project. And if so, you can always create one. Yes, it's not as fast as NativeScript's approach, but let's be realistic about how common this task is.
Also, Flutter's greatly organised and comprehensive documentation, combined with a wealth of tutorials and examples, makes it easier to get up to speed. Especially in contrast with NativeScript's mess of documentation. But again, as for me, the most important part to start with Flutter is not the Dart language or widgets, but the overall understanding of the architecture of the framework.
In my book, NativeScript is ideal if you need two main things — native components with a native look and feel and using a web framework you already know. Some can say that if your app needs tight integration with native capabilities, you also should use NativeScript or React Native approach, but in my experience, if you are really trying to create, for example, a camera app with heavy usage of native bits, you should just use native.
In contrast, if you are sure that your app has a unique and bespoke design that you want to leverage seamlessly on both platforms, I think Flutter is your best bet. Most of the time creating Lapka, we didn't check Android until we created a release or used some native-heavy plugin. And it was intended because in 99% of the times when you open your app, it works exactly the same as the iOS counterpart. This way, we sped up our development by hundreds of percent and were able to deliver new features faster. It had a huge impact on our business as well, as we were able to test our product theories much faster. And of course, thanks to Flutter’s built-in tools for animation and graphics, along with its ability to handle complex gestures and transitions, our app bloomed with cute tiny animations, drastically improving user experience.
In conclusion, I want to say that choosing between Flutter and NativeScript depends on your specific needs and constraints. Flutter offers high performance, an excellent developer experience, and a strong ecosystem, making it ideal for apps requiring a consistent design across platforms. NativeScript or other similar tools like React Native, with its flexible language options and native component access, is better suited for applications needing a native look and feel while reusing your team's web knowledge.
Both frameworks have their strengths, and the best choice will depend on the particular requirements of your project and the expertise of your development team. But for our team, project, and me personally, there is no way to switch from Flutter with probably only one exception — a fully native app.