
Enables local network peer-to-peer communication with automatic leader election, WebSocket messaging, custom event serialization, session-priority elections, UDP discovery and client/server message routing.
A Kotlin Multiplatform library for local network communication with automatic server election and WebSocket-based messaging. Perfect for building peer-to-peer applications that work across Android and iOS.
Add the dependency to your build.gradle.kts:
commonMain.dependencies {
implementation("io.github.marcinsiwak:klocalnet:1.0.6")
}Initialize KLocalNet in your Application class:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
KLocalNet.init(this)
}
}Don't forget to add the required permissions in your AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />Initialize KLocalNet before using it. Create a function in iosMain and call it from your iOS app's entry point (e.g., in AppDelegate):
fun initializeApp() {
KLocalNet.init()
}// Get the manager instance
val manager = KLocalNet.getKLocalNetManager()
// Observe WiFi connection state
launch {
manager.isWifiConnected.collect { isConnected ->
println("WiFi connected: $isConnected")
}
}
// Observe loading state
launch {
manager.isLoading.collect { isLoading ->
println("Loading: $isLoading")
}
}
// Connect to the network
launch {
manager.connect()
}Create custom event classes by extending WebSocketEvent:
@Serializable
data class ChatMessage(
val userId: String,
val message: String,
val timestamp: Long
) : WebSocketEvent()
@Serializable
data class GameStateUpdate(
val gameId: String,
val state: String
) : WebSocketEvent()Before using custom events, register them with the manager:
manager.setCustomEvents(
listOf(
ChatMessage::class to serializer<ChatMessage>(),
GameStateUpdate::class to serializer<GameStateUpdate>()
)
)launch {
manager.serverMessages.collect { event ->
when (event) {
is ClientActions.UserConnected -> {
println("User connected: ${event.id}, isHost: ${event.isHost}")
}
is ClientActions.UserDisconnected -> {
println("User disconnected: ${event.id}")
}
is ChatMessage -> {
println("Received chat: ${event.message}")
// Process the message
}
// Handle other custom events
}
}
}launch {
manager.clientMessages.collect { event ->
when (event) {
is ServerActions.ServerStarted -> {
println("Server has started")
}
is ClientActions.ServerDownDetected -> {
println("Server is down, re-election in progress")
}
is ChatMessage -> {
println("Received chat: ${event.message}")
}
// Handle other custom events
}
}
}val userId = "192.168.1.5"
val message = ChatMessage(
userId = manager.getDeviceId(),
message = "Hello from server!",
timestamp = System.currentTimeMillis()
)
launch {
manager.send(id = userId, webSocketEvent = message)
}val message = GameStateUpdate(
gameId = "game123",
state = "started"
)
launch {
manager.sendToAll(webSocketEvent = message)
}val message = ChatMessage(
userId = manager.getDeviceId(),
message = "Hello from client!",
timestamp = System.currentTimeMillis()
)
launch {
manager.sendFromClient(webSocketEvent = message)
}val isServer = manager.isServerRunning()
if (isServer) {
println("This device is the host")
}val deviceId = manager.getDeviceId()
println("My device ID: $deviceId") // e.g., "192.168.1.5"To influence server election based on existing session state:
// Device with an active session gets priority in server election
manager.setHasSession(
hasSession = true,
lastUpdate = System.currentTimeMillis()
)If you're the server and want to disconnect all clients:
launch {
manager.disconnectUsers()
}Server Device:
Client Devices:
UserConnected(id: String, isHost: Boolean) - A user has connected to the serverUserDisconnected(id: String) - A user has disconnectedServerDownDetected - The server has gone offline (triggers re-election)ServerStarted - The server has successfully started@Serializable
data class ChatMessage(
val userId: String,
val message: String
) : WebSocketEvent()
class ChatManager {
private val manager = KLocalNet.getKLocalNetManager()
init {
// Register custom events
manager.setCustomEvents(
listOf(ChatMessage::class to serializer<ChatMessage>())
)
// Start observing messages
observeMessages()
}
private fun observeMessages() {
// Listen for messages as server
launch {
manager.serverMessages.collect { event ->
when (event) {
is ChatMessage -> {
println("${event.userId}: ${event.message}")
// Broadcast to all clients
manager.sendToAll(event)
}
is ClientActions.UserConnected -> {
println("${event.id} joined the chat")
}
}
}
}
// Listen for messages as client
launch {
manager.clientMessages.collect { event ->
if (event is ChatMessage) {
println("${event.userId}: ${event.message}")
}
}
}
}
suspend fun sendMessage(text: String) {
val message = ChatMessage(
userId = manager.getDeviceId(),
message = text
)
if (manager.isServerRunning()) {
// If we're the server, broadcast to everyone
manager.sendToAll(message)
} else {
// If we're a client, send to server
manager.sendFromClient(message)
}
}
}KLocalNet uses the following libraries:
Copyright 2026 Marcin Siwak
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Contributions are welcome! Please feel free to submit a Pull Request.
Marcin Siwak
A Kotlin Multiplatform library for local network communication with automatic server election and WebSocket-based messaging. Perfect for building peer-to-peer applications that work across Android and iOS.
Add the dependency to your build.gradle.kts:
commonMain.dependencies {
implementation("io.github.marcinsiwak:klocalnet:1.0.6")
}Initialize KLocalNet in your Application class:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
KLocalNet.init(this)
}
}Don't forget to add the required permissions in your AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />Initialize KLocalNet before using it. Create a function in iosMain and call it from your iOS app's entry point (e.g., in AppDelegate):
fun initializeApp() {
KLocalNet.init()
}// Get the manager instance
val manager = KLocalNet.getKLocalNetManager()
// Observe WiFi connection state
launch {
manager.isWifiConnected.collect { isConnected ->
println("WiFi connected: $isConnected")
}
}
// Observe loading state
launch {
manager.isLoading.collect { isLoading ->
println("Loading: $isLoading")
}
}
// Connect to the network
launch {
manager.connect()
}Create custom event classes by extending WebSocketEvent:
@Serializable
data class ChatMessage(
val userId: String,
val message: String,
val timestamp: Long
) : WebSocketEvent()
@Serializable
data class GameStateUpdate(
val gameId: String,
val state: String
) : WebSocketEvent()Before using custom events, register them with the manager:
manager.setCustomEvents(
listOf(
ChatMessage::class to serializer<ChatMessage>(),
GameStateUpdate::class to serializer<GameStateUpdate>()
)
)launch {
manager.serverMessages.collect { event ->
when (event) {
is ClientActions.UserConnected -> {
println("User connected: ${event.id}, isHost: ${event.isHost}")
}
is ClientActions.UserDisconnected -> {
println("User disconnected: ${event.id}")
}
is ChatMessage -> {
println("Received chat: ${event.message}")
// Process the message
}
// Handle other custom events
}
}
}launch {
manager.clientMessages.collect { event ->
when (event) {
is ServerActions.ServerStarted -> {
println("Server has started")
}
is ClientActions.ServerDownDetected -> {
println("Server is down, re-election in progress")
}
is ChatMessage -> {
println("Received chat: ${event.message}")
}
// Handle other custom events
}
}
}val userId = "192.168.1.5"
val message = ChatMessage(
userId = manager.getDeviceId(),
message = "Hello from server!",
timestamp = System.currentTimeMillis()
)
launch {
manager.send(id = userId, webSocketEvent = message)
}val message = GameStateUpdate(
gameId = "game123",
state = "started"
)
launch {
manager.sendToAll(webSocketEvent = message)
}val message = ChatMessage(
userId = manager.getDeviceId(),
message = "Hello from client!",
timestamp = System.currentTimeMillis()
)
launch {
manager.sendFromClient(webSocketEvent = message)
}val isServer = manager.isServerRunning()
if (isServer) {
println("This device is the host")
}val deviceId = manager.getDeviceId()
println("My device ID: $deviceId") // e.g., "192.168.1.5"To influence server election based on existing session state:
// Device with an active session gets priority in server election
manager.setHasSession(
hasSession = true,
lastUpdate = System.currentTimeMillis()
)If you're the server and want to disconnect all clients:
launch {
manager.disconnectUsers()
}Server Device:
Client Devices:
UserConnected(id: String, isHost: Boolean) - A user has connected to the serverUserDisconnected(id: String) - A user has disconnectedServerDownDetected - The server has gone offline (triggers re-election)ServerStarted - The server has successfully started@Serializable
data class ChatMessage(
val userId: String,
val message: String
) : WebSocketEvent()
class ChatManager {
private val manager = KLocalNet.getKLocalNetManager()
init {
// Register custom events
manager.setCustomEvents(
listOf(ChatMessage::class to serializer<ChatMessage>())
)
// Start observing messages
observeMessages()
}
private fun observeMessages() {
// Listen for messages as server
launch {
manager.serverMessages.collect { event ->
when (event) {
is ChatMessage -> {
println("${event.userId}: ${event.message}")
// Broadcast to all clients
manager.sendToAll(event)
}
is ClientActions.UserConnected -> {
println("${event.id} joined the chat")
}
}
}
}
// Listen for messages as client
launch {
manager.clientMessages.collect { event ->
if (event is ChatMessage) {
println("${event.userId}: ${event.message}")
}
}
}
}
suspend fun sendMessage(text: String) {
val message = ChatMessage(
userId = manager.getDeviceId(),
message = text
)
if (manager.isServerRunning()) {
// If we're the server, broadcast to everyone
manager.sendToAll(message)
} else {
// If we're a client, send to server
manager.sendFromClient(message)
}
}
}KLocalNet uses the following libraries:
Copyright 2026 Marcin Siwak
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Contributions are welcome! Please feel free to submit a Pull Request.
Marcin Siwak