@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.
@@ -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.