BRouter 是一个 Kotlin 编写的面向模块 Android 路由库

Overview

简介

BRouter 是一个 Kotlin 编写的面向模块 Android 路由库,支持:

  • 路由
    • 泛平台路由定义
    • 自定义启动流程
    • 统一路由协议
    • 全局/模块/路由三级拦截器
    • 灵活的路由匹配/捕获规则
  • 服务
    • 服务发现
    • 依赖注入
  • 任务
    • 初始化去中心化
    • 自动解析任务 / 服务依赖图
    • 可选执行线程
  • 模块
    • 模块生命周期
  • 数据
    • 可扩展 APT
    • 元数据描述

接入

  • Android Gradle Plugin >= 3.3.0
  • Gradle >= 5.3
  • Java >= 1.8
  • Kotlin 可选
brouter_version = "1.0.0"

dependencies {

    // androidx
    annotationProcessor "com.bilibili.android:brouter-apt:$brouter_version" // or kapt for Kotlin
    implementation "com.bilibili.android:brouter-api:$brouter_version" // API 模块,通常仅需要依赖该模块,不要依赖 core
    implementation "com.bilibili.android:brouter-core:$brouter_version" // 核心模块,仅需要配置初始化时用到
    
    // appcompat
    nnotationProcessor "com.bilibili.android:brouter-apt-appcompat:$brouter_version"
    implementation "com.bilibili.android:brouter-core-appcompat:$brouter_version"
    implementation "com.bilibili.android:brouter-api-appcompat:$brouter_version"
}

Application 模块需要 apply 路由插件有 2 种方式:

buildscript {
    dependencies {
        classpath "com.bilibili.android:brouter-gradle-plugin:$brouter_version"
    }
}

apply plugin: 'com.bilibili.brouter'

plugins {
  id "com.bilibili.brouter" version brouter_version
}

使用

初始化

BRouterCore.setUp(Application) { builder : GlobalConfiguration.Builder ->
    // 自定义初始化配置,可选
}

// 初始化配置 API,每个配置项的含义请参考接口文档
interface GlobalConfiguration.Builder {
    val preMatchInterceptors: MutableList<RouteInterceptor>
    val postMatchInterceptors: MutableList<RouteInterceptor>
    val attributeSchema: AttributeSchema
    fun defaultScheme(defaultScheme: String): Builder
    fun logger(logger: SimpleLogger): Builder
    fun emptyRouteTypeHandler(handler: EmptyTypeHandler): Builder
    fun servicesMissFactory(factory: OnServicesMissFactory): Builder
    fun authenticator(authenticator: RouteAuthenticator): Builder
    fun addPreMatchInterceptor(interceptor: RouteInterceptor): Builder
    fun addPostMatchInterceptor(interceptor: RouteInterceptor): Builder
    fun routeListener(listener: RouteListener): Builder
    fun routeListenerFactory(factory: RouteListener.Factory): Builder
    fun executor(executor: ExecutorService): Builder
    fun attributeSchema(action: (AttributeSchema) -> Unit): Builder
    fun globalLauncher(globalLauncher: GlobalLauncher): Builder
    fun taskExecutionListener(taskExecutionListener: TaskExecutionListener): Builder
    fun taskComparator(comparator: Comparator<Task>): Builder
}

路由

声明一个简单路由
@Routes(
    value = [ // 匹配规则,必填,其他均可选
        "(http|https)://www.bilibili.com/BV{bvid}",
        "bilibili://video/BV{bvid}",
    ], 
    exported = true, // 自动生成相关的 Manifest Deep Link 信息,仅 Library 模块有效
    routeName = "VideoDetails",
    desc = "这是视频详情播放页",
    routeType = StandardRouteType.NATIVE, 
    interceptors = [CheckLoginInterceptor::class],
    launcher = CustomLauncher::class
)
@Attribute("player", "main") // 页面特性,可选
class VideoFragment

路由规则中末尾的/会被忽略,如果没有 scheme 则会使用上面初始化配置中的 defaultScheme。

这里支持 3 类路由规则,请注意,前两类路由规则不能跨越/,且不同路由规则是互斥的,每个段内只能应用一类,优先级从高到低分别是

  1. 复合 (? path1|path2|pathN) ,其中? 是可选的,括号是可嵌套的,如(bilibili|http|https)(bilibili|htt(p|ps))匹配规则是一样的,注意括号中的字符串不能为空,即暂不支持http(|s)这样的格式,段内可以有多个
  2. 通配 {capture_name} 或者 *, 其中*等价{},即匹配但是不捕获,段内只能有一个
  3. 前缀 ** ,只能存在末尾,如https://bilibili.com/**,会匹配https://bilibili.com/a/b/n,但是不能匹配https://bilibili.com,一定会产生捕获,capture_name 为空字符串

上述中所有的捕获会置于匹配后的路由信息中的,通常最终会被启动器作为参数带入页面中。

拦截器

拦截器分为三个级别,依次是 全局 / 模块 / 路由,其中全局又分为匹配前与匹配后,继续上面的示例

@Singleton
@Services
class CheckLoginInterceptor(private val loginService: LoginService) : RouteInterceptor {
    override fun intercept(chain: RouteInterceptor.Chain): RouteResponse {
        return if (!loginService.isLogin) {
            RouteResponse(RouteResponse.Code.UNAUTHORIZED, chain.request)
        } else {
            chain.proceed(chain.request)
        }
    }
}

路由内拦截器实例化的方式有2种,一种是尝试去取对应类的默认服务,即上面的示例, 如果不存在则调用OnServicesMissFactory.create,默认实现是用无参构造函数反射。

该拦截器做了登陆校验,如果未登录会返回未认证,如果上面初始化时配置了 RouteAuthenticator,则会自动重定向到认证页,且认证后自动转发请求,具体用法见工程内示例。

路由请求
RouteRequest

BRouter 通过RouteRequest这个类来启动路由或者获取路由信息,示例:

// 启动
val response = BRouter.routeTo("bilibili://video/BV1tC4y1H7yz".toRouteRequest(), this)

// 获取路由信息
val response = "bilibili://video/BV1tC4y1H7yz".toRouteRequest().newCall(CallParams(RequestMode.ROUTE, this)).execute()
RouteResponse

路由请求的结果是RouteResponse

interface RouteResponse {
    val code: Code
    val request: RouteRequest
    val message: String
    val routeInfo: RouteInfo?
    val obj: Any?
    val redirect: RouteRequest?
    val flags: Int
    val priorFailureResponse: RouteResponse?
    val priorRouteTypeResponse: RouteResponse?
    val prevRequestResponse: RouteResponse?
    enum class Code {
        OK,
        REDIRECT,
        BAD_REQUEST,
        UNAUTHORIZED,
        FORBIDDEN,
        NOT_FOUND,
        ERROR,
        FOUND_STUB,
        UNSUPPORTED,
    }
}

其中RouteInfo是路由匹配的结果,在拦截器中也可以获取到

interface RouteInfo : HasAttributes {
    val routeName: String
    val routeRule: String
    val routeType: String
    val captures: Map
   
    
    val target: Class<*>
    val interceptors: Array
    
     >
    val module: Module
}

    
   

其中每个字段的含义请参阅接口文档。

统一路由协议

RouteRequest是统一路由协议的超集

  • 任意一条统一路由协议的字符串均可转化为一个 RouteRequest 对象
  • 任意一个 RouteRequest 对象也可以转化为一条统一路由协议,但是部分字段可能会丢失

举例:

"bilibili://video/BV1tC4y1H7yz".toBuilder()
    .routeTypes("native", "web")
    .flags(1)
    .prev("https://www.bilibili.com".toRouteRequest())
    .forward("https://baidu.com".toRouteRequest())
    .props {
        it.append("prop1", "1")
            .append("prop1", "2")
            .append("prop2", "2")
    }
    .params {
        it.append("param1", "1")
            .append("param1", "2")
    }
    .attributes {
        it.attribute("player", "main")
    }
    .build()
// 等价于
("bilibili://video/BV1tC4y1H7yz?" +
        "-Bpt.types=native&" +
        "-Bpt.types=web&" +
        "-Bpt.flags=1&" +
        "-Bpt.prev=${Uri.encode("https://bilibili.com")}&" +
        "-Bpt.forward=${Uri.encode("https://baidu.com")}&" +
        "-Bprop1=1&" +
        "-Bprop1=2&" +
        "-Bprop2=2&" +
        "param1=1&" +
        "param1=2&" +
        "-Aplayer=main").toRouteRequest()

RouteRequest中,下面的几个字段是其独享的,无法与统一路由协议互转

  • extras: Bundle,额外参数
  • options: 参见startActivity(Intent intent,Bundle options)
  • animIn, animOut: 参见Activity.overridePendingTransition(int enterAnim, int exitAnim)

其他均可用统一路由协议表示,具体参数参见UniformProtol这个类。

一个最佳实践:

  • 页面应当支持只需要 params 中的参数就可以正常启动,这样可以直接被 URI 唤起
  • props 作为属性参数,通常用于 容器 / UI 的控制参数,而非页面的本身的逻辑参数
  • attributes 用于描述页面的一些性质,当多页面路径匹配且优先级一致时才会发生匹配
  • 可以认为 RouteType 是一个特殊的特质,但是发生在页面路径匹配之前,请求存在多 RouteType 时,会依次查询直到第一个成功为止
更多路由功能

请参阅示例,文档待补齐。

服务

声明服务

BRouter 支持在类上或者静态方法上声明服务

注解在类上

在这种情况,BRouter会查找公开构造函数来创建服务的实例,如果有多个,则寻找@Inject注解的构造函数

interface LoginService

@Singleton // 会缓存第一个实例作为单例
@Named("v1") // 默认为 "default"
@Services(LoginService::class) // 暴露的服务类,不填则为被注解类本身
class LoginManager1 : LoginService
注解在静态方法
storage) { ... } } ">
class LoginManager2 implements LoginService  {
    
    @Named("v2")
    @Services(LoginService.class)
    synchronized public static LoginManager getInstance(@Named("app") Application appProvider<StorageService> storage) {
        ...
    } 
}

注意,无论是注解在类上还是静态方法上,其目标方法中的参数均会作为被依赖的服务来查找,通过添加@Named("service_name")来表示服务的名字,默认为"default"

消费服务

声明在服务的函数参数中

如上所示,服务是可以依赖服务的,通过声明对应的参数即可获取对应依赖的服务

通过 @Inject 注解
@Inject @Named("v2") lateinit var service3: Provider init { BRouter.inject(this) } } ">
class Consumer {
    @Inject
    @Named("v2")
    lateinit var service1: LoginService? // ? 表示 Nullable
    
    @Inject
    @Named("v2")
    lateinit var service2: Provider<LoginService>
    
    @Inject
    @Named("v2")
    lateinit var service3: Provider<out LoginService>

    init {
        BRouter.inject(this)
    }
}

服务的消费不单单可以直接获取实例,也可以以Provider Provider来获取一个延时创建实例的 Provider

直接获取
val service = BRouter.services.getService(LoginService::class.java, "v1") 

值得注意的是,通过注解静态注册的方式,可以获得编译期的检查,确保被依赖的服务均存在且不存在循环依赖,另外可通过添加@Nullable注解或者 Kotlin 的 ? 语法表示服务是可空的

模块

声明模块

所有的路由与服务均归属于模块,通过注解@ModuleOptions来显示声明一个模块,如果不声明模块则会隐式的创建

{ if (service.isEnabled()) { return RouteResponseKt.redirectTo(chain.getRequest(), RouteRequestKt.toRouteRequest("bilibili://teenager")); } else { return chain.proceed(chain.getRequest()); } }); } } ">
@ModuleOptions(name = "test")
public class TestModule extends ModuleActivator {

    private final TeenagerService service;

    public TestModule(TeenagerService service) {
        this.service = service;
    }

    @Override
    public void onCreate(@NotNull ModuleContext context, @NotNull ModuleConfigurationModifier modifier) {
        super.onCreate(context, modifier);
        modifier.addModuleInterceptors(chain -> {
            if (service.isEnabled()) {
                return RouteResponseKt.redirectTo(chain.getRequest(),
                        RouteRequestKt.toRouteRequest("bilibili://teenager"));
            } else {
                return chain.proceed(chain.getRequest());
            }
        });
    }
}

如果模块注解的类可选继承ModuleActivator,以处理对应的回调 onCreate onPostCreate,注意以下几点:

  1. 所有归属这个模块的服务,均隐式的依赖了这个模块的onCreate
  2. 可以构造函数在构造函数中声明对其他模块服务的依赖
  3. onCreateonPostCreate 其实是特殊的 Task,因此可以在上面加TaskOptions注解来配置任务
  4. ON_DEMAND 模块的 onCreate 在模块的路由或者服务第一次被获取时触发或者手动触发
  5. ON_INIT 则在BRouterCore.setUp触发,均为异步
模块 API
val tasks: TaskContainer fun moveStatusTo(status: ModuleStatus) // 手动触发状态转移 @Throws(InterruptedException::class) fun awaitAtLeast(status: ModuleStatus) // 同步等待状态 fun whenStatusAtLeast(status: ModuleStatus, action: (Module) -> Unit) // 异步监听状态 } ">
val module = BRouter.module("test")

// 模块 API
interface Module : HasAttributes {
    val name: String
    val mode: BootStrapMode
    val status: ModuleStatus
    val moduleInterceptors: List<RouteInterceptor>
    val tasks: TaskContainer
    fun moveStatusTo(status: ModuleStatus) // 手动触发状态转移
    @Throws(InterruptedException::class)
    fun awaitAtLeast(status: ModuleStatus) // 同步等待状态
    fun whenStatusAtLeast(status: ModuleStatus, action: (Module) -> Unit) // 异步监听状态
}
多模块

BRouter 支持在一个 Gradle Project 内声明多个模块

annotation class ModuleOptions(
    val name: String,
    val mode: BootStrapMode = BootStrapMode.ON_INIT,
    val desc: String = "",
    val defaultModule: Boolean = true
)

但是请注意,路由 / 服务 / 任务 在未指定所属模块时,会属于默认模块,即当存在多个模块时,应当只能有一个模块为 defaultModule = true。

另外声明路由 / 服务 / 任务 时,可通过注解@BelongsTo来指定所属的模块

annotation class BelongsTo(val value: String)

任务

BRouter 的任务是一次性的,用完即毁,仅适用于启动任务,通过@TaskOptions,标注一个实现TaskAction的类,即可声明一个任务:

@TaskOptions("task2", dependencies = ["lib2.task1"], threadMode = ThreadMode.ANY)
public class Task2(@Named("v1") service: LoginService) : TaskAction {
    override fun execute(task: Task) {
        executor = Executors.newSingleThreadExecutor()
    }

    @Named("cached")
    @TaskOutput(ExecutorService::class, Executor::class)
    lateinit var executor: ExecutorService
}

同样,构造函数可依赖服务,比较特别的是,@TaskOutput会产生服务,而且因为 Task 一次性的性质,@TaskOutput产生的服务是单例的,所以慎用。

Task 也是归属一个模块的,因此在dependencies里声明对其他 Task 名的依赖时,要注意如果是其他模块的 Task 要加上模块名字与点,如果是同模块的可以只写 Task 名。

数据

BRouter 是数据驱动的,所有的 Route / Service / Task / Module 均由 JSON 文件描述,因此拥有了强大的扩展性。

自定义 APT

众所周知 APT 是通过实现 Processor 这个接口并注册到 SPI 中来实现的,BRouter 借鉴了这个思想,将 Processor 中的回调做二次转发给 MetaProcessor 给来收集元信息,并在最后一轮生成元数据文件与 Java 代码。

包括 BRouter 自身的注解也是通过 MetaProcessor 来收集的,有兴趣的同学可以一阅源码,这里给一个简单的示例,我们以 ARouter 中的 Route 注解为例:

@AutoService(MetaProcessor::class)
class CompatRouteProcessor : MetaProcessor {
    override val supportedAnnotations: Set<String>
        get() = setOf(Route::class.java.name)

    override fun process(
        annotations: Set<TypeElement>,
        roundEnv: RoundEnvironment,
        processingEnv: ProcessingEnvironment,
        collector: MetaCollector
    ) {
        roundEnv.getElementsAnnotatedWith(Route::class.java).forEach { element ->
            element as TypeElement
            collector.addRoute { builder ->
                val route = element.getAnnotation(Route::class.java)
                builder.className(element.javaClassName)
                    .routeRules(listOf(route.path.also {
                        require(it.isNotEmpty()) {
                            "Path is empty."
                        }
                    }))

                route.name.let {
                    if (it.isNotEmpty()) {
                        builder.routeName(it)
                    }
                }

                builder.attributes(
                    listOf(
                        "group" to route.group,
                        "extras" to route.extras.toString()
                    )
                )
            }
        }
    }
}

示例里用了 Google 的 AutoService 来自动生成服务描述文件,也可以替换成手写。将其注册到annotationProcessorkapt中即可实现对 ARouter 中@Route注解的兼容。

通过类似上面的方式,可以很方便的将其他版本的路由迁移到 BRouter 中。

数据模型

编译 Application 模块后,BRouter 会在build/outputs/${variant_name}/brouter 下产生描述所有上述功能的元数据 JSON。

通过解析该文件,可用于 CI / CD / 自动化测试 / 数据可视化等。

路由插件

插件的所有产物均在 build/intermediates/brouter 与 build/outputs/brouter 目录下,可自行查找。

路由插件只有一个 id,但是在不同的模块下有不同的效果

apply plugin: 'com.bilibili.brouter'

Application

Application 模块必须要 apply 路由插件来生成必要的类,否则无法使用。

另外可配置导出 Deep Link 的目标类,所有要导出的路由会生成对应的 AndroidManifest 信息,如果不配置类名则忽略。

brouter {
    exportedActivityClass("com.bilibili.brouter.example.EntranceActivity")
}

Library

Library 模块下插件有 2 个功能

Android Test

插件同样可以为 Android Test 生成对应的类,因此当模块 A 依赖模块 B 的暴露服务时,在构建测试 APK 时可以选择依赖模块 B 的实现或者在 A 的 androidTest 下用注解 mock B 的实现。

导出模块服务

当模块需要暴露服务时,可使用路由插件,将需要暴露的 API 定义在 src/${sourceSetName}/api 目录下,目前仅支持 Java。

// A
dependencies {
    // API 编译需要依赖的外部类使用 serviceApi 来依赖,例如暴露的接口用到了 RxJava
    serviceApi "io.reactivex.rxjava3:rxjava:3.0.0"
    
    // 只为 release 依赖
    releaseServiceApi ...
    
    // 依赖另一个模块导出的 API
    serviceOnly project(":B")
}

// B
dependencies {
    serviceOnly project(":A")
    // 如果 Library 打包时 apply 了路由插件,远程依赖也可以通过这种方式来获取暴露的服务
    serviceOnly "com.bilibili:x:1.0.0"
}

// 支持模块之间互相依赖彼此导出的 API

简单的原理图

Java

所有的 Kotlin 示例或者 API 均支持 Java,请自行查阅。

混淆

brouter-core 中自带了混淆规则,无需特别配置

沟通

致谢

License

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.
You might also like...
A Kotlin Native program to show the time since a date, using Kotlin LibUI
A Kotlin Native program to show the time since a date, using Kotlin LibUI

TimeSince A Kotlin Native program to show the time since a date, using Kotlin LibUI Report Bug . Request Feature About The Project TimeSince is a Kotl

RoomJetpackCompose is an app written in Kotlin and shows a simple solution to perform CRUD operations in the Room database using Kotlin Flow in clean architecture.
RoomJetpackCompose is an app written in Kotlin and shows a simple solution to perform CRUD operations in the Room database using Kotlin Flow in clean architecture.

RoomJetpackCompose is an app written in Kotlin and shows a simple solution to perform CRUD operations in the Room database using Kotlin Flow in clean architecture.

Create an application with Kotlin/JVM and Kotlin/JS, and explore features around code sharing, serialization, server- and client
Create an application with Kotlin/JVM and Kotlin/JS, and explore features around code sharing, serialization, server- and client

Practical Kotlin Multiplatform on the Web 본 저장소는 코틀린 멀티플랫폼 기반 웹 프로그래밍 워크숍(강좌)을 위해 작성된 템플릿 프로젝트가 있는 곳입니다. 워크숍 과정에서 코틀린 멀티플랫폼을 기반으로 프론트엔드(front-end)는 Ko

Create an application with Kotlin/JVM and Kotlin/JS, and explore features around code sharing, serialization, server- and client
Create an application with Kotlin/JVM and Kotlin/JS, and explore features around code sharing, serialization, server- and client

Building a Full Stack Web App with Kotlin Multiplatform 본 저장소는 INFCON 2022에서 코틀린 멀티플랫폼 기반 웹 프로그래밍 핸즈온랩을 위해 작성된 템플릿 프로젝트가 있는 곳입니다. 핸즈온 과정에서 코틀린 멀티플랫폼을

Android MVVM framework write in kotlin, develop Android has never been so fun.

KBinding 中文版 Android MVVM framework write in kotlin, base on anko, simple but powerful. It depends on my another project AutoAdapter(A library for sim

Klimatic is an android app built using Kotlin. It try to showcase all the latest technologies used in android.
Klimatic is an android app built using Kotlin. It try to showcase all the latest technologies used in android.

Klimatic Klimatic is an android app built using Kotlin. It try to showcase all the latest technologies used in android. Built using Android Architectu

Gits-android-extensions - A collection of Kotlin extensions to simplify Android development

gits-android-extensions A collection of Kotlin extensions to simplify Android de

Android Clean Architecture in Rorty is a sample project that presents modern, approach to Android application development using Kotlin and latest tech-stack.
Android Clean Architecture in Rorty is a sample project that presents modern, approach to Android application development using Kotlin and latest tech-stack.

Android Clean Architecture in Rorty is a sample project that presents modern, approach to Android application development using Kotlin and latest tech-stack.

Kotlin library for Android
Kotlin library for Android

KAndroid Kotlin library for Android providing useful extensions to eliminate boilerplate code in Android SDK and focus on productivity. Download Downl

Owner
bilibili
bilibili Open Source Task Force
bilibili
Run Kotlin/JS libraries in Kotlin/JVM and Kotlin/Native programs

Zipline This library streamlines using Kotlin/JS libraries from Kotlin/JVM and Kotlin/Native programs. It makes it possible to do continuous deploymen

Cash App 1.5k Dec 30, 2022
A somewhat copy past of Jetbrain's code from the kotlin plugin repo to make it humanly possible to test Intellij IDEA kotlin plugins that work on kotlin

A somewhat copy past of Jetbrain's code from the kotlin plugin repo to make it humanly possible to test Intellij IDEA kotlin plugins that work on kotlin

common sense OSS 0 Jan 20, 2022
Android + Kotlin + Github Actions + ktlint + Detekt + Gradle Kotlin DSL + buildSrc = ❤️

kotlin-android-template ?? A simple Github template that lets you create an Android/Kotlin project and be up and running in a few seconds. This templa

Nicola Corti 1.5k Jan 3, 2023
LifecycleMvp 1.2 0.0 Kotlin is MVP architecture implementation with Android Architecture Components and Kotlin language features

MinSDK 14+ Download Gradle Add to project level build.gradle allprojects { repositories { ... maven { url 'https://jitpack.io' }

Robert 20 Nov 9, 2021
👋 A common toolkit (utils) ⚒️ built to help you further reduce Kotlin boilerplate code and improve development efficiency. Do you think 'kotlin-stdlib' or 'android-ktx' is not sweet enough? You need this! 🍭

Toolkit [ ?? Work in progress ⛏ ?? ??️ ?? ] Snapshot version: repositories { maven("https://s01.oss.sonatype.org/content/repositories/snapshots") }

凛 35 Jul 23, 2022
An app architecture for Kotlin/Native on Android/iOS. Use Kotlin Multiplatform Mobile.

An app architecture for Kotlin/Native on Android/iOS. Use Kotlin Multiplatform Mobile. 项目架构主要分为原生系统层、Android/iOS业务SDK层、KMM SDK层、KMM业务逻辑SDK层、iOS sdkfra

libill 4 Nov 20, 2022
Opinionated Redux-like implementation backed by Kotlin Coroutines and Kotlin Multiplatform Mobile

CoRed CoRed is Redux-like implementation that maintains the benefits of Redux's core idea without the boilerplate. No more action types, action creato

Kittinun Vantasin 28 Dec 10, 2022
Provides Kotlin libs and some features for building Kotlin plugins

Kotlin Plugin Provides Kotlin libs and some features for building awesome Kotlin plugins. Can be used instead of CreeperFace's KotlinLib (don't use to

null 3 Dec 24, 2021
Notes-App-Kotlin - Notes App Built Using Kotlin

Notes-App-Kotlin Splash Screen Home Page Adding New Notes Filter Feature Search

Priyanka 4 Oct 2, 2022
Kotlin-client-dsl - A kotlin-based dsl project for a (Client) -> (Plugin) styled program

kotlin-client-dsl a kotlin-based dsl project for a (Client) -> (Plugin) styled p

jackson 3 Dec 10, 2022