Skip to content

v0.19.3

Compare
Choose a tag to compare
@bitspittle bitspittle released this 15 Nov 23:56

Some fun miscellaneous items in this one, including breakpoint ranges, Kotlinx serialization support for HTTP verbs, and support for small/large/dynamic viewport units.

Important

Planning to upgrade? Review instructions in the README.

Changes

Frontend

  • Fixed development live reload behavior, which now works even if the tab is not visible when the site is finished rebuilding.
  • HTTP fetch methods now support serialization.
    • See "HTTP verb serialization" notes section for more details.
  • You can now specify breakpoint ranges in CSS styles.
    • For example: (Breakpoint.SM until Breakpoint.LG) { ... } targets tablet and desktop screens (but no wider).
    • The README breakpoints section has been updated with more examples.
  • New Modifier.attr and Modifier.dataAttr attribute modifiers.
    • Modifier.attr("key", "value") is a slightly simpler substittute for Modifier.attrsModifier { attr("key", "value") }
    • Modifier.dataAttr("key", "value") is short for Modifier.attrsModifier { attr("data-key", "value") }
  • Added support for repainting canvases manually.
    • See "Repainting canvases manually" notes section for more details.
  • New CSS units for small / large / dynamic viewport sizes
    • These modern (and relatively recent) units let designers create sites that respond better to mobile devices that have UI elements that hide / show regularly.
    • See this article for examples.
  • Fixed a reported issue where attempting to query AppGlobals in InitSilk and InitKobweb methods would crash.
  • ⚠️ routePrefix has been renamed to basePath
    • You will be warned to update this value in your conf.yaml file.
    • This name is more consistent with what other web frameworks use.
  • Simplified a bunch of APIs that used to take autoPrefix parameters.
    • It is not expected that any user outside of the Kobweb library internally actually used this parameter, but they have been removed. At this point, if you set a basePath value in your conf.yaml file, you should just expect that Kobweb will automatically add it every time.

Backend

  • Updated ktor to v3.0.0
    • And migrated / cleaned up code related to SSE (server-sent events), which is used for live reloading.
  • Fixed an issue with spaces in paths in our start.bat server script.
  • Server start scripts now include the server layout
    • In other words, kobweb export --layout fullstack and kobweb export --layout static generate slightly different scripts.

Build

  • Jetbrains Compose dependency updated to 1.7.1
  • Disable compose trace markers for kobweb modules.
    • This was extra compose code that had no effect for Compose HTML sites, so disabling it should result in some JS file size savings.

Notes

HTTP verb + serialization

The HTTP standard provides a core set of verbs (GET, POST, PUT, etc.) which Kobweb provides Kotlin bindings for. For example:

// `get` returns a ByteArray, which we convert into a string. It is up to us to interpret that further,
// perhaps by parsing it
val unparsedData = window.api.get("/user/123").decodeToString()

With this latest release, if your site depends on kotlinx serialization (setup instructions here) and a kobweb serialization artifact (discussed shortly), you can leverage it to get a fully parsed type.

First, you should add a dependency on the artifact that provides the utility methods:

# libs.versions.toml
[libraries]
kobwebx-serialization-kotlinx = { module = "com.varabyte.kobwebx:kobwebx-serialization-kotlinx", version.ref = "kobweb" }
// site/build.gradle.ktx

// apply kotlinx serialization plugin

jsMain.dependencies {
  // ...
  // add kotlinx serialization dependency
  implementation(libs.kobwebx.serialization.kotlinx)
}

And that point, your project should have access to kotlinx-serialization-aware methods:

@Serializable
class UserDataResponse(val id: String, val name: String, val age: Int)

val userData = window.api.get<UserDataResponse>("/user/123")
check(userData.id == "123")

You can also pass in rich types for the body parameter, if you have one:

@Serializable
class ChangeUserNameRequest(val id: String, val name: String)

@Serializable
class ChangeUserNameResponse(val error: String?)

val response = window.api.put<ChangeUserNameRequest, ChangeUserNameResponse>(
   "/user/change-name", body = ChangeUserNameRequest("123", "Kobweb")
)
if (response.error != null) { /* ... */ }

Repainting canvases manually

Canvases (specifically, Canvas2d and CanvasGl widgets) were designed to repaint continuously, which was honestly inspired by my background in gamedev. However, it is absolutely valid to have a canvas that only paints once and then stops until the user interacts with the canvas, or at least, whose repaint is infrequent and controlled by the developer.

To this end, we introduced the new CanvasRepainter class, which you can optionally pass into a canvas and use it to control when that canvas rerenders:

// Before: Continuously rendering canvas
Canvas2d(width, height, minDeltaMs = ONE_FRAME_MS_60_FPS) {
   /* rendered about every 15ms */
}

// New: Repaint on click
val repainter = remember { CanvasRepainter() }
Canvas2d(width, height, Modifier.onClick { repainter.repaint() }, repainter = repainter) {
   /* rerendered every time the user clicks on this canvas */
}

Here is an example which refills the canvas with a random color every click:

const val WIDTH = 320
const val HEIGHT = 240

// Later...

val repainter = remember { CanvasRepainter() }
Canvas2d(
    WIDTH, HEIGHT,
    Modifier.onClick { repainter.repaint() },
    repainter = repainter
) {
    ctx.fillStyle = Color.rgb(Random.nextInt(255), Random.nextInt(255), Random.nextInt(255))
    ctx.fillRect(0.0, 0.0, WIDTH.toDouble(), HEIGHT.toDouble())
}

If you don't care about repainting at all and just want a canvas that renders something unique once and then never again, you can pass in the REPAINT_CANVAS_MANUALLY value to the minDeltaMs parameter and then just never request a repaint:

Canvas2d(width, height, minDeltaMs = REPAINT_CANVAS_MANUALLY) {
   /* renders exactly once */
}

A note on tryRoutingTo / navigateTo and base paths

If you have RoutePrefix.prepend code anywhere in your project (which is probably very few of you all):

ctx.router.tryRoutingTo(RoutePrefix.prepend("/"))

it is no longer necessary to auto-prepend anything yourself. In this release, tryRoutingTo and navigateTo have been updated to prepend these values for you automatically.

In other words, you can find those calls and simplify your code to:

ctx.router.tryRoutingTo("/")

and it will just work.

Old code will continue to work for now, but if you look at your dev console output, you'll find warnings asking the developer to search your code and fix this issue.

If you have code where you still need to keep the RoutePrefix.prepend call in (because, say, you're interacting with a Compose HTML widget that is not aware of Kobweb base paths), the name has been deprecated and you should change it to BasePath.prepend instead.

Thanks!

  • Thanks to new contributor @ChrisGNZ for reporting and helping us fix the issue with our start.bat script (which broke if the user's Java path had at least one space in it...)

  • Thanks to @dead8309 for their spooky Hacktoberfest contribution, fixing #602 and giving us serialization APIs for HTTP verbs.

  • Thanks to @TheDome0 for their help extending CSS units.

  • And to @DennisTsar for so much of his work, as usual, but with a special callout for the ktor 3.0.0 migration, removing unnecessary trace markers (resulting in smaller sites for everyone), and some work he has done in preparation for Kotlin 2.1.0 (to be supported in a future release)


Full Changelog: v0.19.2...v0.19.3