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.gradlefile 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
assetsfolder 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
assetsfolder and select New → Directory. Enterwwwas the name. This folder will serve as the root of the web assets. - Right-click the
wwwfolder and select New → File. Enterindex.htmlas 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
ConstraintLayoutwith aFragmentContainerView. Notice we use theWebFrameFragmentclass from Peregrine, which will be instantiated by Android when this activity is opened. We assign it an ID offragment_container_viewso 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
onCreatelifecycle method. Be sure to configurebaseUrlto point to thewwwfolder in your app. - Find the fragment instance by ID, and associate the Web Frame with it by
setting the
frameproperty.
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
pingfunction 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
functionsoption to the Web Frame configuration and pass in thepingfunction 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 theProxyClientfrom the Peregrine web library and connect it to thewindowinstance.. - 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
pingfunction 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.gradleand 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
MutableSharedFlowwhich will report status events. - Create a
startDownloadsuspended function that imitates downloading a file and sends progress events via thestatusflow. - Add a lambda to the
functionsmap under the'startDownload'key which launches thestartDownloadcouroutine within the activity’s lifecycle scope. - Add the
observablesoption to the Web Frame configuration and pass in thestatusflow 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
startDownloadfunction to initiate the mock download. - Create a
listenForStatusfunction which asynchronously iterates over status events from thedownloadStatus$observable and prints them to the screen. - Call
listenForStatusimmediately 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.