1--- 2title: 'Tutorial: Creating a native view' 3sidebar_title: Creating a native view 4--- 5 6import { CodeBlocksTable } from '~/components/plugins/CodeBlocksTable'; 7import { PlatformTag } from '~/ui/components/Tag'; 8import { APIBox } from '~/components/plugins/APIBox'; 9import { Terminal } from '~/ui/components/Snippet'; 10import { Collapsible } from '~/ui/components/Collapsible'; 11 12In this tutorial we are going to build a module with a native view that will render a WebView. We will be using the [WebView](https://developer.android.com/reference/android/webkit/WebView) component for Android and [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview) for iOS. It is possible to implement web support using [`iframe`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe), but we'll leave that as an exercise for the reader. 13 14## 1. Initialize a new module 15 16First, we'll create a new module. On this page we will use the name `expo-web-view` / `ExpoWebView`. You can name it whatever you like, just adjust the instructions accordingly: 17 18<Terminal cmd={['$ npx create-expo-module expo-web-view']} /> 19 20> **Tip**: Since you aren't going to actually ship this library, you can hit <kbd>return</kbd> for all of the prompts to accept the default values. 21 22## 2. Set up our workspace 23 24Now let's clean up the default module a little bit so we have more of a clean slate and delete the code that we won't use in this guide. 25 26<Terminal 27 cmdCopy="cd expo-web-view && rm src/ExpoWebView.types.ts src/ExpoWebView.web.tsx src/ExpoWebViewModule.ts src/ExpoWebViewModule.web.ts" 28 cmd={[ 29 '$ cd expo-web-view', 30 '$ rm src/ExpoWebView.types.ts src/ExpoWebViewModule.ts', 31 '$ rm src/ExpoWebView.web.tsx src/ExpoWebViewModule.web.ts', 32 ]} 33/> 34 35Find the following files and replace them with the provided minimal boilerplate: 36 37```swift ios/ExpoWebViewModule.swift 38import ExpoModulesCore 39 40public class ExpoWebViewModule: Module { 41 public func definition() -> ModuleDefinition { 42 Name("ExpoWebView") 43 44 View(ExpoWebView.self) {} 45 } 46} 47``` 48 49```kotlin android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt 50package expo.modules.webview 51 52import expo.modules.kotlin.modules.Module 53import expo.modules.kotlin.modules.ModuleDefinition 54 55class ExpoWebViewModule : Module() { 56 override fun definition() = ModuleDefinition { 57 Name("ExpoWebView") 58 59 View(ExpoWebView::class) {} 60 } 61} 62``` 63 64```typescript src/index.ts 65export { default as WebView, Props as WebViewProps } from './ExpoWebView'; 66``` 67 68```typescript src/ExpoWebView.tsx 69import { ViewProps } from 'react-native'; 70import { requireNativeViewManager } from 'expo-modules-core'; 71import * as React from 'react'; 72 73export type Props = ViewProps; 74 75const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView'); 76 77export default function ExpoWebView(props: Props) { 78 return <NativeView {...props} />; 79} 80``` 81 82```typescript example/App.tsx 83import { WebView } from 'expo-web-view'; 84 85export default function App() { 86 return <WebView style={{ flex: 1, backgroundColor: 'purple' }} />; 87} 88``` 89 90## 3. Run the example project 91 92Now let's run the example project to make sure everything is working. We'll need to start the TypeScript compiler to watch for changes and rebuild the module JavaScript, and separately in another terminal window we'll compile and run the example app. 93 94<Terminal 95 cmdCopy="npm run build" 96 cmd={[ 97 '# Run this in the root of the project to start the TypeScript compiler', 98 '$ npm run build', 99 ]} 100/> 101 102<Terminal 103 cmdCopy="cd example && npx expo run:ios" 104 cmd={[ 105 '$ cd example', 106 '# Run the example app on iOS', 107 '$ npx expo run:ios', 108 '# Run the example app on Android', 109 '$ npx expo run:android', 110 ]} 111/> 112 113We should now see... a blank purple screen. That's not very exciting, but it's a good start. Let's actually make it a WebView now. 114 115## 4. Add the system WebView as a subview 116 117Now we are going to add the system WebView with a hardcoded URL as a subview of our ExpoWebView. Our `ExpoWebView` class extends `ExpoView`, which extends `RCTView` from React Native, which finally extends `UIView` on iOS and `View` on Android. We need to ensure that the WebView subview has the same layout as ExpoWebView, whose layout will be calculated by React Native's layout engine. 118 119### iOS view 120 121On iOS, we set `clipsToBounds` to `true` and set the `frame` of the WebView to the bounds of the ExpoWebView in `layoutSubviews` to match the layout. `init` is called when the view is created, and `layoutSubviews` is called when the layout changes. 122 123```swift ios/ExpoWebView.swift 124import ExpoModulesCore 125import WebKit 126 127class ExpoWebView: ExpoView { 128 let webView = WKWebView() 129 130 required init(appContext: AppContext? = nil) { 131 super.init(appContext: appContext) 132 clipsToBounds = true 133 addSubview(webView) 134 135 let url = URL(string:"https://docs.expo.dev/modules/")! 136 let urlRequest = URLRequest(url:url) 137 webView.load(urlRequest) 138 } 139 140 override func layoutSubviews() { 141 webView.frame = bounds 142 } 143} 144``` 145 146### Android view 147 148On Android we use `LayoutParams` to set the layout of the WebView to match the layout of the ExpoWebView. We can do this when we instantiate the WebView. 149 150```kotlin android/src/main/java/expo/modules/webview/ExpoWebView.kt 151package expo.modules.webview 152 153import android.content.Context 154import android.webkit.WebView 155import android.webkit.WebViewClient 156import expo.modules.kotlin.AppContext 157import expo.modules.kotlin.views.ExpoView 158 159class ExpoWebView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { 160 internal val webView = WebView(context).also { 161 it.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) 162 it.webViewClient = object : WebViewClient() {} 163 addView(it) 164 165 it.loadUrl("https://docs.expo.dev/modules/") 166 } 167} 168``` 169 170### Example app 171 172No changes are needed, we can rebuild and run the app and you will see the [Expo Modules API overview page](/modules/). 173 174## 5. Add a prop to set the URL 175 176To set a prop on our view, we'll need to define the prop name and setter inside of `ExpoWebViewModule`. In this case we're going to reach in and access `webView` property directly for convenience, but in many real world cases you will likely want to keep this logic inside of the `ExpoWebView` class and minimize the knowledge that `ExpoWebViewModule` has about the internals of `ExpoWebView`. 177 178We use the [Prop definition component](/modules/module-api/#prop) to define the prop. Within the prop setter block we can access the view and the prop. Note that we specify the url is of type `URL` — the Expo modules API will take care of converting strings to the native `URL` type for us. 179 180### iOS module 181 182 183```swift ios/ExpoWebViewModule.swift 184import ExpoModulesCore 185 186public class ExpoWebViewModule: Module { 187 public func definition() -> ModuleDefinition { 188 Name("ExpoWebView") 189 190 View(ExpoWebView.self) { 191 Prop("url") { (view, url: URL) in 192 if view.webView.url != url { 193 let urlRequest = URLRequest(url: url) 194 view.webView.load(urlRequest) 195 } 196 } 197 } 198 } 199} 200``` 201 202### Android module 203 204```kotlin android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt 205package expo.modules.webview 206 207import expo.modules.kotlin.modules.Module 208import expo.modules.kotlin.modules.ModuleDefinition 209import java.net.URL 210 211class ExpoWebViewModule : Module() { 212 override fun definition() = ModuleDefinition { 213 Name("ExpoWebView") 214 215 View(ExpoWebView::class) { 216 Prop("url") { view: ExpoWebView, url: URL? -> 217 view.webView.loadUrl(url.toString()) 218 } 219 } 220 } 221} 222``` 223 224### TypeScript module 225 226All we need to do here is add the `url` prop to the `Props` type. 227 228```typescript src/ExpoWebView.tsx 229import { ViewProps } from 'react-native'; 230import { requireNativeViewManager } from 'expo-modules-core'; 231import * as React from 'react'; 232 233export type Props = { 234 url?: string; 235} & ViewProps; 236 237const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView'); 238 239export default function ExpoWebView(props: Props) { 240 return <NativeView {...props} />; 241} 242``` 243 244### Example app 245 246Finally, we can pass in a URL to our WebView component in the example app. 247 248```typescript example/App.tsx 249import { WebView } from 'expo-web-view'; 250 251export default function App() { 252 return <WebView style={{ flex: 1 }} url="https://expo.dev" />; 253} 254``` 255 256When you rebuild and run the app, you will now see the Expo homepage. 257 258## 6. Add an event to notify when the page has loaded 259 260[View callbacks](/modules/module-api/#view-callbacks) allow developers to listen for events on components. They are typically registered through props on the component, for example: `<Image onLoad={...} />`. We can use the [Events definition component](/modules/module-api/#events) to define an event for our WebView. We'll call it `onLoad` as well. 261 262### iOS view and module 263 264On iOS, we need to implement `webView(_:didFinish:)` and make ExpoWebView extend `WKNavigationDelegate`. We can then call the `onLoad` from that delegate method. 265 266```swift ios/ExpoWebView.swift 267import ExpoModulesCore 268import WebKit 269 270class ExpoWebView: ExpoView, WKNavigationDelegate { 271 let webView = WKWebView() 272 let onLoad = EventDispatcher() 273 274 required init(appContext: AppContext? = nil) { 275 super.init(appContext: appContext) 276 clipsToBounds = true 277 webView.navigationDelegate = self 278 addSubview(webView) 279 } 280 281 override func layoutSubviews() { 282 webView.frame = bounds 283 } 284 285 func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 286 if let url = webView.url { 287 onLoad([ 288 "url": url.absoluteString 289 ]) 290 } 291 } 292} 293``` 294 295And we need to indicate in ExpoWebViewModule that the `View` has an `onLoad` event. 296 297```swift ios/ExpoWebViewModule.swift 298import ExpoModulesCore 299 300public class ExpoWebViewModule: Module { 301 public func definition() -> ModuleDefinition { 302 Name("ExpoWebView") 303 304 View(ExpoWebView.self) { 305 Events("onLoad") 306 307 Prop("url") { (view, url: URL) in 308 if view.webView.url != url { 309 let urlRequest = URLRequest(url: url) 310 view.webView.load(urlRequest) 311 } 312 } 313 } 314 } 315} 316``` 317 318### Android view and module 319 320On Android, we need to add override the `onPageFinished` function. We can then call the `onLoad` event handler that we defined in the module. 321 322```kotlin android/src/main/java/expo/modules/webview/ExpoWebView.kt 323package expo.modules.webview 324 325import android.content.Context 326import android.webkit.WebView 327import android.webkit.WebViewClient 328import expo.modules.kotlin.AppContext 329import expo.modules.kotlin.viewevent.EventDispatcher 330import expo.modules.kotlin.views.ExpoView 331 332class ExpoWebView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { 333 private val onLoad by EventDispatcher() 334 335 internal val webView = WebView(context).also { 336 it.layoutParams = LayoutParams( 337 LayoutParams.MATCH_PARENT, 338 LayoutParams.MATCH_PARENT 339 ) 340 341 it.webViewClient = object : WebViewClient() { 342 override fun onPageFinished(view: WebView, url: String) { 343 onLoad(mapOf("url" to url)) 344 } 345 } 346 347 addView(it) 348 } 349} 350``` 351 352And we need to indicate in ExpoWebViewModule that the `View` has an `onLoad` event. 353 354```kotlin android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt 355package expo.modules.webview 356 357import expo.modules.kotlin.modules.Module 358import expo.modules.kotlin.modules.ModuleDefinition 359import java.net.URL 360 361class ExpoWebViewModule : Module() { 362 override fun definition() = ModuleDefinition { 363 Name("ExpoWebView") 364 365 View(ExpoWebView::class) { 366 Events("onLoad") 367 368 Prop("url") { view: ExpoWebView, url: URL? -> 369 view.webView.loadUrl(url.toString()) 370 } 371 } 372 } 373} 374``` 375 376### TypeScript module 377 378Note that event payloads are included within the `nativeEvent` property of the event, so to access the `url` from the `onLoad` event we would read `event.nativeEvent.url`. 379 380```typescript src/ExpoWebView.tsx 381import { ViewProps } from 'react-native'; 382import { requireNativeViewManager } from 'expo-modules-core'; 383import * as React from 'react'; 384 385export type OnLoadEvent = { 386 url: string; 387}; 388 389export type Props = { 390 url?: string; 391 onLoad?: (event: { nativeEvent: OnLoadEvent }) => void; 392} & ViewProps; 393 394const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView'); 395 396export default function ExpoWebView(props: Props) { 397 return <NativeView {...props} />; 398} 399``` 400 401### Example app 402 403Now we can update the example app to show an alert when the page has loaded. Copy in the following code, then rebuild and run your app, and you should see the alert! 404 405```typescript example/App.tsx 406import { WebView } from 'expo-web-view'; 407 408export default function App() { 409 return ( 410 <WebView 411 style={{ flex: 1 }} 412 url="https://expo.dev" 413 onLoad={event => alert(`loaded ${event.nativeEvent.url}`)} 414 /> 415 ); 416} 417``` 418 419## 7. Bonus: Build a web browser UI around it 420 421Now that we have a web view, we can build a web browser UI around it. Have some fun trying to rebuild a browser UI, and maybe even add new native capabilities as needed (for example, to support a back or reload buttons). If you'd like some inspiration, there's a simple example below. 422 423<Collapsible summary="example/App.tsx"> 424 425```typescript 426import { useState } from "react"; 427import { 428 ActivityIndicator, 429 Platform, 430 Text, 431 TextInput, 432 View, 433} from "react-native"; 434import { WebView } from "expo-web-view"; 435import { StatusBar } from "expo-status-bar"; 436 437export default function App() { 438 const [inputUrl, setInputUrl] = useState("https://docs.expo.dev/modules/"); 439 const [url, setUrl] = useState(inputUrl); 440 const [isLoading, setIsLoading] = useState(true); 441 442 return ( 443 <View style={{ flex: 1, paddingTop: Platform.OS === "ios" ? 80 : 30 }}> 444 <TextInput 445 value={inputUrl} 446 onChangeText={setInputUrl} 447 returnKeyType="go" 448 autoCapitalize="none" 449 onSubmitEditing={() => { 450 if (inputUrl !== url) { 451 setUrl(inputUrl); 452 setIsLoading(true); 453 } 454 }} 455 keyboardType="url" 456 style={{ 457 color: "#fff", 458 backgroundColor: "#000", 459 borderRadius: 10, 460 marginHorizontal: 10, 461 paddingHorizontal: 20, 462 height: 60, 463 }} 464 /> 465 466 <WebView 467 url={ 468 url.startsWith("https://") || url.startsWith("http://") 469 ? url 470 : `https://${url}` 471 } 472 onLoad={() => setIsLoading(false)} 473 style={{ flex: 1, marginTop: 20 }} 474 /> 475 <LoadingView isLoading={isLoading} /> 476 <StatusBar style="auto" /> 477 </View> 478 ); 479} 480 481function LoadingView({ isLoading }: { isLoading: boolean }) { 482 if (!isLoading) { 483 return null; 484 } 485 486 return ( 487 <View 488 style={{ 489 position: "absolute", 490 bottom: 0, 491 left: 0, 492 right: 0, 493 height: 80, 494 backgroundColor: "rgba(0,0,0,0.5)", 495 paddingBottom: 10, 496 justifyContent: "center", 497 alignItems: "center", 498 flexDirection: "row", 499 }} 500 > 501 <ActivityIndicator 502 animating={isLoading} 503 color="#fff" 504 style={{ marginRight: 10 }} 505 /> 506 <Text style={{ color: "#fff" }}>Loading...</Text> 507 </View> 508 ); 509} 510 511``` 512 513</Collapsible> 514 515 516 517## Next steps 518 519Congratulations, you have created your first simple yet non-trivial Expo module with a native view for iOS and Android! Learn more about the API in the [Expo Module API reference](/modules/module-api/). 520 521if you enjoyed this tutorial and haven't done the native module tutorial, go to the ["creating a native module" tutorial](/modules/native-module-tutorial/) next. 522