
Customizable UI components and comprehensive theming system featuring global and per-component color/font overrides, typography helpers, ready-made buttons and dialogs, and accessibility guidance.
Kotlin Multiplatform core library: shared UI, utilities, data, and permissions for Android, iOS, Desktop, and Web.
Deveng Core KMP is a KMP library that provides:
SharedImage, CameraManager) with expect/actual per platform.| Platform | Support |
|---|---|
| Android | ✅ |
| iOS | ✅ (device + simulator) |
| Desktop | ✅ (JVM) |
| Web | ✅ (Wasm/JS) |
Add the dependency to your module (Kotlin DSL):
dependencies {
implementation("global.deveng:core-kmp:VERSION")
}Replace VERSION with the latest release or the version from the repo.
UI — Wrap your composables in AppTheme and use shared components:
AppTheme {
CustomButton(text = "Click me", onClick = { })
}Platform & utils — Use MultiPlatformUtils (construct with Context on Android; no-arg on other platforms) for platform config and common actions:
val utils = MultiPlatformUtils(context) // Android; use no-arg constructor on iOS/Desktop/Wasm
val config = utils.getPlatformConfig() // platform, language, uuid, deviceName, packageVersionName
utils.copyToClipBoard("text")
utils.openUrl("https://example.com")The deveng-core module is organized as follows:
| Area | Description |
|---|---|
| UI & theming |
ComponentTheme, AppTheme, typography; buttons, dialogs, text fields, date/range pickers, OTP, navigation menus, option lists, chips, search, progress, scrollbars, JSON viewer, etc. |
| Data |
DeviceInfoStorage (e.g. device identifier); PagedListResponse, BasePaginatedResponse for paginated API responses. |
| Domain |
PagedList for pagination; SharedImage, CameraManager, rememberCameraManager for camera/photo capture (Android, iOS; stubs on Desktop/Web). |
| Utilities |
Platform, PlatformConfig, MultiPlatformUtils (dial, clipboard, open URL, maps, share, location); date/time formatting; ImageSizeProcessor and ImageProcessingProfile; StringFormatter, CustomLogger, CustomDispatchers. |
| Permissions |
Permission enum, PermissionsController, PermissionState; Android and iOS implementations. |
| Pagination (UI) |
PaginatedFlowLoader, PaginatedListView, page state. |
Components work out of the box with default colors and Urbanist font:
AppTheme {
CustomButton(text = "Button", onClick = { })
CustomAlertDialog(
isDialogVisible = true,
title = "Title",
description = "Description",
positiveButtonText = "OK",
onPositiveButtonClick = { }
)
}Create a ComponentTheme to override component colors:
val theme = ComponentTheme(
button = ButtonTheme(
containerColor = Color.Blue,
contentColor = Color.White
),
alertDialog = AlertDialogTheme(
headerColor = Color.White,
titleColor = Color.Black
)
)
AppTheme(componentTheme = theme) {
// Components use your custom colors
}Change the default font (Urbanist) globally:
// System fonts
val theme = ComponentTheme(
typography = TypographyTheme(
fontFamily = FontFamily.SansSerif
)
)
// Custom font files
val customFont = FontFamily(
Font(resource = Res.font.my_font_regular, weight = FontWeight(400)),
Font(resource = Res.font.my_font_bold, weight = FontWeight(700))
)
val theme = ComponentTheme(
typography = TypographyTheme(fontFamily = customFont)
)Override theme values for specific components:
AppTheme(componentTheme = theme) {
CustomButton(
text = "Special Button",
containerColor = Color.Red, // Overrides theme
textStyle = BoldTextStyle().copy(fontSize = 20.sp)
)
}ComponentTheme
├── typography: TypographyTheme
│ └── fontFamily: FontFamily? (default: Urbanist)
├── button: ButtonTheme
│ ├── containerColor, contentColor
│ ├── disabledContainerColor, disabledContentColor
│ └── defaultTextStyle
├── alertDialog: AlertDialogTheme
│ ├── headerColor, bodyColor
│ ├── titleColor, descriptionColor
│ ├── positiveButtonColor, negativeButtonColor
│ └── titleTextStyle, descriptionTextStyle, buttonTextStyle
├── surface: SurfaceTheme
│ └── defaultColor, defaultContentColor
└── dialogHeader: DialogHeaderTheme
└── titleColor, iconTint, titleTextStyle
Available functions: RegularTextStyle() (400), MediumTextStyle() (500), SemiBoldTextStyle() (600), BoldTextStyle() (700). All use the font family from ComponentTheme.typography.fontFamily (default: Urbanist).
Text(text = "Text", style = RegularTextStyle())
Text(text = "Custom", style = BoldTextStyle().copy(fontSize = 24.sp, color = Color.Blue))
CustomButton(text = "Button", textStyle = SemiBoldTextStyle().copy(fontSize = 18.sp), onClick = { })CustomButton, CustomIconButton, LabeledSwitch
CustomAlertDialog, CustomDialog, CustomDialogHeader, CustomDialogBody
CustomTextField, SearchField, PickerField, CustomDatePicker, CustomDateRangePicker, OtpView
CustomHeader, RoundedSurface, LabeledSlot, Slot, ChipItem
OptionItemList, OptionItemLazyListDialog, OptionItemMultiSelectLazyListDialog, CustomDropDownMenu
NavigationMenu (horizontal, expanded, collapsed)RatingRow, ProgressIndicatorBars, JsonViewer
PaginatedListView, ScrollbarWithScrollState, ScrollbarWithLazyListState
@Composable
fun MyApp() {
val theme = ComponentTheme(
typography = TypographyTheme(fontFamily = FontFamily.SansSerif),
button = ButtonTheme(
containerColor = Color(0xFF1976D2),
contentColor = Color.White,
defaultTextStyle = SemiBoldTextStyle().copy(fontSize = 18.sp)
),
alertDialog = AlertDialogTheme(
headerColor = Color.White,
titleColor = Color.Black,
titleTextStyle = BoldTextStyle().copy(fontSize = 20.sp)
)
)
AppTheme(componentTheme = theme) {
Column {
CustomButton(text = "Primary Action", onClick = { })
CustomAlertDialog(
isDialogVisible = showDialog,
title = "Confirm",
description = "Are you sure?",
positiveButtonText = "Yes",
negativeButtonText = "No",
onPositiveButtonClick = { showDialog = false },
onNegativeButtonClick = { showDialog = false },
onDismissRequest = { showDialog = false }
)
}
}
}ComponentTheme at app level.Platform & actions — MultiPlatformUtils (use Context on Android; no-arg on iOS/Desktop/Wasm):
val utils = MultiPlatformUtils(context)
val config = utils.getPlatformConfig() // platform, systemLanguage, uuid, deviceName, packageVersionName
utils.copyToClipBoard("Copied text")
utils.openUrl("https://example.com")
utils.openMapsWithLocation(41.0082, 28.9784)
utils.shareText("Share this")
val (lat, lng) = utils.getCurrentLocation() ?: (0.0 to 0.0)Date/time — Format and selectable dates:
val formatted = formatDateTime(localDateTime, isDaily = false) // e.g. "20.02.2025"
val dateString = localDate.format(dotLocalDateFormat)
val selectableDates = CustomSelectableDates(...) // for date picker constraintsImage processing — Resize and compress image bytes:
val processor = ImageSizeProcessor()
val compressed = processor.resizeAndCompressBytes(
inputBytes = imageBytes,
profile = ImageProcessingProfile.MEDIUM // or targetMaxSizePx + quality
)
val bitmap = imageBytes.toImageBitmap()String & logging:
val cleaned = StringFormatter().formatInput(input, clearNonNumeric = true)
CustomLogger.isLoggingEnabled = true
CustomLogger.log("Debug message")Device storage — Persist a generated device identifier:
val storage = DeviceInfoStorageImpl(settings) // Settings from russhwolf/settings
var uuid = storage.getGeneratedPlatformIdentifier()
if (uuid == null) {
uuid = UUID.randomUUID().toString()
storage.setGeneratedPlatformIdentifier(uuid)
}Pagination — Map API response to domain model:
@Serializable
data class MyItem(val id: String, val name: String)
val response: PagedListResponse<MyItemDto> = api.getPage(page, size)
val pagedList = response.mapItems { dto -> MyItem(dto.id, dto.name) }
// pagedList.items, pagedList.hasNextPage, pagedList.totalPageCount, etc.Camera — Capture or pick image (Android/iOS):
val cameraManager = rememberCameraManager(onResult = { sharedImage ->
sharedImage?.let { image ->
val bytes = image.toByteArray()
val name = image.fileName
// upload or use bytes
}
})
// Later: cameraManager.launch()Check and request runtime permissions (Android/iOS):
val factory = rememberPermissionsControllerFactory()
val permissionsController = factory.createPermissionsController()
// Check state
val state = permissionsController.getPermissionState(Permission.CAMERA)
if (state != PermissionState.Granted) {
try {
permissionsController.providePermission(Permission.CAMERA)
} catch (e: DeniedAlwaysException) {
permissionsController.openAppSettings()
} catch (e: RequestCanceledException) {
// User canceled
}
}
val hasLocation = permissionsController.isPermissionGranted(Permission.LOCATION)Load paged data with PaginatedFlowLoader and show in PaginatedListView:
val loader = remember(scope, pageSize) {
PaginatedFlowLoader(
initialKey = 0,
scope = scope,
pageSize = 20,
pageSource = { page, size ->
val response = api.fetchPage(page, size)
PageResult(items = response.items, hasNextPage = response.hasNextPage)
},
getNextKey = { page, _ -> page + 1 }
)
}
LaunchedEffect(Unit) { loader.reset() }
val state by loader.state.collectAsState()
PaginatedListView(
state = state,
onScrollReachNextPageThreshold = { loader.loadNextPage() },
onSwipeAtListsEnd = { loader.reset() },
itemSlot = { item -> ItemRow(item) }
)
// Refresh or new filters
loader.updatePageSource(newKey, newPageSource, reload = true)The sample app in sample/composeApp demonstrates theming, main UI components, and multiplatform run targets. Open the project in Android Studio and run the composeApp configuration for Android, iOS, Desktop, or Wasm.
git clone https://github.com/Deveng-Group/deveng-core-kmp.git
cd deveng-core-kmpOpen in Android Studio or build from the command line:
./gradlew :deveng-core:build
./gradlew :sample:composeApp:assembleDebug # Android sampleLicensed under the Apache License, Version 2.0. See the LICENSE file for details.
Kotlin Multiplatform core library: shared UI, utilities, data, and permissions for Android, iOS, Desktop, and Web.
Deveng Core KMP is a KMP library that provides:
SharedImage, CameraManager) with expect/actual per platform.| Platform | Support |
|---|---|
| Android | ✅ |
| iOS | ✅ (device + simulator) |
| Desktop | ✅ (JVM) |
| Web | ✅ (Wasm/JS) |
Add the dependency to your module (Kotlin DSL):
dependencies {
implementation("global.deveng:core-kmp:VERSION")
}Replace VERSION with the latest release or the version from the repo.
UI — Wrap your composables in AppTheme and use shared components:
AppTheme {
CustomButton(text = "Click me", onClick = { })
}Platform & utils — Use MultiPlatformUtils (construct with Context on Android; no-arg on other platforms) for platform config and common actions:
val utils = MultiPlatformUtils(context) // Android; use no-arg constructor on iOS/Desktop/Wasm
val config = utils.getPlatformConfig() // platform, language, uuid, deviceName, packageVersionName
utils.copyToClipBoard("text")
utils.openUrl("https://example.com")The deveng-core module is organized as follows:
| Area | Description |
|---|---|
| UI & theming |
ComponentTheme, AppTheme, typography; buttons, dialogs, text fields, date/range pickers, OTP, navigation menus, option lists, chips, search, progress, scrollbars, JSON viewer, etc. |
| Data |
DeviceInfoStorage (e.g. device identifier); PagedListResponse, BasePaginatedResponse for paginated API responses. |
| Domain |
PagedList for pagination; SharedImage, CameraManager, rememberCameraManager for camera/photo capture (Android, iOS; stubs on Desktop/Web). |
| Utilities |
Platform, PlatformConfig, MultiPlatformUtils (dial, clipboard, open URL, maps, share, location); date/time formatting; ImageSizeProcessor and ImageProcessingProfile; StringFormatter, CustomLogger, CustomDispatchers. |
| Permissions |
Permission enum, PermissionsController, PermissionState; Android and iOS implementations. |
| Pagination (UI) |
PaginatedFlowLoader, PaginatedListView, page state. |
Components work out of the box with default colors and Urbanist font:
AppTheme {
CustomButton(text = "Button", onClick = { })
CustomAlertDialog(
isDialogVisible = true,
title = "Title",
description = "Description",
positiveButtonText = "OK",
onPositiveButtonClick = { }
)
}Create a ComponentTheme to override component colors:
val theme = ComponentTheme(
button = ButtonTheme(
containerColor = Color.Blue,
contentColor = Color.White
),
alertDialog = AlertDialogTheme(
headerColor = Color.White,
titleColor = Color.Black
)
)
AppTheme(componentTheme = theme) {
// Components use your custom colors
}Change the default font (Urbanist) globally:
// System fonts
val theme = ComponentTheme(
typography = TypographyTheme(
fontFamily = FontFamily.SansSerif
)
)
// Custom font files
val customFont = FontFamily(
Font(resource = Res.font.my_font_regular, weight = FontWeight(400)),
Font(resource = Res.font.my_font_bold, weight = FontWeight(700))
)
val theme = ComponentTheme(
typography = TypographyTheme(fontFamily = customFont)
)Override theme values for specific components:
AppTheme(componentTheme = theme) {
CustomButton(
text = "Special Button",
containerColor = Color.Red, // Overrides theme
textStyle = BoldTextStyle().copy(fontSize = 20.sp)
)
}ComponentTheme
├── typography: TypographyTheme
│ └── fontFamily: FontFamily? (default: Urbanist)
├── button: ButtonTheme
│ ├── containerColor, contentColor
│ ├── disabledContainerColor, disabledContentColor
│ └── defaultTextStyle
├── alertDialog: AlertDialogTheme
│ ├── headerColor, bodyColor
│ ├── titleColor, descriptionColor
│ ├── positiveButtonColor, negativeButtonColor
│ └── titleTextStyle, descriptionTextStyle, buttonTextStyle
├── surface: SurfaceTheme
│ └── defaultColor, defaultContentColor
└── dialogHeader: DialogHeaderTheme
└── titleColor, iconTint, titleTextStyle
Available functions: RegularTextStyle() (400), MediumTextStyle() (500), SemiBoldTextStyle() (600), BoldTextStyle() (700). All use the font family from ComponentTheme.typography.fontFamily (default: Urbanist).
Text(text = "Text", style = RegularTextStyle())
Text(text = "Custom", style = BoldTextStyle().copy(fontSize = 24.sp, color = Color.Blue))
CustomButton(text = "Button", textStyle = SemiBoldTextStyle().copy(fontSize = 18.sp), onClick = { })CustomButton, CustomIconButton, LabeledSwitch
CustomAlertDialog, CustomDialog, CustomDialogHeader, CustomDialogBody
CustomTextField, SearchField, PickerField, CustomDatePicker, CustomDateRangePicker, OtpView
CustomHeader, RoundedSurface, LabeledSlot, Slot, ChipItem
OptionItemList, OptionItemLazyListDialog, OptionItemMultiSelectLazyListDialog, CustomDropDownMenu
NavigationMenu (horizontal, expanded, collapsed)RatingRow, ProgressIndicatorBars, JsonViewer
PaginatedListView, ScrollbarWithScrollState, ScrollbarWithLazyListState
@Composable
fun MyApp() {
val theme = ComponentTheme(
typography = TypographyTheme(fontFamily = FontFamily.SansSerif),
button = ButtonTheme(
containerColor = Color(0xFF1976D2),
contentColor = Color.White,
defaultTextStyle = SemiBoldTextStyle().copy(fontSize = 18.sp)
),
alertDialog = AlertDialogTheme(
headerColor = Color.White,
titleColor = Color.Black,
titleTextStyle = BoldTextStyle().copy(fontSize = 20.sp)
)
)
AppTheme(componentTheme = theme) {
Column {
CustomButton(text = "Primary Action", onClick = { })
CustomAlertDialog(
isDialogVisible = showDialog,
title = "Confirm",
description = "Are you sure?",
positiveButtonText = "Yes",
negativeButtonText = "No",
onPositiveButtonClick = { showDialog = false },
onNegativeButtonClick = { showDialog = false },
onDismissRequest = { showDialog = false }
)
}
}
}ComponentTheme at app level.Platform & actions — MultiPlatformUtils (use Context on Android; no-arg on iOS/Desktop/Wasm):
val utils = MultiPlatformUtils(context)
val config = utils.getPlatformConfig() // platform, systemLanguage, uuid, deviceName, packageVersionName
utils.copyToClipBoard("Copied text")
utils.openUrl("https://example.com")
utils.openMapsWithLocation(41.0082, 28.9784)
utils.shareText("Share this")
val (lat, lng) = utils.getCurrentLocation() ?: (0.0 to 0.0)Date/time — Format and selectable dates:
val formatted = formatDateTime(localDateTime, isDaily = false) // e.g. "20.02.2025"
val dateString = localDate.format(dotLocalDateFormat)
val selectableDates = CustomSelectableDates(...) // for date picker constraintsImage processing — Resize and compress image bytes:
val processor = ImageSizeProcessor()
val compressed = processor.resizeAndCompressBytes(
inputBytes = imageBytes,
profile = ImageProcessingProfile.MEDIUM // or targetMaxSizePx + quality
)
val bitmap = imageBytes.toImageBitmap()String & logging:
val cleaned = StringFormatter().formatInput(input, clearNonNumeric = true)
CustomLogger.isLoggingEnabled = true
CustomLogger.log("Debug message")Device storage — Persist a generated device identifier:
val storage = DeviceInfoStorageImpl(settings) // Settings from russhwolf/settings
var uuid = storage.getGeneratedPlatformIdentifier()
if (uuid == null) {
uuid = UUID.randomUUID().toString()
storage.setGeneratedPlatformIdentifier(uuid)
}Pagination — Map API response to domain model:
@Serializable
data class MyItem(val id: String, val name: String)
val response: PagedListResponse<MyItemDto> = api.getPage(page, size)
val pagedList = response.mapItems { dto -> MyItem(dto.id, dto.name) }
// pagedList.items, pagedList.hasNextPage, pagedList.totalPageCount, etc.Camera — Capture or pick image (Android/iOS):
val cameraManager = rememberCameraManager(onResult = { sharedImage ->
sharedImage?.let { image ->
val bytes = image.toByteArray()
val name = image.fileName
// upload or use bytes
}
})
// Later: cameraManager.launch()Check and request runtime permissions (Android/iOS):
val factory = rememberPermissionsControllerFactory()
val permissionsController = factory.createPermissionsController()
// Check state
val state = permissionsController.getPermissionState(Permission.CAMERA)
if (state != PermissionState.Granted) {
try {
permissionsController.providePermission(Permission.CAMERA)
} catch (e: DeniedAlwaysException) {
permissionsController.openAppSettings()
} catch (e: RequestCanceledException) {
// User canceled
}
}
val hasLocation = permissionsController.isPermissionGranted(Permission.LOCATION)Load paged data with PaginatedFlowLoader and show in PaginatedListView:
val loader = remember(scope, pageSize) {
PaginatedFlowLoader(
initialKey = 0,
scope = scope,
pageSize = 20,
pageSource = { page, size ->
val response = api.fetchPage(page, size)
PageResult(items = response.items, hasNextPage = response.hasNextPage)
},
getNextKey = { page, _ -> page + 1 }
)
}
LaunchedEffect(Unit) { loader.reset() }
val state by loader.state.collectAsState()
PaginatedListView(
state = state,
onScrollReachNextPageThreshold = { loader.loadNextPage() },
onSwipeAtListsEnd = { loader.reset() },
itemSlot = { item -> ItemRow(item) }
)
// Refresh or new filters
loader.updatePageSource(newKey, newPageSource, reload = true)The sample app in sample/composeApp demonstrates theming, main UI components, and multiplatform run targets. Open the project in Android Studio and run the composeApp configuration for Android, iOS, Desktop, or Wasm.
git clone https://github.com/Deveng-Group/deveng-core-kmp.git
cd deveng-core-kmpOpen in Android Studio or build from the command line:
./gradlew :deveng-core:build
./gradlew :sample:composeApp:assembleDebug # Android sampleLicensed under the Apache License, Version 2.0. See the LICENSE file for details.