rubyday 2015

Making hybrid apps that don't suck

Making hybrid apps that don't suck

by David Muto

In his 2015 talk at RubyDay in Turin titled "Making Hybrid Apps That Don't Suck," David Muto discusses the challenges and strategies involved in developing mobile hybrid applications that provide a native feel. He starts by introducing himself and his work at Shopify, which powers over 200,000 stores worldwide. The main focus is on the development of a hybrid app called Sello, designed to facilitate selling products seamlessly.

Key Points Discussed:

- Hybrid App Definition: Muto clarifies that hybrid apps combine elements of both web and native applications, allowing for rapid iteration and reduced need for frequent updates via app stores.

- Framework Challenges: He notes that many existing frameworks did not meet their needs for a native-like experience, leading to a careful evaluation of trade-offs between web technology and native components.

- Design Philosophy: Muto emphasizes a strong aversion to local state management, opting for a web-centric approach where the web serves as the sole source of truth.

- Communication Between Web and Native: He outlines their strategy for enabling communication between web interfaces and native functionality through JavaScript objects, allowing seamless message passing.

- Backend Integration: Muto explains the integration with Rails, leveraging existing infrastructure while ensuring support for multiple app versions in production is manageable.

- Event Handling System: The event hub concept introduced allows for an asynchronous pub-sub model where events can trigger actions in either the native environment or the web view.

- User Experience Concerns: The importance of preemptive validation, error handling, and user notifications based on network conditions are all stressed to enhance user experience.

- Navigational and Modal Challenges: He discusses approaches to handling navigation effectively and how to display modals efficiently, emphasizing the necessity for native event handling to improve the app's responsiveness.

Conclusion: Muto concludes his talk with a call to embrace the careful crafting of hybrid apps rather than relying solely on existing frameworks. He encourages adaptability, suggesting that developers should not fear writing custom implementations that integrate hybrid app features effectively while maintaining a native-like experience. This perspective aims to foster innovation in the hybrid app development landscape and demonstrates that well-designed hybrid apps can function successfully.

Throughout his lecture, David Muto provides practical insights and coding strategies for overcoming the complexities involved in building hybrid applications, underscoring the potential for improved user experience when proper methodologies are employed.

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!