opencode-skills-collection 3.0.42 → 3.0.43
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/README.md +10 -0
- package/bundled-skills/.antigravity-install-manifest.json +3 -1
- package/bundled-skills/2slides-ppt-generator/SKILL.md +8 -18
- package/bundled-skills/accesslint-diff/SKILL.md +5 -2
- package/bundled-skills/android-dev/SKILL.md +524 -0
- package/bundled-skills/android-dev/references/flutter.md +269 -0
- package/bundled-skills/android-dev/references/hybrid.md +158 -0
- package/bundled-skills/android-dev/references/java-android.md +586 -0
- package/bundled-skills/android-dev/references/kmm.md +206 -0
- package/bundled-skills/android-dev/references/native-android.md +239 -0
- package/bundled-skills/android-dev/references/react-native.md +242 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/event-staffing-ordering/SKILL.md +2 -17
- package/bundled-skills/unship/SKILL.md +138 -0
- package/package.json +1 -1
- package/skills_index.json +44 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
# Native Android — Java Reference
|
|
2
|
+
|
|
3
|
+
## When to Use Java
|
|
4
|
+
|
|
5
|
+
Java remains fully supported by Android and Google. Use it when:
|
|
6
|
+
- Maintaining or extending an existing Java codebase
|
|
7
|
+
- Team is Java-fluent without Kotlin experience
|
|
8
|
+
- Integrating Java-only SDKs or legacy modules
|
|
9
|
+
- Gradual migration: new Kotlin modules alongside old Java modules
|
|
10
|
+
|
|
11
|
+
> **Java + Kotlin interop is seamless** — you can have both in the same project. New files can be Kotlin while legacy files stay Java.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Project Structure
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
app/src/main/java/com/example/app/
|
|
19
|
+
├── MyApp.java # Application class
|
|
20
|
+
├── MainActivity.java # Host activity
|
|
21
|
+
├── ui/
|
|
22
|
+
│ └── home/
|
|
23
|
+
│ ├── HomeActivity.java # OR Fragment-based
|
|
24
|
+
│ ├── HomeFragment.java
|
|
25
|
+
│ └── HomeAdapter.java
|
|
26
|
+
├── viewmodel/
|
|
27
|
+
│ └── HomeViewModel.java
|
|
28
|
+
├── repository/
|
|
29
|
+
│ └── ItemRepository.java
|
|
30
|
+
├── data/
|
|
31
|
+
│ ├── remote/
|
|
32
|
+
│ │ ├── ApiService.java # Retrofit interface
|
|
33
|
+
│ │ ├── ApiClient.java # OkHttp + Retrofit setup
|
|
34
|
+
│ │ └── dto/ItemDto.java
|
|
35
|
+
│ └── local/
|
|
36
|
+
│ ├── AppDatabase.java # Room database
|
|
37
|
+
│ ├── ItemDao.java
|
|
38
|
+
│ └── entity/ItemEntity.java
|
|
39
|
+
├── model/
|
|
40
|
+
│ └── Item.java # Domain model
|
|
41
|
+
└── di/ # Manual DI or Hilt
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## ViewModel (Java + LiveData)
|
|
47
|
+
|
|
48
|
+
```java
|
|
49
|
+
public class HomeViewModel extends ViewModel {
|
|
50
|
+
|
|
51
|
+
private final MutableLiveData<UiState<List<Item>>> _uiState =
|
|
52
|
+
new MutableLiveData<>(UiState.loading());
|
|
53
|
+
|
|
54
|
+
public LiveData<UiState<List<Item>>> uiState = _uiState;
|
|
55
|
+
|
|
56
|
+
private final ItemRepository repository;
|
|
57
|
+
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
58
|
+
|
|
59
|
+
// Constructor injection (Hilt or manual)
|
|
60
|
+
public HomeViewModel(ItemRepository repository) {
|
|
61
|
+
this.repository = repository;
|
|
62
|
+
loadItems();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public void loadItems() {
|
|
66
|
+
_uiState.setValue(UiState.loading());
|
|
67
|
+
executor.execute(() -> {
|
|
68
|
+
try {
|
|
69
|
+
List<Item> items = repository.getItems();
|
|
70
|
+
_uiState.postValue(UiState.success(items));
|
|
71
|
+
} catch (Exception e) {
|
|
72
|
+
_uiState.postValue(UiState.error(e.getMessage()));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Override
|
|
78
|
+
protected void onCleared() {
|
|
79
|
+
super.onCleared();
|
|
80
|
+
executor.shutdown();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## UiState Wrapper
|
|
88
|
+
|
|
89
|
+
```java
|
|
90
|
+
public class UiState<T> {
|
|
91
|
+
public enum Status { LOADING, SUCCESS, ERROR }
|
|
92
|
+
|
|
93
|
+
public final Status status;
|
|
94
|
+
public final T data;
|
|
95
|
+
public final String errorMessage;
|
|
96
|
+
|
|
97
|
+
private UiState(Status status, T data, String errorMessage) {
|
|
98
|
+
this.status = status;
|
|
99
|
+
this.data = data;
|
|
100
|
+
this.errorMessage = errorMessage;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public static <T> UiState<T> loading() {
|
|
104
|
+
return new UiState<>(Status.LOADING, null, null);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public static <T> UiState<T> success(T data) {
|
|
108
|
+
return new UiState<>(Status.SUCCESS, data, null);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public static <T> UiState<T> error(String message) {
|
|
112
|
+
return new UiState<>(Status.ERROR, null, message);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public boolean isLoading() { return status == Status.LOADING; }
|
|
116
|
+
public boolean isSuccess() { return status == Status.SUCCESS; }
|
|
117
|
+
public boolean isError() { return status == Status.ERROR; }
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Fragment Observing ViewModel
|
|
124
|
+
|
|
125
|
+
```java
|
|
126
|
+
public class HomeFragment extends Fragment {
|
|
127
|
+
|
|
128
|
+
private HomeViewModel viewModel;
|
|
129
|
+
private FragmentHomeBinding binding; // ViewBinding
|
|
130
|
+
|
|
131
|
+
@Override
|
|
132
|
+
public View onCreateView(@NonNull LayoutInflater inflater,
|
|
133
|
+
ViewGroup container, Bundle savedInstanceState) {
|
|
134
|
+
binding = FragmentHomeBinding.inflate(inflater, container, false);
|
|
135
|
+
return binding.getRoot();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@Override
|
|
139
|
+
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
|
140
|
+
super.onViewCreated(view, savedInstanceState);
|
|
141
|
+
|
|
142
|
+
viewModel = new ViewModelProvider(this,
|
|
143
|
+
new HomeViewModelFactory(new ItemRepository(requireContext())))
|
|
144
|
+
.get(HomeViewModel.class);
|
|
145
|
+
|
|
146
|
+
viewModel.uiState.observe(getViewLifecycleOwner(), state -> {
|
|
147
|
+
binding.progressBar.setVisibility(state.isLoading() ? View.VISIBLE : View.GONE);
|
|
148
|
+
binding.recyclerView.setVisibility(state.isSuccess() ? View.VISIBLE : View.GONE);
|
|
149
|
+
binding.errorView.setVisibility(state.isError() ? View.VISIBLE : View.GONE);
|
|
150
|
+
|
|
151
|
+
if (state.isSuccess()) {
|
|
152
|
+
adapter.submitList(state.data);
|
|
153
|
+
}
|
|
154
|
+
if (state.isError()) {
|
|
155
|
+
binding.errorText.setText(state.errorMessage);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
binding.retryButton.setOnClickListener(v -> viewModel.loadItems());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@Override
|
|
163
|
+
public void onDestroyView() {
|
|
164
|
+
super.onDestroyView();
|
|
165
|
+
binding = null; // CRITICAL — avoid memory leak
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Room Database (Java)
|
|
173
|
+
|
|
174
|
+
```java
|
|
175
|
+
// Entity
|
|
176
|
+
@Entity(tableName = "items")
|
|
177
|
+
public class ItemEntity {
|
|
178
|
+
@PrimaryKey
|
|
179
|
+
@NonNull
|
|
180
|
+
public String id;
|
|
181
|
+
public String title;
|
|
182
|
+
public long updatedAt;
|
|
183
|
+
|
|
184
|
+
public ItemEntity(@NonNull String id, String title, long updatedAt) {
|
|
185
|
+
this.id = id;
|
|
186
|
+
this.title = title;
|
|
187
|
+
this.updatedAt = updatedAt;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// DAO
|
|
192
|
+
@Dao
|
|
193
|
+
public interface ItemDao {
|
|
194
|
+
@Query("SELECT * FROM items ORDER BY updatedAt DESC")
|
|
195
|
+
LiveData<List<ItemEntity>> observeAll();
|
|
196
|
+
|
|
197
|
+
@Query("SELECT * FROM items ORDER BY updatedAt DESC")
|
|
198
|
+
List<ItemEntity> getAll(); // blocking — call off main thread
|
|
199
|
+
|
|
200
|
+
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
201
|
+
void insertAll(List<ItemEntity> items);
|
|
202
|
+
|
|
203
|
+
@Query("DELETE FROM items")
|
|
204
|
+
void deleteAll();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Database
|
|
208
|
+
@Database(entities = {ItemEntity.class}, version = 1, exportSchema = true)
|
|
209
|
+
public abstract class AppDatabase extends RoomDatabase {
|
|
210
|
+
private static volatile AppDatabase INSTANCE;
|
|
211
|
+
|
|
212
|
+
public abstract ItemDao itemDao();
|
|
213
|
+
|
|
214
|
+
public static AppDatabase getInstance(Context context) {
|
|
215
|
+
if (INSTANCE == null) {
|
|
216
|
+
synchronized (AppDatabase.class) {
|
|
217
|
+
if (INSTANCE == null) {
|
|
218
|
+
INSTANCE = Room.databaseBuilder(
|
|
219
|
+
context.getApplicationContext(),
|
|
220
|
+
AppDatabase.class,
|
|
221
|
+
"app_database"
|
|
222
|
+
).build();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return INSTANCE;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Retrofit API Client (Java)
|
|
234
|
+
|
|
235
|
+
```java
|
|
236
|
+
// Interface
|
|
237
|
+
public interface ApiService {
|
|
238
|
+
@GET("items")
|
|
239
|
+
Call<List<ItemDto>> getItems();
|
|
240
|
+
|
|
241
|
+
@GET("items/{id}")
|
|
242
|
+
Call<ItemDto> getItemById(@Path("id") String id);
|
|
243
|
+
|
|
244
|
+
@POST("items")
|
|
245
|
+
Call<ItemDto> createItem(@Body ItemDto item);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Client setup
|
|
249
|
+
public class ApiClient {
|
|
250
|
+
private static final String BASE_URL = BuildConfig.API_BASE_URL;
|
|
251
|
+
private static ApiService INSTANCE;
|
|
252
|
+
|
|
253
|
+
public static ApiService getInstance() {
|
|
254
|
+
if (INSTANCE == null) {
|
|
255
|
+
OkHttpClient client = new OkHttpClient.Builder()
|
|
256
|
+
.connectTimeout(10, TimeUnit.SECONDS)
|
|
257
|
+
.readTimeout(10, TimeUnit.SECONDS)
|
|
258
|
+
.addInterceptor(new AuthInterceptor())
|
|
259
|
+
.addInterceptor(new HttpLoggingInterceptor()
|
|
260
|
+
.setLevel(BuildConfig.DEBUG
|
|
261
|
+
? HttpLoggingInterceptor.Level.BODY
|
|
262
|
+
: HttpLoggingInterceptor.Level.NONE))
|
|
263
|
+
.build();
|
|
264
|
+
|
|
265
|
+
Retrofit retrofit = new Retrofit.Builder()
|
|
266
|
+
.baseUrl(BASE_URL)
|
|
267
|
+
.client(client)
|
|
268
|
+
.addConverterFactory(GsonConverterFactory.create())
|
|
269
|
+
.build();
|
|
270
|
+
|
|
271
|
+
INSTANCE = retrofit.create(ApiService.class);
|
|
272
|
+
}
|
|
273
|
+
return INSTANCE;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Auth interceptor
|
|
278
|
+
public class AuthInterceptor implements Interceptor {
|
|
279
|
+
@NonNull
|
|
280
|
+
@Override
|
|
281
|
+
public Response intercept(@NonNull Chain chain) throws IOException {
|
|
282
|
+
String token = TokenStorage.getInstance().getToken();
|
|
283
|
+
Request request = chain.request().newBuilder()
|
|
284
|
+
.addHeader("Authorization", "Bearer " + token)
|
|
285
|
+
.build();
|
|
286
|
+
return chain.proceed(request);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Repository (Java)
|
|
294
|
+
|
|
295
|
+
```java
|
|
296
|
+
public class ItemRepository {
|
|
297
|
+
private final ItemDao itemDao;
|
|
298
|
+
private final ApiService apiService;
|
|
299
|
+
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
300
|
+
|
|
301
|
+
public ItemRepository(Context context) {
|
|
302
|
+
AppDatabase db = AppDatabase.getInstance(context);
|
|
303
|
+
this.itemDao = db.itemDao();
|
|
304
|
+
this.apiService = ApiClient.getInstance();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Synchronous fetch for ViewModel executor
|
|
308
|
+
public List<Item> getItems() throws Exception {
|
|
309
|
+
Response<List<ItemDto>> response = apiService.getItems().execute();
|
|
310
|
+
if (response.isSuccessful() && response.body() != null) {
|
|
311
|
+
return response.body().stream()
|
|
312
|
+
.map(ItemMapper::toDomain)
|
|
313
|
+
.collect(Collectors.toList());
|
|
314
|
+
} else {
|
|
315
|
+
throw new IOException("HTTP " + response.code());
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Observe cached data (returns LiveData — auto updates UI)
|
|
320
|
+
public LiveData<List<Item>> observeItems() {
|
|
321
|
+
return Transformations.map(itemDao.observeAll(), entities ->
|
|
322
|
+
entities.stream().map(ItemMapper::toDomain).collect(Collectors.toList())
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Refresh from network (call from background thread or executor)
|
|
327
|
+
public void refreshItems(Callback<Void> callback) {
|
|
328
|
+
executor.execute(() -> {
|
|
329
|
+
try {
|
|
330
|
+
Response<List<ItemDto>> response = apiService.getItems().execute();
|
|
331
|
+
if (response.isSuccessful() && response.body() != null) {
|
|
332
|
+
List<ItemEntity> entities = response.body().stream()
|
|
333
|
+
.map(ItemMapper::toEntity)
|
|
334
|
+
.collect(Collectors.toList());
|
|
335
|
+
itemDao.deleteAll();
|
|
336
|
+
itemDao.insertAll(entities);
|
|
337
|
+
callback.onSuccess(null);
|
|
338
|
+
} else {
|
|
339
|
+
callback.onError(new IOException("HTTP " + response.code()));
|
|
340
|
+
}
|
|
341
|
+
} catch (IOException e) {
|
|
342
|
+
callback.onError(e);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
public interface Callback<T> {
|
|
348
|
+
void onSuccess(T result);
|
|
349
|
+
void onError(Exception e);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## RecyclerView Adapter (Java)
|
|
357
|
+
|
|
358
|
+
```java
|
|
359
|
+
public class ItemAdapter extends ListAdapter<Item, ItemAdapter.ItemViewHolder> {
|
|
360
|
+
|
|
361
|
+
private final OnItemClickListener listener;
|
|
362
|
+
|
|
363
|
+
public interface OnItemClickListener {
|
|
364
|
+
void onItemClick(Item item);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
public ItemAdapter(OnItemClickListener listener) {
|
|
368
|
+
super(new DiffUtil.ItemCallback<Item>() {
|
|
369
|
+
@Override
|
|
370
|
+
public boolean areItemsTheSame(@NonNull Item a, @NonNull Item b) {
|
|
371
|
+
return a.getId().equals(b.getId());
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
@Override
|
|
375
|
+
public boolean areContentsTheSame(@NonNull Item a, @NonNull Item b) {
|
|
376
|
+
return a.equals(b);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
this.listener = listener;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
@NonNull
|
|
383
|
+
@Override
|
|
384
|
+
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
|
385
|
+
ItemRowBinding binding = ItemRowBinding.inflate(
|
|
386
|
+
LayoutInflater.from(parent.getContext()), parent, false);
|
|
387
|
+
return new ItemViewHolder(binding);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
@Override
|
|
391
|
+
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
|
|
392
|
+
holder.bind(getItem(position), listener);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
static class ItemViewHolder extends RecyclerView.ViewHolder {
|
|
396
|
+
private final ItemRowBinding binding;
|
|
397
|
+
|
|
398
|
+
ItemViewHolder(ItemRowBinding binding) {
|
|
399
|
+
super(binding.getRoot());
|
|
400
|
+
this.binding = binding;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
void bind(Item item, OnItemClickListener listener) {
|
|
404
|
+
binding.titleText.setText(item.getTitle());
|
|
405
|
+
binding.getRoot().setOnClickListener(v -> listener.onItemClick(item));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## XML Layout Best Practices (Java projects)
|
|
414
|
+
|
|
415
|
+
```xml
|
|
416
|
+
<!-- Use ConstraintLayout — flat hierarchy = better performance -->
|
|
417
|
+
<androidx.constraintlayout.widget.ConstraintLayout
|
|
418
|
+
android:layout_width="match_parent"
|
|
419
|
+
android:layout_height="match_parent">
|
|
420
|
+
|
|
421
|
+
<!-- Always use ?attr/ tokens from MaterialTheme, never hardcoded colors -->
|
|
422
|
+
<TextView
|
|
423
|
+
android:id="@+id/titleText"
|
|
424
|
+
android:textColor="?attr/colorOnSurface"
|
|
425
|
+
android:textAppearance="?attr/textAppearanceTitleMedium"
|
|
426
|
+
android:layout_width="0dp"
|
|
427
|
+
android:layout_height="wrap_content"
|
|
428
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
429
|
+
app:layout_constraintEnd_toEndOf="parent"
|
|
430
|
+
app:layout_constraintTop_toTopOf="parent" />
|
|
431
|
+
|
|
432
|
+
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
- Always use **ViewBinding** (not `findViewById`, not DataBinding for simple cases)
|
|
436
|
+
- Enable in `build.gradle.kts`: `viewBinding { enable = true }`
|
|
437
|
+
- Null `binding` in `onDestroyView()` to prevent Fragment memory leaks
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Error Handling (Java)
|
|
442
|
+
|
|
443
|
+
```java
|
|
444
|
+
// Checked exceptions: always handle explicitly
|
|
445
|
+
public Result<List<Item>> getItemsSafe() {
|
|
446
|
+
try {
|
|
447
|
+
Response<List<ItemDto>> response = apiService.getItems().execute();
|
|
448
|
+
if (!response.isSuccessful()) {
|
|
449
|
+
return Result.failure(new HttpException(response));
|
|
450
|
+
}
|
|
451
|
+
List<Item> items = Objects.requireNonNull(response.body())
|
|
452
|
+
.stream().map(ItemMapper::toDomain).collect(Collectors.toList());
|
|
453
|
+
return Result.success(items);
|
|
454
|
+
} catch (IOException e) {
|
|
455
|
+
return Result.failure(new NetworkException("Network error", e));
|
|
456
|
+
} catch (NullPointerException e) {
|
|
457
|
+
return Result.failure(new ParseException("Empty response body", e));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Custom exception hierarchy
|
|
462
|
+
public class AppException extends Exception {
|
|
463
|
+
public AppException(String message) { super(message); }
|
|
464
|
+
public AppException(String message, Throwable cause) { super(message, cause); }
|
|
465
|
+
}
|
|
466
|
+
public class NetworkException extends AppException { ... }
|
|
467
|
+
public class ParseException extends AppException { ... }
|
|
468
|
+
public class AuthException extends AppException { ... }
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## Hilt DI (Java)
|
|
474
|
+
|
|
475
|
+
```java
|
|
476
|
+
// Application
|
|
477
|
+
@HiltAndroidApp
|
|
478
|
+
public class MyApp extends Application {}
|
|
479
|
+
|
|
480
|
+
// Activity / Fragment — annotate for injection
|
|
481
|
+
@AndroidEntryPoint
|
|
482
|
+
public class HomeFragment extends Fragment {
|
|
483
|
+
@Inject
|
|
484
|
+
ItemRepository repository; // injected by Hilt
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ViewModel
|
|
488
|
+
@HiltViewModel
|
|
489
|
+
public class HomeViewModel extends ViewModel {
|
|
490
|
+
private final ItemRepository repository;
|
|
491
|
+
|
|
492
|
+
@Inject
|
|
493
|
+
public HomeViewModel(ItemRepository repository) {
|
|
494
|
+
this.repository = repository;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Module
|
|
499
|
+
@Module
|
|
500
|
+
@InstallIn(SingletonComponent.class)
|
|
501
|
+
public class DatabaseModule {
|
|
502
|
+
@Provides
|
|
503
|
+
@Singleton
|
|
504
|
+
public AppDatabase provideDatabase(@ApplicationContext Context context) {
|
|
505
|
+
return AppDatabase.getInstance(context);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
@Provides
|
|
509
|
+
public ItemDao provideItemDao(AppDatabase db) {
|
|
510
|
+
return db.itemDao();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## Unit Testing (Java)
|
|
518
|
+
|
|
519
|
+
```java
|
|
520
|
+
@ExtendWith(MockitoExtension.class)
|
|
521
|
+
class HomeViewModelTest {
|
|
522
|
+
|
|
523
|
+
@Mock
|
|
524
|
+
ItemRepository mockRepository;
|
|
525
|
+
|
|
526
|
+
HomeViewModel viewModel;
|
|
527
|
+
|
|
528
|
+
@BeforeEach
|
|
529
|
+
void setup() {
|
|
530
|
+
viewModel = new HomeViewModel(mockRepository);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
@Test
|
|
534
|
+
void loadItems_success_emitsSuccessState() throws Exception {
|
|
535
|
+
List<Item> items = Arrays.asList(new Item("1", "Test"));
|
|
536
|
+
when(mockRepository.getItems()).thenReturn(items);
|
|
537
|
+
|
|
538
|
+
viewModel.loadItems();
|
|
539
|
+
|
|
540
|
+
// Wait for executor — use CountDownLatch or InstantExecutorRule
|
|
541
|
+
UiState<List<Item>> state = viewModel.uiState.getValue();
|
|
542
|
+
assertNotNull(state);
|
|
543
|
+
assertTrue(state.isSuccess());
|
|
544
|
+
assertEquals(items, state.data);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
@Test
|
|
548
|
+
void loadItems_failure_emitsErrorState() throws Exception {
|
|
549
|
+
when(mockRepository.getItems()).thenThrow(new IOException("Network error"));
|
|
550
|
+
|
|
551
|
+
viewModel.loadItems();
|
|
552
|
+
|
|
553
|
+
UiState<List<Item>> state = viewModel.uiState.getValue();
|
|
554
|
+
assertNotNull(state);
|
|
555
|
+
assertTrue(state.isError());
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## Java → Kotlin Migration Path
|
|
563
|
+
|
|
564
|
+
When migrating a Java project to Kotlin incrementally:
|
|
565
|
+
|
|
566
|
+
1. **New files in Kotlin** — Java and Kotlin coexist seamlessly
|
|
567
|
+
2. **Convert utilities first** — `@JvmStatic`, `@JvmField` for interop
|
|
568
|
+
3. **Convert data models** — Java POJOs → Kotlin `data class`
|
|
569
|
+
4. **Convert DAOs and Repositories** — add `suspend` + `Flow`
|
|
570
|
+
5. **Convert ViewModels last** — swap `LiveData` + `MutableLiveData` for `StateFlow`
|
|
571
|
+
6. **Convert Activities/Fragments** — migrate to Compose screen by screen
|
|
572
|
+
7. Annotate Kotlin with `@JvmOverloads`, `@JvmName` where Java callers exist
|
|
573
|
+
|
|
574
|
+
```kotlin
|
|
575
|
+
// Kotlin data class replacing a Java POJO
|
|
576
|
+
data class Item(
|
|
577
|
+
val id: String,
|
|
578
|
+
val title: String,
|
|
579
|
+
val updatedAt: Long = System.currentTimeMillis()
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
// Kotlin extension to consume Java LiveData from Kotlin cleanly
|
|
583
|
+
fun <T> LiveData<T>.observeNonNull(owner: LifecycleOwner, observer: (T) -> Unit) {
|
|
584
|
+
observe(owner) { it?.let(observer) }
|
|
585
|
+
}
|
|
586
|
+
```
|