mobile-wallet/core-base/platform/README.md

18 KiB

Platform Module Documentation

Overview

The platform module provides a comprehensive abstraction layer for platform-specific implementations across multiple targets (Android, Desktop, JS, Native, WasmJs) in a Kotlin Multiplatform project. It follows the expect/actual pattern to create uniform APIs that work seamlessly across platforms while maintaining native functionality.

Core Purpose

  • Isolate platform-specific code to improve maintainability
  • Provide consistent APIs across platforms
  • Enable platform-specific optimizations without changing client code
  • Support Kotlin Multiplatform Project (KMP) architecture

Architecture Design

Composition Pattern

The module uses Jetpack Compose's CompositionLocal pattern to provide platform-specific implementations throughout the application without explicit dependency injection. This creates a hierarchy of providers that can be accessed from any composable function.

App
└── LocalManagerProvider
    ├── LocalAppReviewManager
    ├── LocalIntentManager
    └── LocalAppUpdateManager

Platform Implementation Strategy

For each platform, three levels of abstraction are implemented:

  1. Common Interfaces: Defined in commonMain using expect declarations
  2. Platform Interfaces: Platform-specific abstractions in respective source sets
  3. Concrete Implementations: Platform-specific implementations of the interfaces
commonMain
├── Interfaces (expect)
├── Models
└── Utilities
    
androidMain
├── Concrete implementations
└── Android-specific utilities

desktopMain/jsMain/nativeMain/wasmJsMain
└── Placeholder implementations

Common Interfaces and Types

AppContext

// Platform-agnostic representation of context
expect abstract class AppContext

// Access to current context
expect val LocalContext: ProvidableCompositionLocal<AppContext>

// Access to current activity
expect val AppContext.activity: Any

The AppContext provides a platform-agnostic way to access contextual information needed for platform operations:

Use LocalContext.current to get AppContext Aka android.content.Context

  • AppContext: Represents the platform's context (e.g., android.content.Context)
  • LocalContext: Provides access to the current AppContext through Compose's CompositionLocal
  • AppContext.activity: Provides access to the current activity (e.g., `android

Manager Providers

@Composable
expect fun LocalManagerProvider(
    context: AppContext,
    content: @Composable () -> Unit,
)

This composable function sets up the platform-specific managers and provides them through CompositionLocalProvider. It's designed to wrap your app's content and make all managers available to child composables.

IntentManager

interface IntentManager {
   // Launch a platform-specific intent
   fun startActivity(intent: Any)

   // Open a URI in an appropriate app
   fun launchUri(uri: String)

   // Share text with platform sharing mechanism
   fun shareText(text: String)

   // Share a file with appropriate MIME type
   fun shareFile(fileUri: String, mimeType: MimeType)

   // Extract shared data from incoming intents
   fun getShareDataFromIntent(intent: Any): ShareData?

   // Create an intent for document creation
   fun createDocumentIntent(fileName: String): Any

   // Launch application settings
   fun startApplicationDetailsSettingsActivity()

   // Open default email application
   fun startDefaultEmailApplication()

   // Data wrapper for incoming shared content
   sealed class ShareData {
      data class TextSend(val subject: String?, val text: String) : ShareData()
      // Extensible for future share types (images, files, etc.)
   }
}

The IntentManager provides platform-agnostic operations for working with platform-specific intents and sharing mechanisms. It handles:

  • Activity and URI launching
  • Content sharing
  • Settings navigation
  • Document creation
  • Handling incoming shared content

AppReviewManager

interface AppReviewManager {
    // Trigger platform's native review prompt
    fun promptForReview()

    // Launch custom review implementation
    fun promptForCustomReview()
}

The AppReviewManager abstracts in-app review functionality:

  • On Android: Uses Google Play In-App Review API
  • On other platforms: Provides placeholder implementations for future extension

AppUpdateManager

interface AppUpdateManager {
    // Check for available updates
    fun checkForAppUpdate()

    // Resume interrupted update processes
    fun checkForResumeUpdateState()
}

The AppUpdateManager handles update checking and flow management:

  • On Android: Implements Google Play In-App Update API
  • On other platforms: Provides placeholder implementations

MimeType

enum class MimeType(val value: String, vararg val extensions: String) {
    // Images
    IMAGE_JPEG("image/jpeg", "jpg", "jpeg"),
    IMAGE_PNG("image/png", "png"),
    // ... many more types

    // Default for unknown types
    UNKNOWN("application/octet-stream"),

    companion object {
        // Maps file extensions to MimeType
        private val extensionToMimeType = mutableMapOf<String, MimeType>()

        init {
            // Populate the map during initialization
            entries.forEach { mimeType ->
                mimeType.extensions.forEach { extension ->
                    extensionToMimeType[extension] = mimeType
                }
            }
        }

        // Get MimeType from file extension
        fun fromExtension(extension: String): MimeType

        // Get MimeType from filename
        fun fromFileName(fileName: String): MimeType
    }
}

The MimeType enum provides a comprehensive catalog of MIME types with:

  • String representation for platform APIs
  • Associated file extensions
  • Helper methods for determining types from filenames or extensions
  • Organized categories (images, videos, audio, documents, archives)

Android Implementation

AppContext (Android)

actual typealias AppContext = android.content.Context

actual val LocalContext: ProvidableCompositionLocal<AppContext>
    get() = androidx.compose.ui.platform.LocalContext

actual val AppContext.activity: Any
    @Composable
    get() = requireNotNull(LocalActivity.current)

The Android implementation:

  • Maps AppContext directly to Android's Context
  • Uses Compose UI's LocalContext for provider
  • Returns the current activity from LocalActivity

IntentManagerImpl (Android)

class IntentManagerImpl(private val context: Context) : IntentManager {
    // Implementation details
}

Key implementation features:

  1. URI Handling:

    • Handles androidapp:// scheme for app store links
    • Normalizes schemes for web URLs
    • Handles platform-specific intents
  2. Activity Starting:

    • Uses Android's startActivity to launch intents
    • Catches ActivityNotFoundException to prevent crashes
  3. Sharing:

    • Creates ACTION_SEND intents for text sharing
    • Handles file sharing with appropriate MIME types
    • Adds promotional text to file shares
  4. Intent Processing:

    • Extracts text content from incoming share intents
    • Creates document intents with appropriate MIME types
  5. Settings Navigation:

    • Opens application details settings
    • Launches default email application
  6. Play Store Interaction:

    • Constructs Play Store URIs for app installations
    • Falls back to Play Store when direct app launch fails

AppReviewManagerImpl (Android)

class AppReviewManagerImpl(private val activity: Activity) : AppReviewManager {
    override fun promptForReview() {
        val manager = ReviewManagerFactory.create(activity)
        val request = manager.requestReviewFlow()
        request.addOnCompleteListener { task ->
            if (task.isSuccessful) {
                val reviewInfo = task.result
                manager.launchReviewFlow(activity, reviewInfo)
            } else {
                Log.e("Failed to launch review flow.", task.exception?.message.toString())
            }
        }

        if (BuildConfig.DEBUG) {
            Log.d("ReviewManager", "Prompting for review")
        }
    }

    override fun promptForCustomReview() {
        // TODO:: Implement custom review flow
    }
}

Implementation details:

  • Uses Google Play Core library's ReviewManagerFactory
  • Handles asynchronous flow with callbacks
  • Logs failures for debugging
  • Includes debug logging for development builds
  • Placeholder for custom review implementation

AppUpdateManagerImpl (Android)

class AppUpdateManagerImpl(private val activity: Activity) : AppUpdateManager {
    private val manager = AppUpdateManagerFactory.create(activity)
    private val updateOptions = AppUpdateOptions
        .newBuilder(AppUpdateType.IMMEDIATE)
        .setAllowAssetPackDeletion(false)
        .build()

    // Implementation details
}

Key features:

  • Uses Google Play Core library's AppUpdateManagerFactory
  • Configures for immediate update type
  • Skips update checks in debug builds
  • Checks for and handles interrupted update flows
  • Uses success/failure listeners for async operations
  • Implements request code handling for update flow

LocalManagerProviders (Android)

@Composable
actual fun LocalManagerProvider(
    context: AppContext,
    content: @Composable () -> Unit,
) {
    val activity = context.activity as Activity
    CompositionLocalProvider(
        LocalAppReviewManager provides AppReviewManagerImpl(activity),
        LocalIntentManager provides IntentManagerImpl(activity),
        LocalAppUpdateManager provides AppUpdateManagerImpl(activity),
    ) {
        content()
    }
}

This implementation:

  • Extracts the Android Activity from the context
  • Creates concrete Android implementations of each manager
  • Provides them through CompositionLocalProvider

Non-Android Platform Implementations

For Desktop, JS, Native, and WasmJs platforms, the implementations follow similar patterns:

Context Implementation

actual abstract class AppContext private constructor() {
    companion object {
        val INSTANCE = object : AppContext() {}
    }
}

actual val LocalContext: ProvidableCompositionLocal<AppContext>
    get() = staticCompositionLocalOf { AppContext.INSTANCE }

actual val AppContext.activity: Any
    @Composable
    get() = AppContext.INSTANCE

The non-Android implementations:

  • Use a singleton pattern via companion object
  • Provide the same instance for both context and activity

Manager Implementations

class IntentManagerImpl : IntentManager {
    override fun startActivity(intent: Any) {
        // TODO("Not yet implemented")
    }

    // Other methods with TODO placeholders
}

class AppReviewManagerImpl : AppReviewManager {
    override fun promptForReview() {
        // Empty implementation
    }

    override fun promptForCustomReview() {
        // TODO:: Implement custom review flow
    }
}

class AppUpdateManagerImpl : AppUpdateManager {
    override fun checkForAppUpdate() {
        // Empty implementation
    }

    override fun checkForResumeUpdateState() {
        // Empty implementation
    }
}

These implementations:

  • Provide empty or placeholder implementations
  • Use TODO comments to mark future implementation points
  • Return default values for required return types

Advanced Usage Examples

Using IntentManager for Deep Linking

@Composable
fun DeepLinkHandler(uri: String?) {
    val intentManager = LocalIntentManager.current

    LaunchedEffect(uri) {
        uri?.let {
            intentManager.launchUri(it)
        }
    }
}

Implementing Custom Review Flow

class MyCustomReviewManager(
    private val appReviewManager: AppReviewManager = LocalAppReviewManager.current
) {
    fun showReviewAfterSuccessfulOperation(operationCount: Int) {
        // Track usage and show review at appropriate times
        if (operationCount > 5 && shouldShowReview()) {
            appReviewManager.promptForReview()
            markReviewShown()
        }
    }

    private fun shouldShowReview(): Boolean {
        // Your custom logic
        return true
    }

    private fun markReviewShown() {
        // Your tracking logic
    }
}

Handling Incoming Shared Content

@Composable
fun ShareReceiver(intent: Any) {
    val intentManager = LocalIntentManager.current
    val shareData = intentManager.getShareDataFromIntent(intent)

    when (shareData) {
        is IntentManager.ShareData.TextSend -> {
            // Handle received text
            Text("Received: ${shareData.text}")
        }
        else -> {
            // Handle other types or null
            Text("No sharable content found")
        }
    }
}

Managing App Updates

@Composable
fun UpdateCheckScreen() {
    val updateManager = LocalAppUpdateManager.current
    val networkAvailable = rememberNetworkState()

    LaunchedEffect(networkAvailable) {
        if (networkAvailable) {
            updateManager.checkForAppUpdate()
        }
    }

    // UI content
}

Integration Patterns

Basic Setup in App Root

@Composable
fun App() {
    val context = LocalContext.current

    LocalManagerProvider(context) {
        AppNavigation()
    }
}

With Navigation Component

@Composable
fun AppWithNavigation() {
    val context = LocalContext.current
    val navController = rememberNavController()

    LocalManagerProvider(context) {
        NavHost(navController, startDestination = "home") {
            composable("home") { HomeScreen() }
            composable("settings") { SettingsScreen() }
            // Other destinations
        }
    }
}

In Activities with Manual Initialization

class MainActivity : ComponentActivity() {
    private lateinit var appUpdateManager: AppUpdateManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        appUpdateManager = AppUpdateManagerImpl(this)

        setContent {
            val context = LocalContext.current

            LocalManagerProvider(context) {
                // App content
            }
        }
    }

    override fun onResume() {
        super.onResume()
        appUpdateManager.checkForResumeUpdateState()
    }
}

Best Practices

1. Manager Access

  • Use LocalX.current within composables
  • Inject managers via parameters for testability
  • Don't store managers in view models unless necessary
// Good practice
@Composable
fun MyScreen(
    intentManager: IntentManager = LocalIntentManager.current
) {
    // Use intentManager
}

// For testing
@Test
fun testMyScreen() {
    val mockIntentManager = MockIntentManager()
    composeTestRule.setContent {
        MyScreen(intentManager = mockIntentManager)
    }
}

2. Platform-Specific Code

  • Keep platform-specific code within manager implementations
  • Use conditional compilation for minor platform differences
  • Create separate high-level abstractions for major platform differences

3. Error Handling

  • Handle platform-specific exceptions within manager implementations
  • Provide consistent error reporting across platforms
  • Log detailed errors in debug builds

4. Testing

  • Create test fakes or mocks of manager interfaces
  • Test platform-specific implementations separately
  • Use dependency injection for testability

Extending the Platform Module

Adding New Manager Types

  1. Define the interface in commonMain:

    interface MyNewManager {
        fun doSomething()
    }
    
  2. Create the CompositionLocal provider:

    val LocalMyNewManager: ProvidableCompositionLocal<MyNewManager> = compositionLocalOf {
        error("CompositionLocal MyNewManager not present")
    }
    
  3. Implement for each platform:

    // Android
    class MyNewManagerImpl(private val context: Context) : MyNewManager {
        override fun doSomething() {
            // Android implementation
        }
    }
    
    // Other platforms
    class MyNewManagerImpl : MyNewManager {
        override fun doSomething() {
            // Implementation or placeholder
        }
    }
    
  4. Update LocalManagerProvider for each platform:

    @Composable
    actual fun LocalManagerProvider(
        context: AppContext,
        content: @Composable () -> Unit,
    ) {
        // Existing providers
        CompositionLocalProvider(
            // Existing providers
            LocalMyNewManager provides MyNewManagerImpl(context),
        ) {
            content()
        }
    }
    

Adding New Platform Targets

  1. Create the appropriate source set in build.gradle.kts
  2. Implement the required actual declarations
  3. Create platform-specific manager implementations

Troubleshooting

Common Issues

  1. CompositionLocal errors:

    java.lang.IllegalStateException: CompositionLocal LocalIntentManager not present
    

    Solution: Ensure your composable is called within the scope of a LocalManagerProvider.

  2. Context casting errors:

    java.lang.ClassCastException: android.content.Context cannot be cast to android.app.Activity
    

    Solution: Ensure you're using an Activity context when required.

  3. Permissions issues:

    java.lang.SecurityException: Permission Denial: starting Intent
    

    Solution: Verify required permissions are declared in AndroidManifest.xml.

Platform-Specific Issues

Android:

  • In-App Review not showing: Google limits frequency of review prompts
  • Update flow interruptions: Handle onActivityResult and resume the flow

Desktop/Web/Native:

  • Placeholder implementations: Replace TODOs with actual implementations

Design Philosophy

The platform module follows several key design principles:

  1. Separation of Concerns: Isolates platform-specific code
  2. Interface Segregation: Each manager has a focused responsibility
  3. Dependency Inversion: High-level modules depend on abstractions
  4. Composition Over Inheritance: Uses composition for flexibility

This approach allows for:

  • Platform-specific optimizations
  • Easy extensibility
  • Testability
  • Code reuse across platforms

Relation to Project Architecture

In the overall architecture:

  1. UI components depend on platform managers via CompositionLocal
  2. Managers abstract platform-specific functionality
  3. The common module provides cross-platform interfaces
  4. Each platform module provides concrete implementations

This creates a clean dependency flow:

UI → Managers (Interface) → Platform Implementation