You are a tech lead specialized in web but suddenly one day you are forced to deploy products for mobile platforms in a short time? Or is a front-end/back-end/full-stack web dev looking to move into mobile but don’t know where to start? Or do you have a unique idea for a mobile app that you can earn but only know HTML, CSS and JS? Or are you just a lame skeleton, want to explore and expand knowledge but have limited capacity, can’t cram many things like me
Capacitor is the solution for us. Let’s learn about Capacitor and the advantages it brings to web people in developing a mobile application through this article.
Disclaimer: Capacitor is just one of many solutions to help developers have many options to solve problems depending on requirements, cases, circumstances and resources. So in this article, I only focus on introducing and guiding on Capacitor, not pointing out the advantages and disadvantages compared to similar solutions like Cordova, PWA, or React Native…
A little theory
A cross-platform native runtime for web apps.
A cross-platform native runtime for web applications??? Wtf??? Reading English is difficult to understand, but translating it to sound both bananas and more difficult to understand. But that’s the big text right on the homepage to introduce Capacitor , a product of the Ionic platform by the duo Max Lynch and Ben Sperry co-founder and developer. Simply explained, Capacitor is a tool that allows developers to build mobile applications on Android and iOS with only basic web technologies such as HTML, CSS, JS, or higher, Angular, React, Vue…, or higher, Nextjs, Nuxt, Quasar…
Working mechanism
This paragraph I also only partially translated from Max Lynch’s blog post, you can read his blog post directly to understand better here .
Basically, Capacitor is a bridge that helps web applications written from HTML, CSS, JS to run and communicate with mobile platforms such as Android or iOS. Going a little deeper, you can see that the Capacitor includes the following components:
In the diagram above, please pay attention to an important component, the Web View , which is an integral part of the Android and iOS operating systems. Web View allows web pages to be displayed like a web browser, so web apps should work just as well. Taking advantage of this, Capacitor encapsulates our web app in a Web View, which then acts as a bridge to interact with “native” features (i.e. OS-specific features, for example to open a dialog, Android will have a different implementation, iOS has a different implementation), when you need to use a feature, just call that feature through the API.
For those of you who have experience in the mobile field, it is probably no stranger to this model, this is also known as the Hybrid application model.
With the above method, we can develop mobile apps for multiple platforms with just one codebase written in web language, with any web technology the team has chosen, or even a finished web app. It is still possible to integrate Capacitor to become a mobile app without having to change much about the codebase.
Plugin Ecosystem
As mentioned above, the “native” features of mobile are “bridged” by Capacitor so that our web app can call at any time through the API. Native features exist as “plugins”. For example, my app needs vibration function to interact with users, I can install Haptics plugin, then from JS code call Haptics.vibrate()
to vibrate the user’s device. It’s too simple.
In addition to Haptics, there are many plugins developed by both Capacitor’s core team and the dev community so that we can use them right away. You can see the list of plugins as below:
- Team core official plugin: https://capacitorjs.com/docs/apis
- Community plugin: https://github.com/capacitor-community
These plugins are actually written in OS-specific languages, for example, for the Haptics plugin, to build the Haptics.vibrate()
device vibration API, the Capacitor team had to write both languages for the two platforms. platforms are Android (in Java) and iOS (in Swift) as you can see at this github repo . Thought it was simple but not simple, but rest assured, 99% of the cases the available plugins are enough to serve our needs, the remaining 1% is too difficult to ignore . Create your own plugin
In addition, in that 99%, there are also plugins from Cordova, Capacitor is designed to be compatible with many Cordova/PhoneGap plugins (Capacitor’s predecessor, the same idea and application of the Hybrid apps model). Installation and use are also relatively simple. What’s more convenient, since most of Cordova’s plugins are written in JS, the dev community also builds TypeScript wrappers that are gathered in this repo , making these plugins even easier to use and transparent.
Turn web app into mobile app
Here I choose a random web app To-do list on github:
This App has been fully coded with basic functions, now I try to integrate Capacitor to turn it into an Android app that can use “native” features as follows:
- When the user adds, edits or deletes an item, a toast message will appear – use the Toast plugin
- When a user ticks an item, their device vibrates – use Haptics plugin
With only 3 steps as on the homepage, we will work together.
1. Install CLI + core package and initialize
1 2 3 | npm install @capacitor/cli @capacitor/core npx cap init |
Terminal will then ask to enter the necessary information for initialization, such as the app name, the app’s package ID on the Play Store / App Store, and the directory after the build containing the index.html
file for the Capacitor to recognize. In this example, I entered the following:
2. Install platform packages
Since I just want to create an Android app, I just need to run the following command to install it for Android:
1 2 3 | npm install @capacitor/android npx cap add android |
To install for both platforms, you can add the package @capacitor/ios
and then call npx cap add ios
similarly.
3. Modify the code to use the “native” feature and build the app
As mentioned above, I will install 2 more plugins, Toast and Haptics, to integrate the “native” feature:
1 2 | npm install @capacitor/toast @capacitor/haptics |
In src/tasks.js
, I import the Toast plugin and call the Toast.show()
function to display a message every time the user adds/update/remove an item.
In src/status.js
, I import the Haptics plugin and call the Haptics.vibrate()
function to vibrate the user’s device when they complete an item.
Then build source and sync with android
folder with command:
1 2 3 | npm run build npx cap sync |
3.x. Use Android Studio to package source code into app
At this step, you may wonder: why use Android Studio? Do you think Capacitor turns a web app into a mobile app without the need for a platform/OS specific IDE? The reason is because Capacitor doesn’t work like that. As mentioned above, Capacitor only acts as a bridge between your web codebase and the “native” environment of the operating system, simplifying the steps in the mobile app development process, however, packaging code and build into a mobile app must still be done by IDEs that are Android Studio (for Android) or Xcode (for iOS). But this step is not too complicated so you don’t have to worry.
To install Android Studio, you just go to the download page and then proceed with the normal installation. After installation, you can open Android Studio with your project with the command npx cap open android
. Wait a moment for gradle to automatically install the necessary packages for the app. Finally, you can run the app directly on the simulator or your phone to experience, or build the source into an apk or bundle file to deploy to the store.
In my case, after running the app on my phone, I enter a new item, a Toast shows the result like the code I edited:
So I have an Android app built from the web language, extremely easy and fast
Create a plugin yourself
In case your app is among the unlucky 1%, must use a certain “native” feature but no plugin is available, then Capacitor also supports all the steps so you can create one yourself. plugin for your app. The rest is not too difficult, just find a course to learn more about Java/Kotlin or Swift/Objective-C
Here I try to create a Capacitor plugin for Android (Android again, wish I could write a tutorial on iOS…). Assuming the use case is that my todo app wants to retrieve the device’s language information but no plugin has provided it yet, I will create a locale
plugin by myself following these steps:
1. Initialize from plugin template
First, I used the generator that the Capacitor team developed to create a plugin framework:
1 2 | npm init @capacitor/plugin |
Enter the necessary information for the generator plugin, once completed, you will have a project with a folder structure similar to the following:
Here I will only be interested in 2 folders android
and src
:
android
– Contains the source code of the plugin for Android (inandroid/src/main/java/com/.../LocalePlugin.java
)src
– Contains the source code of the plugin for the web and a set of definitions in TS to bridge with Android or iOS source code.
2. Android side processing code
Accessing the long path to the source code of the Android plugin , you will see 2 files:
Locale.java
– contains code that handles plugin logicLocalePlugin.java
– contains the code to communicate with the web
The above file organization helps to make my code logic clearer and easier to maintain if my plugin is relatively large, but since the plugin I plan to write is very small, I will only write the code in the LocalePlugin.java
file. In this file, a short but complete sample code is available for the processing logic of an API plugin as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span class="token annotation punctuation">@CapacitorPlugin</span> <span class="token punctuation">(</span> name <span class="token operator">=</span> <span class="token string">"Locale"</span> <span class="token punctuation">)</span> <span class="token comment">// <-- tên của plugin để bên phía web nhận diện được khai báo bằng decorator annotation</span> <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">LocalePlugin</span> <span class="token keyword">extends</span> <span class="token class-name">Plugin</span> <span class="token punctuation">{</span> <span class="token keyword">private</span> <span class="token class-name">Locale</span> implementation <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Locale</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token comment">// <-- phần code xử lý logic nếu cần tách</span> <span class="token annotation punctuation">@PluginMethod</span> <span class="token comment">// <-- annotation để khai báo hàm echo là một API của plugin</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">echo</span> <span class="token punctuation">(</span> <span class="token class-name">PluginCall</span> call <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// <-- PluginCall là cục data nhận từ phía web</span> <span class="token class-name">String</span> value <span class="token operator">=</span> call <span class="token punctuation">.</span> <span class="token function">getString</span> <span class="token punctuation">(</span> <span class="token string">"value"</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token comment">// <-- PluginCall chứa các hàm để nhận và chuyển đổi kiểu dữ liệu tương ứng</span> <span class="token class-name">JSObject</span> ret <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">JSObject</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token comment">// <-- tạo một cục data mới để trả về phía web</span> ret <span class="token punctuation">.</span> <span class="token function">put</span> <span class="token punctuation">(</span> <span class="token string">"value"</span> <span class="token punctuation">,</span> implementation <span class="token punctuation">.</span> <span class="token function">echo</span> <span class="token punctuation">(</span> value <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> call <span class="token punctuation">.</span> <span class="token function">resolve</span> <span class="token punctuation">(</span> ret <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token comment">// <-- gọi hàm resolve của PluginCall để trả cục data về khổ chủ</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
I will edit this code to get the device’s locale information as follows:
1 2 3 4 5 6 7 8 9 10 | <span class="token annotation punctuation">@CapacitorPlugin</span> <span class="token punctuation">(</span> name <span class="token operator">=</span> <span class="token string">"Locale"</span> <span class="token punctuation">)</span> <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">LocalePlugin</span> <span class="token keyword">extends</span> <span class="token class-name">Plugin</span> <span class="token punctuation">{</span> <span class="token annotation punctuation">@PluginMethod</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">getLocale</span> <span class="token punctuation">(</span> <span class="token class-name">PluginCall</span> call <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token class-name">Locale</span> locale <span class="token operator">=</span> <span class="token class-name">Locale</span> <span class="token punctuation">.</span> <span class="token function">getDefault</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> call <span class="token punctuation">.</span> <span class="token function">resolve</span> <span class="token punctuation">(</span> locale <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Wow, nice logic…
3. Interface definition
In this step, I will define the getLocale
function coded on the Android side in src/definitions.ts
:
1 2 3 4 5 6 7 | <span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">LocalePlugin</span> <span class="token punctuation">{</span> <span class="token comment">/** * Return the device's locale information. */</span> <span class="token function">getLocale</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token builtin">Promise</span> <span class="token operator"><</span> <span class="token builtin">string</span> <span class="token operator">></span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> |
This interface will make it easy for you or someone else using this plugin to take advantage of intellisense’s type checking or code completion. Commenting on code is optional, but you probably already know the benefits of commenting code, let alone its importance when building APIs. Furthermore, the comments here act as documentation comments and will be generated along with the README file when you run the npm run docgen
command that is already integrated with the plugin template.
4. Web-side processing code
In this step, I just need to create a corresponding getLocale
function on the web side to connect to the Android side to be able to start using the plugin. In the src/web.ts
file, you can also see a sample code available similar to Android. I modified it to return the browser’s language information as follows:
1 2 3 4 5 6 | <span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">LocaleWeb</span> <span class="token keyword">extends</span> <span class="token class-name">WebPlugin</span> <span class="token keyword">implements</span> <span class="token class-name">LocalePlugin</span> <span class="token punctuation">{</span> <span class="token keyword">async</span> <span class="token function">getLocale</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token builtin">Promise</span> <span class="token operator"><</span> <span class="token builtin">string</span> <span class="token operator">></span> <span class="token punctuation">{</span> <span class="token keyword">return</span> navigator <span class="token punctuation">.</span> language <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Note that if your app is targeting multiple platforms at the same time, either Web, Android or iOS, it’s necessary that you write code that handles multiple platforms, but if your app is only aimed For mobile platforms, in this file, you can implement empty functions without writing internal processing code.
Finally, just build the source with the command npm run build
and then push it to npm to share with the community or import the package into your project to use. So I have a simple Capacitor plugin.
Conclude
Capacitor is indeed a powerful weapon for web developers, the simplicity, convenience and speed it brings is extremely valuable. If combined with specialized frameworks to develop cross-platform apps such as Ionic (with the same father), Framework7 , Quasar … it looks professional, no different from a real “native” app. consume. However, there is no such thing as a “silver bullet” technology that solves all problems. Each type of technology has its advantages and disadvantages in certain cases (requirements, scale, human resources, timeline, security, lead, PM, etc.). So it is necessary to ask questions, exchange and evaluate before deciding to choose.
Hope this post is helpful for you, thank you for following the article