mirror of
https://github.com/openMF/mobile-wallet.git
synced 2026-02-06 13:56:52 +00:00
723 lines
18 KiB
Markdown
723 lines
18 KiB
Markdown
# 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
|
|
|
|
```kotlin
|
|
// 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
|
|
|
|
```kotlin
|
|
@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
|
|
|
|
```kotlin
|
|
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
|
|
|
|
```kotlin
|
|
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
|
|
|
|
```kotlin
|
|
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
|
|
|
|
```kotlin
|
|
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)
|
|
|
|
```kotlin
|
|
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)
|
|
|
|
```kotlin
|
|
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)
|
|
|
|
```kotlin
|
|
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)
|
|
|
|
```kotlin
|
|
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)
|
|
|
|
```kotlin
|
|
@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
|
|
|
|
```kotlin
|
|
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
|
|
|
|
```kotlin
|
|
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
|
|
|
|
```kotlin
|
|
@Composable
|
|
fun DeepLinkHandler(uri: String?) {
|
|
val intentManager = LocalIntentManager.current
|
|
|
|
LaunchedEffect(uri) {
|
|
uri?.let {
|
|
intentManager.launchUri(it)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Implementing Custom Review Flow
|
|
|
|
```kotlin
|
|
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
|
|
|
|
```kotlin
|
|
@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
|
|
|
|
```kotlin
|
|
@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
|
|
|
|
```kotlin
|
|
@Composable
|
|
fun App() {
|
|
val context = LocalContext.current
|
|
|
|
LocalManagerProvider(context) {
|
|
AppNavigation()
|
|
}
|
|
}
|
|
```
|
|
|
|
### With Navigation Component
|
|
|
|
```kotlin
|
|
@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
|
|
|
|
```kotlin
|
|
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
|
|
|
|
```kotlin
|
|
// 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`:
|
|
```kotlin
|
|
interface MyNewManager {
|
|
fun doSomething()
|
|
}
|
|
```
|
|
|
|
2. Create the `CompositionLocal` provider:
|
|
```kotlin
|
|
val LocalMyNewManager: ProvidableCompositionLocal<MyNewManager> = compositionLocalOf {
|
|
error("CompositionLocal MyNewManager not present")
|
|
}
|
|
```
|
|
|
|
3. Implement for each platform:
|
|
```kotlin
|
|
// 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:
|
|
```kotlin
|
|
@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
|
|
``` |