Why would you do such a thing?

Imagine you are alone, or two working on a great project. You need to provide a web application + an Android app + an Ios app. What do you do?

Do you create a web application using react, vue? and then maybe use react-native to create mobile apps? React tends to add a lot of complexity and in the end needs experts to maintain it. Here are some podcasts talking about it ruby rogues RR342 ruby rogues mrs 034

I thinks there is a more productive way to do it. This more productive way involves using Turbolinks 5, its headline is: “Get the performance benefits of a single-page application without the added complexity of a client-side JavaScript framework.”.

Guess what by default Rails apps uses Turbolinks :)

This makes it super easy to create a web application (by the way rails is the most productive framework I know of) and mobile application. To create mobile application we will just need to add one or to file to a template project.

Let’s get started

The code source is available at https://github.com/jean-francois-labbe/use-turbolinks.git

First create a Rails app

First you will need to setup ruby, I would recommend you to use ruby-install to install ruby.

Install ruby-install

Then ruby-install ruby

One day you will have or need multiple versions of ruby, to manage this I recommend you chruby

Install chruby

Then you will be able to use chruby ruby to automatically switch to the latest ruby version on your computer.

Now that ruby is installed

Run gem install bundler

Bundler is the great dependency manager that makes it easy to work with dependencies in ruby. Yes, if you know pip, you’ll love it. It even has had a dependencies locking mechanism for years that npm just added few months ago.

Then install Rails

Run gem install rails

Generate a new project: rails new blog

This will scaffold everything for you.

Go into that project cd blog

Take time to explore all the file created. Here is the getting started

The application is empty let’s add something in it.

Run rails g scaffold post title:string body:text

Open the config/routes.rb file and make it look like this:

Rails.application.routes.draw do
  resources :posts
  # For details on the DSL available within this file,
  # see http://guides.rubyonrails.org/routing.html

  root to: 'posts#index'
end

The root to: makes the posts index page the root page of the web application

Run rails db:migrate as we have created a new resource post` the database schema must be updated.

Launch rails server rails s

Open your browser and go to: http://localhost:3000

This is the same as http://localhost:3000/posts because of the root to: 'posts#index'

There you go you can list posts, create new post, delete existing posts.

We’ll see later how to add some beauty in it.

We scratched the surface of what rails can do for you, it provides database migrations, websockets, background jobs, sending emails, preview emails, catching emails en dev, multiple environments configured (dev, test, prod)… In fact most of web applications problems have already been solved in Rails, The beauty of it is that you won’t have to solve them again and most importantly maintain your implementation.

Plus there are a ton of battle tested gems to help you create something faster, such as devise(user authentication), paper_trail(model history), stripe(online payment)…

We have a pretty awesome application we need now to validate early on if using a server side rendered application + Turbolinks would be enough to provide good user experience on mobile apps.

I will use only Android as I don’t have anything to develop for Ios.

First Setup Android Studio

Then Create a new project with an empty activity.

Go to turbolinks-android

Follow their tutorial then you will have an android application that can bind to your rails application.

Your Activity will look like this:

package com.example.you.app;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import com.basecamp.turbolinks.TurbolinksAdapter;
import com.basecamp.turbolinks.TurbolinksSession;
import com.basecamp.turbolinks.TurbolinksView;

public class MainActivity extends AppCompatActivity implements TurbolinksAdapter {

    // Change the BASE_URL to an address that your VM or device can hit.
    private static final String BASE_URL = "http://192.168.1.16:3000"; //Change the ip to your point at the rails server most likely your computer ip
    private static final String INTENT_URL = "intentUrl";

    private String location;
    private TurbolinksView turbolinksView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Find the custom TurbolinksView object in your layout
        turbolinksView = (TurbolinksView) findViewById(R.id.turbolinks_view);

        // For this demo app, we force debug logging on. You will only want to do
        // this for debug builds of your app (it is off by default)
        TurbolinksSession.getDefault(this).setDebugLoggingEnabled(true);

        // For this example we set a default location, unless one is passed in through an intent
        location = getIntent().getStringExtra(INTENT_URL) != null ? getIntent().getStringExtra(INTENT_URL) : BASE_URL;

        // Execute the visit
        TurbolinksSession.getDefault(this)
                .activity(this)
                .adapter(this)
                .view(turbolinksView)
                .visit(location);
    }

    @Override
    protected void onRestart() {
        super.onRestart();

        // Since the webView is shared between activities, we need to tell Turbolinks
        // to load the location from the previous activity upon restarting
        TurbolinksSession.getDefault(this)
                .activity(this)
                .adapter(this)
                .restoreWithCachedSnapshot(false)
                .view(turbolinksView)
                .visit(location);
    }

    @Override
    public void onPageFinished() {

    }

    @Override
    public void onReceivedError(int errorCode) {
        handleError(errorCode);
    }

    @Override
    public void pageInvalidated() {

    }

    @Override
    public void requestFailedWithStatusCode(int statusCode) {
        handleError(statusCode);
    }

    @Override
    public void visitCompleted() {

    }

    @Override
    public void visitProposedToLocationWithAction(String location, String action) {
        TurbolinksSession.getDefault(this)
                .activity(this)
                .adapter(this)
                .view(turbolinksView)
                .visit(location);
    }

    // Simply forwards to an error page, but you could alternatively show your own native screen
    // or do whatever other kind of error handling you want.
    private void handleError(int code) {
        if (code == 404) {
            TurbolinksSession.getDefault(this)
                    .activity(this)
                    .adapter(this)
                    .restoreWithCachedSnapshot(false)
                    .view(turbolinksView)
                    .visit(BASE_URL + "/error");
        }
    }
}

Then use Android Studio to deploy that on your phone.

Ensure the your phone and your computer are on the same wifi, and that your rails server is still up and running.

Your android application will open up and display the rails web application and you will be able to interact with it.

Yeahhh you did it.

Handle outside url

Sometimes your web application need to go to another web site then come back. Those times such as authentication using Gitlab or an Oauth provider.

Currently if your rails application wants to send you to another website it won’t work because it won’t be on the same domain.

We can handle that.

Let’s say that our rails application send us on another domain for authentication, we can catch the authentication url call and use another Activity to display it.

Well do that

Just add an activity such as WebViewActivity

package com.example.you.app;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class WebViewActivity extends AppCompatActivity {

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

        WebView webView = (WebView) findViewById(R.id.webview);

        webView.setWebChromeClient(new WebChromeClient());

        webView.setWebViewClient(new WebViewClient() {

            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                if (url.contains("your_return_url or /")) {
                    finish();
                } else {
                    view.loadUrl(url);
                }
                return true;
            }
        });

        String oauthUrl = getIntent().getStringExtra("location");
        webView.loadUrl(oauthUrl);
    }
}

Then update your main activity


package com.example.you.app;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import com.basecamp.turbolinks.TurbolinksAdapter;
import com.basecamp.turbolinks.TurbolinksSession;
import com.basecamp.turbolinks.TurbolinksView;

public class MainActivity extends AppCompatActivity implements TurbolinksAdapter {

    // Change the BASE_URL to an address that your VM or device can hit.
    private static final String BASE_URL = "http://192.168.1.16:3000"; //Change the ip to your local ip
    private static final String INTENT_URL = "intentUrl";
    public static final String OAUTH_URL = "users/auth/gitlab";

    private String location;
    private TurbolinksView turbolinksView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Find the custom TurbolinksView object in your layout
        turbolinksView = (TurbolinksView) findViewById(R.id.turbolinks_view);

        // For this demo app, we force debug logging on. You will only want to do
        // this for debug builds of your app (it is off by default)
        TurbolinksSession.getDefault(this).setDebugLoggingEnabled(true);

        // For this example we set a default location, unless one is passed in through an intent
        location = getIntent().getStringExtra(INTENT_URL) != null ? getIntent().getStringExtra(INTENT_URL) : BASE_URL;

        // Execute the visit
        TurbolinksSession.getDefault(this)
                .activity(this)
                .adapter(this)
                .view(turbolinksView)
                .visit(location);
    }

    @Override
    protected void onRestart() {
        super.onRestart();

        // Since the webView is shared between activities, we need to tell Turbolinks
        // to load the location from the previous activity upon restarting
        TurbolinksSession.getDefault(this)
                .activity(this)
                .adapter(this)
                .restoreWithCachedSnapshot(false)
                .view(turbolinksView)
                .visit(location);
    }

    @Override
    public void onPageFinished() {

    }

    @Override
    public void onReceivedError(int errorCode) {
        handleError(errorCode);
    }

    @Override
    public void pageInvalidated() {

    }

    @Override
    public void requestFailedWithStatusCode(int statusCode) {
        handleError(statusCode);
    }

    @Override
    public void visitCompleted() {

    }

    @Override
    public void visitProposedToLocationWithAction(String location, String action) {
        if(location.contains(OAUTH_URL)) {
            Intent intent = new Intent(MainActivity.this, WebViewActivity.class);
            intent.putExtra("location", location);
            this.startActivity(intent);
        } else {
        TurbolinksSession.getDefault(this)
                .activity(this)
                .adapter(this)
                .view(turbolinksView)
                .visit(location);
        }
    }

    // Simply forwards to an error page, but you could alternatively show your own native screen
    // or do whatever other kind of error handling you want.
    private void handleError(int code) {
        if (code == 404) {
            TurbolinksSession.getDefault(this)
                    .activity(this)
                    .adapter(this)
                    .restoreWithCachedSnapshot(false)
                    .view(turbolinksView)
                    .visit(BASE_URL + "/error");
        }
    }
}

Notice how visitProposedToLocationWithAction has been updated to catch the authentication url and display it in a new activity that is just an embedded web browser.

This way you can login then return to the main activity and view its content.

Add some beauty

This is working but it’s now awesome.

Let’s make it quick with twitter bootstrap.

Rails fully embrace javascript world and allow us to use yarn and even webpacker that will be preconfigured (another day not lost).

Run yarn add bootstrap

Open app/assets/stylesheets/application.css

Make it look like so:

/*
 *= require bootstrap/dist/css/bootstrap.min.css
 *= require_tree .
 *= require_self
 */

Open app/views/layouts/application.html.erb

Change the <body> to <body class="container-fluid">

Open app/view/posts/_form.html.erb

Make it look like so:

<%= form_with(model: post, local: true) do |form| %>
  <% if post.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>

      <ul>
      <% post.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="form-group">
    <%= form.label :title %>
    <%= form.text_field :title, class: 'form-control' %>
  </div>

  <div class="form-group">
    <%= form.label :body %>
    <%= form.text_area :body, class: 'form-control' %>
  </div>

  <div class="form-group">
    <%= form.submit class: 'btn btn-primary' %>
  </div>
<% end %>

It will look like so on your browser:

And in android:

The end.

That’s it, now you now how to create a basic rails app, scaffold a post, create a basic android application that can display your rails application

The code source is available at https://github.com/jean-francois-labbe/use-turbolinks.git

Do you feel productive?