A library that enables Safe Navigation for you Composable destinations when using Jetpack Compose Navigation

Overview

Medium Blogpost

Monstar Blogpost

Compose Safe Routing

A small code generating library, inspired by SafeArgs for android, that generates helper code that can be used for Jetpack Compose Navigation Component.

Release Notes

Features

  • Removes code duplication when describing your routes and its arguments through out the application
  • Helper functions to declare your Routes using RouteSpec interface
  • Support for Accompanist Animation/Material libraries
  • Helper functions that will allow to obtain passed arguments easily
  • Safety during navigation: RoutesActions.kt always contains arguments your destination needs
  • Mandatory & Optional parameters

Installation

in your project level build.gradle

allprojects {
	repositories {
		...
		maven { url 'https://jitpack.io' }
	}	
}

And then in you app level build.gradle

dependencies { 
    kapt("com.github.levinzonr.compose-safe-routing:compiler:2.3.0")
  
  implementation("com.github.levinzonr.compose-safe-routing:core:2.3.0")
  // or in case you are using animation/material routes from accompanist
  implementation("com.github.levinzonr.compose-safe-routing:accompanist-navigation:2.3.0")
  
}
dependencies {
    kapt 'com.github.levinzonr.compose-safe-routing:compiler:2.3.0'
  
  implementation 'com.github.levinzonr.compose-safe-routing:core:2.3.0'
  // or in case you are using animation/material routes from accompanist
  implementation 'com.github.levinzonr.compose-safe-routing:accompanist-navigation:2.3.0'
}

Description

Safe routing serves as a safeArgs analogue for jetpack compose navigation library. Using annotation processor it generates possible possible Paths And Actions so you don't have to hardcode argument names and names of the routes.

@Route

First apply @Route annotation on the composable that represent a screen in you application

@Route(name = "profile")
@Composable
fun ProfileScreen() {
    /** your screen */
}

@RouteArg

Its also possible describe the arguments for your routes using @RouteArg annotation. It takes a name and the type of the param.

Currently supported types: String, Int, Float, Long, Boolean

You can also specify whether or not the argument is optional or not. This will determine how argument will be attached to the path and if default value should be used. Note that due to Annotations limitations the default value is passed as String and then casted to the type specifed.

@Composable
@Route("details", args = [
    RouteArg("id", RouteArgType.StringType, false),
    RouteArg("number", RouteArgType.IntType, true, defaultValue = "1"),
]) 
fun DetailsScreen() {
  /** sweet composable code ** /
}

Output

After you build your project with these annotations applied several files will be generated for you. First one is Routes, in which you can access all routes with their corresponding paths and arguments Another one is RouteActions where you can build these paths as a valid destination with all arguments applied. With the examples above this file would look like this

Additionally, an Argument wrapper would be generated for each route, so you can easily access it from either NavBackStackEntry or from SavedStateHandle in your ViewModel

This will allow you to declare your composable inside NavHost more easilly by using NavGraphBuilder extensions like so

NavHost(startDestination = Routes.Profile.route) {
  composable(Routes.Profie) { 
     ProfileScreen()
  }
  
   composableWithArgs(Routes.Details) { entry, args -> 
      DetailsScreen(args)
   }
  
  // or in case you want to process args manually 
  composable(Routes.Details) { entry -> 
      DetailsScreen(DetailsRouteArgsFactory.fromBackStackEntry(entry))
  }
}

Routes.kt

= listOf() override val argsFactory: RouteArgsFactory = EmptyArgsFactory } val details: RouteSpec = object : RouteSpec { override val route: String = "details/{id}?number={number}" override val navArgs: List = DetailsRouteArgs.navArgs override val argsFactory: RouteArgsFactory = DetailsRouteArgsFactory } ">
object Routes {
  val profile: RouteSpec = object : RouteSpec {
    override val route: String = "profile"
    override val navArgs: List<NamedNavArgument> = listOf()
    override val argsFactory: RouteArgsFactory<ProfileRouteArgs> = EmptyArgsFactory
  }
  
  val details: RouteSpec = object : RouteSpec {
    override val route: String = "details/{id}?number={number}"
    override val navArgs: List<NamedNavArgument> = DetailsRouteArgs.navArgs
    override val argsFactory: RouteArgsFactory<ProfileRouteArgs> = DetailsRouteArgsFactory

  }

RoutesActions.kt

object RoutesActions {
  fun toProfile(): String = "profile"
	fun toDetails(id: String, number: Int = 1): String = "details/$id?number=$number"
  fun toDetails(id: String): String = "details/$id"}

Details Route Args

data class DetailsRouteArgs(
  val id: String,
  val number: Int
) {
  companion object {
    /**
     * NamedNavArgs representation for DetailsRouteArgs
     */
    val navArgs: List<NamedNavArgument> = listOf(
      navArgument("id") {
        type = NavType.StringType 
        nullable = false
      },

      navArgument("number") {
        type = NavType.IntType 
        nullable = false
        defaultValue = 1
      },

    )
  }
}
Comments
  • Omit nullable String arguments instead of converting them to

    Omit nullable String arguments instead of converting them to "null"

    When specifying a navigation argument as a String type, optional the parameter is still appended with a string "null" instead of being ommitted in the route.

    Is this expected or would this be an enhancement to work on?

    opened by ChristianOrgler 10
  • can we generate Generating Route Actions just like how Room db generates Database.

    can we generate Generating Route Actions just like how Room db generates Database.

    So the idea is RouteActionsis an abstract class which takes NavHostControlleras contructorparameter.

    The user needs to extend this classs

    @Actions
    AudioNavActions: RouteActions(controller: NavHostController){
    }
    

    Now we must provide each root what class it belongs like

    @Route(
        name = "Library",
       class = AudioRouteActions::class
    )
    
    

    Two isues with your current implementation can be solved with it.

    • Your current implementation currently genereates single implementation. which makes code messsy if you are dealing with Nested Graphs.
    • Secondly; it just generates routes not navigates to them.
    • Thirdly, I suggest insetead of overloading methods you shoud use default parameters.
    opened by prime-zs1 5
  • Route not generated for composable with a

    Route not generated for composable with a "Dp" parameter

    No route was generated for my composable when it looked like this:

    @Route(name = "profile")
    @Composable
    fun ProfileScreen(bottompadding: Dp) {}
    

    Removing the "Dp" (androidx.compose.ui.unit.Dp) parameter meant that the route was correctly generated

    I ended up swapping out that parameter for a PaddingValues parameter, and that works fine

    opened by Ezard 5
  • GraphRoutes package

    GraphRoutes package

    During developing our project in couple developers we recognize that is some bug with generating {GraphName}GraphRoutes class. For different developer this class has different package location, so often we have conflict in git. Avery of us have the same versions of code. For example out main package is: com.example.app, and we Create some screen: Splash, Login, Register: com.example.app.splash.Splash com.example.app.auth.Login com.example.app.auth.Regster For all of this screens we are using @Route(name = "NAME_OF_SCREEN")

    For me, lib will create classs MainGraphRoutes in com.example.app.splash.MainGraphRoutes, but for next developer it will be com.example.app.auth.MainGraphRoutes.

    Is any way to define in which package MainGraphRoutes should be generated?

    opened by grabos 4
  • error KAPT version 2.5.2

    error KAPT version 2.5.2

    java.lang.Exception: Error while processing annotations: java.lang.IllegalArgumentException: NavGraph [main] has no start route specified

    use for initialize navgraphSafeRouteNavHost

    opened by hidayat05 1
  • RouteAction for BottomSheet with argument

    RouteAction for BottomSheet with argument

    sample generated RouteActions

    object RoutesActions {
      /**
       * Builds an action to "transaction_status" route
       */
      fun toTransaction_status(status: String): String = "transaction_status/$status"  --> should transaction_status?status={status}
      
    }
    

    there should be a differentiator for Route bottomsheet. Thanks.

    opened by hidayat05 1
  • Automatically create NavHost

    Automatically create NavHost

    Hi, thanks for your library, this is very much missing from the official compose navigation library.

    I still quite don't understand the way the routes are set up in Navigation Compose, but it seems to me that there is still an unnecessary step when creating the NavHost composable.

    NavHost(navController = navController, startDestination = MainGraph.route) {
        navigation(LoginGraph) {
            login { LoginScreen(navController) }
        }
    
        navigation(MainGraph) {
            camera { CameraScreen(navController) }
            home { HomeScreen(navController) }
            item { ItemScreen(navController) }
            profile { ProfileScreen(navController) }
        }
    }
    

    Since all Screens are already annotated with the @Route annotation providing all the information needed, isn't it possible for this library to generate the whole NavHost composable without having to add new screens to it manually every time?

    Sorry if I'm missing something.

    discussion 
    opened by cvb941 5
Releases(2.5.3)
  • 2.5.3(Aug 11, 2022)

  • 2.5.2(May 29, 2022)

    Added

    Configuring SafeRoute

    If you want to customize and/or adjust the way SafeRoute works you can do so by passing arguments to the processor.

    Default Package name

    Some files, such as MainGraph, generated by SafeRoute are generated without specific relation to the package name. Their package name is based on the first element the processor encounters, which is not always ideal and can lead to unintended behaviour To fix this, you can specify the defaultPackageName where files like these will be placed

    kapt {
        arguments {
            arg("safeRoute.defaultPackageName", "cz.levinzonr.saferoute.navigation")
        }
    }
    
    ksp {
        arg("safeRoute.defaultPackageName", "cz.levinzonr.saferoute.navigation")
    }
    
    Source code(tar.gz)
    Source code(zip)
  • 2.5.1(May 20, 2022)

  • 2.5.0-beta02(Apr 23, 2022)

    KSP Support

    From version 2.5.0 Safe Route now has the support for Kotlin Symbol Processing (KSP). The API is the same so the migration should be more or less painless (considering you already using 2.5.0-X version), however, the imports might change. Here is how to get started.

    This is a pre-release based on the 2.5.0-beta01, make sure to also check whats new in beta01 release here

    ⚠️ If you dont want to use KSP you can still use KAPT, KAPT support is not going anywhere :). However in order to keep the modules name aligned, compiler artifact was renamed into processor-kapt

        kapt("com.github.levinzonr.compose-safe-routing:processor-kapt:2.5.0-beta02")
    

    Add KSP Plugin (if you haven't already)

    Gradle

    plugins {
        id  "com.google.devtools.ksp" version '"1.6.10-1.0.3" // 
    }
    

    Kotlin

    plugins {
        id("com.google.devtools.ksp") version "1.6.10-1.0.3" // Depends on your kotlin version
    }
    

    Add KSP Build Directory to your source sets (if you havent already)

    Gradle

    applicationVariants.all { variant ->
        kotlin.sourceSets {
            getByName(variant.name) {
                kotlin.srcDir("build/generated/ksp/${variant.name}/kotlin")
            }
        }
    }
    

    Kotlin

    applicationVariants.all {
        kotlin.sourceSets {
            getByName(name) {
                kotlin.srcDir("build/generated/ksp/$name/kotlin")
            }
        }
    }
    

    Add Safe Route KSP Processor

    The final step would be to add a new dependency (if you've been using kapt processor you can now remove it) and build the project and fix some imports

        ksp("com.github.levinzonr.compose-safe-routing:processor-ksp:2.5.0-beta02")
    
    Source code(tar.gz)
    Source code(zip)
  • 2.5.0-beta01(Apr 10, 2022)

    Disclaimer ⚠️

    This update is quite big in terms of introducing new logic and deprecating the logic that was here since the beginning. Thus, I'd like to gather as much feedback as possible and encourage you to open issues in case you have any suggestions regarding newly introduced stuff. I'm also making this update as beta as there are things I would like to make it in full 2.5.0 and so there is somewhat of a breathing room in case of any feedback :) Thanls!

    NavGraph Support

    Version 2.5.0 introduces support to the Navigation Graphs so you can structure your routes in a more concise way. By default, all your current routes will be a part of the "Main" Graph - the default graph all route annotations have, and, since all NavGraphs need to have a starting point, you need to declare of your routes to be a "start", otherwise the build may fail

    @Route(
        transition = AnimatedRouteTransition.Default::class,
        navGraph = RouteNavGraph(start = true)
    )
    

    Defining Custom Graphs

    If you want to define your graph you can simply set another graph name and set a starting route for it

    @Route(
        name = "PokemonList",
        transition = FadeInFadeOutTransition::class,
        navGraph = RouteNavGraph("pokedex", start = true)
    )
    

    This will generate a new file and called PokedexGraph and PokedexGraphRoutes which you can you for navigation or declaration, like so

    navigation(PokedexGraph) {
        pokemonList {
            // Composable content()
        }
    }
    and navigation
    navController.navigateTo(PokedexGraph)
    navContoller.navigateTo(PokemonDetailsRoute())
    
    

    New Navigation APIs and Deprecation of the Old Ones

    RoutesActions.kt were deprecated in favor of the invoke operator that is a part of RouteSpec. We've added another navController extension navigateTo() that can be used together with it. This results in one less generated file and more straightforward usage. NavController+Routes.kt will also be deleted in the future releases

    2.4.X - Old behaviour

    navController.navigate(RouteActions.toDetails("id")
    navController.navigateToDetails("id")
    

    2.5.0 - New behavior

    navController.navigateTo(DetailsRoute("Id"))
    

    Router API

    2.5.0 Introduces new component called Router, which is basically a wrapper around your usual navController, but is specifically built to handle navigation using the newly introduced extensions. It is also an Interface, which means it would be easier to test your composables. Router can be accessed from Composition Local and used as so:

    val router = LocalRouter.current
    router.navigate(DetailsRoute("id"))
    

    Deprecation of Routes.kt

    Routes.kt is now also deprecated since we've introduced the navigation graphs which allow you to structure in a similar fashion but in a more logical and flexible way. By Default, all your Routes will be a part of the MainGraphRoutes.kt so the migration should be fairly simple. Every new graph will hold its own set of Routes.

    Source code(tar.gz)
    Source code(zip)
  • 2.4.0(Feb 21, 2022)

    2.4.0 Release Notes

    Introducing RouteTransitions

    You can specify the desired transition using route builder.

    route(Routes.Details, DefaultRouteTransition) { /* content */ }
    

    You can also specify the desired transition right inside the @Route annotation like so. Doing so will allow you to use the generated route builder in your NavHost

    @Route(
    		name = "HomeScreen"
        transition = AnimatedRouteTransition.Default::class
    )
    
    // then you can use the generated route builder
    NavHost {
       homeScreen {
         val viewModel = hiltViewModel()
          HomeScreen(viewModel)
       }
    }
    

    Misc

    • name property of @Route annotation is now optional. By default it will take the name of the composable
    • Navigation Compose version upated to 2.4.1
    Source code(tar.gz)
    Source code(zip)
  • 2.3.2(Feb 18, 2022)

    Fixes

    • Skip generating multiple route actions for routes with optional arguments (#19)

    Full Changelog: https://github.com/levinzonr/compose-safe-routing/compare/2.3.1...2.3.2

    Source code(tar.gz)
    Source code(zip)
  • 2.3.1(Dec 7, 2021)

    Fixes

    • #14 - Add :at:null default value for the optional and nullable params, which can be later extracted as proper nulls instead of the "null" string text
    • Fixes an issue with providing default String values
    Source code(tar.gz)
    Source code(zip)
  • 2.3.0(Oct 31, 2021)

    New RouteArgs API

    RouteArgs are now more easily accesseble using CompositionLocal APIs. Using that approach there is no need to pass you arguments down the tree as they will be accessible for you entire Route. Plus, its way more convinient than deailing it NavBackStackEntry directly.

    // OLD 2.2.X
    composable(Routes.Details) { entry -> 
      val args = RouteDetailsArgsFactory.fromBackStackEntry(entry)
      DetailsScreen(args = args)
    }
    
    // NEW 2.3.0
    composable(Routes.Details) {
      val args = LocalDetailsRouteArgs.current
      DetailsScreen(args = args)
    }
    
    // Or, retrieve it elsewhere :)
    composable(Routes.Details) { DetailsScreen() }
    
    DetailsScreen() {
      val args = LocalDetailsRouteArgs.current
      
      // also available through RouteSpec extension
      val args = Routes.Details.currentArgs
    }
    

    DeepLinks Support

    SafeRoute now can handle the DeepLinks for you. Simply provide the deep link param for the desired Route and thats it. Any parameters you expecting being passed through the deeplink will be handled by the RouteArgs API as well, if they have the same names

    @Composable
    @Route(
        name = "details",
        args = [ RouteArg("id", String::class, isOptional =false)],
        deepLinks = [RouteDeeplink("app://deeplink/{id}")]
    )
    fun DetailsScreen() {
      // deeplink args will aslo be here
      val args = LocalDetatailsRouteArgs.current
      
    }
    

    Dialogs support

    You can add use SafeRoute with dialogs.

    fun NavGraphBuilder.mainGraph() {
      dialog(Routes.Popup) { Popup() }
      composable() { HomeScreen() }
    }
    

    API changes

    *withArgs extensions

    composableWithArgs and other *withArgs extensions have been deprecated in favor of CompositionLocalRouteArgs APIs

    New Annotations

    New Annotations with the same name were created inside the :core module to facilitate future development and introduce new features. Old annotations are now deprecated and will still work, however, some new features might be missing

    Migration steps

    1. Replace occurrences of cz.levinzonr.saferoute.annotations.RouteArg with cz.levinzonr.saferoute.core.annotations.RouteArg

    2. Replace occurrences of cz.levinzonr.saferoute.annotations.Route with cz.levinzonr.saferoute.core.annotations.Route

    3. Argument types are now specified using KClass<*>, list of supported types has not changed

    Source code(tar.gz)
    Source code(zip)
  • 2.2.0(Oct 20, 2021)

    NavController Extensions

    RouteActions are now available through the extensions of the navController.

    // 2.1.X
    navController.navigate(RouteActions.toDetails("myId"))
    
    // 2.2.0
    navController.navigateToDetails("myId")
    

    Dependencies Updates

    • Update Navigation Compose to 2.4.0-alpha10
    • Update Accompanist to 0.20.0
    Source code(tar.gz)
    Source code(zip)
  • 2.1.0(Aug 22, 2021)

  • 2.0.0(Aug 18, 2021)

    2.0.0 Release Notes

    NavGraphBuilder Extensions

    The library now provides multiple extension function that you can use to declare your Routes

    1.0.X

    composable(Routes.screen.path, RouteSpec.screen.navArgs) { entry ->
       val args = ScreenArgs.fromBackStackEntry(it)
    }
    

    2.0.0

    composable(Routes.Screen) {
       val args = ScreenArgsFactory.fromBackStackEntry(it)
    }
    
    // or, in case you dont want to handle arguments manually
    composableWithArgs(Routes.Screen) { _, args
       // you screen             
    }
    

    Minor changes

    • Custom NavHost() that can accept the RouteSpec
    • navigation() builder

    Breaking Changes

    Imports

    Annotations were moved to the different package. This can be fixed quicky by using find and replace from cz.levinzonr.router.core to cz.levinzonr.saferoute.annotations

    1.0.X

    import cz.levinzonr.router.core.Route
    import cz.levinzonr.router.core.RouteArg
    import cz.levinzonr.router.core.RouteArgType
    

    2.0.0

    import cz.levinzonr.saferoute.annotations.Route
    import cz.levinzonr.saferoute.annotations.RouteArg
    import cz.levinzonr.saferoute.annotations.RouteArgType
    

    Retrieveing Arguments

    Static functions of Args were replaced with arguments were replaced with ArgsFactories. fromBackStackEntry() is now an extension function

    1.0.X

    SomeRouteArgs.fromSavedStatedHandle(savedStateHandle)
    SomeRouteArgs.fromBackStackEntry(entry)
    

    2.0.0

    SomeRouteArgsFactory.fromSavedStateHandle(savedStateHandle)
    SomeRouteArgsFactory.fromBundle(bundle)
    SomeRouteArgsFactory.fromBackStackEntry(entry) -- an extension, using fromBundle() under the hood
    

    Generated Files

    • RouteSpec.path renamed to RouteSpec.route

    • RouteSpec.route now is always capitalised

    Source code(tar.gz)
    Source code(zip)
Owner
Roman Levinzon
Roman Levinzon
This is a simple video games discovery app showcasing UI using Jetpack Compose with Clean Architecture and also tests for composable UI.

Jetpack-Compose-Video-Games-Example ?? This is a simple video games discovery app showcasing UI using Jetpack Compose and also tests for composable UI

Ruben Quadros 60 Dec 27, 2022
A library that enables reuse of Material themes defined in XML for theming in Jetpack Compose.

MDC-Android Compose Theme Adapter A library that enables reuse of Material Components for Android XML themes for theming in Jetpack Compose. The basis

Material Components 409 Dec 24, 2022
Capturable - 🚀Jetpack Compose utility library for capturing Composable content and transforming it into Bitmap Image🖼️

Capturable ?? A Jetpack Compose utility library for converting Composable content into Bitmap image ??️ . Made with ❤️ for Android Developers and Comp

Shreyas Patil 494 Dec 29, 2022
Kapture - A small library for Jetpack Compose to capture Composable content to Android Bitmap

kapture A small utility library for Jetpack Compose to capture Composable conten

Kaustubh Patange 10 Dec 9, 2022
Flippable - A Jetpack Compose utility library to create flipping Composable views with 2 sides

?? Flippable A Jetpack Compose utility library to create flipping Composable views with 2 sides. Built with ❤︎ by Wajahat Karim and contributors Demo

Wajahat Karim 298 Dec 23, 2022
A Jetpack Compose Modifier that enables Tinder-like card gestures.

Compose Tinder Card A Jetpack Compose Modifier that enables Tinder-like card gestures. Demo Click the play button to see the Modifier.swipeableCard()

Alex Styl 93 Dec 28, 2022
Missing safe arguments generator for Compose Navigation

Safe Arguments Generator Yet another attempt to add safe arguments to Compose Navigation. Why Since routes in Navigation Component don't support safe

Vitali Olshevski 9 Nov 3, 2022
a set of Settings like composable items to help android Jetpack Compose developers build complex settings screens

This library provides a set of Settings like composable items to help android Jetpack Compose developers build complex settings screens without all the boilerplate

Bernat Borrás Paronella 178 Jan 4, 2023
Jetpack Compose Text composable to show html text from resources

HtmlText Current Compose Version: 1.0.3 Compose HtmlText Text composable to show html text from resources Add to your project Add actual HtmlText libr

Alexander Karkossa 57 Dec 23, 2022
Mocking with Jetpack Compose - stubbing and verification of Composable functions

Mockposable A companion to mocking libraries that enables stubbing and verification of functions annotated with @androidx.compose.runtime.Composable.

Jesper Åman 21 Nov 15, 2022
A collection of animations, compositions, UIs using Jetpack Compose. You can say Jetpack Compose cookbook or play-ground if you want!

Why Not Compose! A collection of animations, compositions, UIs using Jetpack Compose. You can say Jetpack Compose cookbook or play-ground if you want!

Md. Mahmudul Hasan Shohag 186 Jan 1, 2023
Compose-navigation - Set of utils to help with integrating Jetpack Compose and Jetpack's Navigation

Jetpack Compose Navigation Set of utils to help with integrating Jetpack Compose

Adam Kobus 5 Apr 5, 2022
Sample app that shows how to create a bitmap from a Jetpack composable

Creating Bitmaps From Jetpack Composables This app demonstrates how to create a bitmap from a Jetpack composable without the need to display the compo

Johann Blake 14 Nov 29, 2022
Row Coloumn Box Compose Constraint Layout Modifier.xyz Animator Tween animation MutableState Creating custom composable Corners Canvas LaunchedEffect

Row Coloumn Box Compose Constraint Layout Modifier.xyz Animator Tween animation MutableState Creating custom composable Corners Canvas LaunchedEffect

Shivaraj M Patil 1 Apr 13, 2022
Simple composable for rendering transitions between backstacks.

compose-backstack Simple library for Jetpack Compose for rendering backstacks of screens and animated transitions when the stack changes. It is not a

Zach Klippenstein 408 Jan 3, 2023
Boat - A scoped and composable way to navigate

Boat Boat is an implementation of a scoped, simple and composable way to navigat

Bloder 5 Feb 9, 2022
Pinocchio is a group of libraries for various common UI components. It could contain Composable, View, and everything related to UI.

Pinocchio Pinocchio is a group of libraries for various common UI components. It could contain Composable, View, and everything related to UI. All UI

NAVER Z 24 Nov 30, 2022
GlassmorphicColumn @Composable

glassmorphic-composables GlassmorphicColumn @Composable GlassmorphicRow @Composable With Non-Image background Setup Gradle: allprojects { reposito

Jakhongir Madaminov 62 Dec 8, 2022
Jetpack Compose Boids | Flocking Insect 🐜. bird or Fish simulation using Jetpack Compose Desktop 🚀, using Canvas API 🎨

?? ?? ?? Compose flocking Ants(boids) ?? ?? ?? Jetpack compose Boids | Flocking Insect. bird or Fish simulation using Jetpack Compose Desktop ?? , usi

Chetan Gupta 38 Sep 25, 2022