This step-by-step tutorial is the quickest way to see Peregrine in action for yourself on Android. As a reminder, you can use the buttons above to switch platforms for this documentation.

In the interest of saving time and reducing complexity, this tutorial is short on words and also not that practical. You are encouraged to embellish the code and be playful! Learning should be fun. 🤓

Create an example app

Use Android Studio to create a throwaway Android app. Make sure to use the Empty Activity template and select Kotlin for language. Review these requirements for more information.

Install Peregrine

The first step is to install the Peregrine Android library.

  1. Open your app’s build.gradle file and add the following dependency.
dependencies {
    implementation 'com.peregrinejs:peregrine:0.0.4'
}
  1. Sync your project with Gradle files by clicking 'Sync Now’.

  2. Click “Make Project” to download and install the dependency.

Add web assets

Before we can use Peregrine, we need an index.html file and any accompanying web assets.

  1. In Android Studio, make sure you have an assets folder in your app. If not, create it by right-clicking your app in the Project panel and selecting New → Folder → Assets Folder.
  2. Right-click the assets folder and select New → Directory. Enter www as the name. This folder will serve as the root of the web assets.
  3. Right-click the www folder and select New → File. Enter index.html as the name.
  4. Paste the following contents into index.html.
<!DOCTYPE html>
<html dir="ltr" lang="en">
  <head>
    <meta charset="utf-8" />
    <title>My App</title>
    <meta
      name="viewport"
      content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
    />
  </head>
  <body>
    <p>Hello World!</p>
  </body>
</html>

Instantiate a Web Frame

For this example, we’ll replace the “Hello World!” native activity template with a Web Frame fragment that will render our index.html.

  1. In Android Studio, open activity_main.xml.
  2. Access the XML code by clicking 'Code’.
  3. Replace the ConstraintLayout with a FragmentContainerView. Notice we use the WebFrameFragment class from Peregrine, which will be instantiated by Android when this activity is opened. We assign it an ID of fragment_container_view so we can access the fragment instance in the activity.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.fragment.app.FragmentContainerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/fragment_container_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="com.peregrinejs.WebFrameFragment"
    tools:context=".MainActivity">
</androidx.fragment.app.FragmentContainerView>
  1. Next, open MainActivity.kt.
  2. Construct a Web Frame in the onCreate lifecycle method. Be sure to configure baseUrl to point to the www folder in your app.
  3. Find the fragment instance by ID, and associate the Web Frame with it by setting the frame property.
package com.example.myapp

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.peregrinejs.*
import com.peregrinejs.ext.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val configuration = WebFrame.Configuration(
           baseUrl = assets.uri("www"),
        )
        val frame = WebFrame(this, configuration = configuration)
        val fragment = supportFragmentManager
            .findFragmentById(R.id.fragment_container_view) as WebFrameFragment
        fragment.frame = frame
    }
}

Run the app

🚧

Under Construction

Add native functionality

Before we can do anything else, we need to give our Android app permission to access the Internet.

  1. In Android Studio, open AndroidManifest.xml
  2. Paste in the following permission.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:allowBackup="true"

Now let’s add some native functionality to our Android app that we can invoke from our web content.

  1. In Android Studio, open MainActivity.kt.
  1. Add a ping function that expects the string 'ping' to be sent as the request data—if so, 'pong' is sent back; otherwise, the request fails with a message and an error code.
import com.peregrinejs.*
import com.peregrinejs.ext.*

fun ping(call: Call) {
    if (call.request.text == "ping") {
        call.respond("pong")
    } else {
        call.fail("Expected 'ping' in request.", "EXPECTED_PING")
    }
}

class MainActivity : AppCompatActivity() {
  1. Add the functions option to the Web Frame configuration and pass in the ping function under the 'ping' key. This key will be used by the web client. Read more about remote interfaces.
        val configuration = WebFrame.Configuration(
            baseUrl = assets.uri("www"),
            functions = mapOf(
                "ping" to ::ping,
            ),
        )
        val frame = WebFrame(this, configuration = configuration)
  1. Open index.html.
  2. Add a script in <head> that imports the ProxyClient from the Peregrine web library and connect it to the window instance..
  3. Add a button to the <body> with an ID of pingButton.
  4. Attach a click event listener to the button by its ID. When clicked, we’ll call the ping function and print the response to the screen. If the request is unsuccessful, we’ll log the error message and code.
      content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
    />
    <script type="module">
      import { ProxyClient } from 'https://unpkg.com/@peregrine/web'
      const client = new ProxyClient()
      client.connect(window)
      const btn = document.getElementById('pingButton')
      btn.addEventListener('click', async () => {
          try {
              const response = await client.ping('ping')
              document.body.append(response, document.createElement('p'))
          } catch (error) {
              console.error(`${error.code}: ${error.message}`)
          }
      })
    </script>
  </head>
  <body>
    <p>Hello World!</p>
    <button id="pingButton">Ping!</button>
    <br />
  </body>
</html>

When we run the app again, we’ll see our button. Whenever the button is tapped, “pong” will be appended to the web view.

Congrats! 🎁

You’ve just built your first bit of cross-platform interaction powered by Peregrine. From here you have a few options:

  • Try some debugging by adding breakpoints in Android Studio and Chrome.
  • Change 'ping' to another string to see the error message and code we’ve defined.
  • Change .ping() to an unknown function name to see what happens.
  • Read about the architecture of Peregrine to learn more.
  • Continue on to learn two other main features of the Web Frame: events and path handlers.

Add native events

Now that we’ve seen how we can implement native functionality, let’s add native events that propagate to the web layer. We’ll be implementing a new function that will initiate a mock download and report status events to the web layer.

We’ll be using Kotlin coroutines to asynchronously send native events to the web layer. Coroutines and flows are quickly becoming the standard way of writing asynchronous code in Android. Coroutines have first-class support in AndroidX and the Kotlin language itself.

  1. In Android Studio, open build.gradle and add the following dependencies. Sync your project with Gradle files by clicking “Sync Now” and click 'Make Project' to download and install the dependency.
dependencies {
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
}
  1. Open MainActivity.kt.
  2. Create a MutableSharedFlow which will report status events.
  3. Create a startDownload suspended function that imitates downloading a file and sends progress events via the status flow.
  4. Add a lambda to the functions map under the 'startDownload' key which launches the startDownload couroutine within the activity’s lifecycle scope.
  5. Add the observables option to the Web Frame configuration and pass in the status flow under the 'downloadStatus$' key.
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.peregrinejs.*
import com.peregrinejs.ext.uri
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch

fun ping(call: Call) {
    if (call.request.text == "ping") {
        call.respond("pong")
    } else {
        call.fail("Expected 'ping' in request.", "EXPECTED_PING")
    }
}

val status = MutableSharedFlow<Event>()

suspend fun startDownload(call: Call) {
    status.emit(Event("pending"))
    call.respond()

    repeat(4) { i ->
        delay(500)
        status.emit(Event("progress: ${(i + 1) * 25}%"))
    }

    status.emit(Event("completed"))
}

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val configuration = WebFrame.Configuration(
            baseUrl = assets.uri("www"),
            remoteInterface = RemoteInterface(
                functions = mapOf(
                    "ping" to ::ping,
                    "startDownload" to { call ->
                        lifecycleScope.launch { startDownload(call) }
                    },
                ),
                observables = mapOf(
                    "downloadStatus$" to status,
                )
            )
  1. Open index.html.
  2. Add a button next to the ping button with the ID of downloadButton.
  3. Attach a click event listener to the button by its ID. When clicked, we’ll call the startDownload function to initiate the mock download.
  4. Create a listenForStatus function which asynchronously iterates over status events from the downloadStatus$ observable and prints them to the screen.
  5. Call listenForStatus immediately to begin listening.
            }
        })

        const downloadBtn = document.getElementById('downloadButton')
        downloadBtn.addEventListener('click', () => {
            client.startDownload()
        })

        const listenForStatus = async () => {
            for await (const status of client.downloadStatus$) {
                document.body.append(status, document.createElement('p'))
            }
        }

        listenForStatus()
    </script>
  </head>
  <body>
    <p>Hello World!</p>
    <button id="pingButton">Ping!</button>
    <button id="downloadButton">Download</button>
    <br />
  </body>

When we run the app again, we’ll see our new download button. When the button is tapped, the mock download statuses will be appended to the web view.

Add path handlers

🚧

Under Construction