00:00:13.130
Thank you all for having me here today. I want to talk about making hybrid apps, and by hybrid, I mean mobile hybrid apps that don't suck. It's challenging, but it's not actually as bad as you might think.
00:00:19.710
A little about me: my name is David Muto. You can find me on the internet as sudowoodo, which is a mandala. I work at Shopify. For those of you who don't know, Shopify is an e-commerce platform that tries to make commerce better for everyone.
00:00:30.660
Currently, we power over 200,000 stores worldwide and things are going well. For the last little while, I have spent time trying to figure out a nice practical way to develop a hybrid application.
00:00:46.680
Specifically, we wanted to develop an app we called Sello. The idea is that it would be the easiest way to sell anything to anyone, anywhere at all. It was a simple goal, but we just wanted to start with the easy stuff.
00:01:05.610
We tried literally every framework you can imagine to come up with hybrid apps and found that none of them were really up to the task. I don't want to name any because I don't want to be that guy who hates on projects, but lots of them had really good intentions and ideas. However, you just don't get a native feel when using them, which made them a non-starter for us.
00:01:25.680
So, we went through a lot of thought and figured out the right trade-offs, determining where we could use web technology and where we had to get down to the metal in the native side.
00:01:30.930
I hope to share some of the big ideas with you today. I'm certainly not going to go through the codebase line by line; there is a lot of code in these slides. I've done my best to make them as big as possible.
00:01:43.979
I'll make sure these slides are available as well. Also, keep in mind that none of the things require you to read all of the code on the screen from top to bottom, so I'll focus on highlighting the parts that are important.
00:02:09.649
Next, I didn't want to test the demo gods because they're forever against us when you're standing up here, so I made a video that shows what our app does to prevent any mix-ups during the presentation.
00:02:28.310
It’s a relatively short video, but here’s just one of the major flows: creating a product to sell. You click the plus button, get a camera, take a picture, scale it, crop it, and enter some text, which is the stuff you'd expect to see. We manage inventory for you too.
00:02:53.569
After you have a product, you can preview it in your store, and then we're going to go and share it to Twitter. You get a custom URL that you can see shared on Twitter, and when you click on it, you can check out your mobile-friendly storefront, which looks pretty good on desktop too.
00:03:06.680
The next step would be that someone actually buys your product. You’ll get a push notification to let you know someone has bought your product, bringing you to the orders section.
00:03:18.380
You can click on the order and see that it came from Twitter, and then you can also see on the map exactly where they came from. In our case, we used our address in Ottawa, but it would work anywhere. Some of the stuff was native, and some was not, and often it wasn’t what you'd expect.
00:03:33.530
I'll leave it as a mystery because that’s kind of more interesting that way. When we started to build this out, we had a strong aversion to local state. What I mean by that is, we didn't want a database anywhere; we didn't want local cached files, or this complicated off-line syncing mode. We wanted to avoid that complexity.
00:03:53.569
The web is the one and only source of truth for us. There is no other data anywhere else that we have to figure out how to validate. Our next goal was to ensure that navigation through the app would be driven by the web but executed natively.
00:04:10.310
If anyone's ever seen web browsers on native devices, they are horrible at this, but native apps are really good at navigation. Thus, we aimed for most things to be done natively when it counts and only when it counts—only when it actually makes a difference.
00:04:22.019
Lastly, native codebases mean native code releases, which means you have to deal with app stores or Play Stores. We wanted to keep the native codebases as small as possible while sharing as much as they could.
00:04:35.520
With these three goals in mind, I'm going to go through a whole bunch of stuff that represents major building blocks and issues we ran into.
00:04:52.469
Everyone here is probably familiar with these categories: there are three types of apps you can create for mobile. There are mobile apps written entirely in the native languages for their respective platforms, communicating with an API if needed.
00:05:11.969
There are web apps, which in my opinion are the worst type. They merely provide a native shell for an application that opens a web view, and everything happens inside that web view, including navigation.
00:05:26.400
Then there's something in the middle, which is what I want to talk about today: hybrid apps.
00:05:32.779
In my experience, this is a good approach as long as you can figure out the right trade-offs. We were looking for things like the rapid iteration of content, which you can achieve from the web app model. If I need to change something, it’s not a native code release; you just test it out, ensure it's good, and push it—everyone has it.
00:05:46.199
Updates can occur without needing the App or Play Stores, and we wanted to leverage the existing technology and teams. There are 300 developers across the company, so I didn’t want to need them to learn everything from the ground up.
00:06:09.900
I wanted to use some technology we already have and some users that we already employ.
00:06:27.089
Before I dive into some code, I must mention that some native mobile developers almost always hate hybrid apps. You don’t even have to tell them your plan; just saying 'hybrid app' often elicits a negative reaction.
00:06:44.969
There's a good chance it’s linked to their bad experiences with existing frameworks. While there are some good frameworks, many others are still working out their kinks, but hybrid apps don't have to suck.
00:07:04.860
Today, I’ll show you some of the building blocks that can make hybrid apps more successful. You may come up with some other ideas, hopefully.
00:07:20.300
Another myth is that you have to be an expert in every mobile technology to get started, but this is not necessary. You do need some experience or a willingness to learn.
00:07:37.680
The language isn’t really the problem for anyone who’s tried to learn Objective-C, Swift, or Java. Learning a new language only takes a couple of weeks if you work with it all the time.
00:07:54.600
However, the hard parts I found for bringing in people without mobile experience were understanding Cocoa, Apple’s infrastructure, or Android's activity and fragment life cycles.
00:08:07.100
Now the very first steps here involve having a JavaScript object on a page that we can use to send messages to the native app.
00:08:17.600
The idea is that you click a link, and something tells the native device to perform an action. These messages are coming not as network requests; they just feel like ones and happen from within a browser already on the device.
00:08:34.210
If you’re using iOS, this framework has JavaScript Core. I don't know of an equivalent on Android, and we didn’t want to rely on two different client-side libraries, so we ended up creating our own.
00:08:43.709
The goal is straightforward; in the first part of the code, our web view is configured with a script message handler that responds to events from the website.
00:09:03.000
The actual body part is defined by the delegate that the script message handler will receive. It will have a body, and we will pull that out to retrieve an event name, providing us with a generic method to deal with events coming in with a JSON object as options.
00:09:22.939
On Android, don’t worry about the configuration details; I’ll share these slides so you can go through it. Most of the complexities have been pared down—error handling and other elements remain.
00:09:35.819
The main idea is to set up a JavaScript interface with a name. In this case, it is called 'native app.' Whenever you execute an event, it can be anything you like.
00:09:53.819
The examples I've given are in CoffeeScript, though I’m a fan of it. ES6 is quite good too, but I prefer CoffeeScript. Calling from it is simple; you expose this as a native app and use the built-in system in each framework.
00:10:07.630
You get a native object called 'native app' that, again, is not a web request—it gets injected beforehand and is available when loading URLs.
00:10:25.110
Now that we have a way to send events from the web to native, we need to set up several things involving Rails.
00:10:38.470
It’s much easier if you’re building a brand new app, but in my case, that wasn’t the situation. I had a lot of existing infrastructure, including a Rails app designed for Shopify.
00:10:54.120
We wanted to leverage as much of that as possible without marring our code base with instances of Sello.
00:11:08.550
In every other case, we’re trying to reuse everything we can. We also wanted to support multiple app versions in production.
00:11:27.110
Once a native app is pushed into production—especially on iOS—people don't like to update immediately. On Android, some devices now default to updating in the background, while you can choose to allow it or not.
00:11:43.370
Once the app is out, we need to continue supporting those versions even if modifications occur in the website’s flow. This part is always a challenge.
00:12:01.059
The best approach begins with identifying requests meant for your app. In our case, we had an existing application, so we made a concern.
00:12:16.670
Be aware of this code pattern. What we do here involves mapping the user agent that comes with requests—set this on the native side to some patterns.
00:12:36.730
Sometimes, these patterns change, but they usually remain consistent once established. Ensure to include the app name, version, and platform in the header for decision-making.
00:12:54.020
You want this data early in the process to avoid shipping a new app version merely due to a required change.
00:13:08.010
Next, we set the request variants. For those unfamiliar with Rails variants, it’s like a special version of the request format. You may see code that says respond_to, followed by format HTML or JSON.
00:13:24.540
These can now incorporate blocks as well, letting you check for particular variants and return a different view. This allows for using the same controllers.
00:13:41.000
The process can be automated by naming your views accordingly. In the example of index.html, you can use index.html+variant where it will render instead of using custom view paths.
00:13:55.539
The scary part about concerns comes when adding filters; it complicates tracing events since they won't show in a controller. It's imperative to make these decisions deliberately.
00:14:11.060
So if it's a seller request, we run the before action to set the request variant to Sello. The next step is setting the app version.
00:14:27.950
Our user agent pattern consists of two groups, which we can split up. The first group matches the platform, either iOS or Android—and we can add other platforms if needed.
00:14:44.059
Next, we use a version gem for all the matching goodness. For instance, if you are showing a camera, you’ll want to know if the device is running an app version that supports that functionality.
00:15:04.849
This prevents a situation where you claim the camera works only for it to crash due to a version mishap.
00:15:18.220
Next is sunset old versions, so if someone installed a beta version, we'll eventually stop adding if statements all over to check versions.
00:15:35.060
We want the app to detect the wrong version on open and send an update required header while shutting down. Use caution selecting the upgrade required header, which is HTTP 426.
00:15:55.280
Some networking libraries may misinterpret this, as the RFC states it's meant to indicate an outdated HTTP version.
00:16:16.670
We didn’t encounter this issue, but it's good to be aware. Another consideration is restricting access to actions.
00:16:27.440
You may want certain actions available only for app requests or web requests. I suggest using a whitelist instead of a blacklist, as being explicit tends to be clearer as your team size grows.
00:16:50.170
On a different note, ActionMailer lacks variant support, which means you'll need to monkey patch it if you want to send different mail templates based on the request.
00:17:04.070
If you find a mechanism to appease the Rails core team, please attempt to submit a PR, though it may be difficult since many rely on ActionMailer for varied purposes.
00:17:25.310
Lastly, you might want to handle custom domains for your requests. As of now, I didn't include any code for that as I felt I had enough code in these slides.
00:17:37.380
So far, we've identified app requests. We can show specific views based on variants, send events from Rails to the native side, and now we need to be able to send events the other way.
00:17:50.330
The next big idea revolves around a concept called the event hub—this acts as a bridge between the native device and the web view.
00:18:06.000
It's based on an asynchronous pub-sub model, meaning you publish an event and anything listening on the other side hears it and handles it.
00:18:22.860
This setup allows native code to trigger web behavior and vice versa. I created a diagram that represents this process.
00:18:41.630
The web view is owned by a controller or fragment, depending on your platform. The event hub is attached to it, and you register receivers for different types of events.
00:19:00.580
If an event comes into the event hub, it checks for receivers registered for it and notifies them accordingly. Those receivers then delegate actions to a handler or callback.
00:19:16.790
Typically, I suggest letting the entity that owns the web view receive these events for easier tracking.
00:19:29.960
I understand there’s lots of code; don’t worry about reading all of it. This is merely the framework setup so that events come in with intent. If it's a global event, we treat it as global; otherwise, it will act as a local intent.
00:19:46.330
The system loops through all event receivers to handle actions as necessary. An example receiver pulls out the URL attribute from the parameters and instructs its delegate to push a new URL.
00:20:02.750
For instance, in iOS, this may involve pushing a new navigation controller. When broadcasting a global event, we utilize iOS's Notification Center.
00:20:18.030
It's critical to avoid using this for navigation since it complicates following app behavior; I advise using local events instead.
00:20:35.160
After this, we can set up JavaScript to execute events that will create event objects and dispatch them to any listening instances.
00:20:51.750
Again, there's a lot of code; here’s the premise: execute an event that will generate in an event object and notify anything that’s listening.
00:21:13.820
We can execute a native event at any point and send it to the event hub on the native side. So far, so good.
00:21:30.120
I think a few suggestions to improve this process would help. First, consider making a base class, as a lot of plumbing code goes into these setups.
00:21:47.000
If you can handle the inherited properties, I suggest doing so. The receivers can be added to subclasses if necessary, but generally, most receivers are similar.
00:22:01.290
You want to handle particular instances like page pushes, pops, and errors.
00:22:17.000
Typically, these should be uniform unless dealing with specific cases like the camera, as I showed earlier.
00:22:32.050
As mentioned earlier, I suggest the controller (fragment or whatever owns the web view) be the delegate for the receivers, ensuring clarity.
00:22:45.150
Now for the major difference between global and local events. Local events are sent specifically to individual event hubs attached to one web view.
00:23:02.270
They instruct the currently displayed web page to do something, while global events go through the notification system, which can dictate across the app on both iOS and Android.
00:23:18.230
These events can correspond to logout actions or other behaviors that need to occur regardless of the page the user is on.
00:23:35.110
We’ve discussed how we can identify app requests and send events between native and web. Now, we can maximize all this in practical applications.
00:23:51.180
The first example I’ll show is loading pages. We want to avoid excessive communication with the web API.
00:24:03.670
Therefore, we prefer sending a single event containing all the necessary information for rendering—this includes setting up action bars or navigation bars.
00:24:19.590
This way, upon loading a page, we transmit title and action information together without separate requests, preventing overhead.
00:24:38.190
As an example, the title and button are displayed natively on their respective platforms but are defined within the webpage returned.
00:24:52.960
We want to trigger a page loaded event as soon as the page is rendered; this happens before DOM readiness as soon as the header is completed.
00:25:08.110
The resultant action will send documents title and any necessary meta-tags back, transmitting this to the device without waiting for complete loading.
00:25:24.860
Once this is complete, you then handle the event for purposes like animation and other tasks after the page has fully loaded.
00:25:36.700
For example, we created a 'Did Finish Loading' event that the native device triggers when it detects the full page load, preparing for subsequent actions.
00:25:52.980
Sometimes, presenting modals is unavoidable. In those cases, set an event to trigger, which creates the modal-like paradigm embedded with title and button parameters.
00:26:10.560
On the native side, this triggers a receiver that processes parameters, so buttons are added as straightforward JSON objects.
00:26:25.280
You can imagine the standard operations for parsing JSON and how they interact with the native code.
00:26:39.180
Now we can identify requests, communicate back and forth, manage displayed items, and even show modals. This leads to navigation.
00:26:54.900
Just getting navigation to behave correctly can make it feel like a functional app. However, mobile browsers struggle with proper navigation.
00:27:10.860
Clicking links can be hit-or-miss; often users may experience partial loading issues while navigating.
00:27:26.860
Every native platform handles navigation stacks differently—iOS uses a UI navigation controller while Android employs back stacks with fragments or activities.
00:27:42.740
We’ve opted for activities which can leverage built-in mechanics while managing memory considerations.
00:27:59.950
For handling push events, the process is manageable across platforms, fetching URLs and passing them along to delegates that facilitate pushing views.
00:28:14.580
Pop events work similarly, where navigating back relies on the native device executing the event.
00:28:31.390
For example, if you want to go back to the root, you'll allow the native device to take charge in this regard.
00:28:46.970
We would handle page load events by extracting parameters to instruct the current web view to load a different page instead of pushing a new view.
00:29:01.950
Of note, it’s crucial to handle invalid URLs—to prevent crashes, validate before rendering.
00:29:16.110
With native event handling, we continue to manage functionalities like link creation by leveraging a custom class that executes native events.
00:29:31.430
This enables basic integration between web experiences and mobile, allowing for effective cross-environment communication.
00:29:47.940
Additionally, a user may wish to submit a form, posing more challenges due to the potential costs of round trips, particularly on mobile.
00:30:03.120
Validation remains key; ensure to handle input checks on the client-side first, highlighting errors immediately to avoid user frustration.
00:30:18.620
You can show notifications for invalid inputs, allowing the user to recognize issues without confusion or repeated errors.
00:30:32.300
Processes like build forms and set rules may not be exceptional but are necessary as they ensure inputs are accurate before submission.
00:30:49.810
Make certain to extend your form handler's default abilities to include AJAX or other communication needed once you've established validations.
00:31:06.670
All forms should block UI during submissions to avoid double handling, and those actions should feature engaging visuals rather than standard loaders.
00:31:24.340
Control the flow through the responses—the app should delegate to the weblib for actions like navigation posts, preventing hard-coded navigations in native code.
00:31:40.780
We must be adaptive; the app's behavior could change when our web library gets updated, allowing for smoother transitions.
00:31:57.190
Errors, especially app crashes, will inevitably occur at inconvenient times. Tracking these via third-party services is essential to capturing crashes that aren’t immediately visible.
00:32:14.280
Handling JavaScript errors within web views is tricky; they can make the app seem unresponsive, which doesn’t inspire user confidence.
00:32:29.500
Implementing logging through the console could help; overwriting console.log to send logs to the native side during JavaScript contexts means you will see those errors.
00:32:47.490
Watch for network errors too—mobile connections fluctuate, and it's critical to design the system to handle real-time availability and failure gracefully.
00:33:05.040
Consider notifying users when there's a network issue, which is especially critical if the web is the sole source of truth and they need an internet connection.
00:33:22.150
We can identify network requests and exchange feedback, display titles and buttons, present modals, navigate, deal with errors, and properly handle form submissions.
00:33:43.270
With this overview across various challenges and solutions, I hope you've seen that even with a challenging code base, it’s prudent to not fear writing your own implementation.
00:34:01.800
Instead of relying heavily on existing frameworks, use them judiciously while crafting functional solutions that integrate the best of hybrid apps.
00:34:21.280
Thanks for your time. My name is David Muto, known as Pseudo Muda on the internet. Feel free to reach out with any questions!