@buivietphi/skill-mobile-mt 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +482 -0
- package/README.md +528 -0
- package/SKILL.md +1399 -0
- package/android/android-native.md +480 -0
- package/bin/install.mjs +976 -0
- package/flutter/flutter.md +304 -0
- package/humanizer/humanizer-mobile.md +295 -0
- package/ios/ios-native.md +182 -0
- package/package.json +56 -0
- package/react-native/react-native.md +743 -0
- package/shared/agent-rules-template.md +343 -0
- package/shared/ai-dlc-workflow.md +237 -0
- package/shared/anti-patterns.md +407 -0
- package/shared/architecture-intelligence.md +416 -0
- package/shared/bug-detection.md +71 -0
- package/shared/ci-cd.md +423 -0
- package/shared/claude-md-template.md +125 -0
- package/shared/code-review.md +133 -0
- package/shared/common-pitfalls.md +117 -0
- package/shared/document-analysis.md +167 -0
- package/shared/error-recovery.md +467 -0
- package/shared/observability.md +688 -0
- package/shared/offline-first.md +377 -0
- package/shared/performance-prediction.md +210 -0
- package/shared/platform-excellence.md +244 -0
- package/shared/prompt-engineering.md +705 -0
- package/shared/release-checklist.md +82 -0
- package/shared/testing-strategy.md +332 -0
- package/shared/ui-ux-mobile.md +667 -0
- package/shared/version-management.md +526 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
# Android Native — Production Patterns
|
|
2
|
+
|
|
3
|
+
> Battle-tested patterns for Android Kotlin development.
|
|
4
|
+
> Multi-module Gradle, Hilt DI, Compose UI, offline-first.
|
|
5
|
+
> Also reference for RN/Flutter Android-side issues.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Clean Architecture (Multi-Module)
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
project/
|
|
13
|
+
├── app/ # Main application module
|
|
14
|
+
│ ├── src/main/
|
|
15
|
+
│ │ ├── java/com/company/app/
|
|
16
|
+
│ │ │ ├── di/ # Hilt modules
|
|
17
|
+
│ │ │ ├── presentation/
|
|
18
|
+
│ │ │ │ ├── features/
|
|
19
|
+
│ │ │ │ │ ├── auth/
|
|
20
|
+
│ │ │ │ │ │ ├── ui/ # Composables
|
|
21
|
+
│ │ │ │ │ │ └── viewmodel/
|
|
22
|
+
│ │ │ │ │ └── home/
|
|
23
|
+
│ │ │ │ ├── navigation/
|
|
24
|
+
│ │ │ │ └── theme/
|
|
25
|
+
│ │ │ └── domain/
|
|
26
|
+
│ │ │ ├── model/ # Domain entities
|
|
27
|
+
│ │ │ ├── usecase/ # Business rules
|
|
28
|
+
│ │ │ └── repository/ # Repository interfaces
|
|
29
|
+
│ │ ├── res/
|
|
30
|
+
│ │ └── AndroidManifest.xml
|
|
31
|
+
│ └── build.gradle.kts
|
|
32
|
+
├── data/ # Data layer module
|
|
33
|
+
│ ├── src/main/java/
|
|
34
|
+
│ │ ├── repository/ # Repository implementations
|
|
35
|
+
│ │ ├── remote/ # API service, DTOs
|
|
36
|
+
│ │ ├── local/ # Room DAOs, entities
|
|
37
|
+
│ │ └── mapper/ # DTO ↔ Entity ↔ Domain mappers
|
|
38
|
+
│ └── build.gradle.kts
|
|
39
|
+
├── common/ # Shared utilities module
|
|
40
|
+
│ └── src/main/java/
|
|
41
|
+
├── build.gradle.kts # Root build file
|
|
42
|
+
├── settings.gradle.kts # Module declarations
|
|
43
|
+
└── gradle/
|
|
44
|
+
└── libs.versions.toml # Version catalog
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Dependency Rule
|
|
48
|
+
```
|
|
49
|
+
app (presentation) → domain/ ← data/
|
|
50
|
+
|
|
51
|
+
Presentation depends on Domain. Data depends on Domain.
|
|
52
|
+
Domain depends on NOTHING.
|
|
53
|
+
app module has access to all modules.
|
|
54
|
+
data module implements domain interfaces.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Compose UI Pattern
|
|
58
|
+
|
|
59
|
+
```kotlin
|
|
60
|
+
@Composable
|
|
61
|
+
fun ProductListScreen(
|
|
62
|
+
viewModel: ProductListViewModel = hiltViewModel(),
|
|
63
|
+
onProductClick: (String) -> Unit,
|
|
64
|
+
) {
|
|
65
|
+
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
|
66
|
+
|
|
67
|
+
Scaffold(
|
|
68
|
+
topBar = { TopAppBar(title = { Text("Products") }) },
|
|
69
|
+
) { padding ->
|
|
70
|
+
when (val state = uiState) {
|
|
71
|
+
is UiState.Loading -> Box(Modifier.fillMaxSize(), Alignment.Center) {
|
|
72
|
+
CircularProgressIndicator()
|
|
73
|
+
}
|
|
74
|
+
is UiState.Empty -> EmptyContent()
|
|
75
|
+
is UiState.Error -> ErrorContent(state.message, onRetry = viewModel::load)
|
|
76
|
+
is UiState.Success -> LazyColumn(
|
|
77
|
+
modifier = Modifier.padding(padding),
|
|
78
|
+
contentPadding = PaddingValues(16.dp),
|
|
79
|
+
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
80
|
+
) {
|
|
81
|
+
items(state.data, key = { it.id }) { product ->
|
|
82
|
+
ProductCard(product, onClick = { onProductClick(product.id) })
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
sealed interface UiState<out T> {
|
|
90
|
+
data object Loading : UiState<Nothing>
|
|
91
|
+
data object Empty : UiState<Nothing>
|
|
92
|
+
data class Success<T>(val data: T) : UiState<T>
|
|
93
|
+
data class Error(val message: String) : UiState<Nothing>
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## ViewModel (Hilt)
|
|
98
|
+
|
|
99
|
+
```kotlin
|
|
100
|
+
@HiltViewModel
|
|
101
|
+
class ProductListViewModel @Inject constructor(
|
|
102
|
+
private val getProducts: GetProductsUseCase,
|
|
103
|
+
) : ViewModel() {
|
|
104
|
+
private val _uiState = MutableStateFlow<UiState<List<Product>>>(UiState.Loading)
|
|
105
|
+
val uiState = _uiState.asStateFlow()
|
|
106
|
+
|
|
107
|
+
init { load() }
|
|
108
|
+
|
|
109
|
+
fun load() {
|
|
110
|
+
viewModelScope.launch {
|
|
111
|
+
_uiState.value = UiState.Loading
|
|
112
|
+
getProducts()
|
|
113
|
+
.catch { _uiState.value = UiState.Error(it.message ?: "Error") }
|
|
114
|
+
.collect { items ->
|
|
115
|
+
_uiState.value = if (items.isEmpty()) UiState.Empty else UiState.Success(items)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## DI (Hilt)
|
|
123
|
+
|
|
124
|
+
```kotlin
|
|
125
|
+
@Module @InstallIn(SingletonComponent::class)
|
|
126
|
+
object NetworkModule {
|
|
127
|
+
@Provides @Singleton
|
|
128
|
+
fun provideRetrofit(): Retrofit = Retrofit.Builder()
|
|
129
|
+
.baseUrl(BuildConfig.API_BASE_URL)
|
|
130
|
+
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
|
|
131
|
+
.build()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@Module @InstallIn(SingletonComponent::class)
|
|
135
|
+
abstract class RepositoryModule {
|
|
136
|
+
@Binds @Singleton
|
|
137
|
+
abstract fun bindProductRepo(impl: ProductRepositoryImpl): ProductRepository
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Data Layer (Retrofit + Room — Offline-First)
|
|
142
|
+
|
|
143
|
+
```kotlin
|
|
144
|
+
// data/remote/ProductApi.kt
|
|
145
|
+
interface ProductApi {
|
|
146
|
+
@GET("products") suspend fun getProducts(): ApiResponse<List<ProductDto>>
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// data/local/ProductDao.kt
|
|
150
|
+
@Dao interface ProductDao {
|
|
151
|
+
@Query("SELECT * FROM products") fun getAll(): Flow<List<ProductEntity>>
|
|
152
|
+
@Upsert suspend fun upsertAll(items: List<ProductEntity>)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// data/repository/ProductRepositoryImpl.kt
|
|
156
|
+
class ProductRepositoryImpl @Inject constructor(
|
|
157
|
+
private val api: ProductApi, private val dao: ProductDao,
|
|
158
|
+
) : ProductRepository {
|
|
159
|
+
override fun getProducts(): Flow<List<Product>> = flow {
|
|
160
|
+
val cached = dao.getAll().first()
|
|
161
|
+
if (cached.isNotEmpty()) emit(cached.map { it.toDomain() })
|
|
162
|
+
try {
|
|
163
|
+
val fresh = api.getProducts()
|
|
164
|
+
dao.upsertAll(fresh.data.map { it.toEntity() })
|
|
165
|
+
} catch (e: Exception) { if (cached.isEmpty()) throw e }
|
|
166
|
+
emitAll(dao.getAll().map { it.map { e -> e.toDomain() } })
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Secure Storage
|
|
172
|
+
|
|
173
|
+
```kotlin
|
|
174
|
+
val prefs = EncryptedSharedPreferences.create(
|
|
175
|
+
context, "secure_prefs",
|
|
176
|
+
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
|
|
177
|
+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
178
|
+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Multi-Module Gradle Setup
|
|
183
|
+
|
|
184
|
+
```kotlin
|
|
185
|
+
// settings.gradle.kts
|
|
186
|
+
include(":app", ":data", ":common")
|
|
187
|
+
|
|
188
|
+
// app/build.gradle.kts
|
|
189
|
+
dependencies {
|
|
190
|
+
implementation(project(":data"))
|
|
191
|
+
implementation(project(":common"))
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Compose Performance Optimization
|
|
196
|
+
|
|
197
|
+
```kotlin
|
|
198
|
+
// @Stable / @Immutable — tell Compose when to skip recomposition
|
|
199
|
+
// Use when your class isn't a data class but values never change
|
|
200
|
+
@Stable
|
|
201
|
+
class UserState(val id: String, val name: String)
|
|
202
|
+
|
|
203
|
+
@Immutable
|
|
204
|
+
data class ProductUiModel(val id: String, val price: Double)
|
|
205
|
+
|
|
206
|
+
// derivedStateOf — compute derived state only when inputs change
|
|
207
|
+
// Prevents recomposition on every scroll position change
|
|
208
|
+
val showFab by remember {
|
|
209
|
+
derivedStateOf { listState.firstVisibleItemIndex > 0 }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// key() in LazyColumn — stable identity prevents full recomposition
|
|
213
|
+
LazyColumn {
|
|
214
|
+
items(products, key = { it.id }) { product ->
|
|
215
|
+
ProductCard(product) // Only recomposes if THIS product changes
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Stateless components — pass data + callbacks, not ViewModel
|
|
220
|
+
@Composable
|
|
221
|
+
fun ProductCard(
|
|
222
|
+
product: Product, // data only
|
|
223
|
+
onClick: () -> Unit, // callback only
|
|
224
|
+
) { /* no ViewModel here */ }
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Baseline Profiles (Startup Optimization)
|
|
228
|
+
|
|
229
|
+
```kotlin
|
|
230
|
+
// app/src/main/baseline-prof.txt (generated by Macrobenchmark)
|
|
231
|
+
// Speeds up cold start 20-30% by AOT-compiling hot code paths
|
|
232
|
+
|
|
233
|
+
// build.gradle.kts
|
|
234
|
+
dependencies {
|
|
235
|
+
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Generate with Macrobenchmark:
|
|
239
|
+
// ./gradlew :app:generateBaselineProfile
|
|
240
|
+
// Commit the generated baseline-prof.txt
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Common Pitfalls
|
|
244
|
+
|
|
245
|
+
| Pitfall | Fix |
|
|
246
|
+
|---------|-----|
|
|
247
|
+
| `!!` assertion | `?.` / `?:` / `requireNotNull` |
|
|
248
|
+
| `collectAsState` | `collectAsStateWithLifecycle()` |
|
|
249
|
+
| Context leak | `@ApplicationContext`, never Activity |
|
|
250
|
+
| Missing ProGuard | Test release builds |
|
|
251
|
+
| Main thread blocking | `Dispatchers.IO` |
|
|
252
|
+
| Unstable lambdas in Compose | `remember { {} }` or move to ViewModel |
|
|
253
|
+
| List without keys | `items(list, key = { it.id })` |
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Java Interop (Legacy Projects)
|
|
258
|
+
|
|
259
|
+
> For projects still using Java. New code should use Kotlin.
|
|
260
|
+
|
|
261
|
+
### Activity + XML Layout (Java)
|
|
262
|
+
|
|
263
|
+
```java
|
|
264
|
+
// MainActivity.java
|
|
265
|
+
@AndroidEntryPoint
|
|
266
|
+
public class MainActivity extends AppCompatActivity {
|
|
267
|
+
|
|
268
|
+
private MainViewModel viewModel;
|
|
269
|
+
private ActivityMainBinding binding;
|
|
270
|
+
|
|
271
|
+
@Override
|
|
272
|
+
protected void onCreate(Bundle savedInstanceState) {
|
|
273
|
+
super.onCreate(savedInstanceState);
|
|
274
|
+
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
|
275
|
+
setContentView(binding.getRoot());
|
|
276
|
+
|
|
277
|
+
viewModel = new ViewModelProvider(this).get(MainViewModel.class);
|
|
278
|
+
|
|
279
|
+
// Observe LiveData (Java equivalent of collectAsStateWithLifecycle)
|
|
280
|
+
viewModel.getUiState().observe(this, state -> {
|
|
281
|
+
if (state instanceof UiState.Loading) {
|
|
282
|
+
binding.progressBar.setVisibility(View.VISIBLE);
|
|
283
|
+
binding.recyclerView.setVisibility(View.GONE);
|
|
284
|
+
} else if (state instanceof UiState.Error) {
|
|
285
|
+
binding.progressBar.setVisibility(View.GONE);
|
|
286
|
+
Toast.makeText(this, ((UiState.Error) state).getMessage(), Toast.LENGTH_SHORT).show();
|
|
287
|
+
} else if (state instanceof UiState.Success) {
|
|
288
|
+
binding.progressBar.setVisibility(View.GONE);
|
|
289
|
+
binding.recyclerView.setVisibility(View.VISIBLE);
|
|
290
|
+
adapter.submitList(((UiState.Success<List<Product>>) state).getData());
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
binding.retryButton.setOnClickListener(v -> viewModel.load());
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### ViewModel + LiveData (Java)
|
|
300
|
+
|
|
301
|
+
```java
|
|
302
|
+
// ProductListViewModel.java
|
|
303
|
+
@HiltViewModel
|
|
304
|
+
public class ProductListViewModel extends ViewModel {
|
|
305
|
+
|
|
306
|
+
private final MutableLiveData<UiState<List<Product>>> _uiState =
|
|
307
|
+
new MutableLiveData<>(new UiState.Loading<>());
|
|
308
|
+
private final LiveData<UiState<List<Product>>> uiState = _uiState;
|
|
309
|
+
|
|
310
|
+
private final GetProductsUseCase getProducts;
|
|
311
|
+
|
|
312
|
+
@Inject
|
|
313
|
+
public ProductListViewModel(GetProductsUseCase getProducts) {
|
|
314
|
+
this.getProducts = getProducts;
|
|
315
|
+
load();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
public LiveData<UiState<List<Product>>> getUiState() { return uiState; }
|
|
319
|
+
|
|
320
|
+
public void load() {
|
|
321
|
+
_uiState.setValue(new UiState.Loading<>());
|
|
322
|
+
// Use RxJava or ExecutorService for async in Java
|
|
323
|
+
ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
324
|
+
executor.execute(() -> {
|
|
325
|
+
try {
|
|
326
|
+
List<Product> items = getProducts.executeSync(); // blocking call
|
|
327
|
+
new Handler(Looper.getMainLooper()).post(() -> {
|
|
328
|
+
if (items.isEmpty()) _uiState.setValue(new UiState.Empty<>());
|
|
329
|
+
else _uiState.setValue(new UiState.Success<>(items));
|
|
330
|
+
});
|
|
331
|
+
} catch (Exception e) {
|
|
332
|
+
new Handler(Looper.getMainLooper()).post(() ->
|
|
333
|
+
_uiState.setValue(new UiState.Error<>(e.getMessage()))
|
|
334
|
+
);
|
|
335
|
+
} finally {
|
|
336
|
+
executor.shutdown();
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Retrofit + Callback (Java)
|
|
344
|
+
|
|
345
|
+
```java
|
|
346
|
+
// ProductApi.java
|
|
347
|
+
public interface ProductApi {
|
|
348
|
+
@GET("products")
|
|
349
|
+
Call<ApiResponse<List<ProductDto>>> getProducts();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ProductRepositoryImpl.java
|
|
353
|
+
public class ProductRepositoryImpl implements ProductRepository {
|
|
354
|
+
private final ProductApi api;
|
|
355
|
+
private final ProductDao dao;
|
|
356
|
+
|
|
357
|
+
@Inject
|
|
358
|
+
public ProductRepositoryImpl(ProductApi api, ProductDao dao) {
|
|
359
|
+
this.api = api;
|
|
360
|
+
this.dao = dao;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
@Override
|
|
364
|
+
public void getProducts(Callback<List<Product>> callback) {
|
|
365
|
+
api.getProducts().enqueue(new retrofit2.Callback<ApiResponse<List<ProductDto>>>() {
|
|
366
|
+
@Override
|
|
367
|
+
public void onResponse(Call<ApiResponse<List<ProductDto>>> call,
|
|
368
|
+
Response<ApiResponse<List<ProductDto>>> response) {
|
|
369
|
+
if (response.isSuccessful() && response.body() != null) {
|
|
370
|
+
List<Product> items = mapToDomain(response.body().getData());
|
|
371
|
+
callback.onSuccess(items);
|
|
372
|
+
} else {
|
|
373
|
+
callback.onError("Server error: " + response.code());
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
@Override
|
|
378
|
+
public void onFailure(Call<ApiResponse<List<ProductDto>>> call, Throwable t) {
|
|
379
|
+
callback.onError(t.getMessage());
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Room DAO (Java)
|
|
387
|
+
|
|
388
|
+
```java
|
|
389
|
+
// ProductDao.java
|
|
390
|
+
@Dao
|
|
391
|
+
public interface ProductDao {
|
|
392
|
+
@Query("SELECT * FROM products")
|
|
393
|
+
LiveData<List<ProductEntity>> getAll(); // LiveData for Java observers
|
|
394
|
+
|
|
395
|
+
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
396
|
+
void insertAll(List<ProductEntity> items);
|
|
397
|
+
|
|
398
|
+
@Query("DELETE FROM products")
|
|
399
|
+
void deleteAll();
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Java-Kotlin Interop Annotations
|
|
404
|
+
|
|
405
|
+
```kotlin
|
|
406
|
+
// When writing Kotlin code that will be called from Java:
|
|
407
|
+
|
|
408
|
+
// @JvmStatic — allows calling companion object functions as static
|
|
409
|
+
class ProductMapper {
|
|
410
|
+
companion object {
|
|
411
|
+
@JvmStatic
|
|
412
|
+
fun toDomain(dto: ProductDto): Product = Product(dto.id, dto.name)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Java: ProductMapper.toDomain(dto) ✅ (without @JvmStatic: ProductMapper.Companion.toDomain(dto))
|
|
416
|
+
|
|
417
|
+
// @JvmField — exposes Kotlin property as Java field (no getter/setter)
|
|
418
|
+
class Config {
|
|
419
|
+
companion object {
|
|
420
|
+
@JvmField val BASE_URL = "https://api.example.com"
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// Java: Config.BASE_URL ✅
|
|
424
|
+
|
|
425
|
+
// @JvmOverloads — generates overloaded methods for default parameters
|
|
426
|
+
class ProductService @JvmOverloads constructor(
|
|
427
|
+
val baseUrl: String,
|
|
428
|
+
val timeout: Int = 30,
|
|
429
|
+
val retries: Int = 3,
|
|
430
|
+
)
|
|
431
|
+
// Java: new ProductService("url") ✅ (without: must pass all 3 params)
|
|
432
|
+
|
|
433
|
+
// @Throws — declares checked exceptions for Java callers
|
|
434
|
+
@Throws(IOException::class)
|
|
435
|
+
fun readFile(path: String): String { /* ... */ }
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Calling Kotlin Suspend from Java (Bridge Pattern)
|
|
439
|
+
|
|
440
|
+
```java
|
|
441
|
+
// Use CoroutineScope from Java via CoroutinesInstrumentationHelper or wrapper
|
|
442
|
+
// ⚠️ Recommended: write a non-suspend wrapper in Kotlin
|
|
443
|
+
|
|
444
|
+
// Kotlin wrapper (bridge.kt)
|
|
445
|
+
object ProductBridge {
|
|
446
|
+
@JvmStatic
|
|
447
|
+
fun getProducts(scope: CoroutineScope, callback: ProductCallback) {
|
|
448
|
+
scope.launch {
|
|
449
|
+
try {
|
|
450
|
+
val items = getProductsUseCase()
|
|
451
|
+
callback.onSuccess(items)
|
|
452
|
+
} catch (e: Exception) {
|
|
453
|
+
callback.onError(e.message ?: "Error")
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Java side
|
|
460
|
+
ProductBridge.getProducts(lifecycleScope, new ProductCallback() {
|
|
461
|
+
@Override public void onSuccess(List<Product> items) { /* update UI */ }
|
|
462
|
+
@Override public void onError(String message) { /* show error */ }
|
|
463
|
+
});
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Java Common Pitfalls
|
|
467
|
+
|
|
468
|
+
| Pitfall | Fix |
|
|
469
|
+
|---------|-----|
|
|
470
|
+
| NullPointerException | `@NonNull` / `@Nullable` + null checks |
|
|
471
|
+
| Memory leak (Activity in async) | `WeakReference<Activity>` or cancel on `onDestroy` |
|
|
472
|
+
| Main thread network call | `Executors.newSingleThreadExecutor()` |
|
|
473
|
+
| No `Call.cancel()` | Store reference, cancel in `onDestroy` |
|
|
474
|
+
| Anonymous inner class holding outer ref | Use static inner class |
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
> Multi-module Gradle + Hilt + Compose + offline-first.
|
|
479
|
+
> Clean Architecture with domain module having zero dependencies.
|
|
480
|
+
> Java legacy: use LiveData + Retrofit callbacks. Bridge Kotlin suspend with wrapper functions.
|