Skip to content

Commit

Permalink
Merge pull request #10 from hotwired/bridge-initialize
Browse files Browse the repository at this point in the history
Initialize `Bridge` instances within the library
  • Loading branch information
jayohms authored Apr 6, 2023
2 parents 35b91bc + 65d75f0 commit 326923d
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 28 deletions.
57 changes: 43 additions & 14 deletions strada/src/main/kotlin/dev/hotwire/strada/Bridge.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,74 +2,82 @@ package dev.hotwire.strada

import android.webkit.JavascriptInterface
import android.webkit.WebView
import androidx.annotation.VisibleForTesting
import kotlinx.serialization.json.JsonElement
import java.lang.ref.WeakReference

// These need to match whatever is set in strada.js
private const val bridgeGlobal = "window.nativeBridge"
private const val bridgeJavascriptInterface = "Strada"

@Suppress("unused")
class Bridge(private val webView: WebView) {
internal var repository = Repository()
class Bridge internal constructor(webView: WebView) {
private var componentsAreRegistered: Boolean = false
private val webViewRef: WeakReference<WebView>

var delegate: BridgeDelegate<*>? = null
internal val webView: WebView? get() = webViewRef.get()
internal var repository = Repository()
internal var delegate: BridgeDelegate<*>? = null

init {
// Use a weak reference in case the WebView is no longer being
// used by the app, such as when the render process is gone.
webViewRef = WeakReference(webView)

// The JavascriptInterface must be added before the page is loaded
webView.addJavascriptInterface(this, bridgeJavascriptInterface)
}

fun register(component: String) {
internal fun register(component: String) {
logEvent("bridgeWillRegisterComponent", component)
val javascript = generateJavaScript("register", component.toJsonElement())
evaluate(javascript)
}

fun register(components: List<String>) {
internal fun register(components: List<String>) {
logEvent("bridgeWillRegisterComponents", components.joinToString())
val javascript = generateJavaScript("register", components.toJsonElement())
evaluate(javascript)
}

fun unregister(component: String) {
internal fun unregister(component: String) {
logEvent("bridgeWillUnregisterComponent", component)
val javascript = generateJavaScript("unregister", component.toJsonElement())
evaluate(javascript)
}

fun send(message: Message) {
internal fun send(message: Message) {
logMessage("bridgeWillSendMessage", message)
val internalMessage = InternalMessage.fromMessage(message)
val javascript = generateJavaScript("send", internalMessage.toJson().toJsonElement())
evaluate(javascript)
}

fun load() {
internal fun load() {
logEvent("bridgeWillLoad")
evaluate(userScript())
}

fun reset() {
internal fun reset() {
logEvent("bridgeDidReset")
componentsAreRegistered = false
}

fun isReady(): Boolean {
internal fun isReady(): Boolean {
return componentsAreRegistered
}

@JavascriptInterface
fun bridgeDidInitialize() {
logEvent("bridgeDidInitialize")
logEvent("bridgeDidInitialize", "success")
runOnUiThread {
delegate?.bridgeDidInitialize()
}
}

@JavascriptInterface
fun bridgeDidUpdateSupportedComponents() {
logEvent("bridgeDidUpdateSupportedComponents")
logEvent("bridgeDidUpdateSupportedComponents", "success")
componentsAreRegistered = true
}

Expand All @@ -85,12 +93,13 @@ class Bridge(private val webView: WebView) {
// Internal

internal fun userScript(): String {
return repository.getUserScript(webView.context)
val context = requireNotNull(webView?.context)
return repository.getUserScript(context)
}

internal fun evaluate(javascript: String) {
logEvent("evaluatingJavascript", javascript)
webView.evaluateJavascript(javascript) {}
webView?.evaluateJavascript(javascript) {}
}

internal fun generateJavaScript(bridgeFunction: String, vararg arguments: JsonElement): String {
Expand All @@ -106,4 +115,24 @@ class Bridge(private val webView: WebView) {
internal fun sanitizeFunctionName(name: String): String {
return name.removeSuffix("()")
}

companion object {
private val instances = mutableListOf<Bridge>()

fun initialize(webView: WebView) {
if (getBridgeFor(webView) == null) {
initialize(Bridge(webView))
}
}

@VisibleForTesting
internal fun initialize(bridge: Bridge) {
instances.add(bridge)
instances.removeIf { it.webView == null }
}

internal fun getBridgeFor(webView: WebView): Bridge? {
return instances.firstOrNull { it.webView == webView }
}
}
}
16 changes: 11 additions & 5 deletions strada/src/main/kotlin/dev/hotwire/strada/BridgeDelegate.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.hotwire.strada

import android.webkit.WebView
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner

Expand Down Expand Up @@ -30,12 +31,17 @@ class BridgeDelegate<D : BridgeDestination>(
bridge?.reset()
}

fun onWebViewAttached(bridge: Bridge?) {
this.bridge = bridge
this.bridge?.delegate = this
fun onWebViewAttached(webView: WebView) {
bridge = Bridge.getBridgeFor(webView)?.apply {
delegate = this@BridgeDelegate
}

if (shouldReloadBridge()) {
loadBridgeInWebView()
if (bridge != null) {
if (shouldReloadBridge()) {
loadBridgeInWebView()
}
} else {
logEvent("bridgeNotInitializedForWebView", destination.bridgeDestinationLocation())
}
}

Expand Down
23 changes: 14 additions & 9 deletions strada/src/test/kotlin/dev/hotwire/strada/BridgeDelegateTest.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package dev.hotwire.strada

import android.webkit.WebView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.testing.TestLifecycleOwner
import com.nhaarman.mockito_kotlin.*
import com.nhaarman.mockito_kotlin.eq
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.never
import com.nhaarman.mockito_kotlin.whenever
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
Expand All @@ -11,6 +15,7 @@ import org.mockito.Mockito.verify
class BridgeDelegateTest {
private lateinit var delegate: BridgeDelegate<AppBridgeDestination>
private val bridge: Bridge = mock()
private val webView: WebView = mock()

private val factories = listOf(
BridgeComponentFactory("one", ::OneBridgeComponent),
Expand All @@ -19,29 +24,30 @@ class BridgeDelegateTest {

@Before
fun setup() {
whenever(bridge.webView).thenReturn(webView)
Bridge.initialize(bridge)

delegate = BridgeDelegate(
destination = AppBridgeDestination(),
componentFactories = factories
)
delegate.bridge = bridge
}

@Test
fun loadBridgeInWebView() {
delegate.onWebViewAttached(bridge)
delegate.loadBridgeInWebView()
verify(bridge, times(2)).load()
verify(bridge).load()
}

@Test
fun resetBridge() {
delegate.onWebViewAttached(bridge)
delegate.resetBridge()
verify(bridge).reset()
}

@Test
fun bridgeDidInitialize() {
delegate.onWebViewAttached(bridge)
delegate.bridgeDidInitialize()
verify(bridge).register(eq(listOf("one", "two")))
}
Expand Down Expand Up @@ -77,30 +83,29 @@ class BridgeDelegateTest {
@Test
fun onWebViewAttached() {
whenever(bridge.isReady()).thenReturn(false)
delegate.onWebViewAttached(bridge)
delegate.onWebViewAttached(webView)

assertEquals(delegate.bridge, bridge)
}

@Test
fun onWebViewAttachedShouldLoad() {
whenever(bridge.isReady()).thenReturn(false)
delegate.onWebViewAttached(bridge)
delegate.onWebViewAttached(webView)

verify(bridge).load()
}

@Test
fun onWebViewAttachedShouldNotLoad() {
whenever(bridge.isReady()).thenReturn(true)
delegate.onWebViewAttached(bridge)
delegate.onWebViewAttached(webView)

verify(bridge, never()).load()
}

@Test
fun onWebViewDetached() {
delegate.onWebViewAttached(bridge)
delegate.onWebViewDetached()

assertNull(delegate.bridge?.delegate)
Expand Down

0 comments on commit 326923d

Please sign in to comment.