Skip to content
This repository has been archived by the owner on Apr 24, 2022. It is now read-only.

Commit

Permalink
Merge pull request #59 from boswelja/serialization-improvements
Browse files Browse the repository at this point in the history
Serialization improvements
  • Loading branch information
boswelja authored Nov 2, 2021
2 parents cb678e9 + e3534db commit fb558be
Show file tree
Hide file tree
Showing 32 changed files with 407 additions and 194 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ A kotlin-first Android library to provide a shared API for interacting with mult

---

### [Serializers](https://github.com/boswelja/WatchConnectionLib/tree/main/serializers)
### [Serialization](https://github.com/boswelja/WatchConnectionLib/tree/main/serialization)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package com.boswelja.watchconnection.common.message
* serialization standard.
* @param messagePaths A [Set] of [Message.path]s this serializer supports.
*/
@Deprecated("Use serialization implementation")
public abstract class MessageSerializer<T>(
public val messagePaths: Set<String>
) {
Expand All @@ -21,7 +22,7 @@ public abstract class MessageSerializer<T>(
* @param bytes The [ByteArray] to deserialize.
* @return The deserialized class [T].
*/
public abstract suspend fun deserialize(bytes: ByteArray): T
public abstract suspend fun deserialize(bytes: ByteArray?): T

/**
* This exists to effectively allow serializing [T] after type erasure.
Expand Down
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ compose = "1.1.0-beta01"
navigationCompose = "2.4.0-beta01"
wearCompose = "1.0.0-alpha09"

turbine = "0.7.0"

[libraries]
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppcompat" }
androidx-arch-test = { module = "androidx.arch.core:core-testing", version.ref = "androidxArchTest" }
Expand Down Expand Up @@ -63,6 +65,8 @@ hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt"
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavCompose"}

turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }

[bundles]
compose-mobile = [
"androidx-compose-ui-core",
Expand Down
45 changes: 14 additions & 31 deletions mobile/mobile-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,48 +20,31 @@ Messages must have a target and path, however data is optional.
Messages are managed via a [MessageHandler](https://github.com/boswelja/WatchConnectionLib/blob/main/mobile/mobile-core/src/commonMain/kotlin/com/boswelja/watchconnection/core/message/MessageHandler.kt).
You'll need to instantiate a MessageClient to be able to send and receive messages.

#### Data Serialization

`MessageClient` can automatically serialize and deserialize message data for you, as long as a [MessageSerializer](https://github.com/boswelja/WatchConnectionLib/blob/main/common/src/commonMain/kotlin/com/boswelja/watchconnection/common/message/MessageSerializer.kt) is provided.

Default serializers for common types are provided with the [serializers module](https://github.com/boswelja/WatchConnectionLib/tree/main/serializers).
#### Create & Send a Message

Creating your own serializer is easy
Sending a message is simple, just pass your target Watch and message to `MessageClient.sendMessage`

```kotlin
object MySerializer : MessageSerializer<MyData>(
messagePaths = setOf(
"path-1",
"path-2"
val result = messageClient.sendMessage(
to = watch.uid,
message = Message(
path = "path",
data = null,
priority = Message.Priority.LOW
)
) {
override suspend fun serialize(data: MyData): ByteArray {
// Serialize MyData to ByteArray
}

override suspend fun deserialize(bytes: ByteArray): MyData {
// Deserialize ByteArray to MyData
}
}
)
```

#### Create & Send a Message

See the [Message class](https://github.com/boswelja/WatchConnectionLib/blob/main/common/src/commonMain/kotlin/com/boswelja/watchconnection/common/message/Message.kt) for more info on creating a `Message`.
#### Receiving messages

To send a message, just pass your target Watch and message to `MessageClient.sendMessage`
You can collect incoming messages while your app is running via the `incomingMessages()` Flows in your `MessageClient`.

```kotlin
messageClient.sendMessage(
to = targetWatch,
message = myMessage
)
messageClient.incomingMessages().collect { message ->
// Do something on message received
}
```

#### Receiving messages

You can collect incoming messages while your app is running via the `incomingMessages()` Flows in your `MessageClient`. There are [multiple incomingMessages variants](https://github.com/boswelja/WatchConnectionLib/blob/main/mobile/mobile-core/src/commonMain/kotlin/com/boswelja/watchconnection/core/message/MessageClient.kt) to choose from. Check out the link and pick the one that works best for you.

### Discovery & Capabilities

[DiscoveryClient](https://github.com/boswelja/WatchConnectionLib/blob/main/mobile/mobile-core/src/commonMain/kotlin/com/boswelja/watchconnection/core/discovery/DiscoveryClient.kt) allows you to find available watches and manage capabilities.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@ import kotlinx.coroutines.flow.merge

/**
* MessageClient takes a number of [MessagePlatform]s, and provides a common interface between them.
* @param serializers A list of [MessageSerializer] to use when serializing/deserializing messages.
* @param platforms The [MessagePlatform]s this MessageClient should support.
*/
public class MessageClient(
private val serializers: List<MessageSerializer<*>> = listOf(),
public class MessageClient @Deprecated("Use MessageHandler for serialization") constructor(
private val serializers: List<MessageSerializer<*>>,
platforms: List<MessagePlatform>
) : BaseClient<MessagePlatform>(platforms),
MessageClient {

public constructor(
platforms: List<MessagePlatform>
) : this(emptyList(), platforms = platforms)

/**
* A [Flow] of [ReceivedMessage]s received by all platforms. Messages collected here have no
* additional processing performed, and thus only contain the raw data in bytes.
Expand All @@ -36,6 +39,7 @@ public class MessageClient(
* messages that [serializer] can deserialize, thus guaranteeing the data type [T].
* @param serializer The [MessageSerializer] to use for deserializing.
*/
@Deprecated("Use MessageHandler instead")
public fun <T> incomingMessages(
serializer: MessageSerializer<T>
): Flow<ReceivedMessage<T>> = incomingMessages()
Expand Down Expand Up @@ -73,6 +77,10 @@ public class MessageClient(
* @param message The [Message] to send.
* @return true if sending the message was successful, false otherwise.
*/
@Deprecated(
"Specify the device UID instead",
replaceWith = ReplaceWith(expression = "sendMessage(target.uid, message)")
)
public suspend fun sendMessage(
target: Watch,
message: Message<Any?>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ object ConcreteMessageSerializer : MessageSerializer<ConcreteDataType>(
return data.data.encodeToByteArray()
}

override suspend fun deserialize(bytes: ByteArray): ConcreteDataType {
return ConcreteDataType(bytes.decodeToString())
override suspend fun deserialize(bytes: ByteArray?): ConcreteDataType {
return ConcreteDataType(bytes!!.decodeToString())
}
}
2 changes: 1 addition & 1 deletion samples/mobile/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ android {

dependencies {
implementation(projects.mobile.platformWearos)
implementation(projects.serializers)
implementation(projects.serialization)

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.watchconnection.sample.di
import android.content.Context
import com.boswelja.watchconnection.core.discovery.DiscoveryClient
import com.boswelja.watchconnection.core.message.MessageClient
import com.boswelja.watchconnection.serializers.StringSerializer
import com.boswelja.watchconnection.wearos.discovery.WearOSDiscoveryPlatform
import com.boswelja.watchconnection.wearos.message.WearOSMessagePlatform
import dagger.Module
Expand All @@ -30,9 +29,6 @@ object ClientsModule {
@Provides
fun messageClient(@ApplicationContext applicationContext: Context): MessageClient =
MessageClient(
serializers = listOf(
StringSerializer(setOf("message-path"))
),
platforms = listOf(
WearOSMessagePlatform(applicationContext)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import com.boswelja.watchconnection.common.message.Message
import com.boswelja.watchconnection.common.message.ReceivedMessage
import com.boswelja.watchconnection.core.discovery.DiscoveryClient
import com.boswelja.watchconnection.core.message.MessageClient
import com.boswelja.watchconnection.serializers.StringSerializer
import com.boswelja.watchconnection.serialization.MessageHandler
import com.boswelja.watchconnection.serialization.StringSerializer
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
Expand All @@ -23,14 +24,18 @@ class MessageViewModel @Inject constructor(
private val messageClient: MessageClient,
private val discoveryClient: DiscoveryClient
) : ViewModel() {

private val messageHandler =
MessageHandler(StringSerializer(setOf("message-path")), messageClient)

var selectedWatch by mutableStateOf<Watch?>(null)
val availableWatches = mutableStateListOf<Watch>()
val incomingMessages = mutableStateListOf<ReceivedMessage<String>>()
val sentMessages = mutableStateMapOf<Watch, List<Message<String>>>()

init {
viewModelScope.launch {
messageClient.incomingMessages(StringSerializer(setOf("message-path"))).collect {
messageHandler.incomingMessages().collect {
incomingMessages.add(0, it)
}
}
Expand All @@ -49,8 +54,8 @@ class MessageViewModel @Inject constructor(
"message-path",
text
)
messageClient.sendMessage(
watch,
messageHandler.sendMessage(
watch.uid,
message
)
val list = sentMessages[watch] ?: emptyList()
Expand Down
2 changes: 1 addition & 1 deletion samples/wearos/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ android {

dependencies {
implementation(projects.wear)
implementation(projects.serializers)
implementation(projects.serialization)

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.wear.input)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.watchconnection.sample.di

import android.content.Context
import com.boswelja.watchconnection.serializers.StringSerializer
import com.boswelja.watchconnection.wear.discovery.DiscoveryClient
import com.boswelja.watchconnection.wear.message.MessageClient
import dagger.Module
Expand All @@ -23,8 +22,5 @@ object ClientsModule {
@Singleton
@Provides
fun messageClient(@ApplicationContext applicationContext: Context): MessageClient =
MessageClient(
applicationContext,
listOf(StringSerializer(setOf("message-path")))
)
MessageClient(applicationContext)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.boswelja.watchconnection.common.message.Message
import com.boswelja.watchconnection.common.message.ReceivedMessage
import com.boswelja.watchconnection.serializers.StringSerializer
import com.boswelja.watchconnection.serialization.MessageHandler
import com.boswelja.watchconnection.serialization.StringSerializer
import com.boswelja.watchconnection.wear.discovery.DiscoveryClient
import com.boswelja.watchconnection.wear.message.MessageClient
import dagger.hilt.android.lifecycle.HiltViewModel
Expand All @@ -18,12 +19,15 @@ class MessageViewModel @Inject constructor(
private val discoveryClient: DiscoveryClient,
private val messageClient: MessageClient
) : ViewModel() {
private val messageHandler =
MessageHandler(StringSerializer(setOf("message-path")), messageClient)

val incomingMessages = mutableStateListOf<ReceivedMessage<String>>()
val sentMessages = mutableStateListOf<Message<String>>()

init {
viewModelScope.launch {
messageClient.incomingMessages(StringSerializer(setOf("message-path"))).collect {
messageHandler.incomingMessages().collect {
incomingMessages.add(0, it)
}
}
Expand All @@ -37,8 +41,8 @@ class MessageViewModel @Inject constructor(
"message-path",
text
)
messageClient.sendMessage(
pairedPhone,
messageHandler.sendMessage(
pairedPhone.uid,
message
)
sentMessages.add(message)
Expand Down
64 changes: 64 additions & 0 deletions serialization/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# serializers

This module provides some basic serialization support for all supported hosts.

## Data Serialization

### Creating a serializer

To create your own serializer, simply extend `MessageSerializer`

```kotlin
object MySerializer : MessageSerializer<MyData>(
messagePaths = setOf(
"path-1",
"path-2"
)
) {
override suspend fun serialize(data: MyData): ByteArray {
// Serialize MyData to ByteArray
}

override suspend fun deserialize(bytes: ByteArray?): MyData {
// Deserialize ByteArray to MyData
}
}
```

The list of message paths specified will be used to filter incoming messages for the serializer to handle.

### Sending and receiving typed data

Typed messages are managed by a [MessageHandler](https://github.com/boswelja/WatchConnectionLib/blob/main/serialization/src/commonMain/kotlin/com/boswelja/watchconnection/serialization/MessageHandler.kt).
`MessageHandler` accepts a base `MessageClient`, as well as a single `MessageSerializer`.

```kotlin
val messageHandler = MessageHandler<MyData>(
MySerializer,
messageClient
)
```

#### Sending messages

Call `messageHandler.send` and provide the target device UID, along with a `Message` with a data type matching your serializer data type.

```kotlin
messageHandler.send(
device.uid,
Message(
"path-1",
MyData()
)
)
```

#### Receiving messages

Messages are received from a `Flow`. Collecting from the Flow automatically filters and deserializes received messages.

```kotlin
messageHandler.incomingMessages().collect { message ->
// message.data is MyData
}
```
10 changes: 8 additions & 2 deletions serializers/build.gradle.kts → serialization/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,18 @@ kotlin {
api(projects.common)
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(libs.turbine)
}
}
val androidMain by getting {
dependencies { }
}
val androidTest by getting {
dependencies {
implementation(kotlin("test-junit5"))
implementation(kotlin("test-junit"))
}
}
}
Expand Down Expand Up @@ -66,7 +72,7 @@ afterEvaluate {
pom {
name.set(this@afterEvaluate.name)
description.set(this@afterEvaluate.description)
url.set(repoUrlFor("serializers"))
url.set(repoUrlFor("serialization"))
licenses(Publishing.licenses)
developers(Publishing.developers)
scm(Publishing.scm)
Expand Down
1 change: 1 addition & 0 deletions serialization/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<manifest package="com.boswelja.watchconnection.serialization" />
Loading

0 comments on commit fb558be

Please sign in to comment.