{"id":13609497,"url":"https://github.com/erdo/n8","last_synced_at":"2025-07-14T12:40:46.690Z","repository":{"id":230980413,"uuid":"714437613","full_name":"erdo/n8","owner":"erdo","description":"state based navigation library","archived":false,"fork":false,"pushed_at":"2025-02-27T13:53:52.000Z","size":741,"stargazers_count":45,"open_issues_count":3,"forks_count":3,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-04-14T11:56:24.123Z","etag":null,"topics":["android","compose","kmp","kotlin","lib","navigation"],"latest_commit_sha":null,"homepage":"","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/erdo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"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}},"created_at":"2023-11-04T21:42:15.000Z","updated_at":"2025-02-26T19:12:49.000Z","dependencies_parsed_at":"2024-04-28T20:21:55.638Z","dependency_job_id":"61522470-77cd-4728-a52a-2eedcc0e8f49","html_url":"https://github.com/erdo/n8","commit_stats":null,"previous_names":["erdo/n8"],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erdo%2Fn8","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erdo%2Fn8/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erdo%2Fn8/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/erdo%2Fn8/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/erdo","download_url":"https://codeload.github.com/erdo/n8/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248878020,"owners_count":21176242,"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","kmp","kotlin","lib","navigation"],"created_at":"2024-08-01T19:01:35.393Z","updated_at":"2025-07-14T12:40:46.442Z","avatar_url":"https://github.com/erdo.png","language":"Kotlin","readme":"## N8 [![circleci](https://circleci.com/gh/erdo/n8.svg?style=svg)](https://circleci.com/gh/erdo/n8)\n\n![n8_logo](n8_logo_400h.png)\n\n*Goals of N8 navigation:* _*pure kotlin, low config, minimally coupled, type safe and have an obvious API*_\n\n(obviously it also doesn't loose the user's location on config change or process death)\n\n⚠️help welcomed 🙏(check the issues) ⚠️\n\n- There are two sample apps in the repo that will make things clearer: one Android, one KMP(android/ios)\n- There are also a large number of unit tests which define N8 behaviour, and log navigation state to the console (which is quite useful to see what is going on)\n\n![example app screenshot landscape view](example-android-app/screenshot-land.png)\n\n### Quick Start\n\nSee the [dev.to launch post](https://dev.to/erdo/ive-just-open-sourced-n8-4foe) for an intro\n\n``` kotlin\nimplementation(\"co.early.n8:n8-core:2.0.0-rc.2\")\nimplementation(\"co.early.n8:n8-compose:2.0.0-rc.2\")\n```\nGPG fingerprint (for optionally verifying the Maven packages): \u003cstrong\u003e5B83EC7248CCAEED24076AF87D1CC9121D51BA24\u003c/strong\u003e see repo root for the public certificate.\n\n_Note: a legacy or hybrid android app that still uses fragments or multiple activities, can't maintain its back stack in the same stateful manner as a 100% compose app can and therefore won't get much utility from N8_\n\n### Details\n\nIt's not necessary to specify navigation routes upfront, N8 just builds the navigation graph\nas you go, ensuring that back operations always make sense. These are the main functions your code\nneeds to call to navigate around the app: ```navigateTo(), navigateBack(), navigateBackTo(), switchTab()```\n\n``` kotlin\nn8.navigateTo(Paris)\nn8.navigateTo(NewYork)\nn8.navigateTo(Mumbai)\nn8.navigateBack() { /* with optional data */ }\n\nn8.switchTab(MainTabs) /* add MainTabs if not yet added */\nn8.navigateTo(Seoul)  /* continue in MainTabs */\nn8.switchTab(2)\nn8.navigateTo(Hanoi(holdBags = 2)) /* navigate to Hanoi with data */\n\nn8.switchTab(SettingsTab) /* add SettingsTab, nested inside MainTabs */\nn8.switchTab(1)  /* continue in SettingsTab, from inside tabIndex 2 of MainTabs */\nn8.switchTab(MainTabs, 0)  /* switch to tabIndex 0 of MainTabs */\n\nn8.navigateTo(London) { null } /* jump out of any nested tabhosts and continue at the top level */\nn8.navigateTo(Krakow)\nn8.navigateTo(Tokyo) { SettingsTab } /* continue back in SettingsTab at tabIndex 1/ */\n\nn8.navigateBackTo(NewYork) { /* with optional data */ }\n\n```\n\nTo use N8 in your app, you don't need to implement any special interfaces on your screens, so your\nUI code remains largely independent of N8 itself.\n\nYou do need to tell N8 what class you are using to keep track of your user's *Location* and your\n*TabHosts* - something like a sealed class works well here, you could use a String if you wanted,\n[but you might not want to](https://github.com/erdo/n8/issues/18). If you don't have any tabbed\nnavigations you can just put Unit, but note the below:\n\n_**KMP considerations** In Swift, whatever you choose for Location and TabHost must conform to the\nHashable protocol. Kotlin enums, data classes and primitives generally are, but there are a few\ngotchas:\n- If your data class has a list in it somewhere, it likely won't be Hashable once translated to the Swift equivalent. In that case you'll need to make it conform to Hashable by implementing the ```hash(into:)``` and ```==``` operators in Swift before using it in iOS \n- Unit is also not Hashable in Swift, but at the moment KMP translates that to KotlinUnit which is Hashable (If that changes you'll have to choose something more class like or implement the Hashable bits yourself)\n\nHere's are some examples. \"Location\" and \"TabHostId\" are your own class and nothing to do with N8 code, you\ncould call them \"CosmicGirl\" and \"Loquat\" if you wanted\n\n``` kotlin\n@Serializable\nsealed class Location {\n\n    @Serializable\n    data object NewYork : Location()\n    \n    @Serializable\n    data object Tokyo : Location()\n    \n    @Serializable\n    data object Paris : Location()\n   \n}\n```\nOr perhaps slightly more realistically:\n\n``` kotlin\n@Serializable\nsealed class Location {\n\n    @Serializable\n    data object Home : Location()\n    \n    @Serializable\n    data object SignIn : Location()\n    \n    @Serializable\n    data class ProductPage(val productId: Int) : Location()\n    \n    @Serializable\n    data object Feed : Location()\n    \n    @Serializable\n    sealed class SignUpFlow : Location() {\n        @Serializable\n        data object Details : SignUpFlow()\n\n        @Serializable\n        data object Email : SignUpFlow()\n\n        @Serializable\n        data object EmailConf : SignUpFlow()\n    }\n\n    @Serializable\n    sealed class Settings : Location() {\n        @Serializable\n        data object VideoSettings : Settings()\n\n        @Serializable\n        data object AudioSettings : Settings()\n    }\n}\n\n@Serializable\nsealed class TabHostId {\n\n    @Serializable\n    data object MainTabs : TabHostId()\n\n    @Serializable\n    data object SettingsTabs : TabHostId()\n\n    @Serializable\n    data object CustomerSupportTabs : TabHostId()\n}\n```\n\nTell N8 what classes you decided on, like this on android:\n\n``` kotlin\nval n8 = NavigationModel\u003cLocation, TabHostId\u003e(\n    homeLocation = Home,\n    stateKType = typeOf\u003cNavigationState\u003cLocation, TabHostId\u003e\u003e(),\n    dataPath = application.filesDir.toOkioPath(),\n)\n```\n(For KMP the dataPath differs based on platform, see the KMP example app in this repo)\n\nThe navigationModel is observable so that your code can remain informed of any navigation\noperations as they happen, but for Compose there is a wrapper that does this whilst handling\nall the lifecycle issues for you. To use the wrapper, first set the navigation model as follows:\n\n``` kotlin\nN8.setNavigationModel(n8)\n```\n\nThen add the N8 navigation host, and your compose code will be updated whenever the navigation\nstate changes (i.e. your user has navigated forward or pressed back)\n\n``` kotlin\nsetContent {\n    AppTheme {\n        ...\n        N8Host { navigationState -\u003e\n        \n            val location = navigationState.currentLocation()\n\n            // the rest of your app goes here, this code runs\n            // whenever your user's location changes as a\n            // result of a navigation operation\n            \n            ModalNavigationDrawer(\n                drawerContent = ...\n                content = ...\n            )\n        }\n    }\n}\n```\n\nPass the N8 instance around the app using your choice of DI, or access it directly like this:\n\n``` kotlin\nN8.n8()\n```\n\nIn Compose style, you can also access the current navigation state from within N8Host scope:\n\n``` kotlin\nval navigationState = LocalN8HostState\n```\n\nCall the navigation functions from within ClickListeners / ViewModels / ActionHandlers etc as\nappropriate for your architecture\n\n``` kotlin\nonClick = {\n  n8.navigateTo(Paris)\n}\n```\n\n### Custom Navigation behaviour\n\nN8 tries to make standard navigation behaviour available to your app using basic functions by\ndefault, but you can implement any behaviour you like by writing a custom navigation mutation\nyourself.\n\nThe N8 navigation state is immutable, but internally it also has parent / child relationships that\ngo in both directions and most of the mutation operations involve recursion, so it's definitely an\nadvance topic, but there are mutation helper functions that N8 uses internally and that are\navailable for client use too (these are the functions that start with an underscore and are marked\nLowLevelApi - and they come with a warning! it's much easier to misuse these functions than the\nregular API)\n\nThere is an example in the sample app in CustomNavigationExt.kt and more information about N8's\ndata structure bellow (which should be considered required reading before attempting to write a\ncustom navigation mutation)\n\n### Interceptors\n\nCustom navigation mutations are hooked in using N8's interceptor API which looks like Ktor's\nplugin API so you can add or remove multiple interceptors for things like custom navigation\nmutations, logging, or analytics\n\n``` kotlin\nn8.installInterceptor(\"someCustomNavigationBehaviour\") { old, new -\u003e\n    new.copy(\n        navigation = someCustomNavigationBehaviour(new.navigation),\n    )\n}.installInterceptor(\"logging\") { old, new -\u003e\n    Log.i(\"navigation\", \"old backsToExit:${old.backsToExit} new backsToExit:${new.backsToExit}\")\n    new\n}\n```\n\n### Back Handling\n\nSystem back operations are intercepted at the n8-compose package level (i.e. it's not a part\nof the core navigation code in n8-core). This is exposed to client apps before any back operation\nis applied so that they can be blocked as required (for example to display a confirmation to the\nuser). The example app implements a confirmation dialog before the app is exited\n\n``` kotlin\nsetContent {\n    AppTheme {\n        ...\n        N8Host(onBack = backInterceptor() ) { navigationState -\u003e\n            ...\n        }\n    }\n}\n```\n\nonBack() is passed the navigation state before back is applied, and is a suspend function to allow \nclients to seek user input before returning.\n\n``` kotlin\nonBack: (suspend (NavigationState\u003cL, T\u003e) -\u003e Boolean)? = null, // true = handled/blocked/intercepted\n```\n\nReturning true means the back has been handled, false means it hasn't and n8 should proceed with \nthe back operation as usual\n\n### Persistence\n\nWhichever classes you chose to use to represent your Locations and TabHosts, make sure they are\nserializable and N8 will take care of persisting the user's navigation graph for you locally\n\nNotice this line in the constructor:\n\n``` kotlin\ntypeOf\u003cNavigationState\u003cLocation, TabHostId\u003e\u003e()\n```\n\nthat's how N8 can serialise and persist your navigation state across rotations or sessions without\nknowing anything about the class you chose for Location or TabHostId in advance. That line is very\nimportant, but it can't be verified by the compiler unfortunately. N8 will let you know if it's\nwrong though, either at construction, or the first time you try to add a TabHost (if one wasn't\nadded during construction).\n\n### DeepLinking\n\nThe current state of the navigation is always exportable/importable. In fact the whole state is\nserialized and persisted to local storage at each navigation step. You can take this serialized\nstate, send the String to someone in a message, import it into their app and thus transfer an entire\nnavigation state to another phone.\n\nFor deep linking you probably want to construct a custom navigation state, which is easy to do\nwith the helper functions, for example:\n\n``` kotlin\nn8.export(\n    backStackOf\u003cLocation, Unit\u003e(\n        endNodeOf(HomeScreen),\n        endNodeOf(ProductReviews),\n        endNodeOf(Review(productId=7898)),\n    )\n)\n```\n\nThe default serialized state is human readable, but not that pretty:\n\n``` kotlin\nbackStackOf\u003ccom.foo.bar.Location, Unit\u003e(endNodeOf(com.foo.bar.Location.HomeScreen),endNodeOf(com.\nfoo.bar.Location.ProductReviews), endNodeOf(com.foo.bar.Location.Review(productId=7898)),)\n```\n\nespecially once URLEncoded:\n\n```\nbackStackOf%3Ccom.foo.bar.Location%2C%20Unit%3E%28endNodeOf%28com.foo.bar.Location.HomeScreen%\n29%2CendNodeOf%28com.%0Afoo.bar.Location.ProductReviews%29%2C%20endNodeOf%28com.foo.bar.Locati\non.Review%28productId%3D7898%29%29%2C%29\n```\n\nSo you might want to encode/decode as you wish before sending it to your users, but that's outside\nthe scope of a navigation library.\n\nAnything more than a very small navigation graph can be quite verbose and I've found that tokenizing\nthe serialised data before trying compression techniques like Zstd or Brotli makes a big difference.\n\nThere's a basic example of using a tokens map for this in NavigationImportExportTest.kt Having said\nthat most deep links have a shallow navigation hierarchy so it might be a non issue for you\n\n### Passing data\n\nWhat data which locations accept, is defined by you. Here the location Sydney takes an optional\nwithSunCreamFactor parameter\n``` kotlin\n@Serializable\ndata class Sydney(val withSunCreamFactor: Int? = null) : Location()\n```\nSo if you want to navigate to the Sydney location, with factor 30 sun cream, you can just do this:\n``` kotlin\nnavigationModel.navigateTo(Sydney(30))\n```\nThat data will be available attached to the location. You can access it wherever you are picking up\nthe location changes in your code (your Compose UI usually). It will also be persisted along with\nthe rest of the navigation graph, so there is no way to loose it by rotating the screen or\nquitting the app, it becomes part of the graph and will still be there when you navigate back.\n\nQuite often you will want to collect some user data on a screen and then pass that data back to\na previous location:\n\n``` kotlin\nnavigationModel.navigateTo(Sydney())\nnavigationModel.navigateTo(SunCreamSelector)\nnavigationModel.navigateBack(\n    setData = {\n        when (it) {\n            is Sydney -\u003e {\n                it.copy(withSunCreamFactor = 50)\n            }\n\n            else -\u003e it\n        }\n    }\n)\n```\n\n### Data Structure\n\nIf you want to know how all this is working, the first step is to understand the\nunderlying data structure used to represent the state of the navigation at any point in time.\n\nThe navigation graph is represented as an immutable tree structure, when N8 logs its state, it logs\nthat\ntree structure from top-left to bottom-right, like a file explorer.\n\nThe first item is drawn on the top-left, and represents the entry into the app. As the user navigates \nto different locations, the tree structure grows down and right as locations are added in such a way \nthat the user can always find their way back to the home location and ultimately to exit the app \nby continually pressing back.\n\nWe call this first item (or the last item before exit as a user navigates back) the \"home\" location. \nThe home location of a nav graph is not necessarily where the user entered because the graph can\nbe arbitrarily re-written. Or the location where the user entered may have been some kind of intro\nscreen and never have even been added to the navigation graph in the first place (using `willBeAddedToHistory = false`)\n\nThe \"current\" location represents the screen the user is currently on and is typically towards the\nbottom right of the graph.\n\nHere's the state of a very simple linear navigation graph showing that the user's home location is the\nLondon screen, and is currently on the Tokyo screen:\n\n``` kotlin\nbackStackOf\u003cLocation, Unit\u003e(\n    endNodeOf(London),     \u003c--- home location\n    endNodeOf(Paris),\n    endNodeOf(Tokyo),     \u003c--- current location\n)\n```\n\nTo exit the app in this case, the user would have to press back 3 times\n(Tokyo -\u003e Paris -\u003e London -\u003e [exit])\n\nThere are a few utility functions that will let you create standalone navigation graphs for use in\nunit tests or constructing deep links etc:\n\n``` kotlin\nbackStackOf()\ntabsOf()\nendNodeOf()\n```\n\nMostly when using the utility functions to construct standalone navigation graphs, the compiler\nwill be able to work out the Location/TabHostId classes for you, but if not you can specify\nthem like this:\n\n``` kotlin\nbackStackOf\u003cMyLocationClass, MyTabHostIdClass\u003e(\n    endNodeOf(Welcome)\n)\n```\n\nHere's a more complicated nested navigation graph:\n\n``` kotlin\nbackStackOf(\n    endNodeOf(Welcome),     \u003c--- home location\n    tabsOf(\n        tabHistory = listOf(0,2),\n        tabHostId = TABHOST_MAIN,\n        backStackOf(\n            endNodeOf(MyFeed),\n            endNodeOf(Trending),\n        ),\n        backStackOf(\n            endNodeOf(Subscriptions),\n        ),\n        backStackOf(\n            endNodeOf(MyAccount),\n            endNodeOf(Settings),\n            tabsOf(\n                tabHistory = listOf(0),\n                tabHostId = TABHOST_SETTINGS,\n                backStackOf(\n                    endNodeOf(Audio),\n                    endNodeOf(Dolby),     \u003c--- current location\n                ),\n                backStackOf(\n                    endNodeOf(Video),\n                )\n            )\n        ),\n    )\n)\n```\n\nTo exit the app in this case, the user would have to press back 7 times, can you work out why? it's\nrelated to the tabHistory list\n\nEach node of the navigation graph is a Navigation item, and as you've probably noticed from the\nexamples, a Navigation item can be one of 3 types:\n\n#### 1. EndNode\n\nContains a single location only. The currentItem is always an EndNode. An EndNode is always\ncontained inside a BackStack. The very top left of the Navigation graph nearest the exit is never\nan unwrapped EndNode (it will always be found inside a BackStack, and sometimes that will be inside\na TabHost)\n\n``` kotlin\nendNodeOf(MyAccount)\n```\n\n#### 2. BackStack\n\nA list of other Navigation items. The _first_ item is the one closest to the exit. And for a simple\nnavigation graph with no TabHosts, the _last_ item is the current item. A BackStack can contain\nEndNodes or TabHosts (but can not directly contain other BackStacks)\n\n``` kotlin\nbackStackOf(\n    endNodeOf(MyAccount),\n    endNodeOf(Settings),\n    tabsOf(...)\n)\n```\n\n#### 3. TabHost\n\nContains a list of BackStacks only (each Tab is represented as a BackStack). A TabHost cannot\ndirectly contain either EndNodes or other TabHosts\n\n``` kotlin\ntabsOf(\n    tabHistory = listOf(0),\n    tabHostId = TABHOST_SETTINGS,\n    backStackOf(\n        endNodeOf(AudioSettings),\n    ),\n    backStackOf(\n        endNodeOf(VideoSettings),\n    )\n)\n```\n\n#### Logging the state\n\nIf some N8 behaviour is confusing, it can be helpful to print out the current state of the\nnavigation graph.\n\n``` kotlin\nn8.toString(diagnostics = false)\n```\n\nwill give you an output\nsimilar to the examples shown above. The outputs are deliberately formatted to be copy-pasteable\ndirectly into your kotlin code with only minor changes so you can re-create the graph for further\nexperimentation.\n\n``` kotlin\nn8.toString(diagnostics = true)\n```\n\nwill display parent and child relationships, that's useful for library developers diagnosing\nissues or clients implementing custom mutations\n\n### TabHost navigation\n\nTabHosts can be nested arbitrarily and are identified by an id. Changing tabs by specifying a\ntabIndex, adding a brand new TabHost at the user's current location, optionally clearing the tab's\nhistory when the user selects that tab, or breaking out of a TabHost completely and continuing on a\ntab\nfrom a parent TabHost is all supported with the same functions:\n\n``` kotlin\n/**\n * to continue in whatever tab you are on (if any)\n *\nnavigationModel.navigateTo(Madrid)\n```\n\n``` kotlin\n/**\n * to break out of the current TabHost (TAB_HOST_SETTINGS say) and continue in\n * the TabHost parent identified by TAB_HOST_MAIN\n *\nnavigationModel.navigateTo(Tokyo) { \"TAB_HOST_MAIN\" }\n```\n\n``` kotlin\n/**\n * to break out of the current TabHost (TAB_HOST_SETTINGS say) and continue in\n * the top level navigation (which may be a TabHost or a plain BackStack)\n *\nnavigationModel.navigateTo(location = SignOutScreen) { null }\n```\n\n##### Structural v Temporal tabs\n\nTabHosts tend to treat the back operation in one of 2 different ways. N8 calls these two modes\n\"Structural\" and \"Temporal\".\n\nBy Structural we mean something akin to the old \"up\" operation in android. Let's say you have an\napp that contains a single TabHost with 3 tabs, let's say the user has built up a history on this\nTabHost by selecting all 3 tabs in turn, but while they have been on the current tab, has\nonly navigated to 2 new locations.\n\nThe navigation graph might look like this:\n\n``` kotlin\ntabsOf(\n    tabHistory = listOf(2),\n    tabHostId = \"TABHOST_MAIN\",\n    backStackOf(\n        endNodeOf(Houston),\n        endNodeOf(Tokyo),\n    ),\n    backStackOf(\n        endNodeOf(Paris),\n        endNodeOf(Sydney),\n    ),\n    backStackOf(\n        endNodeOf(London),\n        endNodeOf(Mumbai),\n        endNodeOf(Shanghai), \u003c--- current location\n    ),\n)\n```\n\n**Structural** back navigation here would mean that when the\nuser presses back, they would visit the previously visited locations in the current tab only, and then\nexit the app (so in the above example, 3 clicks back to exit: Shanghai -\u003e Mumbai -\u003e London -\u003e exit)\n\nBy **Temporal** we mean something more like a time based history. Let's take the example from above, a\ntemporal version might look like this:\n\n``` kotlin\ntabsOf(\n    tabHistory = listOf(1,0,2),\n    tabHostId = \"TABHOST_MAIN\",\n    backStackOf(\n        endNodeOf(Houston),\n        endNodeOf(Tokyo),\n    ),\n    backStackOf(\n        endNodeOf(Paris),\n        endNodeOf(Sydney),\n    ),\n    backStackOf(\n        endNodeOf(London),\n        endNodeOf(Mumbai),\n        endNodeOf(Shanghai), \u003c--- current location\n    ),\n)\n```\n\nIn this case when the user presses back, they would re-trace their steps through the locations\nvisited while on ```tabIndex = 2```, and then do the same for ```tabIndex = 0```, and then\n```tabIndex = 1```, before finally exiting the app.\n\nSo in our example, that would take 7 clicks back to exit:\nShanghai -\u003e Mumbai -\u003e London -\u003e Tokyo -\u003e Houston -\u003e Sydney -\u003e Paris -\u003e [exit]\n\nNote that N8 implements those two modes using only the **tabHistory** field.\n\nYou can set the TabBackMode via the ```switchTab()``` function. The default\nis ```TabBackMode.Temporal```\n\n\n## License\n\n    Copyright 2015-2025 early.co\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n","funding_links":[],"categories":["Kotlin"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ferdo%2Fn8","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ferdo%2Fn8","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ferdo%2Fn8/lists"}