Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract navigation-fragments module from core module #51

Merged
merged 12 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 16 additions & 22 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ val developerEmail by extra("androidteam@basecamp.com")
android {
namespace = "dev.hotwire.core"
compileSdk = 34

testOptions.unitTests.isIncludeAndroidResources = true
testOptions.unitTests.isReturnDefaultValues = true
testOptions.targetSdk = 34

defaultConfig {
minSdk = 28
targetSdk = 34
}

buildTypes {
Expand Down Expand Up @@ -71,50 +72,43 @@ android {

dependencies {
// Kotlin
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.22")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.23")

// Material
implementation("com.google.android.material:material:1.11.0")
implementation("com.google.android.material:material:1.12.0")

// AndroidX
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.lifecycle:lifecycle-common:2.7.0")
implementation("androidx.lifecycle:lifecycle-common:2.8.1")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")

// JSON
implementation("com.google.code.gson:gson:2.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")

// Networking/API
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")

// Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")

// Browser
implementation("androidx.browser:browser:1.7.0")

// Exported AndroidX dependencies
api("androidx.appcompat:appcompat:1.6.1")
api("androidx.core:core-ktx:1.12.0")
api("androidx.webkit:webkit:1.8.0")
api("androidx.activity:activity-ktx:1.8.1")
api("androidx.fragment:fragment-ktx:1.6.2")
api("androidx.navigation:navigation-fragment-ktx:2.7.5")
api("androidx.navigation:navigation-ui-ktx:2.7.5")
api("androidx.appcompat:appcompat:1.7.0")
api("androidx.core:core-ktx:1.13.1")
api("androidx.webkit:webkit:1.11.0")

// Tests
testImplementation("androidx.test:core:1.5.0") // Robolectric
testImplementation("androidx.navigation:navigation-testing:2.7.5")
testImplementation("androidx.navigation:navigation-testing:2.7.7")
testImplementation("androidx.arch.core:core-testing:2.2.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
testImplementation("org.assertj:assertj-core:3.24.2")
testImplementation("org.robolectric:robolectric:4.11.1")
testImplementation("org.mockito:mockito-core:5.2.0")
testImplementation("org.assertj:assertj-core:3.25.3")
testImplementation("org.robolectric:robolectric:4.12.1")
testImplementation("org.mockito:mockito-core:5.11.0")
testImplementation("com.nhaarman:mockito-kotlin:1.6.0")
testImplementation("com.squareup.okhttp3:mockwebserver:4.11.0")
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
testImplementation("junit:junit:4.13.2")
}

Expand Down
6 changes: 3 additions & 3 deletions core/src/main/kotlin/dev/hotwire/core/bridge/Bridge.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package dev.hotwire.core.bridge
import android.webkit.JavascriptInterface
import android.webkit.WebView
import androidx.annotation.VisibleForTesting
import dev.hotwire.core.lib.logging.logEvent
import dev.hotwire.core.logging.logEvent
import kotlinx.serialization.json.JsonElement
import java.lang.ref.WeakReference

Expand All @@ -18,7 +18,7 @@ class Bridge internal constructor(webView: WebView) {

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

init {
// Use a weak reference in case the WebView is no longer being
Expand Down Expand Up @@ -120,7 +120,7 @@ class Bridge internal constructor(webView: WebView) {
companion object {
private val instances = mutableListOf<Bridge>()

internal fun initialize(webView: WebView) {
fun initialize(webView: WebView) {
if (getBridgeFor(webView) == null) {
initialize(Bridge(webView))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package dev.hotwire.core.bridge

import dev.hotwire.core.lib.logging.logWarning
import dev.hotwire.core.logging.logWarning

abstract class BridgeComponent(
abstract class BridgeComponent<in D : BridgeDestination>(
val name: String,
private val delegate: BridgeDelegate
private val delegate: BridgeDelegate<D>
) {
private val receivedMessages = hashMapOf<String, Message>()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package dev.hotwire.core.bridge

class BridgeComponentFactory<out C : BridgeComponent> constructor(
class BridgeComponentFactory<D : BridgeDestination, out C : BridgeComponent<D>> constructor(
val name: String,
private val creator: (name: String, delegate: BridgeDelegate) -> C
private val creator: (name: String, delegate: BridgeDelegate<D>) -> C
) {
fun create(delegate: BridgeDelegate) = creator(name, delegate)
fun create(delegate: BridgeDelegate<D>) = creator(name, delegate)
}
20 changes: 9 additions & 11 deletions core/src/main/kotlin/dev/hotwire/core/bridge/BridgeDelegate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,22 @@ package dev.hotwire.core.bridge
import android.webkit.WebView
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import dev.hotwire.core.config.Hotwire
import dev.hotwire.core.lib.logging.logEvent
import dev.hotwire.core.lib.logging.logWarning
import dev.hotwire.core.turbo.nav.HotwireNavDestination
import dev.hotwire.core.logging.logEvent
import dev.hotwire.core.logging.logWarning

@Suppress("unused")
class BridgeDelegate(
class BridgeDelegate<D : BridgeDestination>(
val location: String,
val destination: HotwireNavDestination
val destination: D,
private val componentFactories: List<BridgeComponentFactory<D, BridgeComponent<D>>>
) : DefaultLifecycleObserver {
internal var bridge: Bridge? = null
private var destinationIsActive: Boolean = false
private val componentFactories = Hotwire.registeredBridgeComponentFactories
private val initializedComponents = hashMapOf<String, BridgeComponent>()
private val initializedComponents = hashMapOf<String, BridgeComponent<D>>()
private val resolvedLocation: String
get() = bridge?.webView?.url ?: location

val activeComponents: List<BridgeComponent>
val activeComponents: List<BridgeComponent<D>>
get() = initializedComponents.map { it.value }.takeIf { destinationIsActive }.orEmpty()

fun onColdBootPageCompleted() {
Expand Down Expand Up @@ -75,7 +73,7 @@ class BridgeDelegate(
}

private fun shouldReloadBridge(): Boolean {
return destination.navigator.session.isReady && bridge?.isReady() == false
return destination.bridgeWebViewIsReady() && bridge?.isReady() == false
}

// Lifecycle events
Expand Down Expand Up @@ -107,7 +105,7 @@ class BridgeDelegate(
activeComponents.filterIsInstance<C>().forEach { action(it) }
}

private fun getOrCreateComponent(name: String): BridgeComponent? {
private fun getOrCreateComponent(name: String): BridgeComponent<D>? {
val factory = componentFactories.firstOrNull { it.name == name } ?: return null
return initializedComponents.getOrPut(name) { factory.create(this) }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package dev.hotwire.core.bridge

interface BridgeDestination {
fun bridgeWebViewIsReady(): Boolean
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package dev.hotwire.core.bridge

import dev.hotwire.core.lib.logging.logError
import dev.hotwire.core.logging.logError
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package dev.hotwire.core.bridge

import dev.hotwire.core.config.Hotwire
import dev.hotwire.core.lib.logging.logError
import dev.hotwire.core.logging.logError
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

Expand Down
63 changes: 1 addition & 62 deletions core/src/main/kotlin/dev/hotwire/core/config/Hotwire.kt
Original file line number Diff line number Diff line change
@@ -1,77 +1,16 @@
package dev.hotwire.core.config

import android.content.Context
import androidx.fragment.app.Fragment
import dev.hotwire.core.bridge.BridgeComponent
import dev.hotwire.core.bridge.BridgeComponentFactory
import dev.hotwire.core.navigation.fragments.HotwireWebBottomSheetFragment
import dev.hotwire.core.navigation.fragments.HotwireWebFragment
import dev.hotwire.core.navigation.routing.AppNavigationRouteDecisionHandler
import dev.hotwire.core.navigation.routing.BrowserRouteDecisionHandler
import dev.hotwire.core.navigation.routing.Router
import dev.hotwire.core.turbo.config.PathConfiguration
import kotlin.reflect.KClass

object Hotwire {
internal var registeredBridgeComponentFactories:
List<BridgeComponentFactory<BridgeComponent>> = emptyList()
private set

internal var registeredFragmentDestinations:
List<KClass<out Fragment>> = listOf(
HotwireWebFragment::class,
HotwireWebBottomSheetFragment::class
)
private set

internal var router = Router(listOf(
AppNavigationRouteDecisionHandler(),
BrowserRouteDecisionHandler()
))

val config: HotwireConfig = HotwireConfig()

/**
* The path configuration that defines your navigation rules.
*/
val pathConfiguration = PathConfiguration()

/**
* Loads the [PathConfiguration] JSON file(s) from the provided location to
* configure navigation rules.
*/
fun loadPathConfiguration(context: Context, location: PathConfiguration.Location) {
pathConfiguration.load(context, location)
}

/**
* Registers the [Router.RouteDecisionHandler] instances that determine whether to route location
* urls within in-app navigation or with alternative custom behaviors.
*/
fun registerRouteDecisionHandlers(decisionHandlers: List<Router.RouteDecisionHandler>) {
router = Router(decisionHandlers)
}

/**
* Register bridge components that the app supports. Every possible bridge
* component, wrapped in a [BridgeComponentFactory], must be provided here.
*/
fun registerBridgeComponents(factories: List<BridgeComponentFactory<BridgeComponent>>) {
registeredBridgeComponentFactories = factories
}

/**
* The default fragment destination for web requests. If you have not
* loaded a path configuration with a matching rule and a `uri` available
* for all possible paths, this destination will be used as the default.
*/
var defaultFragmentDestination: KClass<out Fragment> = HotwireWebFragment::class

/**
* Register fragment destinations that can be navigated to. Every possible
* destination must be provided here.
*/
fun registerFragmentDestinations(destinations: List<KClass<out Fragment>>) {
registeredFragmentDestinations = destinations
config.pathConfiguration.load(context, location)
}
}
19 changes: 18 additions & 1 deletion core/src/main/kotlin/dev/hotwire/core/config/HotwireConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,35 @@ package dev.hotwire.core.config

import android.content.Context
import android.webkit.WebView
import dev.hotwire.core.bridge.BridgeComponent
import dev.hotwire.core.bridge.BridgeComponentFactory
import dev.hotwire.core.bridge.StradaJsonConverter
import dev.hotwire.core.turbo.config.PathConfiguration
import dev.hotwire.core.turbo.http.TurboHttpClient
import dev.hotwire.core.turbo.http.TurboOfflineRequestHandler
import dev.hotwire.core.turbo.views.TurboWebView

class HotwireConfig internal constructor() {
/**
* The path configuration that defines your navigation rules.
*/
val pathConfiguration = PathConfiguration()

var registeredBridgeComponentFactories:
List<BridgeComponentFactory<*, BridgeComponent<*>>> = emptyList()

/**
* Set a custom JSON converter to easily decode Message.dataJson to a data
* object in received messages and to encode a data object back to json to
* reply with a custom message back to the web.
*/
var jsonConverter: StradaJsonConverter? = null

/**
* Experimental: API may be removed, not ready for production use.
*/
var offlineRequestHandler: TurboOfflineRequestHandler? = null

/**
* Enables/disables debug logging. This should be disabled in production environments.
* Disabled by default.
Expand Down Expand Up @@ -55,7 +72,7 @@ class HotwireConfig internal constructor() {
* calling this so the bridge component names are included in your user agent.
*/
fun userAgentSubstring(): String {
val components = Hotwire.registeredBridgeComponentFactories.joinToString(" ") { it.name }
val components = registeredBridgeComponentFactories.joinToString(" ") { it.name }
return "Turbo Native Android; bridge-components: [$components];"
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package dev.hotwire.core.lib.logging
package dev.hotwire.core.logging

import android.util.Log
import dev.hotwire.core.config.Hotwire

internal object HotwireLog {
private const val DEFAULT_TAG = "Hotwire"
internal object CoreLog {
private const val DEFAULT_TAG = "Hotwire-Core"

private val debugEnabled get() = Hotwire.config.debugLoggingEnabled

Expand All @@ -26,20 +26,20 @@ internal object HotwireLog {
private const val PAD_END_LENGTH = 35

internal fun logEvent(event: String, details: String = "") {
HotwireLog.d("$event ".padEnd(PAD_END_LENGTH, '.') + " [$details]")
CoreLog.d("$event ".padEnd(PAD_END_LENGTH, '.') + " [$details]")
}

internal fun logEvent(event: String, attributes: List<Pair<String, Any>>) {
val description = attributes.joinToString(prefix = "[", postfix = "]", separator = ", ") {
"${it.first}: ${it.second}"
}
HotwireLog.d("$event ".padEnd(PAD_END_LENGTH, '.') + " $description")
CoreLog.d("$event ".padEnd(PAD_END_LENGTH, '.') + " $description")
}

internal fun logWarning(event: String, details: String) {
HotwireLog.w("$event ".padEnd(PAD_END_LENGTH, '.') + " [$details]")
CoreLog.w("$event ".padEnd(PAD_END_LENGTH, '.') + " [$details]")
}

internal fun logError(event: String, error: Exception) {
HotwireLog.e("$event: ${error.stackTraceToString()}")
CoreLog.e("$event: ${error.stackTraceToString()}")
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import com.google.gson.annotations.SerializedName
import dev.hotwire.core.config.Hotwire
import dev.hotwire.core.turbo.nav.HotwireDestination
import dev.hotwire.core.turbo.nav.TurboNavPresentation
import dev.hotwire.core.turbo.nav.TurboNavPresentationContext
import dev.hotwire.core.turbo.nav.TurboNavQueryStringPresentation
Expand Down Expand Up @@ -60,7 +58,7 @@ class PathConfiguration {
* Loads and parses the specified configuration file(s) from their local
* and/or remote locations.
*/
internal fun load(context: Context, location: Location) {
fun load(context: Context, location: Location) {
if (loader == null) {
loader = PathConfigurationLoader(context.applicationContext)
}
Expand Down Expand Up @@ -133,9 +131,8 @@ val PathConfigurationProperties.context: TurboNavPresentationContext
TurboNavPresentationContext.DEFAULT
}

val PathConfigurationProperties.uri: Uri
get() = get("uri")?.toUri() ?:
HotwireDestination.from(Hotwire.defaultFragmentDestination).uri.toUri()
val PathConfigurationProperties.uri: Uri?
get() = get("uri")?.toUri()

val PathConfigurationProperties.fallbackUri: Uri?
get() = get("fallback_uri")?.toUri()
Expand Down
Loading
Loading