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.
- Open your app’s
build.gradle
file and add the following dependency.
dependencies {
implementation 'com.peregrinejs:peregrine:0.0.4'
}
-
Sync your project with Gradle files by clicking 'Sync Now’.
-
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.
- 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. - Right-click the
assets
folder and select New → Directory. Enterwww
as the name. This folder will serve as the root of the web assets. - Right-click the
www
folder and select New → File. Enterindex.html
as the name. - 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
.
- In Android Studio, open
activity_main.xml
. - Access the XML code by clicking 'Code’.
- Replace the
ConstraintLayout
with aFragmentContainerView
. Notice we use theWebFrameFragment
class from Peregrine, which will be instantiated by Android when this activity is opened. We assign it an ID offragment_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>
- Next, open
MainActivity.kt
. - Construct a Web Frame in the
onCreate
lifecycle method. Be sure to configurebaseUrl
to point to thewww
folder in your app. - 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.
- In Android Studio, open
AndroidManifest.xml
- 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.
- In Android Studio, open
MainActivity.kt
.
- 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() {
- Add the
functions
option to the Web Frame configuration and pass in theping
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)
- Open
index.html
. - Add a script in
<head>
that imports theProxyClient
from the Peregrine web library and connect it to thewindow
instance.. - Add a button to the
<body>
with an ID ofpingButton
. - 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.
- 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'
}
- Open
MainActivity.kt
. - Create a
MutableSharedFlow
which will report status events. - Create a
startDownload
suspended function that imitates downloading a file and sends progress events via thestatus
flow. - Add a lambda to the
functions
map under the'startDownload'
key which launches thestartDownload
couroutine within the activity’s lifecycle scope. - Add the
observables
option to the Web Frame configuration and pass in thestatus
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,
)
)
- Open
index.html
. - Add a button next to the ping button with the ID of
downloadButton
. - Attach a click event listener to the button by its ID. When clicked, we’ll
call the
startDownload
function to initiate the mock download. - Create a
listenForStatus
function which asynchronously iterates over status events from thedownloadStatus$
observable and prints them to the screen. - 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.