{"id":19495474,"url":"https://github.com/guilhe/jsonbroadcaster","last_synced_at":"2025-04-25T21:32:42.362Z","repository":{"id":65507731,"uuid":"576783011","full_name":"GuilhE/JsonBroadcaster","owner":"GuilhE","description":"Update the UI state of your Android and iOS apps at runtime.","archived":false,"fork":false,"pushed_at":"2024-08-19T12:29:24.000Z","size":139321,"stargazers_count":48,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-04T03:41:15.451Z","etag":null,"topics":["android","compose-multiplatform","desktop-compose","ios","kotlin","kotlin-multiplatform","swift","ui-state-management"],"latest_commit_sha":null,"homepage":"https://guilhe.github.io/JsonBroadcaster/","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/GuilhE.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"custom":"https://www.buymeacoffee.com/GuilhE"}},"created_at":"2022-12-11T00:37:43.000Z","updated_at":"2024-08-19T12:29:28.000Z","dependencies_parsed_at":"2023-02-13T21:01:04.300Z","dependency_job_id":"6ecfa146-6341-4acc-bb0e-2c3b2f0199df","html_url":"https://github.com/GuilhE/JsonBroadcaster","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GuilhE%2FJsonBroadcaster","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GuilhE%2FJsonBroadcaster/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GuilhE%2FJsonBroadcaster/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GuilhE%2FJsonBroadcaster/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/GuilhE","download_url":"https://codeload.github.com/GuilhE/JsonBroadcaster/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250900366,"owners_count":21505042,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","compose-multiplatform","desktop-compose","ios","kotlin","kotlin-multiplatform","swift","ui-state-management"],"created_at":"2024-11-10T21:37:51.717Z","updated_at":"2025-04-25T21:32:37.745Z","avatar_url":"https://github.com/GuilhE.png","language":"Kotlin","funding_links":["https://www.buymeacoffee.com/GuilhE"],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"desktopApp/src/desktopMain/resources/icon.png\" width=\"100\" align=\"right\"\u003e \u003c/br\u003e\u003c/br\u003e\n\n# JsonBroadcaster\n\u003ca href=\"https://jetc.dev/issues/188.html\"\u003e\u003cimg src=\"https://img.shields.io/badge/As_Seen_In-jetc.dev_Newsletter_Issue_%23188-blue?logo=Jetpack+Compose\u0026amp;logoColor=white\" alt=\"As Seen In - jetc.dev Newsletter Issue #188\"\u003e\u003c/a\u003e\n\nUpdate the UI state of your Android and iOS apps at runtime.\n\n## Motivation\n\nUpdating the UI State at runtime is a very useful tool for rapid prototyping and validation purposes. It also adds the benefit that it can be used by the whole testing team, be it developers, designers, quality assurance, etc.\n\nhttps://github.com/GuilhE/JsonBroadcaster/assets/2677139/10cf81ed-0895-4f68-899d-412e28a43481\n\n## How does it work\n\n### Android\n\nAndroid Debug Bridge ([ABD](https://developer.android.com/studio/command-line/adb)) is used to send a broadcast signal to the desired application with a `json` payload as an extra.\n\n```\nadb shell am broadcast -p [package] -a [action] -e [extra key] [extra value]\n```\n\nOn the application side there's a `BroadcastReceiver` listening for theses payloads. Upon successful deserialization, a fresh state will be emitted, consequently triggering a UI update.\n\n**Availability**: all simulators and/or physical devices (even with wifi debug) connected.\n\n### iOS\n\nApple's Xcode developer tools provides a command-line tool for interacting with the iOS Simulator.  \nThis tool allows you to simulate the process of sending push notifications to a device:\n\n```\nxcrun simctl push [UDID] [bundle id] [path to .apns] \n```\n  \nOn the application side there's a `NotificationBroadcaster` actively monitoring incoming notifications. These notifications are then relayed to internal observers within the application. Upon successful deserialization, a fresh state will be emitted, consequently triggering a UI update.\n\n**Availability**: all booted simulators.\n\n## Installation\n\n### Android\n\nAdd the library dependency:\n\n```kotlin\nimplementation(\"com.github.guilhe:json-broadcast-handler:${LATEST_VERSION}'\")\n```\n[![Maven Central](https://img.shields.io/maven-central/v/com.github.guilhe/json-broadcast-handler.svg)](https://search.maven.org/search?q=g:com.github.guilhe%20AND%20json-broadcast-handler)\n\n### iOS\n\n#### Swift Package Manager\n\nThe Swift implementations are available via the Swift Package Manager. Just add it in Xcode by going to `File` \u003e `Add Packages...` and providing the URL https://github.com/GuilhE/JsonBroadcaster.git.\n\n#### CocoaPods\n\nIf you use CocoaPods add the following to your `Podfile`:\n```ruby \npod 'JsonBroadcasterHandler', :git =\u003e 'https://github.com/GuilhE/JsonBroadcaster.git', :tag =\u003e '[desired version]'\n```\n\n## Usage: developers\n\n\u003cdetails\u003e\n    \u003csummary\u003eAndroid\u003c/summary\u003e\n\n1. Your `UiState` classes must be annotated with `kotlinx.serialization.Serializable` ([dependency](https://github.com/Kotlin/kotlinx.serialization)):\n\n   ```kotlin\n   @Serializable\n   data class UiState(val memberA: String, val memberB: String)\n   ```\n\n2. Create a `BroadcastUiModelHost` implementation to listen for state updates, as shown bellow:\n\n   ```kotlin\n   private val host = object : BroadcastUiModelHost\u003cUiState\u003e(coroutineScope, UiState.serializer()) {\n       override fun updateState(new: UiState) {\n           //...\n       }\n   }\n   ```\n   \n3. Add it where it fits best in your project, examples:\n\n   If you are using `androidx.lifecycle.ViewModel` you can do the following:\n   ```kotlin\n   class MatchViewModel : ViewModel() {\n   \n       private val _uiState = MutableStateFlow(MatchUiState(home = Team(\"PRT\", \"🇵🇹\"), away = Team(\"BRA\", \"🇧🇷\")))\n       val uiState: StateFlow\u003cMatchUiState\u003e = _uiState\n   \n       private val host = object : BroadcastUiModelHost\u003cMatchUiState\u003e(viewModelScope, MatchUiState.serializer()) {\n           override fun updateState(new: MatchUiState) {\n               _uiState.update { new }\n           }\n       }\n   }\n   ```\n   \n   But actually you don't need a `ViewModel`, you can simply use a `@Composable` for instance:\n   \n   ```kotlin\n   @Composable\n   fun MatchScreen() {\n       var uiState: MatchUiState by remember { mutableStateOf(MatchUiState(home = Team(\"PRT\", \"🇵🇹\"), away = Team(\"BRA\", \"🇧🇷\"))) }\n       LaunchedEffect(Unit) {\n           val host = object : BroadcastUiModelHost\u003cMatchUiState\u003e(this, MatchUiState.serializer()) {\n               override fun updateState(new: MatchUiState) {\n                   uiState = new\n               }\n           }\n       }\n       Match(uiState)\n   }\n   ```\n\n   And the beauty of it is that you may choose whatever suits you best: `ViewModel`, `@Composable`, `Activity`, `Fragment`, etc...\n\n4. To disable it, for instance in release builds, override the `receiver` declaration in the `AndroidManifest` by adding a `manifestPlaceholders` property in the `build.gradle`:\n\n   ```kotlin\n   android {\n       buildTypes {\n           getByName(\"release\") {\n               manifestPlaceholders[\"enableJsonBroadcastReceiver\"] = false\n           }\n   \n           getByName(\"debug\") {\n               manifestPlaceholders[\"enableJsonBroadcastReceiver\"] = true\n           }\n       }\n   }\n   ```\n   ```xml\n   \u003creceiver\n       android:name=\"com.broadcast.handler.JsonBroadcasterReceiver\"\n       android:exported=\"${enableJsonBroadcastReceiver}\"\n       tools:replace=\"android:exported\"\u003e\n       \u003cintent-filter\u003e\n           \u003caction android:name=\"JsonBroadcaster.extra\" /\u003e\n       \u003c/intent-filter\u003e\n   \u003c/receiver\u003e\n   ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n    \u003csummary\u003eiOS\u003c/summary\u003e\n\n1. Your `UiState` classes must implement the `Codable` protocol:\n\n   ```swift\n   struct UiState: Codable {\n       let memberA: String\n       let memberB: String\n   }\n   ```\n   \n2. Create a `BroadcastUIModelHost` instance inside a `class` to listen for state updates, as shown bellow:\n\n   ```swift\n   private var uiModelHost: BroadcastUIModelHost\u003cMatchUiState\u003e!\n   init() {\n        uiModelHost = BroadcastUIModelHost(state) { [weak self] newState in\n            //...\n        }        \n    }\n   ```\n   \n3. Add it where it fits best in your project, example:\n\n   If you are using an `ObservableObject` you can do the following:\n   ```swift\n   class MatchViewModel: ObservableObject {\n      private var uiModelHost: BroadcastUIModelHost\u003cMatchUiState\u003e!\n      @Published var state: MatchUiState = MatchUiState(home: Team(country:\"PRT\", flag:\"🇵🇹\"), away: Team(country:\"BRA\", flag:\"🇧🇷\"))\n    \n      init() {\n        uiModelHost = BroadcastUIModelHost(state) { [weak self] newState in\n            self?.state = newState\n        }\n      }\n   } \n   ```\n   And the beauty of it is that you may choose whatever suits you best, `SwiftUI` or `UIKit`:\n   ```swift\n   struct MatchScreen: View {\n      @StateObject private var viewModel = MatchViewModel() \n\n      var body: some View {\n        ZStack { }\n        .onReceive(viewModel.$state) { new in\n            //...\n        }\n    }\n   ``` \n   ```swift\n   class MatchScreen: UIViewController {\n      private var viewModel: MatchViewModel!\n      private var cancellables = Set\u003cAnyCancellable\u003e()\n\n      override func viewDidLoad() {\n        super.viewDidLoad()\n        viewModel = MatchViewModel()\n        viewModel.$state\n            .receive(on: DispatchQueue.main)\n            .sink { [weak self] state in\n                self?.updateUI(with: state)\n            }\n            .store(in: \u0026cancellables)\n      }\n      \n      private func updateUI(with state: MatchUiState) {\n        //...\n      }\n   }\n   ```\n   \n4. Inside your `AppDelegate` register for `RemoteNotifications` and forward them with the `NotificationBroadcaster`:\n\n   ```swift\n   import UIKit\n   import UserNotifications\n\n   class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {\n       \n       func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -\u003e Bool {\n           UNUserNotificationCenter.current().delegate = self\n           application.registerForRemoteNotifications()\n           return true\n       }\n       \n       func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -\u003e Void) {\n           NotificationBroadcaster.broadcast(notification)\n       }\n   }   \n   ```\n   \n   **tip**: You may create a compiler custom flags, DEBUG_MODE, to encapsulate the `NotificationBroadcaster`:\n   ```swift       \n   func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -\u003e Void) {\n      #if DEBUG_MODE\n         NotificationBroadcaster.broadcast(notification)\n      #endif\n   }\n   ```\n\n\u003c/details\u003e\n\n## Usage: testing team\n\n### Android\n1. Google's Android SDK must be installed in order to use command line tools;\n2. Ask for an installed version of the app (wifi debug or cable connected);\n3. Use the __desktopApp__ GUI.\n\n### iOS\n1. Apple's XCode must be installed in order to use command line tools;\n2. Open XCode and run a simulator with the app;\n3. Use the __desktopApp__ GUI.\n\n## Desktop app\n\nAlthough we can use the terminal to send commands, it's not practical. The __desktopApp__ provides a simple user interface to help us with that task.\n\nTo run it you can either:  \n- Clone this project and type `./gradlew :desktopApp:run` in the terminal.\n- Download a `.dmg` (only MacOS) and install it. Get it [here](./artifacts/JsonBroadcaster.dmg).\n\n__note:__ due to security reasons, since this app is not from an Identified Developer, MacOS will block its execution. To by pass it you'll need to click in \"Open Anyway\" in System Settings under Security. It's only needed once:\n\n\u003cimg src=\"./media/security.png\"\u003e\n\n(This wont happen with the first approach)\n\n## Playgrounds\n\nUse the following payload to get you started:\n```json\n{\n   \"home\":{\n      \"country\":\"PRT\",\n      \"flag\":\"🇵🇹\"\n   },\n   \"away\":{\n      \"country\":\"BRA\",\n      \"flag\":\"🇧🇷\"\n   },\n   \"homeGoals\":0,\n   \"awayGoals\":0,\n   \"started\": false,\n   \"running\": false,\n   \"finished\": false\n}\n```\n\n### Android\n\nInside the __sample__ module you'll find a playground app ready for you to test it.\n\nTo run it you can either:\n- Clone this project and type `./gradlew :androidApp:installDebug` in the terminal.\n- Download the sample `.apk` and install it. Get it [here](./artifacts/matchday.apk).\n\nThe `applicationId` is _com.jsonbroadcaster.matchday_\n\n### iOS\n\nInside the __sample-ios__ folder you'll find a playground app ready for you to test it.  \n\nTo run it:\n - Open it in Xcode and run standard configuration.\n - Import `JsonBroadcaster` using your [method](README.md#ios-1) of choice.\n\n## LICENSE\n\nCopyright (c) 2022-present GuilhE\n\nLicensed 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\nof the License at\n\n\u003chttp://www.apache.org/licenses/LICENSE-2.0\u003e\n\nUnless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\nWARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under\nthe License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguilhe%2Fjsonbroadcaster","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fguilhe%2Fjsonbroadcaster","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguilhe%2Fjsonbroadcaster/lists"}