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```swift ios/ExpoWebViewModule.swift 183import ExpoModulesCore 184 185public class ExpoWebViewModule: Module { 186 public func definition() -> ModuleDefinition { 187 Name("ExpoWebView") 188 189 View(ExpoWebView.self) { 190 Prop("url") { (view, url: URL) in 191 if view.webView.url != url { 192 let urlRequest = URLRequest(url: url) 193 view.webView.load(urlRequest) 194 } 195 } 196 } 197 } 198} 199``` 200 201### Android module 202 203```kotlin android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt 204package expo.modules.webview 205 206import expo.modules.kotlin.modules.Module 207import expo.modules.kotlin.modules.ModuleDefinition 208import java.net.URL 209 210class ExpoWebViewModule : Module() { 211 override fun definition() = ModuleDefinition { 212 Name("ExpoWebView") 213 214 View(ExpoWebView::class) { 215 Prop("url") { view: ExpoWebView, url: URL? -> 216 view.webView.loadUrl(url.toString()) 217 } 218 } 219 } 220} 221``` 222 223### TypeScript module 224 225All we need to do here is add the `url` prop to the `Props` type. 226 227```typescript src/ExpoWebView.tsx 228import { ViewProps } from 'react-native'; 229import { requireNativeViewManager } from 'expo-modules-core'; 230import * as React from 'react'; 231 232export type Props = { 233 url?: string; 234} & ViewProps; 235 236const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView'); 237 238export default function ExpoWebView(props: Props) { 239 return <NativeView {...props} />; 240} 241``` 242 243### Example app 244 245Finally, we can pass in a URL to our WebView component in the example app. 246 247```typescript example/App.tsx 248import { WebView } from 'expo-web-view'; 249 250export default function App() { 251 return <WebView style={{ flex: 1 }} url="https://expo.dev" />; 252} 253``` 254 255When you rebuild and run the app, you will now see the Expo homepage. 256 257## 6. Add an event to notify when the page has loaded 258 259[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. 260 261### iOS view and module 262 263On iOS, we need to implement `webView(_:didFinish:)` and make ExpoWebView extend `WKNavigationDelegate`. We can then call the `onLoad` from that delegate method. 264 265```swift ios/ExpoWebView.swift 266import ExpoModulesCore 267import WebKit 268 269class ExpoWebView: ExpoView, WKNavigationDelegate { 270 let webView = WKWebView() 271 let onLoad = EventDispatcher() 272 273 required init(appContext: AppContext? = nil) { 274 super.init(appContext: appContext) 275 clipsToBounds = true 276 webView.navigationDelegate = self 277 addSubview(webView) 278 } 279 280 override func layoutSubviews() { 281 webView.frame = bounds 282 } 283 284 func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 285 if let url = webView.url { 286 onLoad([ 287 "url": url.absoluteString 288 ]) 289 } 290 } 291} 292``` 293 294And we need to indicate in ExpoWebViewModule that the `View` has an `onLoad` event. 295 296```swift ios/ExpoWebViewModule.swift 297import ExpoModulesCore 298 299public class ExpoWebViewModule: Module { 300 public func definition() -> ModuleDefinition { 301 Name("ExpoWebView") 302 303 View(ExpoWebView.self) { 304 Events("onLoad") 305 306 Prop("url") { (view, url: URL) in 307 if view.webView.url != url { 308 let urlRequest = URLRequest(url: url) 309 view.webView.load(urlRequest) 310 } 311 } 312 } 313 } 314} 315``` 316 317### Android view and module 318 319On Android, we need to add override the `onPageFinished` function. We can then call the `onLoad` event handler that we defined in the module. 320 321```kotlin android/src/main/java/expo/modules/webview/ExpoWebView.kt 322package expo.modules.webview 323 324import android.content.Context 325import android.webkit.WebView 326import android.webkit.WebViewClient 327import expo.modules.kotlin.AppContext 328import expo.modules.kotlin.viewevent.EventDispatcher 329import expo.modules.kotlin.views.ExpoView 330 331class ExpoWebView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { 332 private val onLoad by EventDispatcher() 333 334 internal val webView = WebView(context).also { 335 it.layoutParams = LayoutParams( 336 LayoutParams.MATCH_PARENT, 337 LayoutParams.MATCH_PARENT 338 ) 339 340 it.webViewClient = object : WebViewClient() { 341 override fun onPageFinished(view: WebView, url: String) { 342 onLoad(mapOf("url" to url)) 343 } 344 } 345 346 addView(it) 347 } 348} 349``` 350 351And we need to indicate in ExpoWebViewModule that the `View` has an `onLoad` event. 352 353```kotlin android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt 354package expo.modules.webview 355 356import expo.modules.kotlin.modules.Module 357import expo.modules.kotlin.modules.ModuleDefinition 358import java.net.URL 359 360class ExpoWebViewModule : Module() { 361 override fun definition() = ModuleDefinition { 362 Name("ExpoWebView") 363 364 View(ExpoWebView::class) { 365 Events("onLoad") 366 367 Prop("url") { view: ExpoWebView, url: URL? -> 368 view.webView.loadUrl(url.toString()) 369 } 370 } 371 } 372} 373``` 374 375### TypeScript module 376 377Note 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`. 378 379```typescript src/ExpoWebView.tsx 380import { ViewProps } from 'react-native'; 381import { requireNativeViewManager } from 'expo-modules-core'; 382import * as React from 'react'; 383 384export type OnLoadEvent = { 385 url: string; 386}; 387 388export type Props = { 389 url?: string; 390 onLoad?: (event: { nativeEvent: OnLoadEvent }) => void; 391} & ViewProps; 392 393const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView'); 394 395export default function ExpoWebView(props: Props) { 396 return <NativeView {...props} />; 397} 398``` 399 400### Example app 401 402Now 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! 403 404```typescript example/App.tsx 405import { WebView } from 'expo-web-view'; 406 407export default function App() { 408 return ( 409 <WebView 410 style={{ flex: 1 }} 411 url="https://expo.dev" 412 onLoad={event => alert(`loaded ${event.nativeEvent.url}`)} 413 /> 414 ); 415} 416``` 417 418## 7. Bonus: Build a web browser UI around it 419 420Now 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. 421 422<Collapsible summary="example/App.tsx"> 423 424```typescript 425import { useState } from 'react'; 426import { ActivityIndicator, Platform, Text, TextInput, View } from 'react-native'; 427import { WebView } from 'expo-web-view'; 428import { StatusBar } from 'expo-status-bar'; 429 430export default function App() { 431 const [inputUrl, setInputUrl] = useState('https://docs.expo.dev/modules/'); 432 const [url, setUrl] = useState(inputUrl); 433 const [isLoading, setIsLoading] = useState(true); 434 435 return ( 436 <View style={{ flex: 1, paddingTop: Platform.OS === 'ios' ? 80 : 30 }}> 437 <TextInput 438 value={inputUrl} 439 onChangeText={setInputUrl} 440 returnKeyType="go" 441 autoCapitalize="none" 442 onSubmitEditing={() => { 443 if (inputUrl !== url) { 444 setUrl(inputUrl); 445 setIsLoading(true); 446 } 447 }} 448 keyboardType="url" 449 style={{ 450 color: '#fff', 451 backgroundColor: '#000', 452 borderRadius: 10, 453 marginHorizontal: 10, 454 paddingHorizontal: 20, 455 height: 60, 456 }} 457 /> 458 459 <WebView 460 url={url.startsWith('https://') || url.startsWith('http://') ? url : `https://${url}`} 461 onLoad={() => setIsLoading(false)} 462 style={{ flex: 1, marginTop: 20 }} 463 /> 464 <LoadingView isLoading={isLoading} /> 465 <StatusBar style="auto" /> 466 </View> 467 ); 468} 469 470function LoadingView({ isLoading }: { isLoading: boolean }) { 471 if (!isLoading) { 472 return null; 473 } 474 475 return ( 476 <View 477 style={{ 478 position: 'absolute', 479 bottom: 0, 480 left: 0, 481 right: 0, 482 height: 80, 483 backgroundColor: 'rgba(0,0,0,0.5)', 484 paddingBottom: 10, 485 justifyContent: 'center', 486 alignItems: 'center', 487 flexDirection: 'row', 488 }}> 489 <ActivityIndicator animating={isLoading} color="#fff" style={{ marginRight: 10 }} /> 490 <Text style={{ color: '#fff' }}>Loading...</Text> 491 </View> 492 ); 493} 494``` 495 496</Collapsible> 497 498 499 500## Next steps 501 502Congratulations, 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/). 503 504if 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. 505