<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Mohab erabi on Medium]]></title>
        <description><![CDATA[Stories by Mohab erabi on Medium]]></description>
        <link>https://medium.com/@mohaberabi98?source=rss-725d0fa3cda9------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*ol5Gh-X6X9hTdwujsoDrDw@2x.jpeg</url>
            <title>Stories by Mohab erabi on Medium</title>
            <link>https://medium.com/@mohaberabi98?source=rss-725d0fa3cda9------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 10:05:56 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@mohaberabi98/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Prevent ScreenShots/Recordings in KMP App For Android & IOS]]></title>
            <link>https://medium.com/@mohaberabi98/prevent-screenshots-recordings-in-kmp-app-for-android-ios-07a9832d1589?source=rss-725d0fa3cda9------2</link>
            <guid isPermaLink="false">https://medium.com/p/07a9832d1589</guid>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[compose-multiplatform]]></category>
            <dc:creator><![CDATA[Mohab erabi]]></dc:creator>
            <pubDate>Thu, 15 Jan 2026 19:27:42 GMT</pubDate>
            <atom:updated>2026-01-15T19:27:42.518Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7lRpvkr-yFVSWEKmQkRJXw.jpeg" /></figure><p>Securing your app screen is something common in sensitive mobile apps like banking apps for example</p><p>We will start preventing the screen shots , screen recording or sharing on both android and ios</p><p>let’s create our common code , start by creating a simple interface that enables the secure screen or disables it , nothing more</p><pre>interface SekureSkreenManager {<br>    fun start()<br>    fun stop()<br>}<br><br>@Composable<br>expect fun rememberSekureSkreenManager(): SekureSkreenManager</pre><p>On Android it’s very simple you just add the secure flag to your activity window <strong>WindowManager.LayoutParams.<em>FLAG_SECURE</em>,</strong></p><pre>class AndroidSekureSkreenManager(private val activity: Activity) : SekureSkreenManager {<br>    override fun start() {<br>        activity.window.setFlags(<br>            WindowManager.LayoutParams.FLAG_SECURE,<br>            WindowManager.LayoutParams.FLAG_SECURE<br>        )<br>    }<br><br>    override fun stop() {<br>        activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)<br>    }<br>}</pre><p>then let’s create the actual implmentation for it</p><pre>@Composable<br>actual fun rememberSekureSkreenManager(): SekureSkreenManager {<br>    val activity = LocalActivity.current ?: error(&quot;Activity is not yet created&quot;)<br>    return remember(activity) { AndroidSekureSkreenManager(activity) }<br>}</pre><p>now that’s all your app screens are secured protected from MediaProjection , screenshots and even Android does not take a sreenshot of it when sliding to see the recent apps ,</p><p>so Android Does it for you , but IOS no instead it tells and informs you of those events and lets you react on them and execute whatever code you want on each event related to those</p><p>so let’s first see what IOS tells you about screen recording or screenshots</p><p>for screen recording ios fires and event once user starts sharing or recording the screen then if you want to hide something you execute your code once this event is fired</p><p>so we can subscribe to that event from the <strong>NotificationCenter<br></strong>using this event name<strong> <em>UIScreenCapturedDidChangeNotification<br></em></strong><em>then you can observe the</em><strong><em> UIscreen.isCaptured()</em></strong></p><p>so i prefer to create a dedicated class for this Usecase</p><pre>class IosScreenRecordingObserver(<br>    private val onRecordingChanged: (isRecording: Boolean) -&gt; Unit<br>) {<br>    /**<br>     * keeping reference to the observer to be able to remove it if needed<br>     */<br>    private var observer: NSObjectProtocol? = null<br><br>    /**<br>     *adds an observer inside [NSNotificationCenter] under that event name [UIScreenCapturedDidChangeNotification]<br>     * [UIScreenCapturedDidChangeNotification] triggers when user captures screens changes<br>     */<br><br>    fun startObserving() {<br>        observer = NSNotificationCenter.defaultCenter.addObserverForName(<br>            name = UIScreenCapturedDidChangeNotification,<br>            `object` = null,<br>            queue = NSOperationQueue.mainQueue()<br>        ) {<br>            val isRecording = UIScreen.mainScreen.isCaptured()<br>            onRecordingChanged(isRecording)<br>        }<br>    }<br><br>    /**<br>     * removing the observer from [NSNotificationCenter] and clearing it&#39;s reference<br>     */<br>    fun stopObserving() {<br>        observer?.let { NSNotificationCenter.defaultCenter.removeObserver(it) }<br>        observer = null<br>    }<br>}</pre><p>now we have a class that fires the <strong>onRecordingChanged: (isRecording: Boolean) -&gt; Unit </strong>whenever the user changes screen capturing state</p><p>let’s think what else we might need to secure , ScreenShots , and RecentApp Snapshot</p><p>RecentApp ScreenShots is something we can easy guess it’s solution whenver app leaves foreground we need to secure our screen , which is something yet we can do using the <strong>NotificationCenter</strong></p><p>by observing the app lifecycle , so let’s also create a dedicated class for this lifecycle observing usecase</p><pre>class IosAppLifeCycleObserver(<br>    private val onResigned: () -&gt; Unit,<br>    private val onResumed: () -&gt; Unit<br>) {<br>    private val center by lazy { NSNotificationCenter.defaultCenter }<br><br>    /**<br>     * keeping reference to the observer to be able to remove it if needed<br>     * [resignedObserver] observing when app  pauses<br>     * [resumedObserver] observing when app enters foreground  and resumes<br>     * [backgroundObserver] observing when app enters background completely<br>     */<br>    private var resignedObserver: NSObjectProtocol? = null<br>    private var resumedObserver: NSObjectProtocol? = null<br><br>    private var backgroundObserver: NSObjectProtocol? = null<br><br>    fun startObserving() {<br>        setupResignedObserver()<br>        setupResumedObserver()<br>        setupBackgroundObserver()<br>    }<br><br>    /**<br>     *adds an observer inside [NSNotificationCenter] under that event name [UIApplicationWillResignActiveNotification]<br>     * [UIApplicationWillResignActiveNotification] triggers when app is not interactable<br>     */<br><br>    private fun setupResignedObserver() {<br>        resignedObserver = center.addObserverForName(<br>            name = UIApplicationWillResignActiveNotification,<br>            `object` = null,<br>            queue = NSOperationQueue.mainQueue()<br>        ) {<br>            onResigned()<br>        }<br>    }<br><br>    /**<br>     *adds an observer inside [NSNotificationCenter] under that event name [UIApplicationDidEnterBackgroundNotification]<br>     * [UIApplicationDidEnterBackgroundNotification] triggers when app is completely on background<br>     */<br>    private fun setupBackgroundObserver() {<br>        backgroundObserver = center.addObserverForName(<br>            name = UIApplicationDidEnterBackgroundNotification,<br>            `object` = null,<br>            queue = NSOperationQueue.mainQueue()<br>        ) { onResigned() }<br><br>    }<br><br>    /**<br>     *adds an observer inside [NSNotificationCenter] under that event name [UIApplicationDidBecomeActiveNotification]<br>     * [UIApplicationDidBecomeActiveNotification] triggers when app is completely on resume and interactable<br>     */<br>    private fun setupResumedObserver() {<br>        resumedObserver = center.addObserverForName(<br>            name = UIApplicationDidBecomeActiveNotification,<br>            `object` = null,<br>            queue = NSOperationQueue.mainQueue()<br>        ) {<br>            onResumed()<br>        }<br>    }<br><br>    /**<br>     * removing observers<br>     */<br>    fun stopObserving() {<br>        resumedObserver?.let { center.removeObserver(it) }<br>        resignedObserver?.let { center.removeObserver(it) }<br>    }<br>}</pre><p>now we have a class fires callbacks whenever lifecycle changes</p><p>so now we secured our app screen when app is on recents or when app is being recorded or shared , 1 thing yet left is to secure it from screenshots for that ios does not give you any info but has another way to solve it , it let’s you wrap your ui components inside an already secured components that by design are secured from the screenshots , but it accepts UIViews from IOS but here is the thing , the whole compose app is still rendered and viewed inside an IOS ViewController so using the same idea we can wrap any composable inside IOS Secured View</p><pre>@Composable<br>expect fun SekuredBox(  content : @Composable ()-&gt;Unit )</pre><p>let’s start by ios at the end we will never do anything inside the android part</p><pre>@Composable<br>actual fun SekuredBox(content: @Composable (() -&gt; Unit)) {<br>    /**<br>     * nothing needs to be done in android side if applied<br>     * [WindowManager.LayoutParams.FLAG_SECURE]<br>     */<br>    content()<br>}</pre><p>we will use the ios secure text field trick and add our composable as an inner child of it so it inherites it’s security and screen shots prevention <strong>( it might be enough to use it only without our previous observers ) , </strong>but i wanted to be more defensive and to cover multiple usecases</p><pre>@OptIn(ExperimentalForeignApi::class)<br>@Composable<br>actual fun SekuredBox(<br>    content: @Composable () -&gt; Unit<br>) {<br>    UIKitView(<br>        modifier = Modifier.fillMaxSize(),<br>        factory = {<br>            /**<br>             * creating the secure text field [UIView]<br>             * [setSecureTextEntry] enables the security for it<br>             * with [UIColor.clearColor] to make it transparent<br>             */<br>            val textField = UITextField().apply {<br>                setSecureTextEntry(true)<br>                userInteractionEnabled = true<br>                backgroundColor = UIColor.clearColor<br>            }<br>            val composeUIViewController = ComposeUIViewController(content = content)<br>            val composeView = composeUIViewController.view<br><br>            /**<br>             *  Find the secure container subview referred as canvas<br>             */<br>            val canvas = textField.subviews.firstOrNull {<br>                it.toString().contains(&quot;Canvas&quot;)<br>            } as? UIView<br><br>            canvas?.let { secureContainer -&gt;<br>                /**<br>                 *  [userInteractionEnabled] allowing the compose to be interactable<br>                 *  [addSubview] adding compose to the canvas as subview to be viewed<br>                 *  [UiView.translatesAutoresizingMaskIntoConstraints] making the view fill entire screen<br>                 */<br>                secureContainer.userInteractionEnabled = true<br>                secureContainer.addSubview(composeView)<br>                composeView.translatesAutoresizingMaskIntoConstraints = false<br>                /**<br>                 * applying constraints the composable with respect to the canvas to fully fit inside<br>                 */<br>                NSLayoutConstraint.activateConstraints(<br>                    listOf(<br>                        composeView.topAnchor.constraintEqualToAnchor(secureContainer.topAnchor),<br>                        composeView.bottomAnchor.constraintEqualToAnchor(secureContainer.bottomAnchor),<br>                        composeView.leadingAnchor.constraintEqualToAnchor(secureContainer.leadingAnchor),<br>                        composeView.trailingAnchor.constraintEqualToAnchor(secureContainer.trailingAnchor)<br>                    )<br>                )<br>            }<br><br>            textField<br>        },<br>        update = { _ -&gt; }<br>    )<br>}</pre><p>now we covered almost all cases we need to secure our screen , let’s put them all together</p><p>we will use this state holder to keep info of our screen state</p><pre>object SekureSkreenState {<br>    private val _isEnabled = MutableStateFlow(false)<br>    val isEnabled = _isEnabled.asStateFlow()<br>    fun update(enable: Boolean) = _isEnabled.update { enable }<br>}</pre><pre>class IosSekureSkreenManager : SekureSkreenManager {<br>    private val lifecycleObserver = IosAppLifeCycleObserver(<br>        onResumed = { SekureSkreenState.update(enable = false) },<br>        onResigned = { SekureSkreenState.update(enable = true) },<br>    )<br><br>    private val recordingObserver = IosScreenRecordingObserver(<br>        onRecordingChanged = { isRecording -&gt;<br>            SekureSkreenState.update(enable = isRecording)<br>        }<br>    )<br><br>    override fun start() {<br>        lifecycleObserver.startObserving()<br>        recordingObserver.startObserving()<br>    }<br><br>    override fun stop() {<br>        lifecycleObserver.stopObserving()<br>        recordingObserver.stopObserving()<br>    }<br>}</pre><p>that’s all let’s create some example app and run and test</p><pre>@Preview<br>fun App() {<br>    val manager = rememberSekureSkreenManager()<br>    LaunchedEffect(Unit) {<br>        manager.start()<br>    }<br>    val isEnabled by SekureSkreenState.isEnabled.collectAsStateWithLifecycle()<br>    MaterialTheme {<br>        Box(Modifier.fillMaxSize()) {<br>            SekuredBox {<br>                HomeScreen()<br>            }<br>            if (isEnabled) {<br>                BlackScreen()<br>            }<br>        }<br>    }<br>}</pre><p>Source Code : <a href="https://github.com/mohaberabi/sekure-skreen">https://github.com/mohaberabi/sekure-skreen</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=07a9832d1589" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Unhandled Kotlin Multiplatform Exceptions on iOS and Making Them Visible in Crashlytics]]></title>
            <link>https://medium.com/@mohaberabi98/unhandled-kotlin-multiplatform-exceptions-on-ios-and-making-them-visible-in-crashlytics-7d5a7649852a?source=rss-725d0fa3cda9------2</link>
            <guid isPermaLink="false">https://medium.com/p/7d5a7649852a</guid>
            <category><![CDATA[compose-multiplatform]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[ios]]></category>
            <dc:creator><![CDATA[Mohab erabi]]></dc:creator>
            <pubDate>Tue, 13 Jan 2026 19:46:18 GMT</pubDate>
            <atom:updated>2026-01-13T19:46:18.068Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XouM4F4POblMqfX5JFFIdA.jpeg" /></figure><p>Mostly all the crash reporting sdks or platforms works the same way for each platform on Android they capture unhandled exception happens on JVM &amp; Sends them to remote console the same way happens also in IOS any fatal error is thrown and unhandled on the native side it is recorded and sent to remote console</p><p>but what happens when the fatal comes from common code in Kotlin Multiplatform Project , it’s already captured and recorded if you are running on Android Device but it’s not the same if you are running on IOS device , yes the app crashes but even if you are using and setup firebase crashlytics or any other crash reporting platform it will not be able to capture it , here is why</p><p>kotlin native throws exception which in return making the ios runtime terminates the process so it might be normal termination from the crash reporting Point Of View</p><p>for that special scenario kotlin already has a solution for it inside the iosMain</p><pre>setUnhandledExceptionHook { throwable -&gt;<br>    NativeFatalCrashMapper.mapAndTerminateFatal(throwable)<br><br></pre><p>The hook is invoked whenever there is an uncaught exception reaching the boundaries of the Kotlin world, i.e. top-level main(), worker boundary, or when Objective-C to Kotlin call not marked with <a href="http://twitter.com/Throws">@Throws</a> throws an exception.</p><p>The hook is in full control of how to process an unhandled exception and proceed further</p><p>this one captures and catches the exception without terminating the app which is something we don’t want we usually want the app to crash in such cases so there is also</p><pre>public external fun terminateWithUnhandledException(throwable: Throwable): Nothing</pre><p>that will give you the Throwable and kills the process but all of those are kotlin types which most of crash reporting will not be able to record and send so instead we will not use the <strong>terminateWithUnhandledException()<br></strong>instead we will catch the exception get whatever info we need it from then raise IOS Itself to capture the error and terminate the process this way we are not altering any native behaviour of the OS + keeping the same logic of the Crash reproting SDK</p><p>so inside iosMain let’s define some class that will carry our information we need to record in case if fatal thrown</p><pre>@Serializable<br>internal data class NativeFatalErrorData(<br>    @SerialName(&quot;message&quot;) val message: String,<br>    @SerialName(&quot;stack_trace&quot;) val stackTrace: String,<br>    @SerialName(&quot;cause_domain&quot;) val causeDomain: String,<br>    @SerialName(&quot;cause_message&quot;) val causeMessage: String? = null,<br>    @SerialName(&quot;cause_stack_trace&quot;) val causeStackTrace: String? = null,<br>)<br><br>internal fun Throwable.toNativeFatalErrorData() = NativeFatalErrorData(<br>    message = message ?: &quot;Unknown Error Message &quot;,<br>    stackTrace = stackTraceToString(),<br>    causeDomain = &quot;KMP.commonMain.UnhandledException&quot;,<br>    causeMessage = cause?.message,<br>    causeStackTrace = cause?.stackTraceToString()<br>)<br><br></pre><p>we will need little swift code to just make the crash happens at it’s side</p><pre>@objc public class  IosFatalCrashMapper : NSObject {<br>    <br>    <br>    private static let  ERROR_DOMAIN = &quot;KMP.commonMain.UnhanldedException&quot;<br>    <br>    private static  let ERROR_CODE = 1<br>    <br>    @objc public static func mapAndTerminateFatal(<br>        data : [String : Any ] = [:]<br>    ){<br>        <br>        let error = NSError(<br>                   domain: ERROR_DOMAIN,<br>                   code: ERROR_CODE,<br>                   userInfo: data<br>               )<br><br>        fatalError(&quot;\(ERROR_DOMAIN): \(error)&quot;)<br>        <br>    }<br>}</pre><p>no we will need to invoke this function inside kotlin code in iosMain</p><pre>  setUnhandledExceptionHook { throwable -&gt;</pre><p>then we will serialize the data inside of the Throwable map it to Swift Map <br>and we will call our kotlin code inside the ExceptionHook</p><p>for creating the def files for ios objective-c interop with kotlin i will use this great plugin</p><pre>    id(&quot;io.github.ttypic.swiftklib&quot;) version &quot;0.6.4&quot;</pre><pre>object NativeFatalCrashMapper {<br>    private val internalJson = Json {<br>        prettyPrint = true<br>        explicitNulls = false<br>        ignoreUnknownKeys = true<br>        encodeDefaults = true<br>    }<br><br>    @OptIn(ExperimentalForeignApi::class)<br>    fun mapAndTerminateFatal(<br>        throwable: Throwable,<br>    ) {<br>        val data = throwable.toNativeFatalErrorData()<br>        val dataJson = internalJson.encodeToString(data)<br>        val map = mapOf&lt;Any?, Any?&gt;(<br>            &quot;data&quot; to dataJson,<br>        )<br>        IosFatalCrashMapper.mapAndTerminateFatalWithData(<br>            data = map<br>        )<br>    }<br>}</pre><p>now for using Firebase Crashlytics itself i will use Kotlin Cocoapods <br>alias(<em>libs</em>.<em>plugins</em>.<em>kotlinCocoapods</em>)</p><pre>    alias(libs.plugins.kotlinCocoapods)<br>    cocoapods {<br>        summary = &quot;&quot;<br>        homepage = &quot;Analytics&quot;<br>        version = &quot;1.0&quot;<br>        ios.deploymentTarget = &quot;16.0&quot;<br>        framework {<br>            baseName = &quot;Analytics&quot;<br>            isStatic = false<br>        }<br>        pod(&quot;FirebaseCrashlytics&quot;) {<br>            extraOpts += listOf(&quot;-compiler-option&quot;, &quot;-fmodules&quot;)<br>        }<br>        pod(&quot;FirebaseAnalytics&quot;) {<br>            extraOpts += listOf(&quot;-compiler-option&quot;, &quot;-fmodules&quot;)<br>        }<br>    }</pre><p>that’s all what all we need now is to hook the error and see it in firebase , we can force crash to test</p><pre>@OptIn(ExperimentalNativeApi::class, ExperimentalForeignApi::class)<br>private fun hookUnhandledCommonError() {<br>    setUnhandledExceptionHook { throwable -&gt;<br>        NativeFatalCrashMapper.mapAndTerminateFatal(throwable)<br>    }<br>}<br><br></pre><p>call it inside the MainViewController in iosMain compose code</p><pre>fun MainViewController() =<br>    ComposeUIViewController(<br>        configure = {<br>            hookUnhandledCommonError()</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d93OMe_iPnH04Gxqy5y8dA.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7d5a7649852a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Permission Handling in Kotlin Multiplatform (KMP) — A Real-World Architecture]]></title>
            <link>https://medium.com/@mohaberabi98/permission-handling-in-kotlin-multiplatform-kmp-a-real-world-architecture-00b50bc2c86d?source=rss-725d0fa3cda9------2</link>
            <guid isPermaLink="false">https://medium.com/p/00b50bc2c86d</guid>
            <category><![CDATA[compose-multiplatform]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Mohab erabi]]></dc:creator>
            <pubDate>Mon, 12 Jan 2026 19:22:17 GMT</pubDate>
            <atom:updated>2026-01-12T19:22:17.290Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*I877sWochsF3xnyy63GQtQ.png" /></figure><h3>Permission Handling in Kotlin Multiplatform (KMP) — A Real-World Architecture</h3><p>Handling permissions in <strong>Kotlin Multiplatform (KMP)</strong> is one of those areas where <em>naive abstractions break very quickly</em>.</p><p>Because <strong>Android and iOS have fundamentally different permission models</strong>:</p><p><strong>Android<br></strong>Centralized permission <br>Runtime permission APIs<br>Can re-request (rationale)<br>Activity result based</p><p><strong>IOS<br></strong>Permission per domain<br>Each framework owns its permission<br>Callback / delegate based</p><p>let’s try to create a combined solution that works well for both of them allowing us to handle it in a scalable simple way , also let’s extend IOS to allow it to handle multiple permisions</p><h3>Core Permission Model (commonMain)</h3><pre>sealed interface AppPermission {<br>    data object Notifications : AppPermission<br>    data object PickImage : AppPermission<br>    data object WriteToCalender : AppPermission<br>    data object ReadFromCalender : AppPermission<br>}</pre><pre>sealed interface AppPermissionStatus {<br>    data object Granted : AppPermissionStatus<br>    data object DeniedForever : AppPermissionStatus<br>    data object Requestable : AppPermissionStatus<br>}</pre><pre>/**<br> *a simple helper for permission manager to be implemented on each platform<br> */<br>interface PermissionManagerDelegate {<br>    /**<br>     *  checks the decision needed to be made by the app which in return requests the permission<br>     *  or grant execution or show blocked ui<br>     */<br>    suspend fun checkDecision(permission: AppPermission): AppPermissionStatus<br><br>    /**<br>     * checks if the required system service is guarded by permission or not<br>     */<br>    suspend fun requiresPermission(permission: AppPermission): Boolean<br><br>    /**<br>     * opens devices settings to control permission<br>     */<br>    suspend fun openAppDeviceSettings()<br><br>}</pre><pre>/**<br> * controls app wide permission<br> */<br>interface PermissionManager {<br>    /**<br>     * checks permission and request if needed and result are reacted upon them<br>     * inside the [rememberPermissionManager]<br>     * [onResponse] Callback<br>     */<br>    suspend fun checkAndRequestIfNeed(permission: AppPermission)<br><br>    /**<br>     * opens devices settings to control permission<br>     */<br>    suspend fun openDeviceSettings()<br><br>    /**<br>     * checks if permission is granted or not<br>     */<br>    suspend fun isGranted(permission: AppPermission): Boolean<br><br>    /**<br>     * checks if the required system service is guarded by permission or not<br>     */<br>    suspend fun isPermissionRequire(permission: AppPermission): Boolean<br><br>    /**<br>     * checks if we still can request permission again ,<br>     * it happens on android if rationale or never requested for example<br>     */<br>    suspend fun canBeRequestedAgain(permission: AppPermission): Boolean<br>}<br><br>@Composable<br><br>expect fun rememberPermissionManager(<br>    onResponse: (AppPermissionStatus) -&gt; Unit,<br>): PermissionManager</pre><pre>internal class DefaultPermissionManager(<br>    private val delegate: PermissionManagerDelegate,<br>    private val onLaunch: suspend (AppPermission) -&gt; Unit,<br>) : PermissionManager</pre><h3>Android Side</h3><pre>internal class AndroidPermissionManagerDelegate(<br>    private val activity: Activity,<br>) : PermissionManagerDelegate {<br>    /**<br>     * checks what should happen for the requested system service permissions<br>     * it returns [AppPermissionStatus.Granted] in case no need to request for permission<br>     * or the permission is already granted<br>     * returns [AppPermissionStatus.Requestable] if [Activity.shouldShowRequestPermissionRationale] returns true<br>     * else it returns [AppPermissionStatus.DeniedForever]<br>     */<br>    override suspend fun checkDecision(permission: AppPermission): AppPermissionStatus {<br>        if (requiresPermission(permission).not()) return AppPermissionStatus.Granted<br>        if (activity.permissionGranted(permission.toAndroid())) return AppPermissionStatus.Granted<br>        val rationale = activity.showPermissionRationale(permission.toAndroid())<br>        if (rationale) return AppPermissionStatus.Requestable<br>        return AppPermissionStatus.DeniedForever<br>    }<br><br>    /**<br>     *checks if the requested system service needs permission or not<br>     */<br><br>    override suspend fun requiresPermission(<br>        permission: AppPermission,<br>    ): Boolean = when (permission) {<br>        AppPermission.Notifications -&gt; requireNotificationPermission()<br>        AppPermission.PickImage -&gt; false<br>        AppPermission.WriteToCalender, AppPermission.ReadFromCalender -&gt; true<br>    }<br><br>    /**<br>     * launching an intent for the [Settings.ACTION_APPLICATION_DETAILS_SETTINGS] for the caller app<br>     */<br>    override suspend fun openAppDeviceSettings() {<br>        val uri = Uri.fromParts(&quot;package&quot;, activity.packageName, null)<br>        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)<br>            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)<br>        activity.startActivity(intent)<br>    }<br>}<br><br></pre><pre>/**<br> * an extension for the [PermissionManager] that will use same impl but adds new functionality allowing<br> * callers to request multiple permissions at once<br> */<br>class AndroidMultiplePermissionManager(<br>    private val singlePermissionManager: PermissionManager,<br>    private val onLaunch: suspend (List&lt;AppPermission&gt;) -&gt; Unit,<br>) : MultiplePermissionManager, PermissionManager by singlePermissionManager {<br>    override suspend fun requestMultiple(permissions: List&lt;AppPermission&gt;) {<br>        onLaunch(permissions)<br>    }<br>}</pre><pre>@Composable<br>actual fun rememberPermissionManager(<br>    onResponse: (AppPermissionStatus) -&gt; Unit,<br>): PermissionManager {<br>    val activity = requireNotNull(LocalActivity.current) {<br>        &quot;Activity is not yet attached&quot;<br>    }<br>    val androidLauncher = rememberLauncherForActivityResult(<br>        contract = ActivityResultContracts.RequestPermission(),<br>    ) { granted -&gt;<br>        if (granted) {<br>            onResponse(AppPermissionStatus.Granted)<br>        } else {<br>            onResponse(AppPermissionStatus.DeniedForever)<br>        }<br>    }<br><br>    val delegate = remember {<br>        AndroidPermissionManagerDelegate(activity = activity)<br>    }<br>    return remember(<br>        onResponse,<br>        androidLauncher,<br>        activity,<br>    ) {<br>        DefaultPermissionManager(<br>            delegate = delegate,<br>            onLaunch = {<br>                if (activity.showPermissionRationale(it.toAndroid())) {<br>                    onResponse(AppPermissionStatus.Requestable)<br>                } else {<br>                    androidLauncher.launch(input = it.toAndroid())<br>                }<br>            },<br>        )<br>    }<br>}</pre><pre>@Composable<br>actual fun rememberMultiplePermissionsManager(<br>    onResponse: (AppPermissionStatus) -&gt; Unit,<br>    onMultipleResponse: (Map&lt;AppPermission, AppPermissionStatus&gt;) -&gt; Unit,<br>): MultiplePermissionManager {<br>    val activity = requireNotNull(<br>        LocalActivity.current<br>    ) {<br>        &quot;multiplePermissionsManager activity not yet attached or detached&quot;<br>    }<br>    val singleLauncher = rememberPermissionManager(onResponse = onResponse)<br>    val multipleLauncher = rememberLauncherForActivityResult(<br>        contract = ActivityResultContracts.RequestMultiplePermissions(),<br>    ) { result -&gt;<br>        val perms = result.map { (permission, granted) -&gt;<br>            val status = if (activity.showPermissionRationale(permission)) {<br>                AppPermissionStatus.Requestable<br>            } else {<br>                if (granted) AppPermissionStatus.Granted else AppPermissionStatus.DeniedForever<br>            }<br>            permission.toAppPermission() to status<br>        }.toMap()<br>        onMultipleResponse(perms)<br>    }<br><br>    return remember(<br>        activity,<br>    ) {<br>        AndroidMultiplePermissionManager(<br>            onLaunch = {<br>                val android = it.map { perm -&gt; perm.toAndroid() }.toTypedArray()<br>                multipleLauncher.launch(android)<br>            },<br>            singlePermissionManager = singleLauncher,<br>        )<br>    }<br><br>}</pre><p>Now IOS Part , it is almost like android but the key idea is ios does not handle permissions like android where is a single place where permissions are handled , no instead each system domain has it’s own permission handling system so we need to isolate each part or each domain and find a centeralized way to make use of all of them</p><pre>/**<br> * as ios works differently than android regarding permissions it does not has this<br> * centralized place for handling permissions instead each domain has it&#39;s own permission handling system<br> * so as inspired from android this delegate will be used for any type of permissions needed and also<br> * will be extended even for the multiple permissions<br> */<br>interface SinglePermissionDelegate {<br>    suspend fun launch(): Boolean<br>    suspend fun checkDecision(): AppPermissionStatus<br>}<br><br>/**<br> * checks the [SinglePermissionDelegate.checkDecision] and suspends until result<br> * upon returned result we react upon , if we need to request , granted or denied<br> */<br>internal suspend fun SinglePermissionDelegate.checkPermissionOrLaunch(): AppPermissionStatus {<br>    val status = checkDecision()<br>    return if (status == AppPermissionStatus.Requestable) {<br>        val granted = launch()<br>        if (granted) AppPermissionStatus.Granted<br>        else AppPermissionStatus.DeniedForever<br>    } else {<br>        status<br>    }<br>}</pre><pre>/**<br> * launches and or requests the permission for Reading Gallery Images<br> */<br>class ReadImagePermissionDelegate : SinglePermissionDelegate {<br>    private val accessReadWrite = PHAccessLevelReadWrite<br><br>    override suspend fun launch(): Boolean {<br>        return suspendOnMain { resume -&gt;<br>            PHPhotoLibrary.requestAuthorizationForAccessLevel(accessLevel = accessReadWrite) { status -&gt;<br>                val granted = when (status) {<br>                    PHAuthorizationStatusAuthorized,<br>                    PHAuthorizationStatusLimited -&gt; true<br><br>                    else -&gt; false<br>                }<br>                resume(granted)<br>            }<br><br>        }<br>    }<br><br>    override suspend fun checkDecision(): AppPermissionStatus {<br><br>        val status = PHPhotoLibrary.authorizationStatusForAccessLevel(accessReadWrite)<br>        return when (status) {<br>            PHAuthorizationStatusAuthorized,<br>            PHAuthorizationStatusLimited -&gt; AppPermissionStatus.Granted<br><br>            PHAuthorizationStatusNotDetermined -&gt; AppPermissionStatus.Requestable<br>            PHAuthorizationStatusDenied,<br>            PHAuthorizationStatusRestricted -&gt; AppPermissionStatus.DeniedForever<br><br>            else -&gt; AppPermissionStatus.DeniedForever<br>        }<br>    }<br><br><br>}</pre><pre>/**<br> * launches and or requests the permission for Reading Reminders or Calender<br> */<br><br>class ReminderPermissionDelegate() : SinglePermissionDelegate {<br>    override suspend fun launch(): Boolean {<br>        return suspendOnMain { resume -&gt;<br>            val store = EKEventStore()<br>            store.requestAccessToEntityType(<br>                entityType = EKEntityType.EKEntityTypeReminder,<br>            ) { granted, error -&gt;<br>                resume(granted)<br>            }<br>        }<br>    }<br><br>    override suspend fun checkDecision(): AppPermissionStatus {<br>        val status = EKEventStore.Companion<br>            .authorizationStatusForEntityType(entityType = EKEntityType.EKEntityTypeReminder)<br>        return when (status) {<br>            EKAuthorizationStatusAuthorized -&gt; AppPermissionStatus.Granted<br>            EKAuthorizationStatusNotDetermined -&gt; AppPermissionStatus.Requestable<br>            else -&gt; AppPermissionStatus.DeniedForever<br>        }<br>    }<br><br><br>}</pre><pre>class IosPermissionManagerDelegate(<br>    private val reminderLauncher: ReminderPermissionDelegate,<br>    private val imageDelegate: ReadImagePermissionDelegate,<br>    private val notificationsPermissionDelegate: NotificationsPermissionDelegate<br>) : PermissionManagerDelegate {<br>    /**<br>     * checks what should happen for the requested system service permissions<br>     * and returns the [AppPermissionStatus] from each permission Delegate for the requested [AppPermission]<br>     */<br>    override suspend fun checkDecision(permission: AppPermission): AppPermissionStatus {<br>        return when (permission) {<br>            AppPermission.Notifications -&gt; notificationsPermissionDelegate.checkDecision()<br>            AppPermission.PickImage -&gt; imageDelegate.checkDecision()<br>            AppPermission.ReadFromCalender,<br>            AppPermission.WriteToCalender -&gt; reminderLauncher.checkDecision()<br>        }<br>    }<br><br>    /**<br>     *checks if the requested system service needs permission or not<br>     */<br>    override suspend fun requiresPermission(permission: AppPermission): Boolean = true<br><br>    /**<br>     * launching an [NSURL] for the [UIApplicationOpenSettingsURLString]<br>     */<br>    override suspend fun openAppDeviceSettings() = suspendCancellableCoroutine { continuation -&gt;<br>        val url = NSURL(string = UIApplicationOpenSettingsURLString)<br>        UIApplication.sharedApplication.openURL(<br>            url = url,<br>            options = mapOf&lt;Any?, Any?&gt;(),<br>            completionHandler = {},<br>        )<br>        continuation.resume(Unit)<br>    }<br><br>}</pre><pre><br>/**<br> * A Facade  that wraps each [SinglePermissionDelegate] for each [AppPermission] and launches the request dialogue<br> * then invokes the result inside [onResponse]<br> */<br>class IosPermissionLauncher(<br>    private val onResponse: (granted: Boolean) -&gt; Unit,<br>    private val reminderPermissionDelegate: ReminderPermissionDelegate,<br>    private val imagePermissionDelegate: ReadImagePermissionDelegate,<br>    private val notificationsPermissionDelegate: NotificationsPermissionDelegate<br>) {<br><br><br>    suspend fun launch(permission: AppPermission) {<br>        val granted = when (permission) {<br>            AppPermission.Notifications -&gt; notificationsPermissionDelegate.launch()<br>            AppPermission.PickImage -&gt; imagePermissionDelegate.launch()<br>            AppPermission.ReadFromCalender -&gt; reminderPermissionDelegate.launch()<br>            AppPermission.WriteToCalender -&gt; reminderPermissionDelegate.launch()<br><br>        }<br>        onResponse(granted)<br>    }<br>}<br><br><br><br>class IosMultiplePermissionManager(<br>    private val onLaunch: suspend (permissions: List&lt;AppPermission&gt;) -&gt; Unit,<br>    private val permissionManager: PermissionManager<br>) : MultiplePermissionManager, PermissionManager by permissionManager {<br>    override suspend fun requestMultiple(permissions: List&lt;AppPermission&gt;) {<br>        onLaunch(permissions)<br>    }<br>}</pre><pre>/**<br> * ios needs all of permission dialogue to be used inside the Main Thread , as a<br> * way of being more defensive added this function to make use of coroutine<br> * that will suspend with a lambda to be used to request permissions for any area in the IOS System<br> */<br>internal suspend inline fun &lt;T&gt; suspendOnMain(<br>    crossinline block: (resume: (T) -&gt; Unit) -&gt; Unit<br>) = suspendCancellableCoroutine { continuation -&gt;<br>    dispatch_async(queue = dispatch_get_main_queue()) {<br>        block { value -&gt;<br>            if (continuation.isActive) {<br>                continuation.resume(value)<br>            }<br>        }<br>    }<br><br>}</pre><pre>/**<br> * acts a dependency container for reusability for all type of permissions inside ios<br> * any new permission needed to be supported should be also added here<br> */<br>internal data class SinglePermissionDelegates(<br>    val reminder: ReminderPermissionDelegate = ReminderPermissionDelegate(),<br>    val image: ReadImagePermissionDelegate = ReadImagePermissionDelegate(),<br>    val notifications: NotificationsPermissionDelegate = NotificationsPermissionDelegate(),<br>)<br><br>internal object SinglePermissionDelegateDefaults {<br>    val delegates by lazy {<br>        SinglePermissionDelegates()<br>    }<br>}<br><br>@Composable<br>internal fun rememberPermissionDelegate(<br>    delegates: SinglePermissionDelegates = SinglePermissionDelegateDefaults.delegates<br>) = remember(<br>    delegates<br>) {<br>    IosPermissionManagerDelegate(<br>        reminderLauncher = delegates.reminder,<br>        imageDelegate = delegates.image,<br>        notificationsPermissionDelegate = delegates.notifications<br>    )<br>}</pre><pre>@Composable<br>actual fun rememberPermissionManager(<br>    onResponse: (AppPermissionStatus) -&gt; Unit,<br>): PermissionManager {<br><br>    val delegate = rememberPermissionDelegate()<br><br>    val launcher = remember {<br>        IosPermissionLauncher(<br>            reminderPermissionDelegate = SinglePermissionDelegateDefaults.delegates.reminder,<br>            imagePermissionDelegate = SinglePermissionDelegateDefaults.delegates.image,<br>            notificationsPermissionDelegate = SinglePermissionDelegateDefaults.delegates.notifications,<br>            onResponse = { granted -&gt;<br>                if (granted) {<br>                    onResponse(AppPermissionStatus.Granted)<br>                } else {<br>                    onResponse(AppPermissionStatus.DeniedForever)<br>                }<br>            },<br>        )<br>    }<br>    return remember() {<br>        DefaultPermissionManager(<br>            delegate = delegate,<br>            onLaunch = {<br>                launcher.launch(it)<br>            },<br>        )<br>    }<br>}</pre><pre>@Composable<br>actual fun rememberMultiplePermissionsManager(<br>    onResponse: (AppPermissionStatus) -&gt; Unit,<br>    onMultipleResponse: (Map&lt;AppPermission, AppPermissionStatus&gt;) -&gt; Unit,<br>): MultiplePermissionManager {<br><br><br>    val multipleLauncher = remember {<br>        IosMultiplePermissionLauncher(<br>            reminderPermissionDelegate = SinglePermissionDelegateDefaults.delegates.reminder,<br>            readImagePermissionDelegate = SinglePermissionDelegateDefaults.delegates.image,<br>            notificationsPermissionDelegate = SinglePermissionDelegateDefaults.delegates.notifications,<br>        )<br>    }<br><br>    val manager = rememberPermissionManager(<br>        onResponse = onResponse<br>    )<br>    return remember {<br>        IosMultiplePermissionManager(<br>            onLaunch = {<br>                val result = multipleLauncher.launchMultiple(permissions = it)<br>                onMultipleResponse(result)<br>            },<br>            permissionManager = manager<br>        )<br>    }<br><br>}</pre><p>that’s it , now let’s add some functionality to observe the permissions based on the lifecycle change of the app and add some ui part to test it</p><p>each time app returns to foreground we will check the permission and react upon them</p><pre>/**<br> * Simple [Composable] that observers the app lifecycle and mutates the Permission Status each time<br> * the user changes app lifecycle to always have the latest permission status from the device settings<br> */<br>@Composable<br>fun rememberPermissionWithLifeCycle(<br>    vararg permission: AppPermission,<br>    onRequested: (Map&lt;AppPermission, AppPermissionStatus&gt;) -&gt; Unit = {},<br>): Boolean {<br>    var isGranted by rememberSaveable {<br>        mutableStateOf(false)<br>    }<br>    var shouldRequestAgain by rememberSaveable {<br>        mutableStateOf(false)<br>    }<br>    val permissionManager = rememberMultiplePermissionsManager { resultMap -&gt;<br>        val allGranted = resultMap.values.all { it == AppPermissionStatus.Granted }<br>        onRequested(resultMap)<br>        isGranted = allGranted<br>    }<br>    LaunchedEffect(<br>        Unit,<br>    ) {<br>        permissionManager.requestMultiple(permission.toList())<br>    }<br><br>    LaunchedEffect(<br>        shouldRequestAgain,<br>    ) {<br>        if (shouldRequestAgain) {<br>            permissionManager.requestMultiple(permission.toList())<br>            shouldRequestAgain = false<br>        }<br>    }<br>    ComposeLifeCycleObserver { event -&gt;<br>        when (event) {<br>            Lifecycle.Event.ON_RESUME -&gt; shouldRequestAgain = true<br>            else -&gt; Unit<br>        }<br>    }<br>    return isGranted<br>}<br><br>@Composable<br>fun ComposeLifeCycleObserver(<br>    onLifeCycleEvent: (Lifecycle.Event) -&gt; Unit,<br>) {<br>    val lifecycleOwner = LocalLifecycleOwner.current<br>    DisposableEffect(lifecycleOwner) {<br>        val observer = LifecycleEventObserver { _, event -&gt;<br>            onLifeCycleEvent(event)<br>        }<br>        lifecycleOwner.lifecycle.addObserver(observer)<br>        onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }<br>    }<br><br>}</pre><p>now let’s add some ui to test the <br>don’t forget to handle the AndroidManifest.xml and Info.Plist</p><pre>fun App() {<br>    MaterialTheme {<br>        var permissions by remember {<br>            mutableStateOf(mapOf&lt;AppPermission, AppPermissionStatus&gt;())<br>        }<br>        val areAllGranted = rememberPermissionWithLifeCycle(<br>            AppPermission.Notifications,<br>            AppPermission.ReadFromCalender,<br>            AppPermission.WriteToCalender,<br>            onRequested = {<br>                permissions = it<br>            }<br>        )<br><br>        Scaffold { padding -&gt;<br>            Column(<br><br>                modifier = Modifier.fillMaxSize()<br>                    .padding(padding)<br>                    .padding(20.dp)<br>            ) {<br><br>                permissions.forEach {<br>                    val name = when (it.key) {<br>                        AppPermission.Notifications -&gt; &quot;Notifications&quot;<br>                        AppPermission.PickImage -&gt; &quot;Pick Image&quot;<br>                        AppPermission.ReadFromCalender -&gt; &quot;Read Calender&quot;<br>                        AppPermission.WriteToCalender -&gt; &quot;Write Calender&quot;<br>                    }<br>                    val value = when (it.value) {<br>                        AppPermissionStatus.DeniedForever -&gt; &quot;Denied Forever &quot;<br>                        AppPermissionStatus.Granted -&gt; &quot;Granted&quot;<br>                        AppPermissionStatus.Requestable -&gt; &quot;Requestable&quot;<br>                    }<br>                    Row(<br>                        modifier = Modifier.fillMaxWidth().padding(8.dp),<br>                        horizontalArrangement = Arrangement.SpaceAround<br>                    ) {<br>                        Text(name)<br>                        Text(value)<br>                    }<br>                }<br>            }<br>        }<br>    }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ThNjRBCcCGjJYzzWhx7XHQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZpNIj2eTS9nxT7QuPwst8w.png" /></figure><p>source code : <a href="https://github.com/mohaberabi/konsent">https://github.com/mohaberabi/konsent</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=00b50bc2c86d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How I Built a Kotlin Compiler Plugin That Makes Everything Mockable]]></title>
            <link>https://medium.com/@mohaberabi98/how-i-built-a-kotlin-compiler-plugin-that-makes-everything-mockable-cc4652b83850?source=rss-725d0fa3cda9------2</link>
            <guid isPermaLink="false">https://medium.com/p/cc4652b83850</guid>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[compose-multiplatform]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[ios]]></category>
            <dc:creator><![CDATA[Mohab erabi]]></dc:creator>
            <pubDate>Mon, 08 Dec 2025 23:17:35 GMT</pubDate>
            <atom:updated>2025-12-08T23:19:28.150Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*jfLiPteSC1bptY94s3JvTQ.jpeg" /></figure><p>You’re using a modern mocking library (like <a href="https://mokkery.dev/">Mokkery</a>) . You’re trying to mock real production classes. but Kotlin makes everything final by default.</p><p>The idea is simple it’s built on top of the Kotlin Compiler Plugins that gives you some Extensions on the Frontend “FIR” Frontend Intermediate Representations that offers multiple hooks or functions that you can listen to and react upon , For Example to make the Classes , Functions , Members of a Final class all Are Open</p><p>And here is the thing Kotlin is A Multiplatform Language so anything you do with the compiler is multiplatform as well and can work for any target or platform</p><p>My usecase here is to try to mock final classes in A Compose Multiplatform App (IOS &amp; Android )</p><h3>Creating the compiler plugin module</h3><p>we will only need the <strong>compiler.embeddable dependency <br></strong>which will give us access to the FIR Extensions we need and to include the <br>Java Library Plugin so we can create the Jar file which we can provide to the compiler args</p><pre>plugins {<br>    kotlin(&quot;jvm&quot;)<br>    `java-library`<br>}<br><br>group = &quot;org.mohaberabi.make-open&quot;<br>version = &quot;1.0.0&quot;<br><br><br>dependencies {<br>    testImplementation(kotlin(&quot;test&quot;))<br>    implementation(libs.kotlin.compiler.embeddable)<br>// org.jetbrains.kotlin:kotlin-compiler-embeddable:&lt;kotlin&gt;<br>}<br><br>tasks.test {<br>    useJUnitPlatform()<br>}<br>kotlin {<br>    jvmToolchain(21)<br>}</pre><p>then we need to extend and create the <strong>CompilerPluginRegistrar</strong></p><p>This class is the <em>bridge</em> between <strong>our plugin code</strong> and the <strong>Kotlin compiler itself</strong>.</p><p>inside the main/koltin</p><pre>@OptIn(ExperimentalCompilerApi::class)<br>class AllOpenComponentRegistrar : CompilerPluginRegistrar() {<br>    override val supportsK2: Boolean<br>        get() = true<br><br>    @OptIn(ExperimentalCompilerApi::class)<br>    override fun ExtensionStorage.registerExtensions(<br>        configuration: CompilerConfiguration<br>    ) {<br><br>        FirExtensionRegistrarAdapter.registerExtension(FirAllOpenExtensionRegistrar())<br>    }<br>}</pre><p>by extending the CompilerPluginRegistrar you set your class as <strong>CompilerPlugin</strong> then we override the supportsK2 to make it enabled for the K2 Compiler as it is not enabled by default even the K1 is deprecated</p><p>then inside the <strong>registerExtensions() , </strong>you pass your extension and modification which you want to attach to compiler</p><pre>class FirAllOpenExtensionRegistrar : FirExtensionRegistrar() {<br>    override fun ExtensionRegistrarContext.configurePlugin() {<br>        +::FirAllOpenStatusTransformer<br>    }<br>}<br><br>class FirAllOpenStatusTransformer(<br>    session: FirSession<br>) : FirStatusTransformerExtension(session) {<br>    override fun needTransformStatus(<br>        declaration: FirDeclaration<br>    ): Boolean = when (declaration) {<br>        is FirRegularClass,<br>        is FirSimpleFunction,<br>        is FirProperty -&gt; true<br><br>        else -&gt; false<br>    }<br><br>    override fun transformStatus(<br>        status: FirDeclarationStatus,<br>        declaration: FirDeclaration<br>    ): FirDeclarationStatus = when (declaration) {<br>        is FirRegularClass,<br>        is FirSimpleFunction,<br>        is FirProperty -&gt;<br>            status.transform(modality = Modality.OPEN)<br><br>        else -&gt; status<br>    }<br>}</pre><p>now your transformer class is where you write the code to attach a modification to the compiler <br><strong>needTransformStatus()</strong> : returns wether we need to transform or not , for now we will transform only <strong>Classes</strong> , <strong>Functions</strong> , <strong>Properties</strong> <br>then<strong> transformStatus(): </strong>applies the transformation if declaration is one of them</p><p>then we add our Transform to Extensions <br>now we need to attach the AllOpenComponentRegistrar into our resources</p><p>so inside src/resources <br>create a <strong>file</strong> “org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar”</p><p>inside the file just declare the Registerar Qualified Name</p><pre>makeopen.AllOpenComponentRegistrar</pre><p>that’s all we created our Compiler Plugin , we now need to make use of it in our app , for that we will need to attach the Jar file to compiler args we will register a simple gradle Task that will do it for us in a separate module “build-logic”</p><p>let’s create An Extension for it</p><pre>abstract class KopenExtension @Inject constructor(<br> objects: ObjectFactory<br>) {<br> val kopenEnabled: Property&lt;Boolean&gt; = objects.property(Boolean::class.java).convention(false)<br>}</pre><p>then our Convention Plugin</p><pre>class KopenConventionPlugin : Plugin&lt;Project&gt; {<br>    override fun apply(target: Project) = with(target) {<br>        val extension = extensions.create(<br>            &quot;kopen&quot;,<br>            KopenExtension::class.java,<br>            objects<br>        )<br><br>        val enableKopenProperty = providers<br>            .gradleProperty(&quot;enableKopen&quot;)<br>            .map { it.toBoolean() }<br>            .orElse(false)<br><br>        extension.kopenEnabled.convention(enableKopenProperty)<br>        evaluationDependsOn(&quot;:make-open&quot;)<br>        val makeOpenJarTask = project(&quot;:make-open&quot;).tasks.named&lt;Jar&gt;(&quot;jar&quot;)<br>        val makeOpenJarFile = makeOpenJarTask.flatMap { it.archiveFile }<br>        tasks.withType&lt;KotlinCompile&gt;().configureEach {<br>            dependsOn(makeOpenJarTask)<br>            compilerOptions.freeCompilerArgs.addAll(<br>                extension.kopenEnabled.zip(makeOpenJarFile) { enabled, jarFile -&gt;<br>                    if (enabled) {<br>                        listOf(&quot;-Xplugin=${jarFile.asFile.absolutePath}&quot;)<br>                    } else {<br>                        emptyList()<br>                    }<br>                }<br>            )<br>        }<br>    }<br>}</pre><p>for simplicity we will pass the flag to toggle the plugin to work not from a gradle property and it’s false by default so for example during tests we can enable and run it like that</p><pre>./gradlew test -PenableKopen=true</pre><p>in the .kts file of your :convention just register the plugin</p><pre>gradlePlugin {<br>    plugins {<br>        register(&quot;kopenPlugin&quot;) {<br>            id = &quot;com.mohaberabi.kopen&quot;<br>            implementationClass = &quot;KopenConventionPlugin&quot;<br>        }<br>    }<br>}</pre><pre>plugins {<br>    alias(libs.plugins.kotlinMultiplatform)<br>    alias(libs.plugins.androidApplication)<br>    alias(libs.plugins.composeMultiplatform)<br>    alias(libs.plugins.composeCompiler)<br>    alias(libs.plugins.kopen.plugin)</pre><p>that’s all we are ready now to mock our classes during tests</p><pre>final class OrderDao {<br><br>    private val orders = MutableStateFlow&lt;List&lt;OrderEntity&gt;&gt;(emptyList())<br><br>    fun observeAll(): Flow&lt;List&lt;OrderEntity&gt;&gt; = orders<br><br>    suspend fun replaceAll(newOrders: List&lt;OrderEntity&gt;) {<br>        orders.value = newOrders<br>    }<br><br>    suspend fun clear() {<br>        orders.value = emptyList()<br>    }<br>}<br></pre><pre>final class OrderLocalDataSource(<br>    private val dao: OrderDao<br>) {<br><br>    fun observeOrders(): Flow&lt;List&lt;OrderEntity&gt;&gt; = dao.observeAll()<br><br>    suspend fun overwriteOrders(orders: List&lt;OrderEntity&gt;) {<br>        dao.replaceAll(orders)<br>    }<br><br>    suspend fun clear() {<br>        dao.clear()<br>    }<br>}</pre><pre>final class OrderRemoteDataSource(<br>    private val client: NetworkClient<br>) {<br><br>    suspend fun fetchOrders(): List&lt;OrderDto&gt; {<br>        return client.getOrders()<br>    }<br>}</pre><pre>final class NetworkClient(<br>    private val baseUrl: String<br>) {<br><br>    suspend fun getOrders(): List&lt;OrderDto&gt; {<br>        delay(5.seconds)<br>        return emptyList()<br>    }<br>}</pre><pre>final class OrderRepository(<br>    private val remote: OrderRemoteDataSource,<br>    private val local: OrderLocalDataSource,<br>    private val mapper: OrderMapper,<br>    private val ioDispatcher: CoroutineDispatcher<br>) {<br><br>    fun observeOrders(): Flow&lt;List&lt;Order&gt;&gt; {<br>        return local<br>            .observeOrders()<br>            .map { entities -&gt;<br>                entities.map { mapper.entityToDomain(it) }<br>            }<br>    }<br><br>    suspend fun sync(): Result&lt;Unit&gt; {<br>        return withContext(ioDispatcher) {<br>            runCatching {<br>                val remoteOrders = remote.fetchOrders()<br>                val now = 1000000L<br>                val entities = remoteOrders.map { mapper.dtoToEntity(it, now) }<br>                local.clear()<br>                local.overwriteOrders(entities)<br>            }<br>        }<br>    }<br>}</pre><pre>final class SyncLogger {<br><br>    private val _events = MutableSharedFlow&lt;String&gt;(replay = 0)<br>    val events: SharedFlow&lt;String&gt; = _events.asSharedFlow()<br><br>    suspend fun logSyncStarted() {<br>        _events.emit(&quot;sync_started&quot;)<br>    }<br><br>    suspend fun logSyncSuccess(count: Int) {<br>        _events.emit(&quot;sync_success_$count&quot;)<br>    }<br><br>    suspend fun logSyncFailure(message: String) {<br>        _events.emit(&quot;sync_failure_$message&quot;)<br>    }<br>}</pre><pre>sealed class OrdersUiState {<br>    data object Idle : OrdersUiState()<br>    data object Loading : OrdersUiState()<br>    data class Loaded(val orders: List&lt;Order&gt;) : OrdersUiState()<br>    data class Error(val message: String) : OrdersUiState()<br>}<br><br>final class OrdersViewModel(<br>    private val repository: OrderRepository,<br>    private val syncUseCase: SyncOrdersUseCase,<br>    private val dispatcher: CoroutineDispatcher,<br>    private val externalScope: CoroutineScope<br>) {<br><br>    private val _state = MutableStateFlow&lt;OrdersUiState&gt;(OrdersUiState.Idle)<br>    val state: StateFlow&lt;OrdersUiState&gt; = _state.asStateFlow()<br><br>    private val _isSyncing = MutableStateFlow(false)<br>    val isSyncing: StateFlow&lt;Boolean&gt; = _isSyncing.asStateFlow()<br><br>    init {<br>        observeOrders()<br>    }<br><br>    private fun observeOrders() {<br>        externalScope.launch(dispatcher) {<br>            repository.observeOrders().collect { orders -&gt;<br>                if (orders.isNotEmpty()) {<br>                    _state.value = OrdersUiState.Loaded(orders)<br>                } else if (_state.value is OrdersUiState.Idle) {<br>                    _state.value = OrdersUiState.Loading<br>                }<br>            }<br>        }<br>    }<br><br>    fun refresh() {<br>        if (_isSyncing.value) return<br>        _isSyncing.value = true<br><br>        externalScope.launch(dispatcher) {<br>            val result = syncUseCase.invoke()<br>            _isSyncing.value = false<br><br>            result.exceptionOrNull()?.let {<br>                _state.value = OrdersUiState.Error(it.message ?: &quot;unknown&quot;)<br>            }<br>        }<br>    }<br>}</pre><pre>class SyncOrdersUseCaseTest {<br><br>    private val repository = mock&lt;OrderRepository&gt;()<br>    private val logger = mock&lt;SyncLogger&gt;()<br><br>    private val useCase = SyncOrdersUseCase(<br>        repository = repository,<br>        logger = logger<br>    )<br><br>    @Test<br>    fun `execute logs start and success on successful sync`() = runTest {<br>        everySuspend { repository.sync() } returns Result.success(Unit)<br>        everySuspend { logger.logSyncStarted() } returns Unit<br>        everySuspend { logger.logSyncSuccess(any()) } returns Unit<br>        val result = useCase.invoke()<br>        assertTrue(result.isSuccess)<br>        verifySuspend { logger.logSyncStarted() }<br>        verifySuspend { repository.sync() }<br>        verifySuspend { logger.logSyncSuccess(any()) }<br>    }<br><br>    @Test<br>    fun `execute logs failure when repository sync fails`() = runTest {<br>        val error = RuntimeException(&quot;network&quot;)<br>        everySuspend { repository.sync() } returns Result.failure(error)<br>        everySuspend { logger.logSyncStarted() } returns Unit<br>        everySuspend { logger.logSyncFailure(any()) } returns Unit<br>        val result = useCase.invoke()<br>        assertTrue(result.isFailure)<br>        verifySuspend { logger.logSyncStarted() }<br>        verifySuspend { repository.sync() }<br>        verifySuspend { logger.logSyncFailure(&quot;network&quot;) }<br>    }<br>}</pre><pre>@OptIn(ExperimentalCoroutinesApi::class)<br>class OrdersViewModelTest {<br><br>    @Test<br>    fun `viewModel emits Loaded when repository returns orders`() = runTest {<br>        val dispatcher = UnconfinedTestDispatcher(testScheduler)<br>        val repository = mock&lt;OrderRepository&gt;()<br>        val syncUseCase = mock&lt;SyncOrdersUseCase&gt;()<br>        val orders = listOf(<br>            Order(id = &quot;1&quot;, total = 10.0, isSynced = true),<br>            Order(id = &quot;2&quot;, total = 20.0, isSynced = false)<br>        )<br>        val flow = MutableStateFlow(orders)<br>        every { repository.observeOrders() } returns flow<br>        everySuspend { syncUseCase.invoke() } returns Result.success(Unit)<br><br>        val vm = OrdersViewModel(<br>            repository = repository,<br>            syncUseCase = syncUseCase,<br>            dispatcher = dispatcher,<br>            externalScope = backgroundScope,<br>        )<br>        val state = vm.state.value<br>        assertTrue(state is OrdersUiState.Loaded)<br>        assertEquals(2, state.orders.size)<br>        verify { repository.observeOrders() }<br>    }<br><br>    @Test<br>    fun `refresh triggers sync and sets Error on failure`() = runTest {<br>        val dispatcher = UnconfinedTestDispatcher(testScheduler)<br>        val repository = mock&lt;OrderRepository&gt;()<br>        val syncUseCase = mock&lt;SyncOrdersUseCase&gt;()<br>        val emptyFlow = MutableStateFlow(emptyList&lt;Order&gt;())<br>        every { repository.observeOrders() } returns emptyFlow<br>        everySuspend { syncUseCase.invoke() } returns Result.failure(Exception(&quot;boom&quot;))<br>        val vm = OrdersViewModel(<br>            repository = repository,<br>            syncUseCase = syncUseCase,<br>            dispatcher = dispatcher,<br>            externalScope = backgroundScope,<br>        )<br><br>        vm.refresh()<br>        verifySuspend { syncUseCase.invoke() }<br>        val state = vm.state.value<br>        assertTrue(state is OrdersUiState.Error)<br>        assertEquals(&quot;boom&quot;, state.message)<br>    }<br>}</pre><pre>@OptIn(ExperimentalCoroutinesApi::class)<br>class OrderRepositoryTest {<br><br>    private val dispatcher = StandardTestDispatcher()<br>    private val testScope = TestScope(dispatcher)<br><br>    private val remote = mock&lt;OrderRemoteDataSource&gt;()<br>    private val local = mock&lt;OrderLocalDataSource&gt;()<br>    private val mapper = OrderMapper()<br><br>    @Test<br>    fun `sync should fetch remote orders and write to local`() = testScope.runTest {<br>        val dtoList = listOf(<br>            OrderDto(id = &quot;1&quot;, total = 100.0, status = &quot;NEW&quot;),<br>            OrderDto(id = &quot;2&quot;, total = 200.0, status = &quot;PAID&quot;)<br>        )<br>        everySuspend { remote.fetchOrders() } returns dtoList<br>        everySuspend { local.clear() } returns Unit<br>        everySuspend { local.overwriteOrders(any()) } returns Unit<br>        val repo = OrderRepository(<br>            remote = remote,<br>            local = local,<br>            mapper = mapper,<br>            ioDispatcher = dispatcher<br>        )<br>        val result = repo.sync()<br>        assertTrue(result.isSuccess)<br>        verifySuspend() { remote.fetchOrders() }<br>        verifySuspend() { local.clear() }<br>        verifySuspend() { local.overwriteOrders(any()) }<br>    }<br><br>    @Test<br>    fun `observeOrders should map entities to domain`() = testScope.runTest {<br>        val dao = OrderDao()<br>        val localReal = OrderLocalDataSource(dao)<br>        val repo = OrderRepository(<br>            remote = remote,<br>            local = localReal,<br>            mapper = mapper,<br>            ioDispatcher = dispatcher<br>        )<br><br>        val entities = listOf(<br>            OrderEntity(&quot;1&quot;, 10.0, &quot;NEW&quot;, syncedAtMillis = null),<br>            OrderEntity(&quot;2&quot;, 20.0, &quot;NEW&quot;, syncedAtMillis = 1234L)<br>        )<br>        dao.replaceAll(entities)<br>        val result = repo.observeOrders().first()<br>        assertEquals(2, result.size)<br>        assertEquals(false, result[0].isSynced)<br>        assertEquals(true, result[1].isSynced)<br>    }<br>}</pre><p>now run</p><pre>./gradlew test -PenableKopen=true</pre><p>source code : <a href="https://github.com/mohaberabi/kopen">https://github.com/mohaberabi/kopen</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cc4652b83850" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How I Built Network Inspector Tool For Kotlin Multiplatform ( IOS & Android )]]></title>
            <link>https://medium.com/@mohaberabi98/how-i-built-network-inspector-tool-for-kotlin-multiplatform-ios-android-a618bba708c0?source=rss-725d0fa3cda9------2</link>
            <guid isPermaLink="false">https://medium.com/p/a618bba708c0</guid>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[compose-multiplatform]]></category>
            <dc:creator><![CDATA[Mohab erabi]]></dc:creator>
            <pubDate>Sat, 29 Nov 2025 15:02:50 GMT</pubDate>
            <atom:updated>2025-11-29T15:02:50.803Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*f3IEgtxp39xTcDWq__TT6A.png" /></figure><p>Logging http calls is one of the most needed essential setup during your application development , i really always use Chucker ( Android ) but i wanted to see how it might be if i wanted the same experience on IOS , but since my background is not that enough to do it in IOS i tried to build it in Kotlin MultiPlatform , and here is the thing it was really a great and amazing expereince</p><p>So I built <strong>Inspekt</strong> — a fully Kotlin Multiplatform network inspector:</p><ul><li>Works with <strong>Ktor HTTP client</strong></li><li>Logs <strong>requests + responses</strong></li><li>Pretty-prints JSON</li><li>Handles <strong>binary bodies</strong></li><li>Saves logs using <strong>Room on Android + Room KMP on iOS</strong></li><li>Sends <strong>local notifications</strong> on both platforms</li><li>Adds <strong>app shortcuts</strong></li><li>Opens a UI overlay on iOS using <strong>pure Kotlin/Native</strong></li><li>Shows a full UI built with <strong>Compose Multiplatform</strong></li><li>0 lines of Swift</li><li>0 lines of Objective-C</li></ul><h4><strong>A full Compose Multiplatform screen showing:</strong></h4><ul><li>Requests</li><li>Responses</li><li>Headers</li><li>Status codes</li><li>Body pretty-printing</li><li>Metrics</li><li>Colors</li><li>Durations</li><li>Size</li></ul><h3>Notifications</h3><p>Every request triggers a local notification with:</p><ul><li>URL</li><li>Status</li><li>Tap → open Inspekt screen</li></ul><h3>Home-screen shortcuts</h3><p>Tap “Open Inspekt” → jump directly to the inspector.</p><h3>Full Ktor plugin</h3><p>Works exactly like Chucker but cross-platform.</p><p>Below is our data that will be saved inside Room Database</p><pre>@Serializable<br>enum class InspektStatus {<br> RequestSent,<br> Completed,<br> Unknown,<br> Failure,<br>}<br>@Serializable<br>data class HttpLogEntry(<br> @SerialName(&quot;id&quot;) val id: String,<br> @SerialName(&quot;timestamp_millis&quot;) val timestampMillis: Long,<br> @SerialName(&quot;response_millis&quot;) val responseTimeStampMillis: Long? = null,<br> @SerialName(&quot;method&quot;) val method: String,<br> @SerialName(&quot;url&quot;) val url: String,<br> @SerialName(&quot;status_code&quot;) val statusCode: Int? = null,<br> @SerialName(&quot;request_headers&quot;) val requestHeaders: Map&lt;String, String&gt;,<br> @SerialName(&quot;response_headers&quot;) val responseHeaders: Map&lt;String, String&gt;,<br> @SerialName(&quot;request_body&quot;) val requestBody: String? = null,<br> @SerialName(&quot;response_body&quot;) val responseBody: String? = null,<br> @SerialName(&quot;duration_millis&quot;) val durationMillis: Long?,<br> @SerialName(&quot;error&quot;) val error: String? = null,<br> @SerialName(&quot;request_size_bytes&quot;) val requestSizeBytes: Long = 0L,<br> @SerialName(&quot;response_size_bytes&quot;) val responseSizeBytes: Long? = null,<br> @SerialName(&quot;protocol&quot;) val protocol: String? = null,<br> @SerialName(&quot;ssl&quot;) val ssl: Boolean? = null,<br> @SerialName(&quot;inspekt_status&quot;) val inspektStatus: InspektStatus = InspektStatus.Unknown,<br> @SerialName(&quot;host&quot;) val host: String? = null,<br> @SerialName(&quot;path&quot;) val endPoint: String? = null,<br> @SerialName(&quot;content_type&quot;) val contentType: String? = null,<br> @SerialName(&quot;accept_charset&quot;) val acceptCharset: String? = null,<br> @SerialName(&quot;content_length&quot;) val contentLength: Long? = null,<br>)</pre><pre>fun MainViewController() = ComposeUIViewController(<br>    configure = {<br>        val strategy = InspektConfigStrategy.Actual(<br>            config = InspektConfig(),<br>            notificationEnabled = true,<br>            shortcutEnabled = true,<br>            maxPersistenceEntry = 50<br>        )<br>        GlobalInspekt.configure(<br>            strategy = strategy<br>        )<br>        requestNotificationPermission()<br>        setupDelegates()<br>    }<br>) { App() }</pre><pre>class InspektApplication : Application() {<br>    override fun onCreate() {<br>        super.onCreate()<br>        GlobalInspekt.configure(<br>            strategy = InspektConfigStrategy.Actual(<br>                config = InspektConfig(this),<br>                notificationEnabled = true,<br>                shortcutEnabled = true,<br>                maxPersistenceEntry = 10<br>            )<br>        )<br>    }<br>}</pre><h3>Building the Ktor Plugin: Capturing Requests, Responses &amp; Bodies Without Breaking the Pipeline</h3><pre>fun InspektPlugin() = createClientPlugin(<br>    name = NAME,<br>    createConfiguration = { ... }<br>) {<br>    onRequest { request, body -&gt; ... }<br><br>    client.receivePipeline.intercept(HttpReceivePipeline.After) { response -&gt; ... }<br>}</pre><h3>Request Pipeline</h3><p>Called <em>before</em> the HTTP call is executed.</p><p>Best place to extract:</p><ul><li>URL</li><li>Path</li><li>Query</li><li>Headers</li><li>Method</li><li>Request body</li></ul><h3>Receive Pipeline</h3><p>Called <em>after</em> receiving the raw response bytes, BEFORE the user gets them.</p><p>This is where we capture:</p><ul><li>Response headers</li><li>Body</li><li>Status code</li><li>Duration</li></ul><p>And MOST importantly:</p><p>We <strong>clone</strong> the response body so the user can still call body&lt;T&gt;() without error.</p><h3>Logging Requests</h3><pre>internal fun ClientPluginBuilder&lt;InspektConfig&gt;.onInspektRequest() {<br>    val inspekt = pluginConfig.inspekt<br>    val scope = pluginConfig.scope<br>    val requestDecoder = pluginConfig.requestBodyDecoder<br><br>    onRequest { request, _ -&gt;<br>        val time = getTimeMillis()<br>        val callId = generateCallId(time = time, value = request.hashCode())<br><br>        val entry = request.toLogEntry(callId = callId, millis = time)<br><br>        val rawBytes = request.body.extractRawContent()<br><br>        val decodedBody: String = requestDecoder<br>            .decode(entry, rawBytes)<br>            ?: rawBytes.decodeReadable(request.headers.extractContentType())<br><br>        val newEntry = entry.copy(requestBody = decodedBody)<br><br>        request.attributes.put(CallIdKey, callId)<br>        request.attributes.put(CallStartMillisKey, time)<br>        request.attributes.put(EntryKey, entry)<br><br>        scope.launch {<br>            inspekt.logCall(newEntry)<br>        }<br>    }<br>}</pre><pre>internal fun Any?.extractRawContent() = when (this) {<br>    null -&gt; byteArrayOf()<br>    is String -&gt; encodeToByteArray()<br>    is ByteArray -&gt; this<br>    is OutgoingContent.ByteArrayContent -&gt; this.bytes()<br>    is OutgoingContent.ReadChannelContent -&gt; &quot;&lt;streaming body&gt;&quot;.encodeToByteArray()<br>    else -&gt; this.toString().encodeToByteArray()<br>}</pre><pre>val decodedBody = requestDecoder.decode(entry, rawBytes)<br>    ?: rawBytes.decodeReadable(request.headers.extractContentType())</pre><h3>Logging Responses — The Hard Part</h3><p>Capture the body</p><p>Clone it</p><p>Preserve original behavior</p><p>Pretty-print if JSON</p><p>Decode custom formats</p><pre>internal fun HttpClient.inspektReceive(<br>    inspekt: Inspekt,<br>    scope: CoroutineScope,<br>    responseDecoder: InspektBodyDecoder,<br>) {<br>    receivePipeline.intercept(HttpReceivePipeline.After) { response -&gt;<br><br>        val start = response.request.attributes.getOrNull(CallStartMillisKey) ?: getTimeMillis()<br><br>        val call = response.call<br>        val channel = response.bodyAsChannel()<br><br>        val bytes = channel.readRemaining().readByteArray()<br><br>        val newCall = call.replaceResponse {<br>            ByteReadChannel(bytes)<br>        }<br><br>        val now = getTimeMillis()<br>        val duration = now - start<br><br>        val entry = call.attributes.getOrNull(EntryKey) ?: return@intercept<br><br>        val completedEntry = entry.copy(<br>            durationMillis = duration,<br>            responseHeaders = response.headers.entries()<br>                .associate { it.key to it.value.joinToString() },<br>            statusCode = response.status.value,<br>            responseSizeBytes = bytes.size.toLong(),<br>            inspektStatus = InspektStatus.Completed<br>        )<br><br>        val rawBytes = bytes.extractRawContent()<br><br>        val decodedBody: String = responseDecoder<br>            .decode(entry, rawBytes)<br>            ?: rawBytes.decodeReadable(entry.contentType)<br><br>        val newEntry = completedEntry.copy(responseBody = decodedBody)<br><br>        scope.launch { inspekt.logCall(newEntry) }<br><br>        proceedWith(newCall.response)<br>    }<br>}</pre><p><strong>bodyAsChannel()</strong></p><p>This gives you access to the <em>raw bytes</em> of the server response.</p><p>But reading it <strong>consumes it</strong>.</p><p>So after reading:</p><pre>val bytes = channel.readRemaining().readByteArray()</pre><p>You cannot return this channel to the user.</p><p><strong>Rebuilding the response</strong></p><pre>val newCall = call.replaceResponse {<br>    ByteReadChannel(bytes)<br>}</pre><p>We:</p><ol><li>Read bytes once</li><li>Rebuild a new response body</li><li>Send the new cloned body back to Ktor</li></ol><p>This ensures:</p><p>User can still call .body&lt;T&gt;()</p><p>Inspekt gets a full copy</p><p>No crashes</p><p>No double read</p><h3>Notifications, Shortcuts &amp; a Floating Overlay Window…</h3><p><strong>On Android:</strong></p><ul><li>We can launch new activities</li><li>We can inject activities via manifest</li><li>We can overlay windows</li><li>We can create shortcuts dynamically</li><li>We can intercept notification taps easily</li><li>We can use PendingIntent</li></ul><p><strong>On iOS:</strong></p><ul><li>No Activities</li><li>No deep dynamic window creation</li><li>Only ONE UIApplicationDelegate allowed</li><li>Notifications need a UNUserNotificationCenterDelegate</li><li>Shortcuts need a UIApplicationDelegate callback</li><li>UIKit windows cannot be manually inserted incorrectly</li><li>App extensions cannot be shipped inside libraries</li></ul><p>so on ios we will need to hock into the delegates without overriding them so i decided to give user a callback to invoke them inside their delegates</p><pre>class IosShortcutDelegate(<br>    private val onItemOpenedApp: (item: UIApplicationShortcutItem) -&gt; Unit = {},<br>) : UIResponder(), UIApplicationDelegateProtocol {<br><br>    override fun application(<br>        application: UIApplication,<br>        performActionForShortcutItem: UIApplicationShortcutItem,<br>        completionHandler: (Boolean) -&gt; Unit<br>    ) {<br>        onItemOpenedApp(performActionForShortcutItem)<br>    }<br>}</pre><pre>class InspektNotificationDelegate(<br>    private val onReceivedNotification: (UNNotification) -&gt; Unit = {},<br>    private val onNotificationClicked: (UNNotification) -&gt; Unit = {}<br>) : NSObject(), UNUserNotificationCenterDelegateProtocol {<br><br>    override fun userNotificationCenter(<br>        center: UNUserNotificationCenter,<br>        didReceiveNotificationResponse: UNNotificationResponse,<br>        withCompletionHandler: () -&gt; Unit<br>    ) {<br>        onNotificationClicked(didReceiveNotificationResponse.notification)<br>        withCompletionHandler()<br>    }<br>}</pre><pre>class IosNotificationManager : InspektNotificationManager {<br><br>    override fun showLocalNotification(<br>        title: String,<br>        body: String,<br>        id: Int,<br>        config: NotificationConfig<br>    ) = dispatch_async(queue = dispatch_get_main_queue()) {<br>        val content = UNMutableNotificationContent()<br>            .apply {<br>                setTitle(title)<br>                setBody(body)<br>                setSound(UNNotificationSound.defaultSound())<br>                setUserInfo(config.userInfo)<br>            }<br><br>        val triggerTime = UNTimeIntervalNotificationTrigger.triggerWithTimeInterval(<br>            timeInterval = 5.0,<br>            repeats = false<br>        )<br><br>        val request = UNNotificationRequest.requestWithIdentifier(<br>            identifier = &quot;$id&quot;,<br>            content = content,<br>            trigger = triggerTime,<br><br>            )<br>        UNUserNotificationCenter.currentNotificationCenter()<br>            .addNotificationRequest(request = request)<br>            { error -&gt;<br>                println(&quot;IosNotificationManager---${error}&quot;)<br>            }<br>    }<br>}</pre><pre>actual class NotificationConfig(<br>    val userInfo: Map&lt;Any?, Any?&gt; = mapOf()<br>)</pre><p>on Android it’s really simple you just add your pending intent inside the notification and that’s it no delegates</p><pre>class AndroidInspektNotificationManager(<br>    private val context: Context<br>) : InspektNotificationManager {<br>    companion object {<br>        const val CHANNEL_ID = &quot;Inspekt&quot;<br>    }<br><br><br>    private val notificationManager by lazy { requireNotNull(context.getSystemService&lt;NotificationManager&gt;()); }<br>    override fun showLocalNotification(<br>        title: String,<br>        body: String,<br>        id: Int,<br>        config: NotificationConfig<br>    ) {<br><br>        val builder = NotificationCompat.Builder(<br>            context,<br>            CHANNEL_ID<br>        )<br>        val notification = builder<br>            .setContentTitle(title)<br>            .setContentText(body)<br>            .setChannelId(CHANNEL_ID)<br>            .setSmallIcon(R.drawable.ic_launcher_foreground)<br>            .setContentIntent(config.contentPendingIntent)<br>            .build()<br>        notificationManager.notify(id, notification)<br>    }<br><br>    fun initChannel() {<br>        if (Build.VERSION.SDK_INT &gt;= 33) {<br>            runCatching {<br>                val channel = NotificationChannel(<br>                    CHANNEL_ID,<br>                    CHANNEL_ID,<br>                    NotificationManager.IMPORTANCE_DEFAULT<br>                )<br>                notificationManager.createNotificationChannel(channel)<br>            }<br>        }<br><br>    }<br>}</pre><pre>actual class NotificationConfig(<br>    val contentPendingIntent: PendingIntent<br>)</pre><p><strong>Now Shortcuts to open the logging screen</strong></p><pre>interface AppShortcutManager {<br>    fun addShortcut(shortcut: AppShortcut)<br>    fun removeShortcutById(id: String)<br>}</pre><pre>expect class AppShortcutConfig</pre><pre>data class AppShortcut(<br>    val id: String,<br>    val longName: String,<br>    val shortName: String,<br>    val config: AppShortcutConfig<br>)</pre><p>on Android and IOS They are almost exactly the same , only the difference that you receive the intention of clicking the shortcut via Delegate in IOS</p><p>Android</p><pre>class AndroidShortcutManager(<br>    private val context: Context,<br>) : AppShortcutManager {<br>    override fun addShortcut(shortcut: AppShortcut) {<br>        val icon = shortcut.config.icon<br>        val intent = shortcut.config.intent<br>        val shortcut = ShortcutInfoCompat.Builder(context, shortcut.id)<br>            .setShortLabel(shortcut.shortName)<br>            .setLongLabel(shortcut.longName)<br>            .setIcon(icon)<br>            .setIntent(intent)<br>            .build()<br>        ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)<br>    }<br><br>    override fun removeShortcutById(id: String) {<br>        ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(id))<br>    }</pre><pre>actual class AppShortcutConfig(<br>    val intent: Intent,<br>    val icon: IconCompat<br>)</pre><p>IOS</p><pre>class IosShortcutManager() : AppShortcutManager {<br>    override fun addShortcut(shortcut: AppShortcut) {<br>        val app = UIApplication.sharedApplication<br>        val shortcuts = app.shortcutItems.orEmpty()<br>        val exists = shortcuts.any {<br>            val item = it as? UIApplicationShortcutItem ?: return@any false<br>            if (item.type == shortcut.id) return@any true<br>            false<br>        }<br>        if (exists) return<br>        val newShortcuts = mutableListOf&lt;Any?&gt;()<br>        newShortcuts.addAll(shortcuts)<br>        val shortcut = UIApplicationShortcutItem(<br>            type = shortcut.id,<br>            localizedTitle = shortcut.shortName,<br>            localizedSubtitle = shortcut.longName,<br>            userInfo = shortcut.config.userInfo,<br>            icon = shortcut.config.icon,<br>        )<br>        newShortcuts.add(shortcut)<br>        app.shortcutItems = newShortcuts<br>    }<br><br>    override fun removeShortcutById(id: String) {<br>        val app = UIApplication.sharedApplication<br>        if (app.shortcutItems.isNullOrEmpty()) return<br>        val shortcuts = app.shortcutItems.orEmpty().filter {<br>            if (it is UIApplicationShortcutItem) {<br>                it.type != id<br>            } else {<br>                true<br>            }<br>        }<br>        app.shortcutItems = shortcuts<br>    }<br>}</pre><pre>actual class AppShortcutConfig(<br>    val userInfo: Map&lt;Any?, Any?&gt;,<br>    val icon: UIApplicationShortcutIcon,<br>)</pre><p>as we already know it’s very easy now to use the Room Jetpack from Android inside IOS As Well , so we will have a simple DAO as for now</p><pre>@Dao<br>interface HttpLogDao {<br><br>    @Upsert<br>    suspend fun upsert(entity: HttpLogEntity)<br><br>    @Query(&quot;SELECT COUNT(*) FROM http_log&quot;)<br>    suspend fun count(): Long<br><br><br>    @Query(&quot;DELETE FROM http_log&quot;)<br>    suspend fun clear()<br><br>    @Query(&quot;SELECT * FROM http_log ORDER BY timeStamp DESC&quot;)<br>    fun getAll(): Flow&lt;List&lt;HttpLogEntity&gt;&gt;<br>}</pre><p>and that’s it we created now our interfaces that our tool will be using</p><p>let’s create our actual library Facade in :commonMain</p><pre>expect class InspektConfig {}</pre><pre>sealed interface InspektConfigStrategy {<br>    data object NoOperation : InspektConfigStrategy<br>    data class Actual(<br>        val config: InspektConfig,<br>        val notificationEnabled: Boolean = true,<br>        val shortcutEnabled: Boolean = true,<br>        val maxPersistenceEntry: Long = 100L<br>    ) : InspektConfigStrategy<br>}</pre><pre>internal const val INSPEKT = &quot;inspekt&quot;<br>internal const val SHORTCUT_TITLE = &quot;Open Inspekt&quot;<br>internal const val SHORTCUT_SUBTITLE = &quot;to inspect and log your http calls&quot;<br><br>internal const val DB_NAME = &quot;${INSPEKT}.db&quot;<br>internal fun inspektNotInitializedError(): Nothing =<br>    error(&quot;GlobalInspekt instance is not initialized u forgot to call configure or configureNonImpl&quot;)<br><br>@Suppress(&quot;EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING&quot;)<br>expect class GlobalInspekt {<br>    companion object {<br>        val sharedInstance: Inspekt<br>        fun configure(strategy: InspektConfigStrategy)<br><br>    }<br><br>}<br><br></pre><pre>interface Inspekt {<br><br>    suspend fun logCall(entry: HttpLogEntry)<br><br>    fun observeEntries(): Flow&lt;List&lt;HttpLogEntry&gt;&gt;<br>}</pre><pre>internal class InspektImpl(<br>    private val strategy: InspektConfigStrategy.Actual,<br>    private val notificationManager: InspektNotificationManager,<br>    private val dao: HttpLogDao,<br>    private val notificationConfigProvider: NotificationConfigProvider<br>) : Inspekt {<br>    private val mutex = Mutex()<br>    override suspend fun logCall(<br>        entry: HttpLogEntry,<br>    ) = supervisorScope {<br>        mutex.withLock {<br>            val jobs = buildList {<br>                addIf(strategy.notificationEnabled) { sendNotification(entry) }<br>                add(upsert(entry))<br>            }<br>            jobs.joinAll()<br>        }<br>    }<br><br>    private fun CoroutineScope.upsert(<br>        entry: HttpLogEntry,<br>    ) = launch {<br>        val count = dao.count()<br>        if (count &gt;= strategy.maxPersistenceEntry) {<br>            dao.clear()<br>        }<br>        dao.upsert(HttpLogEntity(entry))<br>    }<br><br>    private fun CoroutineScope.sendNotification(<br>        entry: HttpLogEntry,<br>    ) = launch {<br>        val status = entry.statusCode?.let { &quot;$it&quot; } ?: &quot;&quot;<br>        notificationManager.showLocalNotification(<br>            title = entry.url,<br>            body = &quot;Logging the call $status&quot;,<br>            id = entry.id.hashCode(),<br>            config = notificationConfigProvider.provideInspektNotificationConfig()<br>        )<br>    }<br><br>    override fun observeEntries(): Flow&lt;List&lt;HttpLogEntry&gt;&gt; {<br>        return dao.getAll().map { list -&gt; list.map { it.log } }<br>    }<br><br>    private inline fun &lt;reified T&gt; MutableList&lt;T&gt;.addIf(<br>        predicate: Boolean,<br>        block: () -&gt; T<br>    ) {<br>        if (predicate) {<br>            add(block())<br>        }<br>    }<br>}</pre><pre>internal expect class NotificationConfigProvider {<br><br>    fun provideInspektNotificationConfig(): NotificationConfig<br>}</pre><pre>internal expect class ShortcutConfigProvider {<br><br>    fun provideInspektShortcutConfig(): AppShortcutConfig<br>}</pre><p>Android Side :</p><pre>actual class GlobalInspekt {<br>    actual companion object {<br>        private var androidInstance: Inspekt? = null<br><br>        actual val sharedInstance: Inspekt<br>            get() = androidInstance ?: inspektNotInitializedError()<br><br>        actual fun configure(<br>            strategy: InspektConfigStrategy<br>        ) {<br>            when (strategy) {<br>                is InspektConfigStrategy.Actual -&gt; configureActualImpl(strategy)<br>                InspektConfigStrategy.NoOperation -&gt; configureNoOperation()<br>            }<br>        }<br><br>        private fun configureActualImpl(<br>            strategy: InspektConfigStrategy.Actual,<br>        ) {<br>            if (androidInstance != null) return<br>            val config = strategy.config<br>            val context = config.context<br>            val notificationManager = AndroidInspektNotificationManager(context = context)<br>                .apply { initChannel() }<br>            val builder = RoomDatabaseBuilder(context)<br>            val database = builder.create&lt;InspektDatabase&gt;(DB_NAME).build()<br>            val dao = database.httpLodDao()<br>            val notificationConfigProvider =<br>                NotificationConfigProvider(context)<br>            val inspekt = InspektImpl(<br>                notificationManager = notificationManager,<br>                dao = dao,<br>                notificationConfigProvider = notificationConfigProvider,<br>                strategy = strategy<br>            )<br>            configureShortcut(<br>                context = context,<br>                shortcutEnabled = strategy.shortcutEnabled<br>            )<br>            androidInstance = inspekt<br>        }<br><br>        private fun configureShortcut(<br>            context: Context,<br>            shortcutEnabled: Boolean,<br>        ) {<br>            runCatching {<br>                val manager = AndroidShortcutManager(context)<br>                if (shortcutEnabled) {<br>                    val configProvider = ShortcutConfigProvider(context)<br>                    val config = configProvider.provideInspektShortcutConfig()<br>                    val shortcut = AppShortcut(<br>                        id = INSPEKT,<br>                        longName = SHORTCUT_TITLE,<br>                        shortName = SHORTCUT_SUBTITLE,<br>                        config = config<br>                    )<br>                    manager.addShortcut(shortcut)<br>                } else {<br>                    manager.removeShortcutById(INSPEKT)<br>                }<br><br>            }<br><br>        }<br><br>        private fun configureNoOperation() {<br>            androidInstance = EmptyInspekt<br>        }<br>    }<br><br><br>}</pre><pre>internal actual class NotificationConfigProvider(<br>    private val context: Context<br>) {<br>    actual fun provideInspektNotificationConfig(): NotificationConfig {<br>        val intent = Intent(context, InspektActivity::class.java)<br>            .applyInspekt(context.getAppName())<br>        val pendingIntent = PendingIntent.getActivity(<br>            context,<br>            201098,<br>            intent,<br>            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT<br>        )<br>        return NotificationConfig(contentPendingIntent = pendingIntent)<br>    }<br>}</pre><pre>internal actual class ShortcutConfigProvider(<br>    private val context: Context<br>) {<br>    actual fun provideInspektShortcutConfig(): AppShortcutConfig {<br>        val intent = Intent(context, InspektActivity::class.java)<br>            .applyInspekt(context.getAppName())<br>        return AppShortcutConfig(<br>            intent = intent,<br>            icon = IconCompat.createWithResource(<br>                context,<br>                R.drawable.ic_launcher_foreground<br>            )<br>        )<br>    }<br>}</pre><p>some utils used inside android side</p><pre>internal fun Context.getAppName(): String {<br>    val labelRes = applicationInfo.labelRes<br>    return if (labelRes != 0) {<br>        getString(labelRes)<br>    } else {<br>        applicationInfo.nonLocalizedLabel.toString()<br>    }<br>}<br><br><br>internal fun Intent.applyInspekt(appName: String) = apply {<br>    action = Intent.ACTION_VIEW<br>    flags += Intent.FLAG_ACTIVITY_NEW_TASK<br>    putExtra(&quot;root_launcher&quot;, appName)<br>}</pre><p>IOS Side :</p><pre>actual class GlobalInspekt {<br>    actual companion object {<br><br>        private var iosInstance: Inspekt? = null<br><br>        private val inspektViewController by lazy {<br>            InspektViewController(<br>                onDismiss = InspektViewControllerPresenter::dismiss,<br>            )<br>        }<br><br>        actual val sharedInstance: Inspekt<br>            get() = iosInstance ?: inspektNotInitializedError()<br><br>        actual fun configure(<br>            strategy: InspektConfigStrategy<br>        ) {<br>            when (strategy) {<br>                is InspektConfigStrategy.Actual -&gt; configureActual(strategy)<br>                InspektConfigStrategy.NoOperation -&gt; configureNoOperation()<br>            }<br>        }<br><br>        private fun configureActual(<br>            strategy: InspektConfigStrategy.Actual,<br>        ) {<br>            if (iosInstance != null) return<br>            val dao = setupDatabase()<br>            val inspekt = InspektImpl(<br>                notificationManager = IosNotificationManager(),<br>                dao = dao,<br>                notificationConfigProvider = NotificationConfigProvider(),<br>                strategy = strategy<br>            )<br>            setupShortcut(shortcutEnabled = strategy.shortcutEnabled)<br>            iosInstance = inspekt<br>        }<br><br>        private fun configureNoOperation() {<br>            iosInstance = EmptyInspekt<br>        }<br><br>        private fun setupDatabase(): HttpLogDao {<br>            val builder = RoomDatabaseBuilder()<br>            val database = builder.create&lt;InspektDatabase&gt;(DB_NAME).build()<br>            val dao = database.httpLodDao()<br>            return dao<br>        }<br><br>        private fun setupShortcut(<br>            shortcutEnabled: Boolean,<br>        ) {<br>            runCatching {<br>                val shortcutManager = IosShortcutManager()<br>                if (shortcutEnabled) {<br>                    val shortCutConfigProvider = ShortcutConfigProvider()<br>                    val shortCut = AppShortcut(<br>                        id = INSPEKT,<br>                        longName = SHORTCUT_SUBTITLE,<br>                        shortName = SHORTCUT_TITLE,<br>                        config = shortCutConfigProvider.provideInspektShortcutConfig()<br>                    )<br>                    shortcutManager.addShortcut(shortCut)<br>                } else {<br>                    shortcutManager.removeShortcutById(INSPEKT)<br>                }<br>            }<br><br>        }<br><br>        fun shortcutOpenedApp(item: UIApplicationShortcutItem) {<br>            if (item.type == INSPEKT) {<br>                openInspektView()<br>            }<br>        }<br><br><br>        fun notificationClicked(notification: UNNotification) {<br>            val data = notification.request.content.userInfo<br>            if (data[INSPEKT] != null) {<br>                openInspektView()<br>            }<br>        }<br><br>        private fun openInspektView() {<br>            InspektViewControllerPresenter.show(inspektViewController)<br>        }<br>    }<br><br><br>}</pre><pre>internal actual class NotificationConfigProvider {<br>    actual fun provideInspektNotificationConfig(): NotificationConfig {<br>        return NotificationConfig(<br>            userInfo = mapOf(&quot;inspekt&quot; to true)<br>        )<br>    }<br>}</pre><pre>internal actual class ShortcutConfigProvider {<br>    actual fun provideInspektShortcutConfig(): AppShortcutConfig {<br>        return AppShortcutConfig(<br>            userInfo = mapOf(),<br>            icon = UIApplicationShortcutIcon.iconWithSystemImageName(&quot;paperplane.fill&quot;)<br>        )<br>    }<br>}</pre><p>that’s all we just now need to create some client app and install our plugin inside it’s Ktor Client then inspekt the calls</p><pre>@Serializable<br>data class PostDto(<br>    val id: Int,<br>    val title: String,<br>    val content: String<br>)<br><br>@Composable<br>fun App() {<br>    MaterialTheme { HomeScreen(); }<br>}<br><br><br>@Composable<br>fun HomeScreen() {<br>    val scope = rememberCoroutineScope()<br>    Scaffold { padding -&gt;<br>        Column(<br>            modifier = Modifier.fillMaxSize()<br>                .padding(padding)<br>                .padding(20.dp),<br>            horizontalAlignment = Alignment.CenterHorizontally,<br>            verticalArrangement = Arrangement.Center<br>        ) {<br>            Text(&quot;My Client App&quot;)<br>            Button(<br>                onClick = {<br>                    scope.launch {<br>                        val jobs = (1..10).map {<br>                            scope.launch {<br>                                val client = httpClient()<br>                                val post: PostDto =<br>                                    client.get(&quot;https://jsonplaceholder.org/posts/1&quot;).body()<br>                            }<br>                        }.joinAll()<br>                    }<br><br>                },<br>            ) {<br>                Text(&quot;Make 10 http call&quot;)<br>            }<br><br>        }<br>    }<br>}</pre><pre>fun httpClient(<br>) = HttpClient(createEngine()) {<br><br>    install(Logging) {<br>        logger = object : Logger {<br>            override fun log(message: String) {<br>                println(message)<br>            }<br>        }<br>    }<br>    install(ContentNegotiation) {<br>        json(<br>            json = Json {<br>                encodeDefaults = true<br>                explicitNulls = false<br>                ignoreUnknownKeys = true<br>            },<br>        )<br>    }<br>    install(InspektPlugin())<br><br>    defaultRequest {<br>        accept(ContentType.Application.Json)<br>    }<br>}<br><br><br>expect fun createEngine(): HttpClientEngine</pre><p>Now Let’s Initialize Our Library</p><pre>fun MainViewController() = ComposeUIViewController(<br>    configure = {<br>        val strategy = InspektConfigStrategy.Actual(<br>            config = InspektConfig(),<br>            notificationEnabled = true,<br>            shortcutEnabled = true,<br>            maxPersistenceEntry = 50<br>        )<br>        GlobalInspekt.configure(<br>            strategy = strategy<br>        )<br>        requestNotificationPermission()<br>        setupDelegates()<br>    }<br>) { App() }<br><br>private val rootAppDelegate = RootAppDelegate(<br>    onItemOpenedApp = GlobalInspekt::shortcutOpenedApp<br>)<br><br>private val notificationDelegate = UNNotificationDelegate(<br>    onNotificationClicked = GlobalInspekt::notificationClicked<br>)<br><br><br>private fun setupDelegates() {<br>    UNUserNotificationCenter.currentNotificationCenter().delegate = notificationDelegate<br>    UIApplication.sharedApplication.setDelegate(rootAppDelegate)<br>}<br><br>fun requestNotificationPermission() {<br>    val center = UNUserNotificationCenter.currentNotificationCenter()<br>    center.requestAuthorizationWithOptions(<br>        options = UNAuthorizationOptionAlert or<br>                UNAuthorizationOptionSound or<br>                UNAuthorizationOptionBadge<br>    ) { granted, _ -&gt; ; }<br>}</pre><pre>class InspektApplication : Application() {<br>    override fun onCreate() {<br>        super.onCreate()<br>        GlobalInspekt.configure(<br>            strategy = InspektConfigStrategy.Actual(<br>                config = InspektConfig(this),<br>                notificationEnabled = true,<br>                shortcutEnabled = true,<br>                maxPersistenceEntry = 10<br>            )<br>        )<br>    }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tVYOEbgOYTYWvcPLSKpvug.jpeg" /></figure><p>Source Code : <a href="https://github.com/mohaberabi/inspekt">https://github.com/mohaberabi/inspekt</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a618bba708c0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[One Source of Truth: Managing Android & iOS Versions — Flavors with a Kotlin Multiplatform Gradle…]]></title>
            <link>https://medium.com/@mohaberabi98/one-source-of-truth-managing-android-ios-versions-flavors-with-a-kotlin-multiplatform-gradle-a252832e41bd?source=rss-725d0fa3cda9------2</link>
            <guid isPermaLink="false">https://medium.com/p/a252832e41bd</guid>
            <category><![CDATA[android]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[compose-multiplatform]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[kotlin]]></category>
            <dc:creator><![CDATA[Mohab erabi]]></dc:creator>
            <pubDate>Fri, 21 Nov 2025 21:47:09 GMT</pubDate>
            <atom:updated>2025-11-21T21:47:09.095Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*hKbAPoZOQYKKBXf4ltnlaQ.png" /></figure><h3>One Source of Truth: Managing Android &amp; iOS Versions — Flavors with a Kotlin Multiplatform Gradle Plugin</h3><p>Ever found yourself hardcoding version codes, build types, or bundle identifiers across your Kotlin Multiplatform project? Wished you could access Android’s BuildConfig values in your iOS code, or vice versa? Meet <strong>Kommon Versions</strong> — a Gradle convention plugin that generates platform-specific build configuration that&#39;s accessible from shared code.</p><p>In a typical KMP project, managing build configurations is fragmented. Android has BuildConfig, iOS relies on Info.plist and xcconfig files, and your shared code is left in the dark. You end up with:</p><ul><li>Duplicated version strings across platform-specific files</li><li>No type-safe way to access build metadata in common code</li><li>Manual synchronization nightmares when updating versions or flavors</li></ul><p>Kommon Versions is a custom Gradle plugin that automatically generates a unified KommonVersions object accessible from all your KMP targets. It bridges the gap between Android&#39;s build system and iOS configuration, exposing everything through Kotlin&#39;s expect/actual pattern.</p><pre>// Access anywhere in your shared code!<br>Text(&quot;App ID: ${KommonVersions.APPLICATION_ID}&quot;)<br>Text(&quot;Version: ${KommonVersions.VERSION_NAME}&quot;)<br>Text(&quot;Build Type: ${KommonVersions.BUILD_TYPE}&quot;)<br>Text(&quot;Flavor: ${KommonVersions.FLAVOR}&quot;)</pre><p>The plugin uses Gradle’s convention plugins and code generation to create platform-specific implementations:</p><h3>1. Common Code Generation</h3><p>The plugin generates an expect object in commonMain:</p><pre>expect object KommonVersions {<br> val APPLICATION_ID: String<br> val BUILD_TYPE: String<br> val VERSION_CODE: Int<br> val VERSION_NAME: String<br> val FLAVOR: String<br>}</pre><h3>2. Android Implementation</h3><p>For Android, it taps into the existing BuildConfig class:</p><pre>actual object KommonVersions {<br> actual val APPLICATION_ID: String = BuildConfig.APPLICATION_ID<br> actual val BUILD_TYPE: String = BuildConfig.BUILD_TYPE<br> actual val VERSION_CODE: Int = BuildConfig.VERSION_CODE<br> actual val VERSION_NAME: String = BuildConfig.VERSION_NAME<br> actual val FLAVOR: String = BuildConfig.FLAVOR<br>}</pre><h3>3. iOS Implementation</h3><p>For iOS, it reads values from the app bundle at runtime:</p><pre>actual object KommonVersions {<br> private val bundleMap = NSBundle.mainBundle.infoDictionary<br> <br> actual val APPLICATION_ID = getString(&quot;CFBundleIdentifier&quot;)<br> actual val BUILD_TYPE = if (Platform.isDebugBinary) &quot;debug&quot; else &quot;release&quot;<br> actual val VERSION_CODE = getString(&quot;CFBundleVersion&quot;).toIntOrNull() ?: 1<br> actual val VERSION_NAME = getString(&quot;CFBundleShortVersionString&quot;)<br> actual val FLAVOR = getString(&quot;AppScheme&quot;)<br>}</pre><h3>4. XCConfig File Generation</h3><p>The plugin even generates iOS xcconfig files automatically, keeping your Xcode project in sync:</p><pre>TEAM_ID=<br>PRODUCT_BUNDLE_IDENTIFIER=com.mohaberabi.buildversionkmp.dev<br>PRODUCT_NAME=KmpVersionDevelop<br>MARKETING_VERSION=0.0.1<br>CURRENT_PROJECT_VERSION=1<br>KOTLIN_FRAMEWORK_BUILD_TYPE=Debug<br>XCODE_CONFIGURATION=Debug-development<br>APP_SCHEME=development</pre><h3>Configuration DSL</h3><p>The plugin provides a clean, DSL for configuring both platforms:</p><pre>commonVersions {<br>    android = android {<br>        namespace = &quot;com.example.app&quot;<br>        versionCode = 1<br>        versionName = &quot;0.0.1&quot;<br>        flavorDimensions += &quot;default&quot;<br>        <br>        flavors += flavor {<br>            name = &quot;dev&quot;<br>            dimension = &quot;default&quot;<br>            versionNameSuffix = &quot;-dev&quot;<br>            appName = &quot;MyApp&quot;<br>            appNameSuffix = &quot;Dev&quot;<br>            applicationIdSuffix = &quot;.dev&quot;<br>        }<br>    }<br>    <br>    ios = ios {<br>        projectFolderName = &quot;iosApp&quot;<br>        appName = &quot;MyApp&quot;<br>        bundleId = &quot;com.example.app&quot;<br>        marketingVersion = &quot;0.0.1&quot;<br>        currentVersion = &quot;1&quot;<br>        <br>        schemas += schema {<br>            name = &quot;development&quot;<br>            nameSuffix = &quot;Dev&quot;<br>            bundleSuffix = &quot;dev&quot;<br>        }<br>    }<br>}</pre><p>The CommonVersionConventionPlugin applies the KMP plugin and registers code generation tasks:</p><pre>class CommonVersionConventionPlugin : Plugin&lt;Project&gt; {<br>    override fun apply(target: Project) {<br>        with(target) {<br>            pluginManager.apply(&quot;org.jetbrains.kotlin.multiplatform&quot;)<br>            <br>            val ext = extensions.create(<br>                &quot;commonVersions&quot;,<br>                CommonVersionsExtension::class.java<br>            )<br>            <br>            afterEvaluate {<br>                configureAndroid(ext.android.orNull)<br>                configureIos(ext.ios.orNull)<br>                registerCodeGenTask()<br>            }<br>        }<br>    }<br>}</pre><pre>fun Project.configureKmpVersion() {<br>    extensions.configure&lt;KotlinMultiplatformExtension&gt; {<br>        val buildDir = layout.buildDirectory.dir(&quot;generated/kotlin&quot;)<br>        sourceSets[&quot;commonMain&quot;]?.kotlin?.srcDir(buildDir)<br>        sourceSets[&quot;androidMain&quot;]?.kotlin?.srcDir(buildDir)<br>        sourceSets[&quot;iosMain&quot;]?.kotlin?.srcDir(buildDir)<br>    }<br>}</pre><pre>tasks.matching { task -&gt;<br>    task.name.contains(&quot;ios&quot;, ignoreCase = true) &amp;&amp; <br>    task.name.contains(&quot;Link&quot;, ignoreCase = true)<br>}.configureEach {<br>    dependsOn(generateIosConfigTask)<br>}</pre><ul><li><strong>Single Source of Truth</strong>: Define versions once in gradle/libs.versions.toml</li><li><strong>Platform Consistency</strong>: Same configuration values accessible across Android, iOS, and shared code</li><li><strong>Automated XCConfig</strong>: No manual Xcode project editing needed</li><li><strong>Flavor Support</strong>: Full support for Android product flavors and iOS schemes</li></ul><p>source : <a href="https://github.com/mohaberabi/kommon-versions">https://github.com/mohaberabi/kommon-versions</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xNkcPleYxchCqRSnhq1Iug.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-fXXp110ITA0z7C4u5IaOA.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a252832e41bd" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Secure Storage in Kotlin Multiplatform]]></title>
            <link>https://medium.com/@mohaberabi98/secure-storage-in-kotlin-multiplatform-7bb47a6fea39?source=rss-725d0fa3cda9------2</link>
            <guid isPermaLink="false">https://medium.com/p/7bb47a6fea39</guid>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[compose-multiplatform]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Mohab erabi]]></dc:creator>
            <pubDate>Sat, 15 Nov 2025 11:16:16 GMT</pubDate>
            <atom:updated>2025-11-15T11:16:43.386Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*jmSaGshjQRaGc89aMdpdNA.png" /></figure><p>Building secure apps means protecting sensitive data — tokens, secrets, user credentials, encryption keys — at rest.</p><p>For Android developers this usually means <strong>Keystore</strong>, while iOS developers rely on <strong>Keychain</strong>.</p><p><strong>This article walks through the exact pattern you should use:</strong></p><ul><li>A unified secure storage interface in commonMain</li><li>Android implementation using <strong>AES-GCM + Android Keystore</strong></li><li>iOS implementation using <strong>AES-GCM + Keychain</strong></li></ul><h3>Step 1 — Common API (commonMain)</h3><pre>expect object Krypto {</pre><pre>    fun encrypt(bytes: ByteArray): ByteArray<br>    fun decrypt(bytes: ByteArray): ByteArray<br>}</pre><p>it’s very common that we will use this also for strings and text so let’s make a utils functions that will help us</p><pre>@OptIn(ExperimentalEncodingApi::class)<br>fun Krypto.encrypt(text: String): String {<br>    if (text.isBlank()) return &quot;&quot;<br>    val plainBytes = text.encodeToByteArray()<br>    val cipherBytes = Krypto.encrypt(plainBytes)<br>    return Base64.encode(cipherBytes)<br>}</pre><pre>@OptIn(ExperimentalEncodingApi::class)<br>fun Krypto.decrypt(encryptedText: String): String {<br>    if (encryptedText.isBlank()) return &quot;&quot;<br>    val cipherBytes = Base64.decode(encryptedText)<br>    val plainBytes = Krypto.decrypt(cipherBytes)<br>    return plainBytes.decodeToString()<br>}</pre><h3>Step 2 — Android Implementation (androidMain)</h3><pre>actual object Krypto {<br>    private const val PROVIDER = &quot;AndroidKeyStore&quot;<br>    private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES<br>    private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC<br>    private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7<br>    private const val TRANSFORMATION = &quot;$ALGORITHM/$BLOCK_MODE/$PADDING&quot;</pre><pre>    private val keyGenerator by lazy { KeyGenerator.getInstance(ALGORITHM, PROVIDER); }<br>    private val cipher by lazy { Cipher.getInstance(TRANSFORMATION); }<br>    private val keyStore by lazy { KeyStore.getInstance(PROVIDER).apply { load(null); }; }</pre><pre>    actual fun encrypt(bytes: ByteArray): ByteArray {<br>        cipher.init(Cipher.ENCRYPT_MODE, getKey())<br>        val iv = cipher.iv<br>        val encrypted = cipher.doFinal(bytes)<br>        return iv + encrypted<br>    }</pre><pre>    actual fun decrypt(bytes: ByteArray): ByteArray {<br>        val iv = bytes.copyOfRange(0, cipher.blockSize)<br>        val data = bytes.copyOfRange(cipher.blockSize, bytes.size)<br>        cipher.init(Cipher.DECRYPT_MODE, getKey(), IvParameterSpec(iv))<br>        return cipher.doFinal(data)<br>    }<br></pre><pre>    private fun getKey(): SecretKey {<br>        val existing = keyStore.getEntry(AES_KEY_ALIAS, null) as? KeyStore.SecretKeyEntry<br>        return existing?.secretKey ?: createKey()<br>    }<br></pre><pre>    private fun createKey(): SecretKey {<br>        val builder = KeyGenParameterSpec.Builder(<br>            AES_KEY_ALIAS,<br>            KeyProperties.PURPOSE_ENCRYPT or<br>                    KeyProperties.PURPOSE_DECRYPT<br>        ).setBlockModes(BLOCK_MODE)<br>            .setEncryptionPaddings(PADDING)<br>            .setRandomizedEncryptionRequired(true)<br>            .setUserAuthenticationRequired(false)<br>            .setKeySize(256)<br>        return keyGenerator.apply { init(builder.build()); }.generateKey()<br>    }<br>}</pre><pre>private const val PROVIDER = &quot;AndroidKeyStore&quot;<br>    private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES<br>    private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC<br>    private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7<br>    private const val TRANSFORMATION = &quot;$ALGORITHM/$BLOCK_MODE/$PADDING&quot;</pre><ul><li><strong>PROVIDER = “AndroidKeyStore”</strong></li><li>We tell Java Crypto: <strong>“Use the Android keystore system”</strong> as the place where the secret key lives.</li><li><strong>ALGORITHM = AES</strong></li><li>We’re using <strong>AES</strong> (symmetric encryption). Same key for encryption &amp; decryption.</li><li><strong>BLOCK_MODE = CBC</strong></li><li>We use <strong>Cipher Block Chaining (CBC)</strong> mode.</li><li>Each block depends on previous one, needs an <strong>IV</strong> (initialization vector).</li><li><strong>PADDING = PKCS7</strong></li><li>If the data isn’t a multiple of AES block size (16 bytes), we add padding bytes so it fits.</li><li><strong>TRANSFORMATION = “AES/CBC/PKCS7Padding”</strong></li><li><strong>Algorithm / BlockMode / Padding</strong></li><li>That’s exactly what you pass into Cipher.getInstance(…).</li></ul><pre>private val keyGenerator by lazy { KeyGenerator.getInstance(ALGORITHM, PROVIDER); }<br>    private val cipher by lazy { Cipher.getInstance(TRANSFORMATION); }<br>    private val keyStore by lazy { KeyStore.getInstance(PROVIDER).apply { load(null); }; }</pre><p><strong>keyGenerator : </strong>KeyGenerator.getInstance(ALGORITHM, PROVIDER)</p><ul><li>“Give me a <strong>KeyGenerator</strong> for AES, and let it be backed by <strong>AndroidKeyStore</strong>.”</li><li>This generator knows how to <strong>create a key that lives inside Keystore</strong> (non-exportable).</li></ul><p><strong>cipher</strong></p><ul><li>Cipher.getInstance(TRANSFORMATION)</li><li>“Give me a <strong>Cipher</strong> configured for AES/CBC/PKCS7.”</li><li>We’ll reuse this cipher for encrypt/decrypt</li></ul><p><strong>keyStore</strong></p><ul><li>KeyStore.getInstance(PROVIDER)</li><li>Give me the Android keystore as a KeyStore object.”</li><li><strong>load(null)</strong>: Initializes the keystore so we can read/write keys by alias.</li></ul><pre>actual fun encrypt(bytes: ByteArray): ByteArray {<br>    cipher.init(Cipher.ENCRYPT_MODE, getKey())<br>    val iv = cipher.iv<br>    val encrypted = cipher.doFinal(bytes)<br>    return iv + encrypted<br>}</pre><p><strong>cipher.init(Cipher.ENCRYPT_MODE, getKey())</strong></p><ul><li>Puts cipher in <strong>ENCRYPT_MODE</strong> using the secret key from getKey().</li><li>For CBC mode, when you init in ENCRYPT_MODE, it will <strong>randomly generate an IV</strong> (because we set setRandomizedEncryptionRequired(true) later).</li></ul><p><strong>val iv = cipher.iv</strong></p><ul><li>After initialization, cipher.iv contains the <strong>random IV</strong> it just generated.</li><li>IV is required for decrypting later.</li></ul><p><strong>val encrypted = cipher.doFinal(bytes)</strong></p><ul><li>This <strong>encrypts the input bytes</strong>.</li><li>doFinal = process all data and finish encryption.</li></ul><p><strong>return iv + encrypted</strong></p><ul><li>We <strong>prepend the IV to the ciphertext</strong>.</li><li>Why? Because to decrypt, we must know the IV that was used.</li><li>IV is <strong>not secret</strong>, only random. So we store it together with the ciphertext.</li></ul><p><strong>So encrypt() does:</strong></p><p><em>Generate random IV → encrypt → return [IV][CIPHERTEXT] in one ByteArray.</em></p><pre>actual fun decrypt(bytes: ByteArray): ByteArray {<br>    val iv = bytes.copyOfRange(0, cipher.blockSize)<br>    val data = bytes.copyOfRange(cipher.blockSize, bytes.size)<br>    cipher.init(Cipher.DECRYPT_MODE, getKey(), IvParameterSpec(iv))<br>    return cipher.doFinal(data)<br>}</pre><blockquote>We expect the input bytes to be: [IV][CIPHERTEXT].</blockquote><p><strong>val iv = bytes.copyOfRange(0, cipher.blockSize</strong></p><ul><li>AES block size = 16 bytes.</li><li>Takes the <strong>first 16 bytes</strong> as the IV.</li></ul><p><strong>val data = bytes.copyOfRange(cipher.blockSize, bytes.size)</strong></p><ul><li>Takes the <strong>rest of the bytes</strong> as the actual encrypted data.</li></ul><p>cipher.init(Cipher.DECRYPT_MODE, getKey(), IvParameterSpec(iv))</p><ul><li>Now init cipher in <strong>DECRYPT_MODE</strong>.</li><li>Use <strong>the same key</strong> (getKey()) and <strong>the extracted IV</strong>.</li><li>IvParameterSpec(iv) tells the cipher which IV was originally used</li></ul><p>return cipher.doFinal(data)</p><ul><li>Decrypts the data and returns the original plaintext.</li></ul><p><strong>So decrypt() does:</strong></p><p><em>Split IV + ciphertext → initialize cipher with IV → decrypt → return plaintext.</em></p><h3>Step 3 — IOS Implementation (iosMain)</h3><p>the implementation in the ios side is not that straightforward and easy as in android especially when we need to implmenet it in Kotlin Native <br>we have 2 options</p><p>1- Write everything in the kotlin way <strong>( iosMain)</strong> you will need to write it in lower level<strong> Objective-C</strong></p><p>2- Write it in <strong>Swift</strong> side then call it directly in <strong>Kotlin</strong> using <strong>Interops</strong></p><p>for that complexity i wrote the documentation inside the code</p><p>you can find it here <br><a href="https://github.com/mohaberabi?tab=repositories">https://github.com/mohaberabi?tab=repositories</a></p><h3>Step 3-A — IOS Implementation Using Kotlin</h3><pre>/**<br> * this object does the logic with the native ios keychain<br> * Generate 32-byte key (AES -256)<br> * If we already have the key we return it<br> * else we create a new random one and save it then return it<br> *Stores data safely using iOS Security framework (SecItemAdd, SecItemCopyMatching)<br> */<br>@OptIn(ExperimentalForeignApi::class)</pre><pre>object IosKryptoDelegate {<br>    private const val AES_KEY_SIZE_BYTES = 32</pre><pre>    val defaultKey by lazy { getOrCreateKey(); }</pre><pre>    /**<br>     *  If key already exists in Keychain → return it.<br>     *  If not → generate and save a new one.<br>     * So this gives you a stable AES key on this device.<br>     */<br>    private fun getOrCreateKey(): ByteArray {<br>        loadKey()?.let { return it }<br>        val newKey = generateRandomKey()<br>        saveKey(newKey)<br>        return newKey<br>    }</pre><pre>    /**<br>     *[kSecClass] type of item we are saving [kSecClassKey] means CRYPTO KEY<br>     *[kSecAttrApplicationTag] the tag of the data [AES_KEY_ALIAS] or Key Alias<br>     *[kSecValueData] the actual key that maps to the key we will use to save<br>     *[kSecAttrAccessible] the key that informs ios when to access the key<br>     *[kSecAttrAccessibleWhenUnlocked]Key is only available when the device is unlocked.<br>     */<br>    private fun saveKey(key: ByteArray) = memScoped {<br>        val keyData = key.toCFData() ?: return<br>        val tagData = AES_KEY_ALIAS.encodeToByteArray().toCFData() ?: return<br>        val dict = CFDictionaryCreateMutable(<br>            kCFAllocatorDefault,<br>            0,<br>            null,<br>            null<br>        ) ?: return<br>        CFDictionaryAddValue(dict, kSecClass, kSecClassKey)<br>        CFDictionaryAddValue(dict, kSecAttrApplicationTag, tagData)<br>        CFDictionaryAddValue(dict, kSecAttrAccessible, kSecAttrAccessibleWhenUnlocked)<br>        CFDictionaryAddValue(dict, kSecValueData, keyData)<br>        val status = SecItemAdd(dict, null)<br>        check(status == errSecSuccess || status == errSecDuplicateItem) {<br>            &quot;SecItemAdd failed with status: $status&quot;<br>        }<br>    }</pre><pre>    /**<br>     *[kSecClass] type of item we are saving [kSecClassKey] means CRYPTO KEY<br>     *[kSecAttrApplicationTag] the tag of the data [com.mohaberabi.krypto.krypto.AES_KEY_ALIAS] or Key Alias<br>     *[kSecValueData] the actual key that maps to the key we will use to save<br>     *[platform.Security.kSecReturnData] we are telling the Query to return the key<br>     *[platform.Security.kSecMatchLimitOne] Limit the result to exaclty one key only<br>     *Keychain reads output into a pointer.<br>     *Kotlin/Native requires a scoped memory region for this.<br>     * memScoped allocates temporary C memory for the duration of the block Automatically freed afterward.<br>     */<br>    private fun loadKey(): ByteArray? = memScoped {<br>        val tagData = AES_KEY_ALIAS.encodeToByteArray().toCFData()<br>        val query = CFDictionaryCreateMutable(<br>            allocator = kCFAllocatorDefault,<br>            capacity = 0,<br>            keyCallBacks = null,<br>            valueCallBacks = null<br>        ) ?: return@memScoped null<br>        CFDictionaryAddValue(query, kSecClass, kSecClassKey)<br>        CFDictionaryAddValue(query, kSecAttrApplicationTag, tagData)<br>        CFDictionaryAddValue(query, kSecReturnData, kCFBooleanTrue)<br>        CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitOne)<br>        val resultVar = alloc&lt;CFTypeRefVar&gt;()<br>        val status = SecItemCopyMatching(query, resultVar.ptr)<br>        if (status != errSecSuccess) return@memScoped null<br>        val cfData = resultVar.value<br>        if (cfData == null) return@memScoped null<br>        (cfData as CFDataRef).toByteArray()<br>    }</pre><pre>    /**<br>     *  Creates a 32-byte random array (AES-256).<br>     *  Each byte is random from 0–255.<br>     *  [usePinned]  --&gt; read below to understand it<br>     *  kotlin native normally moves the arrays in memory but IOS needs<br>     *  a stable Pointer ( memory must not move)<br>     *  so the [usePinned] Pins the byteArray :<br>     *  it makes sure that memory address stays fixed during block is executed<br>     *  so after we are done we pass the pointer to IOS<br>     *  [addressOf] gives pointer to the index of the byte<br>     *  [NSData.create] creates an ios NSData object from the bytes<br>     *  [NSData] is exactly same as [ByteArray] in JVM<br>     *  at the end this is the key we will be using to store the data in IOS Chain<br>     *<br>     */</pre><pre>    fun generateRandomKey(): ByteArray = randomBytes(AES_KEY_SIZE_BYTES)</pre><pre>    fun randomBytes(length: Int): ByteArray {<br>        val array = ByteArray(length)<br>        array.usePinned { pinned -&gt;<br>            val status = SecRandomCopyBytes(<br>                kSecRandomDefault,<br>                length.convert(),<br>                pinned.addressOf(0)<br>            )<br>            check(status == errSecSuccess) { &quot;SecRandomCopyBytes failed: $status&quot; }<br>        }<br>        return array<br>    }<br>}</pre><pre>@OptIn(ExperimentalForeignApi::class)<br>object KotlinNativeKrypto {<br>    private val AES_BLOCK_SIZE = kCCBlockSizeAES128.toInt()</pre><pre>    /**<br>     * UByteArray(BLOCK_SIZE.toInt()) { ... } → create an array of unsigned bytes of length 16.<br>     *  For each element: (0..255).random().toUByte() → generate random number 0–255, convert to UByte.<br>     *  toByteArray() → convert to normal ByteArray.<br>     *  Why IV?<br>     *  •  IV makes sure that encrypting the same data twice gives different ciphertext.<br>     *  •  Very important for security.<br>     *  •  IV is not secret, just random &amp; unique.<br>     *  val encryptedBytes = ByteArray(bytes.size + BLOCK_SIZE.toInt())<br>     *      •  You allocate an array to hold the encrypted data.<br>     *  •  Its size is:<br>     *  •  original size bytes.size<br>     *  •  one extra block (16 bytes) to handle PKCS7 padding.<br>     *  •  AES with padding can produce up to input length + block size.<br>     *  op = [kCCEncrypt]Operation type. , kCCEncrypt means encrypt mode.<br>     *  alg = [kCCAlgorithmAES] the algorithm to encrypt with<br>     *  options = [kCCOptionPKCS7Padding] Use PKCS#7 padding, a standard padding method so data length can be a multiple of block size.<br>     *  key = [key.size]Pointer to the raw key bytes.<br>     *  •  key is NSData.<br>     *  •  key.bytes is like void* pointer to its bytes in memory.<br>     *   iv = [iv.refTo(0)] -Pointer to IV data - iv is a ByteArray. - iv.refTo(0) gives a C pointer to the first element.<br>     *   CommonCrypto uses IV to initialize the block cipher state.<br>     *   dataIn = bytes.refTo(0), bytes.size.convert()<br>     *   dataIn pointer → your plaintext data, first byte.<br>     *   bytes.size.convert() → number of bytes of input data.<br>     *  dataOut = encryptedBytes.refTo(0), encryptedBytes.size.convert()<br>     *  dataOut pointer → buffer where ciphertext will be written.<br>     *  encryptedBytes.size.convert() → maximum number of bytes it can write.<br>     *  dataOutMoved = outLength.ptr<br>     *  •  Pointer to your size_tVar variable.<br>     *  •  After the call, outLength.value will contain how many bytes of ciphertext were actually written.<br>     */<br>    fun encrypt(bytes: ByteArray): ByteArray {<br>        val key = IosKryptoDelegate.defaultKey<br>        val iv = IosKryptoDelegate.randomBytes(AES_BLOCK_SIZE)<br>        val outBuffer = ByteArray(bytes.size + AES_BLOCK_SIZE)<br>        var outLength = 0<br>        memScoped {<br>            val outLengthVar = alloc&lt;size_tVar&gt;()<br>            val status = key.usePinned { keyPinned -&gt;<br>                iv.usePinned { ivPinned -&gt;<br>                    bytes.usePinned { inPinned -&gt;<br>                        outBuffer.usePinned { outPinned -&gt;<br>                            CCCrypt(<br>                                op = kCCEncrypt,<br>                                alg = kCCAlgorithmAES,<br>                                options = kCCOptionPKCS7Padding,<br>                                key = keyPinned.addressOf(0),<br>                                keyLength = key.size.convert(),<br>                                iv = ivPinned.addressOf(0),<br>                                dataIn = inPinned.addressOf(0),<br>                                dataInLength = bytes.size.convert(),<br>                                dataOut = outPinned.addressOf(0),<br>                                dataOutAvailable = outBuffer.size.convert(),<br>                                dataOutMoved = outLengthVar.ptr<br>                            )<br>                        }<br>                    }<br>                }<br>            }</pre><pre>            if (status != kCCSuccess) {<br>                return bytes<br>            }<br>            outLength = outLengthVar.value.toInt()<br>        }</pre><pre>        val cipher = outBuffer.copyOf(outLength)<br>        return iv + cipher<br>    }</pre><pre>    fun decrypt(bytes: ByteArray): ByteArray {<br>        if (bytes.size &lt;= AES_BLOCK_SIZE) return bytes<br>        val key = IosKryptoDelegate.defaultKey<br>        val iv = bytes.copyOfRange(0, AES_BLOCK_SIZE)<br>        val cipher = bytes.copyOfRange(AES_BLOCK_SIZE, bytes.size)<br>        val outBuffer = ByteArray(cipher.size + AES_BLOCK_SIZE)<br>        var outLength: Int = 0</pre><pre>        memScoped {<br>            val outLengthVar = alloc&lt;size_tVar&gt;()<br>            val status = key.usePinned { keyPinned -&gt;<br>                iv.usePinned { ivPinned -&gt;<br>                    cipher.usePinned { inPinned -&gt;<br>                        outBuffer.usePinned { outPinned -&gt;<br>                            CCCrypt(<br>                                op = kCCDecrypt,<br>                                alg = kCCAlgorithmAES,<br>                                options = kCCOptionPKCS7Padding,<br>                                key = keyPinned.addressOf(0),<br>                                keyLength = key.size.convert(),<br>                                iv = ivPinned.addressOf(0),<br>                                dataIn = inPinned.addressOf(0),<br>                                dataInLength = cipher.size.convert(),<br>                                dataOut = outPinned.addressOf(0),<br>                                dataOutAvailable = outBuffer.size.convert(),<br>                                dataOutMoved = outLengthVar.ptr<br>                            )<br>                        }<br>                    }<br>                }<br>            }<br>            if (status != kCCSuccess) {<br>                return bytes<br>            }<br>            outLength = outLengthVar.value.toInt()<br>        }<br>        return outBuffer.copyOf(outLength)<br>    }<br>}</pre><h3>Step 3-B — IOS Implementation Using Swift</h3><pre>import Foundation<br>import Security<br>import CommonCrypto</pre><pre>@objcMembers<br>public class IosKrypto: NSObject {</pre><pre>    private static let keyAlias = &quot;com.mohaberabi.krypto.aesKey&quot;<br>    private static let keyLength = 32 // 32 bytes =&gt; AES-256</pre><pre>    // MARK: - Public API</pre><pre>    private static func getOrCreateKey() -&gt; Data? {<br>        if let existing = loadKey() {<br>            return existing<br>        }<br>        guard let key = generateRandomKey() else { return nil }<br>        saveKey(key)   <br>        return key<br>    }</pre><pre>    /// Encrypts data using AES/CBC/PKCS7.<br>    /// Returns [IV (16 bytes)] + [ciphertext]<br>    public static func encrypt(_ plain: Data) -&gt; Data? {<br>        guard let key = getOrCreateKey() else { return nil }</pre><pre>        // 1) Random IV<br>        var iv = Data(count: kCCBlockSizeAES128)<br>        let ivStatus = iv.withUnsafeMutableBytes { ivBytes in<br>            guard let base = ivBytes.baseAddress else { return errSecParam }<br>            return SecRandomCopyBytes(<br>                kSecRandomDefault,<br>                kCCBlockSizeAES128,<br>                base<br>            )<br>        }<br>        if ivStatus != errSecSuccess { return nil }</pre><pre>        // 2) Encrypt<br>        let cryptLength = plain.count + kCCBlockSizeAES128<br>        var cryptData = Data(count: cryptLength)<br>        var numBytesEncrypted: size_t = 0</pre><pre>        let status = cryptData.withUnsafeMutableBytes { cryptBytes in<br>            iv.withUnsafeBytes { ivBytes in<br>                plain.withUnsafeBytes { plainBytes in<br>                    key.withUnsafeBytes { keyBytes in<br>                        CCCrypt(<br>                            CCOperation(kCCEncrypt),<br>                            CCAlgorithm(kCCAlgorithmAES),<br>                            CCOptions(kCCOptionPKCS7Padding),<br>                            keyBytes.baseAddress, key.count,<br>                            ivBytes.baseAddress,<br>                            plainBytes.baseAddress, plain.count,<br>                            cryptBytes.baseAddress, cryptLength,<br>                            &amp;numBytesEncrypted<br>                        )<br>                    }<br>                }<br>            }<br>        }</pre><pre>        guard status == kCCSuccess else { return nil }</pre><pre>        // Trim output to actual size<br>        cryptData.removeSubrange(numBytesEncrypted..&lt;cryptData.count)</pre><pre>        // 3) Return IV + ciphertext<br>        var result = Data()<br>        result.append(iv)<br>        result.append(cryptData)<br>        return result<br>    }</pre><pre>    /// Decrypts [IV (16 bytes)] + [ciphertext]<br>    public static func decrypt(_ cipher: Data) -&gt; Data? {<br>        guard cipher.count &gt; kCCBlockSizeAES128 else { return nil }<br>        guard let key = getOrCreateKey() else { return nil }</pre><pre>        let iv = cipher.subdata(in: 0..&lt;kCCBlockSizeAES128)<br>        let encrypted = cipher.subdata(in: kCCBlockSizeAES128..&lt;cipher.count)</pre><pre>        var decryptedData = Data(count: encrypted.count + kCCBlockSizeAES128)<br>        let decryptedLength = decryptedData.count      <br>        var numBytesDecrypted: size_t = 0</pre><pre>        let status = decryptedData.withUnsafeMutableBytes { decryptedBytes in<br>            iv.withUnsafeBytes { ivBytes in<br>                encrypted.withUnsafeBytes { encBytes in<br>                    key.withUnsafeBytes { keyBytes in<br>                        CCCrypt(<br>                            CCOperation(kCCDecrypt),<br>                            CCAlgorithm(kCCAlgorithmAES),<br>                            CCOptions(kCCOptionPKCS7Padding),<br>                            keyBytes.baseAddress, key.count,<br>                            ivBytes.baseAddress,<br>                            encBytes.baseAddress, encrypted.count,<br>                            decryptedBytes.baseAddress, decryptedLength, <br>                            &amp;numBytesDecrypted<br>                        )<br>                    }<br>                }<br>            }<br>        }</pre><pre>        guard status == kCCSuccess else { return nil }</pre><pre>        decryptedData.removeSubrange(numBytesDecrypted..&lt;decryptedData.count)<br>        return decryptedData<br>    }<br>    </pre><pre>    // MARK: - Key / Keychain internals</pre><pre>    private static func generateRandomKey() -&gt; Data? {<br>        var data = Data(count: keyLength)<br>        let status = data.withUnsafeMutableBytes { bytes in<br>            guard let base = bytes.baseAddress else { return errSecParam }<br>            return SecRandomCopyBytes(kSecRandomDefault, keyLength, base)<br>        }<br>        return (status == errSecSuccess) ? data : nil<br>    }</pre><pre>    private static func saveKey(_ key: Data) {<br>        guard let tag = keyAlias.data(using: .utf8) else { return }</pre><pre>        let query: [String: Any] = [<br>            kSecClass as String: kSecClassKey,<br>            kSecAttrApplicationTag as String: tag,<br>            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked,<br>            kSecValueData as String: key<br>        ]</pre><pre>        SecItemDelete(query as CFDictionary)<br>        SecItemAdd(query as CFDictionary, nil)<br>    }</pre><pre>    private static func loadKey() -&gt; Data? {<br>        guard let tag = keyAlias.data(using: .utf8) else { return nil }</pre><pre>        let query: [String: Any] = [<br>            kSecClass as String: kSecClassKey,<br>            kSecAttrApplicationTag as String: tag,<br>            kSecReturnData as String: true,<br>            kSecMatchLimit as String: kSecMatchLimitOne,<br>        ]</pre><pre>        var item: CFTypeRef?<br>        let status = SecItemCopyMatching(query as CFDictionary, &amp;item)<br>        guard status == errSecSuccess else { return nil }<br>        return item as? Data<br>    }<br>}</pre><p>now the actual Object we can use any of the implementation inside</p><pre>actual object Krypto {<br></pre><pre>    @OptIn(ExperimentalForeignApi::class)<br>    actual fun encrypt(bytes: ByteArray): ByteArray {<br>        if (bytes.isEmpty()) return byteArrayOf()<br>        return KotlinNativeKrypto.encrypt(bytes)<br>// or the IosKrypto from IOS Side <br>    }<br></pre><pre>    @OptIn(ExperimentalForeignApi::class)<br>    actual fun decrypt(bytes: ByteArray): ByteArray {<br>        if (bytes.isEmpty()) return byteArrayOf()<br>        return KotlinNativeKrypto.decrypt(bytes)<br>// or the IosKrypto from IOS Side<br>    }<br>}</pre><p>let’s try this code with simple app</p><pre>@OptIn(ExperimentalEncodingApi::class)<br>@Composable<br>@Preview<br>fun App() {<br>    var text by remember {<br>        mutableStateOf(&quot;&quot;)<br>    }<br>    var encrypted by remember {<br>        mutableStateOf(&quot;&quot;)<br>    }<br>    var decrypted by remember {<br>        mutableStateOf(&quot;&quot;)<br>    }<br>    MaterialTheme {<br>        Scaffold { padding -&gt;<br>            Column(<br>                horizontalAlignment = Alignment.CenterHorizontally,<br>                verticalArrangement = Arrangement.Center,<br>                modifier = Modifier.fillMaxSize()<br>                    .verticalScroll(rememberScrollState())<br>                    .padding(padding)<br>                    .padding(20.dp)<br>            ) {</pre><pre>                Spacer(Modifier.height(20.dp))<br>                TextField(<br>                    modifier = Modifier.fillMaxWidth(),<br>                    label = { Text(&quot;Type here your text&quot;) },<br>                    value = text,<br>                    onValueChange = { text = it },<br>                )</pre><pre>                Button(<br>                    onClick = {<br>                        encrypted = Krypto.encrypt(text)<br>                    },<br>                ) {<br>                    Text(&quot;Encrypt&quot;)<br>                }</pre><pre>                Text(<br>                    &quot;Below is encrypted&quot;,<br>                    fontSize = 20.sp, fontWeight = FontWeight.Bold<br>                )<br>                Text(encrypted)<br>                Button(<br>                    onClick = {<br>                        decrypted = Krypto.decrypt(encrypted)<br>                    },<br>                ) {<br>                    Text(&quot;Decrypt&quot;)<br>                }<br>                Text(<br>                    &quot;Below is decrypted&quot;,<br>                    fontSize = 20.sp,<br>                    fontWeight = FontWeight.Bold<br>                )<br>                Text(decrypted)<br>            }<br>        }<br>    }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*U6RCf_vC1mfk00K-yVPDmQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*c_J35QQKsVyKCp2noGuzhw.png" /></figure><p>Source Code: <a href="https://github.com/mohaberabi/krypto">https://github.com/mohaberabi/krypto</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7bb47a6fea39" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How I Accidentally Built a Compose BottomSheetNavigator that also works on Compose Multiplatform]]></title>
            <link>https://medium.com/@mohaberabi98/how-i-accidentally-built-a-compose-bottomsheetnavigator-that-also-works-on-compose-multiplatform-58b790a3f52d?source=rss-725d0fa3cda9------2</link>
            <guid isPermaLink="false">https://medium.com/p/58b790a3f52d</guid>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[compose-multiplatform]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Mohab erabi]]></dc:creator>
            <pubDate>Mon, 06 Oct 2025 16:38:51 GMT</pubDate>
            <atom:updated>2025-10-06T16:42:37.036Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*x_EyrpMNSvvu-2KKMrsrTg.png" /></figure><p>Google’s BottomSheetNavigator is great — until you need it to work on Compose Multiplatform or Material 3 or you need to use the new type safe navigation in compose or compose multiplatform</p><p>So I reused something they already built (DialogNavigator) and turned it into a <strong>cross-platform, ViewModel-aware, type-safe BottomSheet navigation system</strong></p><p>If you’ve used <strong>Navigation Compose</strong>, you know it supports:</p><pre>composable(…)<br>dialog(…)</pre><p>But what about <strong>bottom sheets</strong>?</p><p>You’ll quickly find accompanist-navigation-material — a Google library that adds:</p><pre>bottomSheet(&quot;route&quot;) { … }</pre><p>Sounds perfect, right?</p><p>Except it:</p><ul><li>Works <strong>only on Android</strong></li><li>Doesn’t compile in compose multiplatform</li><li>Doesn’t support type safe</li><li>Adds an extra Navigator and internal state logic which add the end exactly same as you show a modal bottom sheet inside a composable function , too many code and logic for a problem that was already solved</li></ul><p>at our company our product which is a very complex POS Android app which is also Compose Multiplatform our design depends alot on bottom sheets , too many bottom sheets more than 90 bottom sheets where each one needs and does alot of business logic handles alot of events holds huge states , it was a hell to to nest all the sheets with all the data it needs inside the composable screen that needs to show or open it ,</p><p>While reading the Navigation Compose source, I realized something:</p><blockquote><em>The </em><strong><em>dialog()</em></strong><em> destination already gives you a fully scoped backstack entry, lifecycle, and ViewModel store.</em></blockquote><p>which means we can deal with it as if it was a screen isolated away from any other screens , viewmodel, savedStateHandle ,</p><p>and since the dialog() is an overlay component which still allows us to see the previous screen content while dialog is open , doesn’t it ring a bell ?</p><p>yes … the bottom sheet has the same exact behavior , the <strong>dialog&lt;T&gt;()</strong></p><p>does not care or even know of what content it carry it just needs a composable function that can be composed and scoped to backStack and if a viewmodel is attached to it , it also clears it when the route which is the <strong>dialog</strong> (bottomSheet) is popped from the backstack ,</p><p>so it doesn’t matter if we added a scaffold , dialogue , column , card or even a ModalBottomSheet…..</p><p>That’s <strong>everything</strong> a bottom sheet needs — except for the UI.</p><p>So I thought:</p><p>“Why not reuse the dialog() destination, but display a bottom sheet instead?”</p><p>Here’s what I ended up writing:</p><pre>import androidx.compose.runtime.Composable<br>import androidx.compose.ui.window.DialogProperties<br>import androidx.navigation.NamedNavArgument<br>import androidx.navigation.NavBackStackEntry<br>import androidx.navigation.NavDeepLink<br>import androidx.navigation.NavGraphBuilder<br>import androidx.navigation.NavType<br>import androidx.navigation.compose.DialogNavigator<br>import androidx.navigation.compose.DialogNavigatorDestinationBuilder<br>import androidx.navigation.compose.composable<br>import androidx.navigation.compose.dialog<br>import androidx.navigation.get<br>import kotlin.jvm.JvmSuppressWildcards<br>import kotlin.reflect.KClass<br>import kotlin.reflect.KType<br><br>data class BottomSheetProperties(<br>    val dismissOnBackPress: Boolean = true,<br>    val dismissOnClickOutside: Boolean = true,<br>    val usePlatformDefaultWidth: Boolean = true,<br>)<br><br><br>fun BottomSheetProperties.toDialogProperties() = DialogProperties(<br>    dismissOnBackPress = dismissOnBackPress,<br>    dismissOnClickOutside = dismissOnClickOutside,<br>    usePlatformDefaultWidth = usePlatformDefaultWidth<br>)<br><br>fun NavGraphBuilder.bottomSheet(<br>    route: String,<br>    arguments: List&lt;NamedNavArgument&gt; = emptyList(),<br>    deepLinks: List&lt;NavDeepLink&gt; = emptyList(),<br>    bottomSheetProperties: BottomSheetProperties = BottomSheetProperties(),<br>    content: @Composable (NavBackStackEntry) -&gt; Unit<br>) {<br>    destination(<br>        navDestination = DialogNavigatorDestinationBuilder(<br>            navigator = provider[DialogNavigator::class],<br>            route = route,<br>            dialogProperties = bottomSheetProperties.toDialogProperties(),<br>            content = content<br>        ).apply {<br>            arguments.forEach { (argumentName, argument) -&gt; argument(argumentName, argument) }<br>            deepLinks.forEach { deepLink -&gt; deepLink(deepLink) }<br>        }<br>    )<br>}<br><br><br>inline fun &lt;reified T : Any&gt; NavGraphBuilder.bottomSheet(<br>    typeMap: Map&lt;KType, @JvmSuppressWildcards NavType&lt;*&gt;&gt; = emptyMap(),<br>    deepLinks: List&lt;NavDeepLink&gt; = emptyList(),<br>    bottomSheetProperties: BottomSheetProperties = BottomSheetProperties(),<br>    noinline content: @Composable (NavBackStackEntry) -&gt; Unit<br>) {<br>    dialog(<br>        route = T::class,<br>        typeMap = typeMap,<br>        deepLinks = deepLinks,<br>        dialogProperties = bottomSheetProperties.toDialogProperties(),<br>        content = content<br>    )<br>}<br><br>fun &lt;T : Any&gt; NavGraphBuilder.bottomSheet(<br>    route: KClass&lt;T&gt;,<br>    typeMap: Map&lt;KType, @JvmSuppressWildcards NavType&lt;*&gt;&gt; = emptyMap(),<br>    deepLinks: List&lt;NavDeepLink&gt; = emptyList(),<br>    bottomSheetProperties: BottomSheetProperties = BottomSheetProperties(),<br>    content: @Composable (NavBackStackEntry) -&gt; Unit<br>) {<br>    destination(<br>        DialogNavigatorDestinationBuilder(<br>            provider[DialogNavigator::class],<br>            route = route,<br>            typeMap = typeMap,<br>            dialogProperties = bottomSheetProperties.toDialogProperties(),<br>            content = content<br>        ).apply { deepLinks.forEach { deepLink -&gt; deepLink(deepLink) } }<br>    )<br>}</pre><p>let’s create a simple navigation flow</p><p>first inside your .toml file add</p><pre>kotlinx-serialization = &quot;1.9.0&quot;<br>navigation-compose = &quot;2.9.0&quot;<br><br>[libraries]<br>....<br>navigation-compose = { module = &quot;org.jetbrains.androidx.navigation:navigation-compose&quot;, version.ref = &quot;navigation-compose&quot; }<br>kotlinx-serialization = { module = &quot;org.jetbrains.kotlinx:kotlinx-serialization-json&quot;, name = &quot;kotlinx-serialization&quot;, version.ref = &quot;kotlinx-serialization&quot; }<br><br>[plugins]<br>....<br>serialization = { id = &quot;org.jetbrains.kotlin.plugin.serialization&quot;, version.ref = &quot;kotlin&quot; }</pre><p>and inside your .kts file</p><pre>plugins {<br>     ......<br>    alias(libs.plugins.serialization)<br><br>}<br>sourceSets {<br> .... <br>    commonMain.dependencies {<br>           ....<br>        implementation(libs.navigation.compose)<br>    }<br><br>}</pre><pre>@Serializable<br>data object HomeRoute<br><br>@Composable<br>fun HomeScreen(<br>    onGoCounter: () -&gt; Unit<br>) {<br>    Scaffold { padding -&gt;<br>        Column(<br>            modifier = Modifier.fillMaxSize()<br>                .padding(padding)<br>                .padding(20.dp),<br>            horizontalAlignment = Alignment.CenterHorizontally,<br>            verticalArrangement = Arrangement.Center<br>        ) {<br><br><br>            Button(<br>                onClick = onGoCounter,<br>            ) {<br>                Text(&quot;Go to counter&quot;)<br>            }<br>        }<br>    }<br>}</pre><pre>@Serializable<br>data class CounterRoute(<br>    val initialCounter: Int = 0<br>)<br><br>@OptIn(ExperimentalMaterial3Api::class)<br>@Composable<br>fun CounterBottomSheet(<br>    onDismiss: () -&gt; Unit,<br>    viewModel: CounterViewModel = viewModel()<br>) {<br>    val counter by viewModel.counter.collectAsStateWithLifecycle()<br>    ModalBottomSheet(<br>        onDismissRequest = onDismiss<br>    ) {<br><br>        Column(<br>            horizontalAlignment = Alignment.CenterHorizontally,<br>            verticalArrangement = Arrangement.Center<br>        ) {<br><br>            Row(<br>                horizontalArrangement = Arrangement.Center,<br>                verticalAlignment = Alignment.CenterVertically,<br>                modifier = Modifier.fillMaxWidth().padding(36.dp)<br>            ) {<br>                TextButton(<br>                    onClick = { viewModel.decrement() },<br>                ) {<br>                    Text(&quot;Decrement&quot;)<br>                }<br>                Text(<br>                    counter.toString(),<br>                    style = TextStyle(<br>                        fontSize = 20.sp,<br>                        fontWeight = FontWeight.Bold<br>                    )<br>                )<br>                TextButton(<br>                    onClick = { viewModel.increment() },<br>                ) {<br>                    Text(&quot;Increment&quot;)<br>                }<br>            }<br>        }<br>    }<br>}</pre><pre>class CounterViewModel(<br>    savedStateHandle: SavedStateHandle<br>) : ViewModel() {<br><br>    private val route = savedStateHandle.toRoute&lt;CounterRoute&gt;()<br><br><br>    private val _counter = MutableStateFlow(route.initialCounter)<br>    val counter = _counter.asStateFlow()<br><br><br>    fun increment() = _counter.update { it + 1 }<br>    fun decrement() = _counter.update { it - 1 }<br>}</pre><pre>@Composable<br>@Preview<br>fun App() {<br>    MaterialTheme {<br><br>        val navController = rememberNavController()<br>        NavHost(<br>            navController = navController,<br>            startDestination = HomeRoute,<br>        ) {<br>            composable&lt;HomeRoute&gt;() {<br>                HomeScreen(<br>                    onGoCounter = {<br>                        navController.navigate(CounterRoute(100))<br>                    },<br>                )<br>            }<br>            bottomSheet&lt;CounterRoute&gt;() {<br>                CounterBottomSheet(<br>                    onDismiss = navController::popBackStack,<br>                )<br>            }<br>        }<br><br>    }<br>}<br></pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GeqqiWmoRfp5cqDy89oSRQ.gif" /></figure><ul><li>Creates a dedicated <strong>NavBackStackEntry</strong></li><li>Provides a <strong>ViewModelStoreOwner</strong></li><li>Manages <strong>dismissal</strong> and <strong>lifecycle</strong></li><li>Works <strong>everywhere Compose runs</strong></li></ul><p>All I did was <strong>replace the dialog UI</strong> with a bottom sheet surface.</p><p>Everything else — state, lifecycle, navigation — came for free.</p><h3>Beyond Bottom Sheets</h3><p>Because it’s just a dialog() extension, you can:</p><ul><li>Create <strong>custom pop-ups</strong>, <strong>snacker</strong>, or <strong>anything</strong> as destinations</li><li>Pass <strong>args safely</strong> (dialog&lt;T&gt;(…))</li></ul><p>This is the same “pattern reuse” mindset that makes Compose so powerful.</p><p>source code :</p><p><a href="https://github.com/mohaberabi/cmp-bottom-sheets-navigation">GitHub - mohaberabi/cmp-bottom-sheets-navigation: a simple app shows using bottom sheets navigation in compose multiplatform</a></p><p>if you want to see how to test a viewmodel with SavedStateHandle in compose multiplatform</p><p><a href="https://medium.com/@mohaberabi98/unit-testing-viewmodels-with-savedstatehandle-in-kmp-03ba2068e57a">Unit Testing ViewModels With SavedStateHandle In KMP</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=58b790a3f52d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Unit Testing ViewModels With SavedStateHandle In KMP]]></title>
            <link>https://medium.com/@mohaberabi98/unit-testing-viewmodels-with-savedstatehandle-in-kmp-03ba2068e57a?source=rss-725d0fa3cda9------2</link>
            <guid isPermaLink="false">https://medium.com/p/03ba2068e57a</guid>
            <category><![CDATA[software-testing]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <category><![CDATA[compose-multiplatform]]></category>
            <category><![CDATA[kotl]]></category>
            <category><![CDATA[kmp]]></category>
            <dc:creator><![CDATA[Mohab erabi]]></dc:creator>
            <pubDate>Sat, 04 Oct 2025 12:26:05 GMT</pubDate>
            <atom:updated>2025-10-04T12:30:25.010Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1000/1*oVreMGA3yMZlbpLU6zK98Q.png" /></figure><p>If you’ve ever unit-tested a ViewModel that reads its navigation arguments from SavedStateHandle, you’ve probably run into this:</p><pre>Method putInt in android.os.BaseBundle not mocked.<br>See https://developer.android.com/r/studio-ui/build/not-mocked for details.</pre><h3>The setup</h3><p>I’m using <strong>Compose Navigation</strong> with <strong>Kotlinx Serialization</strong> and a custom NavType to pass complex types.</p><pre>@Serializable data object HomeScreenRoute<br><br>@Serializable data class FavoriteItem(val id: String, val name: String)<br><br>@Serializable<br>data class ComplexScreenRoute(<br>    val name: String,<br>    val id: String,<br>    val age: Int,<br>    val weight: Double,<br>    val favorites: List&lt;FavoriteItem&gt;<br>)</pre><p>NavHost with a strongly-typed route</p><pre>@Composable<br>fun AppNavHost(navController: NavHostController = rememberNavController()) {<br>    NavHost(startDestination = HomeScreenRoute, navController = navController) {<br>        composable&lt;HomeScreenRoute&gt; {<br>            HomeScreen(<br>                onGoComplexScreen = {<br>                    val route = ComplexScreenRoute(<br>                        name = &quot;kotlin&quot;,<br>                        id = &quot;kt&quot;,<br>                        age = 27,<br>                        weight = 75.5,<br>                        favorites = listOf(<br>                            FavoriteItem(&quot;1&quot;, &quot;compose&quot;),<br>                            FavoriteItem(&quot;2&quot;, &quot;coroutines&quot;),<br>                        )<br>                    )<br>                    navController.navigate(route)<br>                }<br>            )<br>        }<br><br>        composable&lt;ComplexScreenRoute&gt;(<br>            typeMap = mapOf(createNavTypePair&lt;List&lt;FavoriteItem&gt;&gt;())<br>        ) {<br>            ComplexScreen()<br>        }<br>    }<br>}</pre><p>CustomNavType for lists (serialize/deserialize)</p><pre>val navigationJson = Json {<br>    encodeDefaults = true<br>    ignoreUnknownKeys = true<br>    explicitNulls = false<br>}<br><br>inline fun &lt;reified T&gt; createNavTypePair(type: NavType&lt;T&gt; = createAppNavType&lt;T&gt;()) =<br>    typeOf&lt;T&gt;() to type<br><br>inline fun &lt;reified T&gt; createAppNavType(allowNulls: Boolean = false) =<br>    object : NavType&lt;T&gt;(isNullableAllowed = allowNulls) {<br>        override fun put(bundle: SavedState, key: String, value: T) {<br>            bundle.write { putString(key, navigationJson.encodeToString(value)) }<br>        }<br>        override fun get(bundle: SavedState, key: String): T? =<br>            bundle.read { navigationJson.decodeFromString(getString(key)) }<br>        override fun parseValue(value: String): T =<br>            navigationJson.decodeFromString(value)<br>        override fun serializeAsValue(value: T): String =<br>            navigationJson.encodeToString(value)<br>    }</pre><p>The ViewModel (the “classic” approach)</p><pre>class ComplexViewModel(<br>    private val savedStateHandle: SavedStateHandle<br>) : ViewModel() {<br><br>    val route: ComplexScreenRoute =<br>        savedStateHandle.toRoute(<br>            typeMap = mapOf(createNavTypePair&lt;List&lt;FavoriteItem&gt;&gt;())<br>        )<br>}</pre><p>Koin wiring</p><pre>val appModule = module {<br> viewModelOf(::ComplexViewModel)<br>}</pre><p>now let’s make use of the route we passed to the viewmodel</p><pre>@Composable<br>fun ComplexScreen(viewModel: ComplexViewModel = koinViewModel()) {<br> val r = viewModel.route<br> Text(&quot;Name: ${r.name}&quot;)<br> Text(&quot;Age: ${r.age}&quot;)<br> Text(&quot;Weight: ${r.weight}&quot;)<br> Text(&quot;Favorites:&quot;); r.favorites.forEach { Text(it.name) }<br>}</pre><p>now let’s run the app , but make sure you configured Koin for both IOS &amp; Android</p><pre>fun initKoin(config: KoinAppDeclaration? = null) {<br>    startKoin {<br>        config?.invoke(this)<br>        modules(<br>            appModule<br>        )<br>    }<br>}</pre><pre>class ComplexApp : Application() {<br><br>    override fun onCreate() {<br>        super.onCreate()<br>        initKoin {<br>            androidContext(this@ComplexApp)<br>        }<br>    }<br>}<br></pre><pre>fun MainViewController() = ComposeUIViewController(<br>    configure = {<br>        initKoin()<br>    }<br>) { App() }</pre><pre>@Composable<br>@Preview<br>fun App() {<br>    MaterialTheme {<br>        AppNavHost(<br><br>        )<br>    }<br>}<br><br><br></pre><p>now run the app and naviagate</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FYhrqeIn39wbEZIvFbEXEQ.png" /></figure><p>let’s now try to test the viewmdel</p><pre>class ComplexViewModelTest {<br><br><br>    private lateinit var viewModel: ComplexViewModel<br>    private val savedStateHandle = SavedStateHandle()<br><br>    private val savedState = savedState {<br>        putInt(&quot;age&quot;, 1)<br>        putDouble(&quot;weight&quot;, 1.5)<br>    }<br><br>    @BeforeTest<br>    fun setup() {<br>        viewModel = ComplexViewModel(<br>            savedStateHandle = savedStateHandle<br>        )<br>    }<br><br><br>    @Test<br>    fun ` test args passed correct`() {<br>        val route = viewModel.route<br>        assertEquals(1, route.age)<br>    }<br><br>}</pre><p>now after you run the tests</p><pre>Method putInt in android.os.BaseBundle not mocked.<br>See https://developer.android.com/r/studio-ui/build/not-mocked for details.</pre><p>that’s because savedStateHandle uses Android Bundle which needs an android tests , or to mock it but we can solve it to run it in commonTests</p><p>let’s create our interface with it’s implementation</p><pre>/**<br> * works only if used inside a viewmodel , do not exepct it to work outside a viewmodel or outside a place<br> * that is not scoped to navGraph that uses [SavedStateHandle]<br> * made to allow us to abstract the usage of navigation args provider<br> * to make us not depend on Android OS Bundle which is hard to be mocked during tests<br> * for the viewmodels that depends on [SavedStateHandle]<br> */<br>interface NavArgsProvider {<br>    fun &lt;T : Any&gt; provideArg(<br>        route: KClass&lt;T&gt;,<br>        typeMap: Map&lt;KType, @JvmSuppressWildcards NavType&lt;*&gt;&gt; = emptyMap(),<br>    ): T<br><br>    /**<br>     * for debugging to make sure that [org.koin.core.Koin] provides exactly<br>     * same instance of [SavedStateHandle] scoped to the viewmodel that depends on [NavArgsProvider]<br>     *     private fun test() {<br>     *       val sameRef = ViewModel.savedStateHandle === navArgsProvider.provideOriginalHandle()<br>     *         println(sameStateRef) true<br>     *         val sameInstance = ViewModel.savedStateHandle == navArgsProvider.provideOriginalHandle()<br>     *         println(sameStateInst) true<br>     *     }<br>     */<br>    fun provideOriginalHandle(): SavedStateHandle<br>}<br><br>class DefaultNavArgsProvider(<br>    private val savedStateHandle: SavedStateHandle<br>) : NavArgsProvider {<br>    override fun &lt;T : Any&gt; provideArg(<br>        route: KClass&lt;T&gt;,<br>        typeMap: Map&lt;KType, @JvmSuppressWildcards NavType&lt;*&gt;&gt;<br>    ): T {<br>        return savedStateHandle.toRoute(route = route)<br>    }<br><br>    override fun provideOriginalHandle(): SavedStateHandle {<br>        return savedStateHandle<br>    }<br>}</pre><p>it’s very simple method to provide the nav args but the trick here is how we will get the SavedStateHandle and also the same one that is attached to the current viewmodel instance , thanks to Koin which is navigation and viewmodel lifecycle aware by default which creates the SavedStateHandle Automatically to the current ViewModel at Runtime</p><ol><li><strong>Navigation/Compose</strong> creates a ViewModelStoreOwner per destination (usually the current NavBackStackEntry).</li><li><strong>AndroidX</strong> builds a SavedStateHandle tied to that owner (so it survives config changes/process death).</li><li><strong>Koin’s viewModel { … } factory</strong> plugs into the same ViewModelProvider pipeline, <em>optionally</em> receiving that SavedStateHandle as a <strong>parameter</strong>.</li><li>From inside your viewModel block, you can forward the <strong>exact same</strong> SavedStateHandle to <em>any</em> dependency (like DefaultNavArgsProvider) by calling get { parametersOf(state) }.</li><li>Your ViewModel then depends on NavArgsProvider (not on Android classes), keeping tests JVM-only.</li></ol><pre>val appModule = module {<br>    viewModelOf(::ComplexViewModel)<br>    factoryOf(::DefaultNavArgsProvider).bind(NavArgsProvider::class)<br>}</pre><p>here we are sure that Koin once see</p><pre>factoryOf(::DefaultNavArgsProvider).bind(NavArgsProvider::class)</pre><p>it will look for it in the dependency graph , but we did not inject it … yes but once the <strong>NavArgsProvider</strong> is needed means that it is needed by <strong>ComplexViewModel … </strong>which is now alive with it’s <strong>SavedStateHandle so </strong>koin will take this instance and provide it to our<strong> DefaultNavArgsProvider</strong></p><pre>class ComplexViewModel(<br>    private val navArgsProvider: NavArgsProvider,<br>) : ViewModel() {<br><br><br>    val route = navArgsProvider.provideArg(<br>        route = ComplexScreenRoute::class,<br>        typeMap = mapOf(<br>            createNavTypePair&lt;List&lt;FavoriteItem&gt;&gt;()<br>        )<br>    )<br><br><br>}</pre><p>now run the app and you will see nothing was changed we are still able to get the args and route inside the viewmodel and the screen</p><p>but let’s do a quick check to make sure we received same <strong>SavedStateHandle</strong> in our <strong>NavArgsProvider</strong></p><pre>package com.example.testingsavedstatehandle.viewmodel<br><br>import androidx.lifecycle.SavedStateHandle<br>import androidx.lifecycle.ViewModel<br>import androidx.navigation.toRoute<br>import com.example.testingsavedstatehandle.navigation.ComplexScreenRoute<br>import com.example.testingsavedstatehandle.navigation.DefaultNavArgsProvider<br>import com.example.testingsavedstatehandle.navigation.FavoriteItem<br>import com.example.testingsavedstatehandle.navigation.NavArgsProvider<br>import com.example.testingsavedstatehandle.navigation.createNavTypePair<br><br>class ComplexViewModel(<br>    private val navArgsProvider: NavArgsProvider,<br>    private val savedStateHandle: SavedStateHandle<br>) : ViewModel() {<br><br><br>    val route = navArgsProvider.provideArg(<br>        route = ComplexScreenRoute::class,<br>        typeMap = mapOf(<br>            createNavTypePair&lt;List&lt;FavoriteItem&gt;&gt;()<br>        )<br>    )<br><br><br>    init {<br>        test()<br>    }<br><br>    private fun test() {<br>        val sameRef = savedStateHandle === navArgsProvider.provideOriginalHandle()<br>        println(&quot;sameRef---${sameRef}&quot;)<br>        val sameContent = savedStateHandle == navArgsProvider.provideOriginalHandle()<br>        println(&quot;sameContent---${sameContent}&quot;)<br>    }<br>}</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gMjhQ9OE2bKn9PCp39KniA.png" /></figure><p>now remove all that debug code and let’s retest our viemwodel i will use a fake instance as for now , but you can also mock the object and pass it to the viewmodel</p><pre>class ComplexViewModelTest {<br><br><br>    private lateinit var viewModel: ComplexViewModel<br>    private val fakeNavArgsProvider = object : NavArgsProvider {<br>        override fun &lt;T : Any&gt; provideArg(<br>            route: KClass&lt;T&gt;,<br>            typeMap: Map&lt;KType, @JvmSuppressWildcards NavType&lt;*&gt;&gt;<br>        ): T {<br>            return ComplexScreenRoute(<br>                name = &quot;testpass&quot;,<br>                id = &quot;testPassId&quot;,<br>                age = 1,<br>                weight = 1.5,<br>                favorites = listOf()<br>            ) as T<br>        }<br><br>        override fun provideOriginalHandle(): SavedStateHandle {<br>            TODO(&quot;Not yet implemented&quot;)<br>        }<br><br>    }<br><br>    @BeforeTest<br>    fun setup() {<br><br>        viewModel = ComplexViewModel(<br>            navArgsProvider = fakeNavArgsProvider<br>        )<br>    }<br><br><br>    @Test<br>    fun ` test args passed correct`() {<br>        val route = viewModel.route<br>        assertEquals(&quot;testpass&quot;, route.name)<br>    }<br><br>}</pre><p>run tests and enjoy the green light<br>source code :</p><p><a href="https://github.com/mohaberabi/savedstatehandle-testing-kmp">https://github.com/mohaberabi/savedstatehandle-testing-kmp</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=03ba2068e57a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Develop Your Own Networking Client In Compose MultiPlatform]]></title>
            <link>https://medium.com/@mohaberabi98/develop-your-own-networking-client-in-compose-multiplatform-fac265189e40?source=rss-725d0fa3cda9------2</link>
            <guid isPermaLink="false">https://medium.com/p/fac265189e40</guid>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[system-design-interview]]></category>
            <category><![CDATA[kotlin-multiplatform]]></category>
            <dc:creator><![CDATA[Mohab erabi]]></dc:creator>
            <pubDate>Fri, 29 Nov 2024 14:34:48 GMT</pubDate>
            <atom:updated>2024-11-29T14:34:48.989Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*GH0_LbvULuAXmn_-nBmRCA.png" /></figure><p>As mobile app development increasingly shifts toward cross-platform solutions, Kotlin Multiplatform (KMP) and Compose Multiplatform have become essential tools for developers. In this article, we’ll explore the native HTTP client built using Kotlin Multiplatform and Compose Multiplatform. This project is designed to provide seamless networking capabilities for Android and iOS using their specific implementation</p><p>let’s start by making the intrfaces and expect classes</p><p>in the common Source Set</p><p>create a domain pacakge and include the below classes</p><pre>package com.mohaberabi.kmp.klient.domain.model<br><br>enum class ContentType(<br>    val value: String<br>) {<br>    Json(&quot;application/json&quot;),<br>    TEXT(&quot;text/plain&quot;)<br>}</pre><p>this is used for defining the request content type</p><pre>package com.mohaberabi.kmp.klient.domain.model<br><br>enum class NetworkMethod {<br>    GET,<br>    DELETE,<br>    PUT,<br>    POST;<br><br>    val shouldAddBody: Boolean get() = this == POST || this == PUT<br>}</pre><p>this is used for defining the request method name</p><pre>package com.mohaberabi.kmp.klient.domain.model<br><br>data class NetworkResponse(<br>    val statusCode: Int,<br>    val headers: Map&lt;String, String&gt;,<br>    val body: String?,<br>)</pre><p>a class encapsualtes the server response</p><pre>package com.mohaberabi.kmp.klient.domain.model<br><br><br>data class NetworkingRequest(<br>    val url: String,<br>    val method: NetworkMethod,<br>    val headers: Map&lt;String, String&gt; = mapOf(),<br>    val body: String? = null,<br>    val params: Map&lt;String, Any&gt; = mapOf()<br>)</pre><p>and this for the request model</p><pre>package com.mohaberabi.kmp.klient.domain.model<br><br>data class NetworkingConfiguration(<br>    val connectionTimeOut: Int = 30 * 1000,<br>    val readTimeOut: Int = 30 * 1000,<br>    val contentType: ContentType = ContentType.Json<br>)</pre><p>some configuration that might be useful when making requests</p><p>lets also make some class that we will expect it to be returned from servr for testing</p><pre>package com.mohaberabi.kmp.klient.domain.model<br><br>data class PostModel(<br>    val title: String,<br>    val body: String,<br>    val userId: Int<br>)<br><br></pre><p>now let’s start making the networking logic</p><pre>package com.mohaberabi.kmp.klient.domain.source<br><br>import com.mohaberabi.kmp.klient.domain.model.NetworkResponse<br><br>interface Networking {<br><br><br>    suspend fun get(<br>        url: String,<br>        params: Map&lt;String, Any&gt; = mapOf(),<br>        headers: Map&lt;String, String&gt; = mapOf()<br>    ): NetworkResponse<br><br><br>    suspend fun post(<br>        url: String,<br>        headers: Map&lt;String, String&gt; = mapOf(),<br>        body: String? = null<br>    ): NetworkResponse<br><br>    suspend fun put(<br>        url: String,<br>        headers: Map&lt;String, String&gt; = mapOf(),<br>        body: String? = null<br>    ): NetworkResponse<br><br>    suspend fun delete(<br>        url: String,<br>        headers: Map&lt;String, String&gt; = mapOf(),<br>        queryParams: Map&lt;String, Any&gt; = emptyMap()<br>    ): NetworkResponse<br>}</pre><p>we can also make our own interceptors , but a simple one for now</p><pre>package com.mohaberabi.kmp.klient.domain.source<br><br>import com.mohaberabi.kmp.klient.domain.model.NetworkResponse<br>import com.mohaberabi.kmp.klient.domain.model.NetworkingRequest<br><br>interface NetworkInterceptor {<br>    suspend fun intercept(<br>        request: NetworkingRequest,<br>        chain: InterceptorChain,<br>    ): NetworkResponse<br>}<br><br></pre><p>now we make the class that appends and processes the intercpetors</p><pre>package com.mohaberabi.kmp.klient.domain.source<br><br>import com.mohaberabi.kmp.klient.domain.model.NetworkResponse<br>import com.mohaberabi.kmp.klient.domain.model.NetworkingRequest<br><br>class InterceptorChain(<br>    private val interceptors: List&lt;NetworkInterceptor&gt;,<br>    private val index: Int,<br>    private val onReady: suspend (NetworkingRequest) -&gt; NetworkResponse<br>) {<br><br>    suspend fun proceed(<br>        request: NetworkingRequest<br>    ): NetworkResponse {<br>        return if (index &gt;= interceptors.size) {<br>            onReady(request)<br>        } else {<br>            val nextChain = InterceptorChain(<br>                interceptors = interceptors,<br>                index = index + 1,<br>                onReady = onReady<br>            )<br>            val nextInterceptor = interceptors[index]<br>            return nextInterceptor.intercept(request, nextChain)<br>        }<br><br>    }<br>}</pre><p>this class will keep intercepting the request until no interceptors yet to process then we execute the suspend onReady() function that is actual pivot method of the class</p><p>we now make the clinet class for encapuslates the networking</p><pre>package com.mohaberabi.kmp.klient.domain.source<br><br>import com.mohaberabi.kmp.klient.domain.model.NetworkResponse<br>import com.mohaberabi.kmp.klient.domain.model.NetworkingRequest<br><br><br>interface NetworkClient {<br>    suspend fun makeRequest(<br>        request: NetworkingRequest,<br>    ): NetworkResponse<br><br>    fun addInterceptor(<br>        interceptor: NetworkInterceptor,<br>    )<br>}</pre><p>now lets make a class that we will make use of it to test the client we are making , it has nothing to do with networking logic we will implement it with the help of our client we will implement</p><pre>package com.mohaberabi.kmp.klient.domain.source<br><br>import com.mohaberabi.kmp.klient.domain.model.PostModel<br><br>interface ApiPlayGround {<br><br>    suspend fun getPost(<br>        id: Int<br>    ): Result&lt;PostModel&gt;<br><br>    suspend fun addPost(<br>        post: PostModel<br>    ): Result&lt;Unit&gt;<br><br>    suspend fun deletePost(<br>        id: Int<br>    ): Result&lt;Unit&gt;<br>}</pre><p>now lets make the platform specific expect classes</p><p>lets make the engine that each platform will use to make a remote requests</p><pre>package com.mohaberabi.kmp.klient.platform<br><br>import com.mohaberabi.kmp.klient.domain.model.NetworkResponse<br>import com.mohaberabi.kmp.klient.domain.model.NetworkingRequest<br><br>expect class HttpEngine {<br>    suspend fun makeRequest(<br>        request: NetworkingRequest,<br>    ): NetworkResponse<br><br>}</pre><p>we can also make a factory for it for ease of creating it</p><pre>package com.mohaberabi.kmp.klient.platform<br><br>import com.mohaberabi.kmp.klient.domain.model.NetworkingConfiguration<br><br>expect class HttpEngineFactory() {<br>    fun create(<br>        config: NetworkingConfiguration,<br>    ): HttpEngine<br>}</pre><p>lets make some logging to use it as logging interceptor as well</p><pre>package com.mohaberabi.kmp.klient.platform<br><br>expect object AppLogger {<br>    fun e(tag: String, message: String, throwable: Throwable? = null)<br>    fun d(tag: String, message: String)<br>    fun i(tag: String, message: String)<br><br>}</pre><p>for ease of use and because each has some encoding mechanism we will make a url builder for each of ios and android</p><pre>package com.mohaberabi.kmp.klient.platform<br><br>expect fun buildUrl(<br>    url: String,<br>    params: Map&lt;String, Any?&gt; = mapOf(),<br>): String</pre><p>now let us start implmenting the android part we will use the Java HttpUrlConnection for making requests as the engine for android ,</p><p>for both of androdi and ios it will be one function that makes any remote reequest get , post , delete …etc but we pass to it the type of the method we need to make and the required needed params for the method to be exectued we will also wrap them all inside of a suspended function to avoid blocking calls</p><p>Android Engine</p><pre>package com.mohaberabi.kmp.klient.platform<br><br>import com.mohaberabi.kmp.klient.domain.model.NetworkResponse<br>import com.mohaberabi.kmp.klient.domain.model.NetworkingConfiguration<br>import com.mohaberabi.kmp.klient.domain.model.NetworkingRequest<br>import kotlinx.coroutines.suspendCancellableCoroutine<br>import java.net.HttpURLConnection<br>import java.net.URL<br>import kotlin.coroutines.cancellation.CancellationException<br>import kotlin.coroutines.resume<br>import kotlin.coroutines.resumeWithException<br><br>actual class HttpEngine(<br>    private val config: NetworkingConfiguration<br>) {<br>    actual suspend fun makeRequest(request: NetworkingRequest): NetworkResponse {<br>        return suspendCancellableCoroutine { continuation -&gt;<br>            try {<br>                val method = request.method<br>                val headers = request.headers<br>                val body = request.body<br>                val url = request.url<br>                val connection = URL(url).openConnection() as HttpURLConnection<br>                connection.apply {<br>                    requestMethod = method.name<br>                    connectTimeout = config.connectionTimeOut<br>                    readTimeout = config.readTimeOut<br>                    headers.forEach { (key, value) -&gt;<br>                        addRequestProperty(key, value)<br>                        if (body != null &amp;&amp; method.shouldAddBody) {<br>                            doOutput = true<br>                            val encodedBody = body.encodeToByteArray()<br>                            outputStream.use { stream -&gt;<br>                                stream.write(encodedBody)<br>                            }<br>                        }<br>                    }<br>                }<br><br>                val code = connection.responseCode<br>                val responseBody = connection.inputStream.bufferedReader().use { it.readText() }<br>                val responseHeaders = connection.headerFields.filterKeys { it != null }<br>                connection.disconnect()<br>                val response = NetworkResponse(<br>                    statusCode = code,<br>                    body = responseBody,<br>                    headers = responseHeaders.mapValues { it.value.joinToString { &quot;, &quot; } },<br>                )<br>                continuation.resume(response)<br>            } catch (e: Exception) {<br>                if (e is CancellationException) {<br>                    throw e<br>                }<br>                e.printStackTrace()<br>                continuation.resumeWithException(e)<br>            }<br><br>        }<br><br><br>    }<br><br><br>}</pre><p>Android Engine Factory</p><pre>package com.mohaberabi.kmp.klient.platform<br><br>import com.mohaberabi.kmp.klient.domain.model.NetworkingConfiguration<br><br>actual class HttpEngineFactory {<br>    actual fun create(<br>        config: NetworkingConfiguration,<br>    ): HttpEngine = HttpEngine(config)<br><br>}</pre><p>Android Logger</p><pre>package com.mohaberabi.kmp.klient.platform<br><br>import android.util.Log<br><br>actual object AppLogger {<br>    actual fun e(tag: String, message: String, throwable: Throwable?) {<br>        if (throwable != null) {<br>            Log.e(tag, message, throwable)<br>        } else {<br>            Log.e(tag, message)<br>        }<br>    }<br><br>    actual fun d(tag: String, message: String) {<br>        Log.d(tag, message)<br>    }<br><br>    actual fun i(tag: String, message: String) {<br>        Log.i(tag, message)<br>    }<br><br><br>}</pre><p>Android Url Builder</p><pre>package com.mohaberabi.kmp.klient.platform<br><br>actual fun buildUrl(<br>    url: String,<br>    params: Map&lt;String, Any?&gt;<br>): String {<br>    if (params.isEmpty()) return url<br>    val encodedParams = params.map { (key, value) -&gt;<br>        &quot;${key}=${value.toString().urlEncode()}&quot;<br>    }.joinToString(&quot;&amp;&quot;)<br>    return &quot;$url?$encodedParams&quot;<br><br>}<br><br>fun String.urlEncode(): String = java.net.URLEncoder.encode(this, &quot;UTF-8&quot;)</pre><p>now for IOS Part</p><p>IOS Engine</p><pre>package com.mohaberabi.kmp.klient.platform<br><br>import com.mohaberabi.kmp.klient.domain.model.NetworkResponse<br>import com.mohaberabi.kmp.klient.domain.model.NetworkingConfiguration<br>import com.mohaberabi.kmp.klient.domain.model.NetworkingRequest<br>import com.mohaberabi.kmp.klient.utils.toNSData<br>import com.mohaberabi.kmp.klient.utils.toUtf8String<br>import kotlinx.coroutines.suspendCancellableCoroutine<br>import platform.Foundation.HTTPBody<br>import platform.Foundation.HTTPMethod<br>import platform.Foundation.NSHTTPURLResponse<br>import platform.Foundation.NSMutableURLRequest<br>import platform.Foundation.NSURL<br>import platform.Foundation.NSURLSession<br>import platform.Foundation.dataTaskWithRequest<br>import platform.Foundation.setValue<br>import kotlin.coroutines.resume<br>import kotlin.coroutines.resumeWithException<br><br>actual class HttpEngine(<br>    private val config: NetworkingConfiguration,<br>) {<br>    actual suspend fun makeRequest(request: NetworkingRequest): NetworkResponse {<br>        return suspendCancellableCoroutine { contintuation -&gt;<br>            val url = request.url<br>            val nsUrl = NSURL.URLWithString(url)<br>                ?: run {<br>                    contintuation.resumeWithException(Exception(&quot;Url is badly formatted&quot;))<br>                    return@suspendCancellableCoroutine<br>                }<br>            val nsRequest = NSMutableURLRequest.requestWithURL(nsUrl)<br>            nsRequest.apply {<br>                setTimeoutInterval((config.connectionTimeOut / 1000).toDouble())<br>                setAllowsCellularAccess(true)<br>                HTTPMethod = request.method.name<br>                request.headers.forEach { (key, value) -&gt;<br>                    setValue(value = value, forHTTPHeaderField = key)<br>                }<br>                request.body?.let {<br>                    HTTPBody = request.body.encodeToByteArray().toNSData()<br>                }<br>            }<br>            val session = NSURLSession.sharedSession<br><br><br>            val task = session.dataTaskWithRequest(<br>                request = nsRequest,<br>                completionHandler = { data, response, error -&gt;<br>                    if (error != null) {<br>                        contintuation.resumeWithException(<br>                            Throwable(<br>                                message = error.localizedDescription,<br>                                cause = Throwable(error.localizedFailureReason)<br>                            )<br>                        )<br>                        return@dataTaskWithRequest<br>                    }<br><br>                    val httpResponse = response as? NSHTTPURLResponse<br><br>                    val status = httpResponse?.statusCode?.toInt() ?: 500<br>                    val headers = httpResponse?.allHeaderFields?.entries?.associate {<br>                        it.key.toString() to it.value.toString()<br>                    }.orEmpty()<br>                    val body = data?.toUtf8String()<br>                    contintuation.resume(NetworkResponse(status, headers, body))<br>                },<br>            )<br>            task.resume()<br>        }<br><br><br>    }<br><br><br>}</pre><p>IOS Engine Factory</p><pre>package com.mohaberabi.kmp.klient.platform<br><br>import com.mohaberabi.kmp.klient.domain.model.NetworkingConfiguration<br><br>actual class HttpEngineFactory {<br>    actual fun create(<br>        config: NetworkingConfiguration,<br>    ): HttpEngine = HttpEngine(config)<br><br><br>}</pre><p>Ios Url Builder</p><pre>package com.mohaberabi.kmp.klient.platform<br><br>import platform.Foundation.*<br><br>actual fun buildUrl(<br>    url: String,<br>    params: Map&lt;String, Any?&gt;<br>): String {<br>    val components = NSURLComponents.componentsWithString(url) ?: return url<br><br>    if (params.isNotEmpty()) {<br>        val queryItems = params.map { (key, value) -&gt;<br>            NSURLQueryItem(name = key, value = value?.toString())<br>        }<br>        components.queryItems = queryItems<br>    }<br><br>    return components.URL?.absoluteString ?: url<br>}</pre><p>Ios Logger</p><pre>package com.mohaberabi.kmp.klient.platform<br><br>import platform.Foundation.NSLog<br><br>actual object AppLogger {<br>    actual fun e(tag: String, message: String, throwable: Throwable?) {<br><br>        if (throwable != null) {<br>            NSLog(&quot;ERROR: [$tag] $message. Throwable: $throwable CAUSE ${throwable.cause}&quot;)<br>        } else {<br>            NSLog(&quot;ERROR: [$tag] $message&quot;)<br>        }<br>    }<br><br>    actual fun d(tag: String, message: String) {<br>        NSLog(&quot;DEBUG: [$tag] $message&quot;)<br>    }<br><br>    actual fun i(tag: String, message: String) {<br>        NSLog(&quot;INFO: [$tag] $message&quot;)<br>    }<br><br>}</pre><p>now we have our engine for each platform lets go back again to our common code</p><p>we will need some serialization mechanism we could have used any kotlin serialization library but for simplicty and as the data is small we will make serializaion our own for ios and android using their specific tools</p><p>so lets make it a rule we serialize into and from a map</p><pre>package com.mohaberabi.kmp.klient.platform<br><br><br>expect fun Map&lt;String, Any?&gt;.toJsonString(): String</pre><p>for android we will use java json object it is straightforward and simple</p><pre>package com.mohaberabi.kmp.klient.platform<br><br>import org.json.JSONArray<br>import org.json.JSONException<br>import org.json.JSONObject<br><br>actual fun Map&lt;String, Any?&gt;.toJsonString(): String {<br>    val jsonObject = JSONObject()<br>    forEach { (key, value) -&gt;<br>        jsonObject.put(key, value)<br>    }<br>    return jsonObject.toString()<br>}<br><br><br>fun String.toJsonObject() = JSONObject(this)<br>fun &lt;T&gt; JSONObject.safeGet(<br>    default: T,<br>    block: JSONObject.() -&gt; T,<br>): T {<br>    return try {<br>        block()<br>    } catch (e: JSONException) {<br>        default<br>    }<br>}<br><br><br>fun JSONArray.toList(): List&lt;String&gt; {<br>    val list = mutableListOf&lt;String&gt;()<br>    for (i in 0 until this.length()) {<br>        list.add(this.getString(i))<br>    }<br>    return list<br>}</pre><p>we made an extension function safe get for null safe and avoiding excptions while parsing</p><p>ios has same mechanism but slightly different , ios has a class Dictionary wich is in the end a map</p><pre>package com.mohaberabi.kmp.klient.platform<br><br>import com.mohaberabi.kmp.klient.utils.toUtf8String<br>import kotlinx.cinterop.ExperimentalForeignApi<br>import platform.Foundation.NSArray<br>import platform.Foundation.NSDictionary<br>import platform.Foundation.NSJSONSerialization<br>import platform.Foundation.NSMutableArray<br>import platform.Foundation.NSMutableDictionary<br>import platform.Foundation.NSNull<br>import platform.Foundation.setValue<br><br>@OptIn(ExperimentalForeignApi::class)<br>actual fun Map&lt;String, Any?&gt;.toJsonString(): String {<br>    val nsDictionary = this.toNSDictionary()<br>    val jsonData = NSJSONSerialization.dataWithJSONObject(nsDictionary, 0u, null)<br>    return jsonData?.toUtf8String() ?: throw Exception(&quot;Failed to serialize to JSON&quot;)<br>}<br><br>fun Map&lt;String, Any?&gt;.toNSDictionary(): NSDictionary {<br>    val dictionary = NSMutableDictionary()<br>    forEach { (key, value) -&gt;<br>        val nsValue = when (value) {<br>            null -&gt; NSNull()<br>            is String -&gt; value<br>            is Number -&gt; value<br>            is Boolean -&gt; value<br>            is Map&lt;*, *&gt; -&gt; (value as Map&lt;String, Any?&gt;).toNSDictionary()<br>            is List&lt;*&gt; -&gt; (value as List&lt;Any?&gt;).toNSArray()<br>            else -&gt; throw IllegalArgumentException(&quot;Unsupported type: ${value::class}&quot;)<br>        }<br>        dictionary.setValue(value = nsValue, forKey = key)<br>    }<br>    return dictionary<br>}<br><br>fun List&lt;Any?&gt;.toNSArray(): NSArray {<br>    val array = NSMutableArray()<br>    forEach { item -&gt;<br>        val nsValue = when (item) {<br>            null -&gt; NSNull()<br>            is String -&gt; item<br>            is Number -&gt; item<br>            is Boolean -&gt; item<br>            is Map&lt;*, *&gt; -&gt; (item as Map&lt;String, Any?&gt;).toNSDictionary()<br>            is List&lt;*&gt; -&gt; (item as List&lt;Any?&gt;).toNSArray()<br>            else -&gt; throw IllegalArgumentException(&quot;Unsupported type: ${item::class}&quot;)<br>        }<br>        array.addObject(nsValue)<br>    }<br>    return array<br>}</pre><p>and now we are ready to implment the networkign logic with serialization</p><p>lets create a class that accpets seerialization to be mapped to our class “Post”</p><pre>package com.mohaberabi.kmp.klient.data<br><br>import com.mohaberabi.kmp.klient.domain.model.PostModel<br>import com.mohaberabi.kmp.klient.platform.toJsonString<br><br><br>data class PostDto(<br>    val title: String,<br>    val body: String,<br>    val userId: Int<br>)<br><br>fun PostDto.toPost() = PostModel(<br>    title, body, userId<br>)<br><br>fun PostModel.toPostRequestDto() = PostDto(<br>    title, body, userId<br>)<br><br>fun PostDto.toJson() = mapOf(<br>    &quot;title&quot; to title,<br>    &quot;body&quot; to body,<br>    &quot;userId&quot; to userId<br>).toJsonString()</pre><p>we made a map of our class so we can be able to easy parse it</p><p>one last thing each one of the platforms will have it is own parsing logic</p><pre>package com.mohaberabi.kmp.klient<br><br>import com.mohaberabi.kmp.klient.data.BookDto<br>import com.mohaberabi.kmp.klient.data.PostDto<br><br><br>expect fun String.toBookDto(): BookDto<br>expect fun String.toPostDto(): PostDto</pre><p>Android Part</p><pre>package com.mohaberabi.kmp.klient<br><br>import com.mohaberabi.kmp.klient.data.PostDto<br>import com.mohaberabi.kmp.klient.platform.safeGet<br>import com.mohaberabi.kmp.klient.platform.toJsonObject<br><br><br>actual fun String.toPostDto(): PostDto {<br>    val json = toJsonObject()<br>    val title = json.safeGet(&quot;unknown&quot;) { getString(&quot;title&quot;) }<br>    val body = json.safeGet(&quot;unknown&quot;) { getString(&quot;body&quot;) }<br>    val userId = json.safeGet(1) { getInt(&quot;userId&quot;) }<br>    return PostDto(title, body, userId)<br>}</pre><p>Ios Part</p><pre>package com.mohaberabi.kmp.klient<br><br>import com.mohaberabi.kmp.klient.data.PostDto<br>import com.mohaberabi.kmp.klient.utils.toNSData<br>import kotlinx.cinterop.ExperimentalForeignApi<br>import platform.Foundation.NSArray<br>import platform.Foundation.NSDictionary<br>import platform.Foundation.NSJSONReadingAllowFragments<br>import platform.Foundation.NSJSONSerialization<br>import platform.Foundation.valueForKey<br><br><br>fun &lt;T&gt; NSDictionary.safeGet(<br>    default: T,<br>    block: NSDictionary.() -&gt; T?<br>): T {<br>    return try {<br>        block() ?: default<br>    } catch (e: Exception) {<br>        default<br>    }<br>}<br><br><br>@OptIn(ExperimentalForeignApi::class)<br>actual fun String.toPostDto(): PostDto {<br>    val jsonData = this.encodeToByteArray().toNSData()<br>    val jsonObject = NSJSONSerialization.JSONObjectWithData(<br>        jsonData,<br>        NSJSONReadingAllowFragments,<br>        null<br>    ) as? NSDictionary ?: throw Exception(&quot;Invalid JSON format&quot;)<br>    val title = jsonObject.safeGet(&quot;unknown&quot;) { valueForKey(&quot;title&quot;) as? String }<br><br>    val body = jsonObject.safeGet(&quot;unknown&quot;) { valueForKey(&quot;body&quot;) as? String }<br>    val userId = jsonObject.safeGet(1) { valueForKey(&quot;userId&quot;) as? Int }<br><br><br>    return PostDto(title, body, userId)<br><br>}</pre><p>back again to our common source set</p><p>networking implementation</p><pre>package com.mohaberabi.kmp.klient.data<br><br>import com.mohaberabi.kmp.klient.platform.buildUrl<br>import com.mohaberabi.kmp.klient.domain.model.NetworkMethod<br>import com.mohaberabi.kmp.klient.domain.model.NetworkResponse<br>import com.mohaberabi.kmp.klient.domain.model.NetworkingRequest<br>import com.mohaberabi.kmp.klient.domain.source.NetworkClient<br>import com.mohaberabi.kmp.klient.domain.source.Networking<br><br><br>class DefaultNetworking(<br>    private val client: NetworkClient,<br>) : Networking {<br>    override suspend fun get(<br>        url: String,<br>        params: Map&lt;String, Any&gt;,<br>        headers: Map&lt;String, String&gt;<br>    ): NetworkResponse {<br>        val fullUrl = buildUrl(url, params)<br>        val request = NetworkingRequest(<br>            method = NetworkMethod.GET,<br>            url = fullUrl,<br>            headers = headers<br>        )<br>        return client.makeRequest(request)<br>    }<br><br>    override suspend fun post(<br>        url: String,<br>        headers: Map&lt;String, String&gt;,<br>        body: String?,<br>    ): NetworkResponse {<br>        val fullUrl = buildUrl(url)<br>        val request = NetworkingRequest(<br>            method = NetworkMethod.POST,<br>            url = fullUrl,<br>            headers = headers,<br>            body = body<br>        )<br>        return client.makeRequest(request)<br>    }<br><br>    override suspend fun put(<br>        url: String,<br>        headers: Map&lt;String, String&gt;,<br>        body: String?,<br>    ): NetworkResponse {<br>        val fullUrl = buildUrl(url)<br>        val request = NetworkingRequest(<br>            method = NetworkMethod.PUT,<br>            url = fullUrl,<br>            headers = headers,<br>            body = body<br>        )<br>        return client.makeRequest(request)<br>    }<br><br>    override suspend fun delete(<br>        url: String,<br>        headers: Map&lt;String, String&gt;,<br>        queryParams: Map&lt;String, Any&gt;<br>    ): NetworkResponse {<br>        val fullUrl = buildUrl(url)<br>        val request = NetworkingRequest(<br>            method = NetworkMethod.DELETE,<br>            url = fullUrl,<br>            headers = headers,<br>        )<br>        return client.makeRequest(request)<br>    }<br><br><br>}</pre><p>client impl</p><pre>package com.mohaberabi.kmp.klient.data<br><br>import com.mohaberabi.kmp.klient.platform.HttpEngine<br>import com.mohaberabi.kmp.klient.domain.model.NetworkResponse<br>import com.mohaberabi.kmp.klient.domain.model.NetworkingRequest<br>import com.mohaberabi.kmp.klient.domain.source.InterceptorChain<br>import com.mohaberabi.kmp.klient.domain.source.NetworkClient<br>import com.mohaberabi.kmp.klient.domain.source.NetworkInterceptor<br><br>class DefaultNetworkingClient(<br>    private val engine: HttpEngine<br>) : NetworkClient {<br><br>    private val interceptors = mutableListOf&lt;NetworkInterceptor&gt;()<br>    override fun addInterceptor(interceptor: NetworkInterceptor) {<br>        interceptors.add(interceptor)<br>    }<br><br>    override suspend fun makeRequest(request: NetworkingRequest): NetworkResponse {<br>        val chain = InterceptorChain(<br>            interceptors = interceptors,<br>            index = 0,<br>            onReady = { engine.makeRequest(request) })<br>        return chain.proceed(request)<br><br>    }<br>}</pre><p>now lets use our library to get , add , delete posts</p><pre>package com.mohaberabi.kmp.klient.data<br><br>import com.mohaberabi.kmp.klient.domain.model.PostModel<br>import com.mohaberabi.kmp.klient.domain.source.ApiPlayGround<br>import com.mohaberabi.kmp.klient.domain.source.Networking<br>import com.mohaberabi.kmp.klient.toBookDto<br>import com.mohaberabi.kmp.klient.toPostDto<br>import kotlinx.coroutines.Dispatchers<br>import kotlinx.coroutines.IO<br>import kotlinx.coroutines.withContext<br>import kotlin.coroutines.cancellation.CancellationException<br><br>class DefaultApiPlayground(<br>    private val networking: Networking,<br>) : ApiPlayGround {<br><br><br>    override suspend fun getPost(id: Int): Result&lt;PostModel&gt; {<br>        return withContext(Dispatchers.IO) {<br>            kotlin.runCatching {<br>                val response = networking.get(&quot;https://jsonplaceholder.typicode.com/posts/$id&quot;)<br>                val body = response.body ?: throw Exception(&quot;Response is null&quot;)<br>                val postDto = body.toPostDto()<br>                val post = postDto.toPost()<br>                post<br>            }.onFailure {<br>                if (it is CancellationException) {<br>                    throw it<br>                }<br>            }<br>        }<br>    }<br><br>    override suspend fun addPost(post: PostModel): Result&lt;Unit&gt; {<br><br>        return withContext(Dispatchers.IO) {<br>            kotlin.runCatching {<br>                val response = networking.post(<br>                    &quot;https://jsonplaceholder.typicode.com/posts&quot;,<br>                    body = post.toPostRequestDto().toJson()<br>                )<br>            }.onFailure {<br>                if (it is CancellationException) {<br>                    throw it<br>                }<br>            }<br>        }<br>    }<br><br>    override suspend fun deletePost(id: Int): Result&lt;Unit&gt; {<br>        return kotlin.runCatching {<br>            val response = networking.delete(<br>                &quot;https://jsonplaceholder.typicode.com/posts/${id}&quot;,<br>            )<br>        }.onFailure {<br>            if (it is CancellationException) {<br>                throw it<br>            }<br>        }<br><br>    }<br>}<br><br></pre><p>let us also use our interceptor to add some interceptors to our chain</p><pre>package com.mohaberabi.kmp.klient.data<br><br>import com.mohaberabi.kmp.klient.platform.AppLogger<br>import com.mohaberabi.kmp.klient.domain.model.ContentType<br>import com.mohaberabi.kmp.klient.domain.model.NetworkResponse<br>import com.mohaberabi.kmp.klient.domain.model.NetworkingRequest<br>import com.mohaberabi.kmp.klient.domain.source.InterceptorChain<br>import com.mohaberabi.kmp.klient.domain.source.NetworkInterceptor<br><br>class LoggingInterceptor : NetworkInterceptor {<br>    override suspend fun intercept(<br>        request: NetworkingRequest,<br>        chain: InterceptorChain<br>    ): NetworkResponse {<br>        AppLogger.d(&quot;LoggingInterceptor&quot;, &quot;REQUEST:Url-&gt;${request.url}&quot;)<br>        AppLogger.d(&quot;LoggingInterceptor&quot;, &quot;REQUEST:Body-&gt;${request.body}&quot;)<br>        AppLogger.d(&quot;LoggingInterceptor&quot;, &quot;REQUEST:Headers-&gt;${request.headers}&quot;)<br>        AppLogger.d(&quot;LoggingInterceptor&quot;, &quot;REQUEST:Params-&gt;${request.params}&quot;)<br>        val response = chain.proceed(request)<br>        AppLogger.d(&quot;LoggingInterceptor&quot;, &quot;RESPONSE:Status-&gt;${response.statusCode}&quot;)<br>        AppLogger.d(&quot;LoggingInterceptor&quot;, &quot;RESPONSE:Headers-&gt;${response.headers}&quot;)<br>        AppLogger.d(&quot;LoggingInterceptor&quot;, &quot;RESPONSE:Body-&gt;${response.body}&quot;)<br>        return response<br>    }<br>}<br><br>class DefaultInterceptor(<br>    private val type: ContentType<br>) : NetworkInterceptor {<br>    override suspend fun intercept(<br>        request: NetworkingRequest,<br>        chain: InterceptorChain<br>    ): NetworkResponse {<br>        val headers = request.headers.toMutableMap()<br>        headers[&quot;Content-Type&quot;] = type.value<br>        val newRequest = request.copy(headers = headers.toMap())<br>        return chain.proceed(newRequest)<br>    }<br>}</pre><p>lets make some composbales sceen to test our app</p><pre>package com.mohaberabi.kmp.klient<br><br>import androidx.compose.foundation.layout.Arrangement<br>import androidx.compose.foundation.layout.Column<br>import androidx.compose.foundation.layout.fillMaxSize<br>import androidx.compose.foundation.layout.padding<br>import androidx.compose.material.Button<br>import androidx.compose.material.CircularProgressIndicator<br>import androidx.compose.material.Scaffold<br>import androidx.compose.material.Text<br><br>import androidx.compose.runtime.Composable<br>import androidx.compose.runtime.LaunchedEffect<br>import androidx.compose.runtime.getValue<br>import androidx.compose.runtime.mutableStateOf<br>import androidx.compose.runtime.remember<br>import androidx.compose.runtime.rememberCoroutineScope<br>import androidx.compose.runtime.setValue<br>import androidx.compose.ui.Alignment<br>import androidx.compose.ui.Modifier<br>import androidx.compose.ui.text.font.FontWeight<br>import androidx.compose.ui.text.style.TextAlign<br>import androidx.compose.ui.unit.sp<br>import com.mohaberabi.kmp.klient.data.DefaultInterceptor<br>import com.mohaberabi.kmp.klient.data.DefaultNetworking<br>import com.mohaberabi.kmp.klient.data.DefaultNetworkingClient<br>import com.mohaberabi.kmp.klient.data.LoggingInterceptor<br>import com.mohaberabi.kmp.klient.data.DefaultApiPlayground<br>import com.mohaberabi.kmp.klient.domain.model.ContentType<br>import com.mohaberabi.kmp.klient.domain.model.NetworkingConfiguration<br>import com.mohaberabi.kmp.klient.domain.model.PostModel<br>import com.mohaberabi.kmp.klient.platform.AppLogger<br>import com.mohaberabi.kmp.klient.platform.HttpEngineFactory<br>import kotlinx.coroutines.launch<br><br>@Composable<br>fun HomeScreen(modifier: Modifier = Modifier) {<br><br>    val client = remember {<br>        DefaultNetworkingClient(<br>            HttpEngineFactory().create(<br>                NetworkingConfiguration(),<br>            )<br>        ).apply {<br>            addInterceptor(LoggingInterceptor())<br>            addInterceptor(DefaultInterceptor(ContentType.Json))<br>        }<br>    }<br>    val networking = remember {<br>        DefaultNetworking(client)<br>    }<br>    val source = remember {<br>        DefaultApiPlayground(networking)<br>    }<br>    var loading by remember {<br><br>        mutableStateOf(false)<br>    }<br>    var post by remember {<br>        mutableStateOf&lt;PostModel?&gt;(null)<br>    }<br><br>    val scope = rememberCoroutineScope()<br>    LaunchedEffect(<br>        key1 = Unit,<br>    ) {<br>        loading = true<br>        source.getPost(1)<br>            .onFailure {<br>                it.printStackTrace()<br>                loading = false<br>            }.onSuccess {<br>                post = it<br>                loading = false<br>            }<br><br>    }<br><br>    Scaffold { padding -&gt;<br>        Column(<br>            modifier = modifier<br>                .fillMaxSize()<br>                .padding(padding),<br>            horizontalAlignment = Alignment.CenterHorizontally,<br>            verticalArrangement = Arrangement.Center<br>        ) {<br>            if (loading) {<br>                CircularProgressIndicator()<br>            } else {<br>                post?.let {<br>                    Text(text = &quot;Post Found&quot;)<br>                    Text(<br>                        text = it.title,<br>                        fontSize = 20.sp,<br>                        textAlign = TextAlign.Center,<br>                        fontWeight = FontWeight.Bold<br>                    )<br>                } ?: Text(text = &quot;Something Went Wrong&quot;)<br>            }<br><br><br>            Button(<br>                onClick = {<br>                    scope.launch {<br>                        source.addPost(PostModel(&quot;test&quot;, &quot;test&quot;, 1))<br>                            .onFailure {<br>                                it.printStackTrace()<br>                            }.onSuccess { AppLogger.d(&quot;post&quot;, &quot;Post Added&quot;) }<br>                    }<br>                },<br>            ) {<br>                Text(&quot;Add Post Test&quot;)<br>            }<br>            Button(<br>                onClick = {<br>                    scope.launch {<br>                        source.deletePost(111)<br>                            .onFailure {<br>                                it.printStackTrace()<br>                            }.onSuccess { AppLogger.d(&quot;post&quot;, &quot;Post Deleted&quot;) }<br>                    }<br>                },<br>            ) {<br>                Text(&quot;Delete Post Test&quot;)<br>            }<br>        }<br>    }<br>}</pre><pre>import klient.composeapp.generated.resources.Res<br>import klient.composeapp.generated.resources.compose_multiplatform<br><br>@Composable<br>@Preview<br>fun App() {<br>    MaterialTheme {<br>        HomeScreen()<br>    }<br>}</pre><p>Github Repository : <a href="https://github.com/mohaberabi/klient">https://github.com/mohaberabi/klient</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fac265189e40" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>