Migrate Image Cropping to a KMP-Supported Library - #2627
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review infoConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughRefactors file upload APIs from PlatformFile to ByteArray+metadata, adds camera capture and image-cropping workflows, makes the cropper aspect ratio configurable, updates multipart request construction across features, and adds a Camera icon reference. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant Screen as ClientProfileEditScreen
participant VM as ClientProfileEditViewModel
participant Camera as PlatformCameraLauncher
participant Cropper as ImageCropper
participant API as UploadAPI
User->>Screen: Tap upload image
Screen->>VM: OpenCamera / OpenImagePicker
alt Camera chosen
VM->>Camera: launch()
Camera-->>VM: Photo result (bytes)
else Gallery chosen
VM-->>VM: open file picker (returns bytes)
end
VM->>Cropper: start crop (image bytes + aspectRatio)
Cropper->>Screen: ShowImageCrop dialog
User->>Screen: Confirm crop
Screen->>VM: CropResult.Success (cropped bytes)
VM->>API: multipartRequestBody(croppedBytes, name, extension)
API-->>VM: Upload response
VM->>Screen: Update UI state
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@core/ui/src/commonMain/kotlin/com/mifos/core/ui/util/MultipartRequestBody.kt`:
- Around line 28-29: Normalize and validate the extension before using it to
build fileName and to call getMimeTypeFromPlatformFile: trim whitespace, strip
any leading '.' from the extension, and if the resulting extension is blank use
the original name without adding a trailing '.' (i.e. keep fileName = name) and
pass a safe empty/fallback value into getMimeTypeFromPlatformFile so MIME lookup
won't misbehave; apply the same normalization logic to both places where
fileName and mimeType are constructed (the occurrences around fileName and
mimeType and the later similar block at lines 46-47) and update references to
the normalized extension variable rather than the raw extension.
In `@feature/client/src/commonMain/composeResources/values/strings.xml`:
- Line 385: The string resource "camera" has a leading space in its value ("
Camera") causing uneven rendering; update the string with the exact resource
name string name="camera" to remove the leading whitespace so the value becomes
"Camera" (no leading space), and save the change so UI option lists render
correctly.
In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsViewModel.kt`:
- Around line 71-78: The call to imageFile.readBytes() happens outside the
DataState/error-handling boundary and can throw before saveClientImage's
try/catch runs; move or wrap the file-reading so any IO errors are thrown inside
the coroutine that collects uploadClientImageUseCase. Specifically, in
saveClientImage (the coroutine that calls uploadClientImageUseCase) ensure you
read the file bytes and build the multipartRequestBody (or use runCatching/try
around imageFile.readBytes()) inside that coroutine/try block so exceptions from
imageFile.readBytes() are caught and converted to a DataState error before
calling uploadClientImageUseCase.
In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.kt`:
- Around line 329-335: The "More" bottom-sheet option
(MifosBottomSheetOptionItem with label
stringResource(Res.string.client_signature_more) and icon MifosIcons.MoreHoriz)
is currently tappable but has an empty onClick; hide or disable it until real
behavior exists by either removing it from the options list or wrapping it in a
boolean guard/feature flag (e.g., showClientSignatureMore) and only add the
MifosBottomSheetOptionItem when the flag is true, or set a disabled/visible
property if the composable supports it; also add a brief TODO referencing
implementation work for the onClick handler so future maintainers know why it
was hidden.
In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditViewModel.kt`:
- Around line 196-205: Inside the MutableStateFlow.update block, avoid using the
captured outer `state` variable which can cause stale-state overwrites; instead
use the lambda parameter `it` to perform an atomic transformation: compute
newDialogState based on `it.dialogState` (checking for
ClientProfileEditState.DialogState.ShowStatusDialog), set `profileImage` to
imageToByteArray(result.data), and return `it.copy(...)` with the updated
profileImage and dialogState so updates are applied atomically.
In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentifiersAddUpdateViewModel.kt`:
- Around line 542-549: ClientIdentifiersAddUpdateViewModel currently silently
no-ops when state.documentImageFile is null because create/update is guarded
with state.documentImageFile?.let { ... } and has no else branch; update the
create and update paths (where createDocument(...) and the corresponding
updateDocument(...) call are guarded) to handle the null case by emitting a UI
error/validation event (e.g., call the ViewModel's existing
showValidationError/emitUiEvent or create a submitDocumentError event) so the
user receives feedback like "Please attach a document image" instead of nothing;
locate the guards around state.documentImageFile, createDocument,
route.uniqueKeyForHandleDocument and the matching update call and add an else
branch that sends the error event.
- Around line 281-283: The code in ClientIdentifiersAddUpdateViewModel that
builds multipart metadata currently substitutes empty strings for name and
extension (the assignments where name = name ?: "" and extension =
state.fileExtension ?: "" and the similar block around lines 330-332); instead
validate that name and state.fileExtension are present and non-empty before
creating the metadata—if either is blank, surface a validation error
(return/fail/emit UI state) or skip the upload rather than sending empty
fallbacks, and update any callers to handle that validation result; locate the
metadata construction logic in the ViewModel (the method that sets
description/extension/name for the upload) and replace the empty-string defaults
with explicit checks and early error handling.
In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientSignature/ClientSignatureScreen.kt`:
- Around line 157-160: The call to MifosImageCropperDialog uses a named
parameter (state = it) followed by a positional parameter (AspectRatio(2, 1)),
which is invalid in Kotlin; update the call sites (ClientSignatureScreen's
MifosImageCropperDialog and the identical usage in ClientProfileEditScreen) to
pass the aspect ratio as a named argument (e.g., aspectRatio = AspectRatio(2,
1)) so that all arguments after the named parameter are also named.
In
`@feature/document/src/commonMain/kotlin/com/mifos/feature/document/documentDialog/DocumentDialogViewModel.kt`:
- Around line 78-83: DocumentDialogViewModel calls file.readBytes() directly
inside multipartRequestBody creation (in both createDocument and updateDocument
paths), which can throw and bypass the DataState.Error UI update; wrap the file
read in a try/catch and handle IO failures before constructing the multipart
body: call file.readBytes() inside a guarded block (catching
IOException/Throwable), set the view state to DataState.Error (or appropriate
error type) and return/early-exit so multipartRequestBody (and subsequent calls)
are not executed; apply the same pattern to the updateDocument flow to ensure UI
state is updated on file-read failures.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/icon/MifosIcons.ktcore/ui/src/commonMain/kotlin/com/mifos/core/ui/components/MifosImageCropperDialog.ktcore/ui/src/commonMain/kotlin/com/mifos/core/ui/util/MultipartRequestBody.ktfeature/client/src/androidMain/kotlin/com/mifos/feature/client/utils/OpenFileInDefaultExternalApp.android.ktfeature/client/src/commonMain/composeResources/values/strings.xmlfeature/client/src/commonMain/kotlin/com/mifos/feature/client/clientDetails/ClientDetailsViewModel.ktfeature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.ktfeature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditViewModel.ktfeature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentifiersAddUpdateViewModel.ktfeature/client/src/commonMain/kotlin/com/mifos/feature/client/clientIdentifiersAddUpdate/ClientIdentitiesAddUpdateScreen.ktfeature/client/src/commonMain/kotlin/com/mifos/feature/client/clientSignature/ClientSignatureScreen.ktfeature/client/src/commonMain/kotlin/com/mifos/feature/client/clientSignature/ClientSignatureViewModel.ktfeature/client/src/commonMain/kotlin/com/mifos/feature/client/createNewClient/CreateNewClientViewModel.ktfeature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PlatformFile.ktfeature/client/src/nativeMain/kotlin/com/mifos/feature/client/utils/PlatformCameraLauncher.native.ktfeature/document/src/commonMain/kotlin/com/mifos/feature/document/documentDialog/DocumentDialogViewModel.kt
💤 Files with no reviewable changes (1)
- feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PlatformFile.kt
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.kt (1)
327-333:⚠️ Potential issue | 🟡 MinorDisable or hide the “More” option until behavior exists.
This option is clickable but has no action, creating a dead-end interaction.
Suggested patch
- MifosBottomSheetOptionItem( - label = stringResource(Res.string.client_signature_more), - icon = MifosIcons.MoreHoriz, - onClick = { - // it implement further - }, - ) + // TODO: Re-enable "More" option once its action is implemented.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.kt` around lines 327 - 333, The "More" option renders a clickable MifosBottomSheetOptionItem with label stringResource(Res.string.client_signature_more) but has an empty onClick handler; hide or disable it until behavior exists by either: a) wrap the MifosBottomSheetOptionItem in a conditional so it is not composed (e.g., if (showSignatureMore) { MifosBottomSheetOptionItem(...) }), or b) set a disabled state on the item (if MifosBottomSheetOptionItem supports an enabled/disabled parameter) and remove the onClick body; update the location where MifosBottomSheetOptionItem is declared in ClientProfileEditScreen so the "More" entry is not clickable until you implement its behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.kt`:
- Around line 281-284: The call to MifosImageCropperDialog mixes a named
argument (state = it) with a positional one (AspectRatio(1, 1)) which won't
compile; update the invocation to pass the AspectRatio as a named parameter
(e.g., aspectRatio = AspectRatio(1, 1)) so both arguments are named and the call
in ClientProfileEditScreen using MifosImageCropperDialog compiles.
---
Duplicate comments:
In
`@feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.kt`:
- Around line 327-333: The "More" option renders a clickable
MifosBottomSheetOptionItem with label
stringResource(Res.string.client_signature_more) but has an empty onClick
handler; hide or disable it until behavior exists by either: a) wrap the
MifosBottomSheetOptionItem in a conditional so it is not composed (e.g., if
(showSignatureMore) { MifosBottomSheetOptionItem(...) }), or b) set a disabled
state on the item (if MifosBottomSheetOptionItem supports an enabled/disabled
parameter) and remove the onClick body; update the location where
MifosBottomSheetOptionItem is declared in ClientProfileEditScreen so the "More"
entry is not clickable until you implement its behavior.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
feature/client/src/commonMain/kotlin/com/mifos/feature/client/clientEditProfile/ClientProfileEditScreen.kt
|
therajanmaurya
left a comment
There was a problem hiding this comment.
PR Review: Migrate Image Cropping to a KMP-Supported Library
Summary
This PR migrates from platform-specific image handling to a KMP-compatible approach using crop-krop-ui library. The core change is shifting from PlatformFile to ByteArray + metadata pattern, which enables proper cross-platform compatibility.
✅ What's Working Well
-
Clean API Migration: The
multipartRequestBodyfunction now accepts raw bytes with explicit metadata - this is the right pattern for KMP, avoiding platform-specific file APIs. -
Proper ImageCropper Integration: The
imageCropper()state is correctly held in ViewModel and passed to composables. The crop dialog flow is well-structured. -
Consistent Pattern Across ViewModels: All ViewModels (
ClientDetailsViewModel,ClientSignatureViewModel,CreateNewClientViewModel,DocumentDialogViewModel) now use the same pattern withfile.readBytes(),file.name, andfile.extension. -
Platform Camera Launcher: The native implementation in
PlatformCameraLauncher.native.ktproperly wrapsrememberCameraPickerLauncher. -
Removed Dead Code:
PlatformFile.ktwith its TODO comments is appropriately deleted.
⚠️ Suggestions for Improvement
1. Error Message Hardcoded
ClientProfileEditViewModel.kt:149 - The "Unexpected error" string should be extracted to a string resource for i18n consistency.
2. "More" Option Without Implementation
ClientProfileEditScreen.kt:309-312 - The "More" bottom sheet option has an empty onClick handler with comment "it implement further". Consider either removing this option until implemented or adding a proper TODO comment explaining the plan.
3. CropState Held in Data Class
ClientProfileEditViewModel.kt:305 - Holding stateful objects like ImageCropper in a data class may cause issues with equals/hashCode and copy semantics. Consider moving cropState outside the data class or making it a separate property.
✅ Architecture Compliance
- Follows existing MVI pattern ✅
- Uses Koin DI correctly ✅
- Proper separation of UI and business logic ✅
- KMP-compatible (no platform-specific code in common) ✅
📊 Verdict
LGTM - The migration is well-executed with consistent patterns across all affected files. The suggestions above are minor improvements and don't block merging.



Fixes - Jira-#677
Removed
feature/client/src/commonMain/kotlin/com/mifos/feature/client/utils/PlatformFile.ktfile and updated the image upload flow to improve KMP compatibility.Reason:
outFile.write()andoutFile.write(this)are not supported on JS and wasmJs targets. Because of this limitation, creating or writing files dynamically using FileKit was not working across all platforms, which prevented sending images to the server reliably.Changes made:
Result:
This approach works consistently across all KMP targets (Android, iOS, Desktop, JS, wasmJs) and avoids unsupported file system operations on web platforms.
AndroidClient.ClientProfileEditScreen.kt.AndroidClient.feature.client.commonMain.2026-02-28.17-40-10.mp4
Summary by CodeRabbit
New Features
Improvements
Removals