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 7 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.config.HotwireCore
import dev.hotwire.core.logging.logError
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

Expand All @@ -15,7 +15,7 @@ abstract class StradaJsonConverter {
"or use the provided KotlinXJsonConverter."

inline fun <reified T> toObject(jsonData: String): T? {
val converter = requireNotNull(Hotwire.config.jsonConverter) { NO_CONVERTER }
val converter = requireNotNull(HotwireCore.config.jsonConverter) { NO_CONVERTER }

return when (converter) {
is KotlinXJsonConverter -> converter.toObject<T>(jsonData)
Expand All @@ -25,7 +25,7 @@ abstract class StradaJsonConverter {
}

inline fun <reified T> toJson(data: T): String {
val converter = requireNotNull(Hotwire.config.jsonConverter) { NO_CONVERTER }
val converter = requireNotNull(HotwireCore.config.jsonConverter) { NO_CONVERTER }

return when (converter) {
is KotlinXJsonConverter -> converter.toJson(data)
Expand Down
27 changes: 26 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,43 @@ 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() {
jayohms marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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)
}

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.
jayohms marked this conversation as resolved.
Show resolved Hide resolved
*/
var offlineRequestHandler: TurboOfflineRequestHandler? = null

/**
* Enables/disables debug logging. This should be disabled in production environments.
* Disabled by default.
Expand Down Expand Up @@ -55,7 +80,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
5 changes: 5 additions & 0 deletions core/src/main/kotlin/dev/hotwire/core/config/HotwireCore.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package dev.hotwire.core.config

object HotwireCore {
val config: HotwireConfig = HotwireConfig()
}
45 changes: 45 additions & 0 deletions core/src/main/kotlin/dev/hotwire/core/logging/CoreLog.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package dev.hotwire.core.logging

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

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

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

internal fun d(msg: String) = log(Log.DEBUG, msg)

internal fun w(msg: String) = log(Log.WARN, msg)

internal fun e(msg: String) = log(Log.ERROR, msg)

private fun log(logLevel: Int, msg: String) {
when (logLevel) {
Log.DEBUG -> if (debugEnabled) Log.d(DEFAULT_TAG, msg)
Log.WARN -> Log.w(DEFAULT_TAG, msg)
Log.ERROR -> Log.e(DEFAULT_TAG, msg)
}
}
}

private const val PAD_END_LENGTH = 35

internal fun logEvent(event: String, details: String = "") {
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}"
}
CoreLog.d("$event ".padEnd(PAD_END_LENGTH, '.') + " $description")
}

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

internal fun logError(event: String, error: Exception) {
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package dev.hotwire.core.turbo.config

import android.content.Context
import com.google.gson.reflect.TypeToken
import dev.hotwire.core.lib.logging.logError
import dev.hotwire.core.lib.logging.logEvent
import dev.hotwire.core.logging.logError
import dev.hotwire.core.logging.logEvent
import dev.hotwire.core.turbo.util.dispatcherProvider
import dev.hotwire.core.turbo.util.toObject
import kotlinx.coroutines.CoroutineScope
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package dev.hotwire.core.turbo.config
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import dev.hotwire.core.lib.logging.logError
import dev.hotwire.core.logging.logError
import dev.hotwire.core.turbo.http.TurboHttpClient
import dev.hotwire.core.turbo.util.dispatcherProvider
import dev.hotwire.core.turbo.util.toJson
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package dev.hotwire.core.turbo.config

import com.google.gson.annotations.SerializedName
import dev.hotwire.core.BuildConfig
import dev.hotwire.core.lib.logging.logError
import dev.hotwire.core.logging.logError
import java.util.regex.PatternSyntaxException

internal data class PathConfigurationRule(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import android.content.Intent
import android.net.Uri
import android.provider.MediaStore
import android.webkit.WebChromeClient.FileChooserParams
import dev.hotwire.core.lib.logging.logError
import dev.hotwire.core.logging.logError
import dev.hotwire.core.turbo.util.TurboFileProvider
import java.io.File
import java.io.IOException
Expand Down
Loading
Loading