Making iOS & Android Apps with Rails & Turbolinks

Mark Biegel
June 7, 2017

So you want to build an app? One of the first decisions that needs to be made is one of time and resources. For a business, it is crucial to know your market in order to successfully launch your product. But what about the App market? Which platform you do you choose to launch to first? Apple? Android? Web?

At this point you might have already considered a HTML5 web app so you can be cross platform and launch to everyone. Or perhaps you decided against it, due to poor performance and not having a “native app” feel.

Turbolinks is a library that is designed to speed up web apps. By doing so, it makes web apps run at fantastic speed on mobile devices essentially overcoming the slow nature of traditional HTML5 web apps.

But the real benefit of building a Turbolinks-enabled web app is that Turbolinks itself comes with iOS and Android native wrappers that give the app a natural feel. Another bonus is being able to submit your app and have it discoverable in the app stores.

How to Get Started

To get started you will need a few key ingredients:

  • A Ruby on Rails app with Turbolinks enabled.
  • The Turbolinks iOS wrapper.
  • Xcode software (Install via App store)
  • The Android iOS wrapper.
  • Android Studio

Once you have your web app running, you can start on the very quick mobile implementation.

Regardless of which platform you choose to start with (iOS/Android), once you have your project set up, the theory is the same. Your mobile app will consist of three core parts:

  • A Base Controller/Activity.
  • A Turbolinks Navigation Controller/Activity.
  • A Javascript Notification Handler.

The Base Controller/Activity is the entry point to your app. This is where you set up default menus and authentication checks if you require user authentication.

//iOS Base Controller example class BaseVisitableViewController: Turbolinks.VisitableViewController{ // Perform any native setup. // Turbolinks.VisitableViewController is where the magic happens }

``` // Android Base Activity example public class BaseActivity extends AppCompatActivity { TurbolinksView turbolinksView; private TurbolinksHelper turbolinksHelper;

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_base);

    location = getIntent().hasExtra(INTENT_URL) ? getIntent().getStringExtra(INTENT_URL) : getString(R.string.base_url);
    turbolinksHelper = new TurbolinksHelper(this, this, turbolinksView);
    turbolinksHelper.visit(location + path);
  } } ```

The Turbolinks Navigation Controller/Activity is the heart of your native app.

This controls what happens when you click the links in your web app such as:

  • Open a link in an external browser.
  • Push navigation to a new screen.
  • Open the page as a modal.
  • Dismiss a modal.
  • Open the Camera.
  • Show Native alert messages.

``` // iOS Turbolinks Controller example class TurbolinksNavigationController: UINavigationController{ private lazy var session: Session = { let session = Session(webViewConfiguration: self.webViewConfiguration) session.delegate = self return session }()

private lazy var webViewConfiguration: WKWebViewConfiguration = { let configuration = WKWebViewConfiguration() configuration.userContentController.addScriptMessageHandler(self, name: “YourAppUserAgent”) return configuration }()

func visit(URL: NSURL) {
    // open a native camera action
  showVisitableViewControllerWithUrl(URL, action: .Advance)
private func showVisitableViewControllerWithUrl(URL: NSURL, action: Action) {
  let visitable : BaseVisitableViewController = BaseVisitableViewController(URL: finalUrl)
  switch action {
    case .Advance:
        pushViewController(visitable, animated: true)
    case .Replace:
        if viewControllers.count == 1 {
            setViewControllers([visitable], animated: false)
        } else {
            pushViewController(visitable, animated: false)
} } ```

``` // Android Turbolinks Activity example public class TurbolinksHelper implements JsListener, TurbolinksAdapter {

public TurbolinksHelper(Context context, Activity activity, TurbolinksView turbolinksView) { setupTurbolinks(); } private void setupTurbolinks() { TurbolinksSession.getDefault(context).getWebView().getSettings().setJavaScriptEnabled(true); TurbolinksSession.getDefault(context).getWebView().addJavascriptInterface(new JsBridge(context, this), “android”); } public void visit(String url, boolean withCachedSnapshot) { if (withCachedSnapshot) { TurbolinksSession.getDefault(context).activity(activity).adapter(this).restoreWithCachedSnapshot(true).view(turbolinksView).visit(url); } else { TurbolinksSession.getDefault(context).activity(activity).adapter(this).view(turbolinksView).visit(url); } } // Handle your URL requests @Override public void visitProposedToLocationWithAction(String location, String action) { if (action.equals(“replace”)) { visit(location, true); } else if(location.contains(“/photo/new”)){ // Open a Camera intent rather than a page }else { visitToNewActivity(location); } } } ```

Lastly you will need a Javascript Notification Handler.

This will allow your web app to interact with the native side of your iOS/Android app.

When your web app performs actions like navigation and form submission it does so using javascript. Since your web app is running inside an iOS or Android Webview, you can handle the javascript responses and direct the app to perform actions with the returned data.

``` // iOS Javascript Message Handler Example extension TurbolinksNavigationController: WKScriptMessageHandler { func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { switch { case “YourAppUserAgent”: //handle your JS messages

        fatalError("Unexpected message")
} } ``` ``` // Android Javascript Message Handler Example

public interface JsListener { void loginSuccessful(); void signoutSuccessful(); } public class JsBridge {

private Context context;
private JsListener listener;
public JsBridge(@NonNull Context context, @NonNull JsListener listener) {
    this.context = context;
    this.listener = listener;
public void postMessage(String jsonString)  {
    try {
      JSONObject jsonObject = new JSONObject(jsonString);
      // handle your JS message
    }catch (JSONException e){
} } ```

From your Rails app you would communicate with the JS message handler like this:

<script> TurbolinksNativeMessageHandler.postMessage(JSON.stringify({your_app_user_agent: "Your Message Data"})); </script>

A Note on Authentication

It is a good idea to handle the authentication process outside of the Turbolinks flow.

If you want to stay signed into a Turbolinks app then, like most apps, you should use a token-based authentication system, and store the token in the iOS/Android apps local storage.

If you were to simply use Turbolinks to handle the sign-in, you would need to be aware of the state of other Turbolinks views that are already loaded. This is especially relevant if you are using a tab-based menu system as all the tabs are preloaded and therefore would all be set to a sign-in page.

The End Result

With these few files in place you can quickly have your web app running with a full native feel on iOS and Android devices.

Not only can this save you countless hours of initial development, but unlike HTML5 web apps, you will be able to submit your native apps to the Apple AppStore and Google Play store.

Into the Future

Future development does not need to be limited to Turbolinks at all. You can always take your mobile apps further and start building out the pages as full native components if you wish to do so.

The benefit of the Turbolinks framework works is that you control how the app functions, and if you want a “click” to open a section of your app that used to be web app-based and is now native, then you can simply tell the Turbolinks Navigation controller to do so.