📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-06-10 16:02:03 +00:00
parent 4dc7bc6de1
commit abb0f863b4
173 changed files with 7745 additions and 400 deletions
@@ -0,0 +1,269 @@
# Flutter Reference (Dart)
## Project Structure
```
lib/
├── main.dart # Entry point
├── app/
│ ├── app.dart # MaterialApp + router setup
│ ├── theme/ # ThemeData, colors, typography, spacing
│ └── router/ # go_router config, guards
├── features/
│ └── home/
│ ├── data/
│ │ ├── datasource/ # Remote + local data sources
│ │ ├── dto/ # JSON models (freezed)
│ │ └── repository/ # Repo implementations
│ ├── domain/
│ │ ├── model/ # Domain models (freezed)
│ │ ├── repository/ # Abstract repo interfaces
│ │ └── usecase/ # Use cases
│ └── presentation/
│ ├── bloc/ # Bloc/Cubit + state + event
│ └── screen/ # Widgets + page files
├── core/
│ ├── network/ # Dio client, interceptors
│ ├── database/ # Drift DB setup
│ ├── widgets/ # Shared design system widgets
│ └── error/ # Failure types, error handling
└── injection.dart # GetIt service locator setup
```
## State Management (BLoC)
```dart
// States
@freezed
class HomeState with _$HomeState {
const factory HomeState.initial() = _Initial;
const factory HomeState.loading() = _Loading;
const factory HomeState.success(List<Item> items) = _Success;
const factory HomeState.failure(String message) = _Failure;
}
// Events
@freezed
class HomeEvent with _$HomeEvent {
const factory HomeEvent.loadItems() = _LoadItems;
const factory HomeEvent.refreshItems() = _RefreshItems;
}
// Bloc
class HomeBloc extends Bloc<HomeEvent, HomeState> {
final GetItemsUseCase _getItems;
HomeBloc(this._getItems) : super(const HomeState.initial()) {
on<_LoadItems>(_onLoad);
}
Future<void> _onLoad(_LoadItems event, Emitter<HomeState> emit) async {
emit(const HomeState.loading());
final result = await _getItems();
result.fold(
(failure) => emit(HomeState.failure(failure.message)),
(items) => emit(HomeState.success(items)),
);
}
}
```
## State Management (Riverpod — alternative)
```dart
@riverpod
class HomeNotifier extends _$HomeNotifier {
@override
FutureOr<List<Item>> build() => _load();
Future<List<Item>> _load() async {
final repo = ref.read(itemRepositoryProvider);
return repo.getItems().getOrThrow();
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(_load);
}
}
```
## Screen Widget Pattern
```dart
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (ctx) => sl<HomeBloc>()..add(const HomeEvent.loadItems()),
child: const _HomeView(),
);
}
}
class _HomeView extends StatelessWidget {
const _HomeView();
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocConsumer<HomeBloc, HomeState>(
listener: (ctx, state) {
state.maybeWhen(
failure: (msg) => ScaffoldMessenger.of(ctx)
.showSnackBar(SnackBar(content: Text(msg))),
orElse: () {},
);
},
builder: (ctx, state) => state.when(
initial: () => const SizedBox(),
loading: () => const Center(child: CircularProgressIndicator()),
success: (items) => _ItemList(items: items),
failure: (msg) => ErrorView(message: msg,
onRetry: () => ctx.read<HomeBloc>().add(
const HomeEvent.loadItems())),
),
),
);
}
}
```
## go_router Setup
```dart
final router = GoRouter(
initialLocation: '/home',
redirect: (context, state) {
final isLoggedIn = ref.read(authStateProvider).isLoggedIn;
if (!isLoggedIn && !state.matchedLocation.startsWith('/auth')) {
return '/auth/login';
}
return null;
},
routes: [
GoRoute(
path: '/home',
name: AppRoutes.home,
builder: (ctx, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'detail/:id',
builder: (ctx, state) =>
DetailScreen(id: state.pathParameters['id']!),
),
],
),
],
);
```
## Drift Database
```dart
@DriftDatabase(tables: [Items])
class AppDatabase extends _$AppDatabase {
AppDatabase(QueryExecutor e) : super(e);
@override
int get schemaVersion => 1;
Stream<List<Item>> watchAllItems() =>
(select(items)..orderBy([(t) => OrderingTerm.desc(t.updatedAt)])).watch();
Future<void> upsertItems(List<ItemsCompanion> rows) =>
batch((b) => b.insertAllOnConflictUpdate(items, rows));
}
```
## Key pubspec.yaml Dependencies
```yaml
dependencies:
flutter_bloc: ^8.1.5
freezed_annotation: ^2.4.1
riverpod: ^2.5.1 # alternative to bloc
flutter_riverpod: ^2.5.1
go_router: ^14.1.0
dio: ^5.4.3
drift: ^2.18.0
sqflite: ^2.3.3
get_it: ^7.7.0
injectable: ^2.4.1
dartz: ^0.10.1 # Either/Option for FP error handling
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.9
freezed: ^2.5.2
json_serializable: ^6.8.0
drift_dev: ^2.18.0
mocktail: ^1.0.3
bloc_test: ^9.1.7
```
## Error Handling (Either/Failure pattern)
```dart
abstract class Failure {
final String message;
const Failure(this.message);
}
class NetworkFailure extends Failure {
const NetworkFailure([super.message = 'Network error occurred']);
}
class CacheFailure extends Failure {
const CacheFailure([super.message = 'Cache error occurred']);
}
// Repository
Future<Either<Failure, List<Item>>> getItems() async {
try {
final remote = await _remoteSource.fetchItems();
await _localSource.saveItems(remote);
return Right(remote.map(_mapper.toDomain).toList());
} on DioException catch (e) {
return Left(NetworkFailure(e.message ?? 'Network error'));
} on Exception {
return const Left(CacheFailure());
}
}
```
## Testing
```dart
void main() {
group('HomeBloc', () {
late HomeBloc bloc;
late MockGetItemsUseCase mockUseCase;
setUp(() {
mockUseCase = MockGetItemsUseCase();
bloc = HomeBloc(mockUseCase);
});
tearDown(() => bloc.close());
blocTest<HomeBloc, HomeState>(
'emits [loading, success] when loadItems succeeds',
build: () {
when(() => mockUseCase()).thenAnswer(
(_) async => Right([Item(id: '1', title: 'Test')]),
);
return bloc;
},
act: (b) => b.add(const HomeEvent.loadItems()),
expect: () => [
const HomeState.loading(),
isA<HomeState>().having((s) => s, 'success',
const HomeState.success([Item(id: '1', title: 'Test')])),
],
);
});
}
```
@@ -0,0 +1,158 @@
# Hybrid Android Reference (Capacitor + Ionic / React)
## When to Use Hybrid
✅ Good fit:
- Web team building a companion Android app
- Content-heavy apps (news, docs, forms)
- PWA upgrade to installable app
- Rapid prototyping
❌ Avoid for:
- Real-time games / heavy animations
- Deep native sensor / hardware access
- Apps requiring 60fps custom animations
- Bluetooth/NFC intensive apps (use plugins, but complex)
## Stack Options
| Option | UI Framework | Best For |
|--------|-------------|---------|
| Capacitor + Ionic | Ionic components | Full mobile-optimized UI |
| Capacitor + React | React + Tailwind | Web team reuse |
| Capacitor + Vue | Vue + Ionic | Vue teams |
| Capacitor + Angular | Angular + Ionic | Enterprise Angular teams |
## Project Structure (Capacitor + React)
```
src/
├── App.tsx
├── pages/ # Screen components
├── components/ # Shared UI components
├── hooks/ # Business logic hooks
├── services/ # API, storage services
└── store/ # State management
android/ # Native Android project (generated)
├── app/src/main/
│ ├── AndroidManifest.xml
│ └── java/.../MainActivity.kt
capacitor.config.ts # Capacitor configuration
```
## Capacitor Config
```typescript
// capacitor.config.ts
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.app',
appName: 'My App',
webDir: 'dist',
server: {
androidScheme: 'https',
},
android: {
buildOptions: {
releaseType: 'APK', // or AAB for Play Store
},
},
plugins: {
SplashScreen: {
launchShowDuration: 0,
backgroundColor: '#FFFFFF',
},
PushNotifications: {
presentationOptions: ['badge', 'sound', 'alert'],
},
},
};
```
## Native Plugin Usage
```typescript
import { Camera, CameraResultType } from '@capacitor/camera';
import { Preferences } from '@capacitor/preferences';
import { PushNotifications } from '@capacitor/push-notifications';
import { Geolocation } from '@capacitor/geolocation';
// Camera
const takePhoto = async () => {
const photo = await Camera.getPhoto({
quality: 90,
allowEditing: false,
resultType: CameraResultType.Uri,
});
return photo.webPath;
};
// Secure storage
const saveToken = async (token: string) => {
await Preferences.set({ key: 'auth_token', value: token });
};
const getToken = async (): Promise<string | null> => {
const { value } = await Preferences.get({ key: 'auth_token' });
return value;
};
// Push notifications
const initPush = async () => {
const permission = await PushNotifications.requestPermissions();
if (permission.receive === 'granted') {
await PushNotifications.register();
}
PushNotifications.addListener('registration', ({ value: token }) => {
console.log('FCM Token:', token);
});
};
```
## Performance Best Practices
- Ensure hardware acceleration is enabled for the application in AndroidManifest.xml (default in Capacitor)
- Enable HTTP caching in Android WebView settings
- Lazy-load routes with React.lazy / dynamic imports
- Avoid `setTimeout`/`setInterval` for animations; use CSS transitions
- Use `@ionic/react` components — they handle mobile-specific touch handling
- Ionic virtual scroll for long lists
## Build & Deploy
```bash
# Build web assets
npm run build
# Sync to native
npx cap sync android
# Open in Android Studio
npx cap open android
# Build release APK/AAB via Android Studio or:
cd android && ./gradlew bundleRelease
```
## Custom Native Plugin (when built-in plugins don't cover it)
```kotlin
// android/app/src/main/java/.../MyPlugin.kt
@CapacitorPlugin(name = "MyPlugin")
class MyPlugin : Plugin() {
@PluginMethod
fun doNativeWork(call: PluginCall) {
val value = call.getString("input") ?: return call.reject("No input")
// Do native work
val result = JSObject()
result.put("output", "processed: $value")
call.resolve(result)
}
}
// TypeScript usage
import { registerPlugin } from '@capacitor/core';
const MyPlugin = registerPlugin<{ doNativeWork: (opts: { input: string }) => Promise<{ output: string }> }>('MyPlugin');
const result = await MyPlugin.doNativeWork({ input: 'hello' });
```
@@ -0,0 +1,586 @@
# Native Android — Java Reference
## When to Use Java
Java remains fully supported by Android and Google. Use it when:
- Maintaining or extending an existing Java codebase
- Team is Java-fluent without Kotlin experience
- Integrating Java-only SDKs or legacy modules
- Gradual migration: new Kotlin modules alongside old Java modules
> **Java + Kotlin interop is seamless** — you can have both in the same project. New files can be Kotlin while legacy files stay Java.
---
## Project Structure
```
app/src/main/java/com/example/app/
├── MyApp.java # Application class
├── MainActivity.java # Host activity
├── ui/
│ └── home/
│ ├── HomeActivity.java # OR Fragment-based
│ ├── HomeFragment.java
│ └── HomeAdapter.java
├── viewmodel/
│ └── HomeViewModel.java
├── repository/
│ └── ItemRepository.java
├── data/
│ ├── remote/
│ │ ├── ApiService.java # Retrofit interface
│ │ ├── ApiClient.java # OkHttp + Retrofit setup
│ │ └── dto/ItemDto.java
│ └── local/
│ ├── AppDatabase.java # Room database
│ ├── ItemDao.java
│ └── entity/ItemEntity.java
├── model/
│ └── Item.java # Domain model
└── di/ # Manual DI or Hilt
```
---
## ViewModel (Java + LiveData)
```java
public class HomeViewModel extends ViewModel {
private final MutableLiveData<UiState<List<Item>>> _uiState =
new MutableLiveData<>(UiState.loading());
public LiveData<UiState<List<Item>>> uiState = _uiState;
private final ItemRepository repository;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
// Constructor injection (Hilt or manual)
public HomeViewModel(ItemRepository repository) {
this.repository = repository;
loadItems();
}
public void loadItems() {
_uiState.setValue(UiState.loading());
executor.execute(() -> {
try {
List<Item> items = repository.getItems();
_uiState.postValue(UiState.success(items));
} catch (Exception e) {
_uiState.postValue(UiState.error(e.getMessage()));
}
});
}
@Override
protected void onCleared() {
super.onCleared();
executor.shutdown();
}
}
```
---
## UiState Wrapper
```java
public class UiState<T> {
public enum Status { LOADING, SUCCESS, ERROR }
public final Status status;
public final T data;
public final String errorMessage;
private UiState(Status status, T data, String errorMessage) {
this.status = status;
this.data = data;
this.errorMessage = errorMessage;
}
public static <T> UiState<T> loading() {
return new UiState<>(Status.LOADING, null, null);
}
public static <T> UiState<T> success(T data) {
return new UiState<>(Status.SUCCESS, data, null);
}
public static <T> UiState<T> error(String message) {
return new UiState<>(Status.ERROR, null, message);
}
public boolean isLoading() { return status == Status.LOADING; }
public boolean isSuccess() { return status == Status.SUCCESS; }
public boolean isError() { return status == Status.ERROR; }
}
```
---
## Fragment Observing ViewModel
```java
public class HomeFragment extends Fragment {
private HomeViewModel viewModel;
private FragmentHomeBinding binding; // ViewBinding
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
binding = FragmentHomeBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(this,
new HomeViewModelFactory(new ItemRepository(requireContext())))
.get(HomeViewModel.class);
viewModel.uiState.observe(getViewLifecycleOwner(), state -> {
binding.progressBar.setVisibility(state.isLoading() ? View.VISIBLE : View.GONE);
binding.recyclerView.setVisibility(state.isSuccess() ? View.VISIBLE : View.GONE);
binding.errorView.setVisibility(state.isError() ? View.VISIBLE : View.GONE);
if (state.isSuccess()) {
adapter.submitList(state.data);
}
if (state.isError()) {
binding.errorText.setText(state.errorMessage);
}
});
binding.retryButton.setOnClickListener(v -> viewModel.loadItems());
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null; // CRITICAL — avoid memory leak
}
}
```
---
## Room Database (Java)
```java
// Entity
@Entity(tableName = "items")
public class ItemEntity {
@PrimaryKey
@NonNull
public String id;
public String title;
public long updatedAt;
public ItemEntity(@NonNull String id, String title, long updatedAt) {
this.id = id;
this.title = title;
this.updatedAt = updatedAt;
}
}
// DAO
@Dao
public interface ItemDao {
@Query("SELECT * FROM items ORDER BY updatedAt DESC")
LiveData<List<ItemEntity>> observeAll();
@Query("SELECT * FROM items ORDER BY updatedAt DESC")
List<ItemEntity> getAll(); // blocking — call off main thread
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertAll(List<ItemEntity> items);
@Query("DELETE FROM items")
void deleteAll();
}
// Database
@Database(entities = {ItemEntity.class}, version = 1, exportSchema = true)
public abstract class AppDatabase extends RoomDatabase {
private static volatile AppDatabase INSTANCE;
public abstract ItemDao itemDao();
public static AppDatabase getInstance(Context context) {
if (INSTANCE == null) {
synchronized (AppDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.getApplicationContext(),
AppDatabase.class,
"app_database"
).build();
}
}
}
return INSTANCE;
}
}
```
---
## Retrofit API Client (Java)
```java
// Interface
public interface ApiService {
@GET("items")
Call<List<ItemDto>> getItems();
@GET("items/{id}")
Call<ItemDto> getItemById(@Path("id") String id);
@POST("items")
Call<ItemDto> createItem(@Body ItemDto item);
}
// Client setup
public class ApiClient {
private static final String BASE_URL = BuildConfig.API_BASE_URL;
private static ApiService INSTANCE;
public static ApiService getInstance() {
if (INSTANCE == null) {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.addInterceptor(new AuthInterceptor())
.addInterceptor(new HttpLoggingInterceptor()
.setLevel(BuildConfig.DEBUG
? HttpLoggingInterceptor.Level.BODY
: HttpLoggingInterceptor.Level.NONE))
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
INSTANCE = retrofit.create(ApiService.class);
}
return INSTANCE;
}
}
// Auth interceptor
public class AuthInterceptor implements Interceptor {
@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
String token = TokenStorage.getInstance().getToken();
Request request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer " + token)
.build();
return chain.proceed(request);
}
}
```
---
## Repository (Java)
```java
public class ItemRepository {
private final ItemDao itemDao;
private final ApiService apiService;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
public ItemRepository(Context context) {
AppDatabase db = AppDatabase.getInstance(context);
this.itemDao = db.itemDao();
this.apiService = ApiClient.getInstance();
}
// Synchronous fetch for ViewModel executor
public List<Item> getItems() throws Exception {
Response<List<ItemDto>> response = apiService.getItems().execute();
if (response.isSuccessful() && response.body() != null) {
return response.body().stream()
.map(ItemMapper::toDomain)
.collect(Collectors.toList());
} else {
throw new IOException("HTTP " + response.code());
}
}
// Observe cached data (returns LiveData — auto updates UI)
public LiveData<List<Item>> observeItems() {
return Transformations.map(itemDao.observeAll(), entities ->
entities.stream().map(ItemMapper::toDomain).collect(Collectors.toList())
);
}
// Refresh from network (call from background thread or executor)
public void refreshItems(Callback<Void> callback) {
executor.execute(() -> {
try {
Response<List<ItemDto>> response = apiService.getItems().execute();
if (response.isSuccessful() && response.body() != null) {
List<ItemEntity> entities = response.body().stream()
.map(ItemMapper::toEntity)
.collect(Collectors.toList());
itemDao.deleteAll();
itemDao.insertAll(entities);
callback.onSuccess(null);
} else {
callback.onError(new IOException("HTTP " + response.code()));
}
} catch (IOException e) {
callback.onError(e);
}
});
}
public interface Callback<T> {
void onSuccess(T result);
void onError(Exception e);
}
}
```
---
## RecyclerView Adapter (Java)
```java
public class ItemAdapter extends ListAdapter<Item, ItemAdapter.ItemViewHolder> {
private final OnItemClickListener listener;
public interface OnItemClickListener {
void onItemClick(Item item);
}
public ItemAdapter(OnItemClickListener listener) {
super(new DiffUtil.ItemCallback<Item>() {
@Override
public boolean areItemsTheSame(@NonNull Item a, @NonNull Item b) {
return a.getId().equals(b.getId());
}
@Override
public boolean areContentsTheSame(@NonNull Item a, @NonNull Item b) {
return a.equals(b);
}
});
this.listener = listener;
}
@NonNull
@Override
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemRowBinding binding = ItemRowBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false);
return new ItemViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
holder.bind(getItem(position), listener);
}
static class ItemViewHolder extends RecyclerView.ViewHolder {
private final ItemRowBinding binding;
ItemViewHolder(ItemRowBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
void bind(Item item, OnItemClickListener listener) {
binding.titleText.setText(item.getTitle());
binding.getRoot().setOnClickListener(v -> listener.onItemClick(item));
}
}
}
```
---
## XML Layout Best Practices (Java projects)
```xml
<!-- Use ConstraintLayout — flat hierarchy = better performance -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Always use ?attr/ tokens from MaterialTheme, never hardcoded colors -->
<TextView
android:id="@+id/titleText"
android:textColor="?attr/colorOnSurface"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
```
- Always use **ViewBinding** (not `findViewById`, not DataBinding for simple cases)
- Enable in `build.gradle.kts`: `viewBinding { enable = true }`
- Null `binding` in `onDestroyView()` to prevent Fragment memory leaks
---
## Error Handling (Java)
```java
// Checked exceptions: always handle explicitly
public Result<List<Item>> getItemsSafe() {
try {
Response<List<ItemDto>> response = apiService.getItems().execute();
if (!response.isSuccessful()) {
return Result.failure(new HttpException(response));
}
List<Item> items = Objects.requireNonNull(response.body())
.stream().map(ItemMapper::toDomain).collect(Collectors.toList());
return Result.success(items);
} catch (IOException e) {
return Result.failure(new NetworkException("Network error", e));
} catch (NullPointerException e) {
return Result.failure(new ParseException("Empty response body", e));
}
}
// Custom exception hierarchy
public class AppException extends Exception {
public AppException(String message) { super(message); }
public AppException(String message, Throwable cause) { super(message, cause); }
}
public class NetworkException extends AppException { ... }
public class ParseException extends AppException { ... }
public class AuthException extends AppException { ... }
```
---
## Hilt DI (Java)
```java
// Application
@HiltAndroidApp
public class MyApp extends Application {}
// Activity / Fragment — annotate for injection
@AndroidEntryPoint
public class HomeFragment extends Fragment {
@Inject
ItemRepository repository; // injected by Hilt
}
// ViewModel
@HiltViewModel
public class HomeViewModel extends ViewModel {
private final ItemRepository repository;
@Inject
public HomeViewModel(ItemRepository repository) {
this.repository = repository;
}
}
// Module
@Module
@InstallIn(SingletonComponent.class)
public class DatabaseModule {
@Provides
@Singleton
public AppDatabase provideDatabase(@ApplicationContext Context context) {
return AppDatabase.getInstance(context);
}
@Provides
public ItemDao provideItemDao(AppDatabase db) {
return db.itemDao();
}
}
```
---
## Unit Testing (Java)
```java
@ExtendWith(MockitoExtension.class)
class HomeViewModelTest {
@Mock
ItemRepository mockRepository;
HomeViewModel viewModel;
@BeforeEach
void setup() {
viewModel = new HomeViewModel(mockRepository);
}
@Test
void loadItems_success_emitsSuccessState() throws Exception {
List<Item> items = Arrays.asList(new Item("1", "Test"));
when(mockRepository.getItems()).thenReturn(items);
viewModel.loadItems();
// Wait for executor — use CountDownLatch or InstantExecutorRule
UiState<List<Item>> state = viewModel.uiState.getValue();
assertNotNull(state);
assertTrue(state.isSuccess());
assertEquals(items, state.data);
}
@Test
void loadItems_failure_emitsErrorState() throws Exception {
when(mockRepository.getItems()).thenThrow(new IOException("Network error"));
viewModel.loadItems();
UiState<List<Item>> state = viewModel.uiState.getValue();
assertNotNull(state);
assertTrue(state.isError());
}
}
```
---
## Java → Kotlin Migration Path
When migrating a Java project to Kotlin incrementally:
1. **New files in Kotlin** — Java and Kotlin coexist seamlessly
2. **Convert utilities first**`@JvmStatic`, `@JvmField` for interop
3. **Convert data models** — Java POJOs → Kotlin `data class`
4. **Convert DAOs and Repositories** — add `suspend` + `Flow`
5. **Convert ViewModels last** — swap `LiveData` + `MutableLiveData` for `StateFlow`
6. **Convert Activities/Fragments** — migrate to Compose screen by screen
7. Annotate Kotlin with `@JvmOverloads`, `@JvmName` where Java callers exist
```kotlin
// Kotlin data class replacing a Java POJO
data class Item(
val id: String,
val title: String,
val updatedAt: Long = System.currentTimeMillis()
)
// Kotlin extension to consume Java LiveData from Kotlin cleanly
fun <T> LiveData<T>.observeNonNull(owner: LifecycleOwner, observer: (T) -> Unit) {
observe(owner) { it?.let(observer) }
}
```
@@ -0,0 +1,206 @@
# Kotlin Multiplatform (KMM) Reference
## Project Structure
```
project/
├── shared/ # Shared KMM module
│ ├── src/
│ │ ├── commonMain/kotlin/ # Business logic, domain, data
│ │ │ ├── domain/
│ │ │ │ ├── model/
│ │ │ │ ├── repository/ # Interfaces
│ │ │ │ └── usecase/
│ │ │ ├── data/
│ │ │ │ ├── remote/ # Ktor client + DTOs
│ │ │ │ ├── local/ # SQLDelight DAOs
│ │ │ │ └── repository/ # Implementations
│ │ │ └── di/ # Koin modules
│ │ ├── androidMain/kotlin/ # Android-specific actual implementations
│ │ └── iosMain/kotlin/ # iOS-specific actual (if needed)
│ └── build.gradle.kts
├── androidApp/ # Android app module
│ ├── src/main/java/
│ │ ├── ui/ # Jetpack Compose screens
│ │ ├── presentation/ # Android ViewModels
│ │ └── di/ # Android-specific DI
│ └── build.gradle.kts
└── build.gradle.kts
```
## Shared Module: Ktor HTTP Client
```kotlin
// commonMain
expect fun httpClient(config: HttpClientConfig<*>.() -> Unit): HttpClient
// androidMain
actual fun httpClient(config: HttpClientConfig<*>.() -> Unit): HttpClient =
HttpClient(OkHttp) {
config(this)
engine { addInterceptor(/* logging, auth */) }
}
// Shared usage
val client = httpClient {
install(ContentNegotiation) { json() }
install(HttpTimeout) { requestTimeoutMillis = 10_000 }
defaultRequest {
url(BuildKonfig.BASE_URL)
header(HttpHeaders.ContentType, ContentType.Application.Json)
}
}
```
## SQLDelight Setup
```sql
-- ItemEntity.sq
CREATE TABLE ItemEntity (
id TEXT NOT NULL PRIMARY KEY,
title TEXT NOT NULL,
updatedAt INTEGER NOT NULL DEFAULT 0
);
selectAll:
SELECT * FROM ItemEntity ORDER BY updatedAt DESC;
upsertItem:
INSERT OR REPLACE INTO ItemEntity (id, title, updatedAt)
VALUES (?, ?, ?);
```
```kotlin
// commonMain — Database driver expect/actual
expect class DatabaseDriverFactory {
fun createDriver(): SqlDriver
}
// androidMain
actual class DatabaseDriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver =
AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
}
```
## Shared Repository
```kotlin
// commonMain
class ItemRepositoryImpl(
private val remoteSource: ItemRemoteDataSource,
private val localSource: ItemLocalDataSource,
) : ItemRepository {
override fun observeItems(): Flow<List<Item>> =
localSource.observeAll().map { entities ->
entities.map { it.toDomain() }
}
override suspend fun refreshItems(): Result<Unit> = runCatching {
val items = remoteSource.fetchItems()
localSource.upsertAll(items.map { it.toEntity() })
}
}
```
## Android ViewModel consuming shared Flow
```kotlin
@HiltViewModel
class HomeViewModel @Inject constructor(
private val observeItems: ObserveItemsUseCase, // from shared module
private val refreshItems: RefreshItemsUseCase // from shared module
) : ViewModel() {
val uiState = observeItems()
.map { HomeUiState.Success(it) as HomeUiState }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = HomeUiState.Loading
)
}
```
## Koin DI (Shared + Android)
```kotlin
// commonMain — shared Koin modules
val sharedModule = module {
single { DatabaseDriverFactory(get()) }
single { AppDatabase(get<DatabaseDriverFactory>().createDriver()) }
single<ItemRepository> { ItemRepositoryImpl(get(), get()) }
factory { ObserveItemsUseCase(get()) }
factory { RefreshItemsUseCase(get()) }
}
// androidApp — Android-specific module
val androidModule = module {
single<Context> { androidApplication() }
viewModel { HomeViewModel(get(), get()) }
}
// Application class
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApp)
modules(sharedModule, androidModule)
}
}
}
```
## Key Gradle Dependencies (shared/build.gradle.kts)
```kotlin
kotlin {
androidTarget()
// Add other targets as needed (jvm, iosArm64, etc.)
sourceSets {
commonMain.dependencies {
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.sqldelight.runtime)
implementation(libs.koin.core)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)
}
androidMain.dependencies {
implementation(libs.ktor.client.okhttp)
implementation(libs.sqldelight.android.driver)
implementation(libs.koin.android)
}
}
}
```
## Compose Multiplatform (for shared UI)
Use when you want to share UI across Android + Desktop + Web:
```kotlin
// commonMain — shared composable
@Composable
fun HomeScreenContent(
state: HomeUiState,
onRetry: () -> Unit
) {
when (state) {
is HomeUiState.Loading -> CircularProgressIndicator()
is HomeUiState.Success -> ItemList(state.items)
is HomeUiState.Error -> ErrorView(state.message, onRetry)
}
}
// androidApp — wraps with Android ViewModel
@Composable
fun HomeScreen(viewModel: HomeViewModel = koinViewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
HomeScreenContent(state, onRetry = viewModel::refresh)
}
```
@@ -0,0 +1,239 @@
# Native Android Reference (Kotlin + Jetpack Compose)
## Project Structure
```
app/
├── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/com.example.app/
│ │ │ ├── MyApp.kt # Application class, Hilt entry point
│ │ │ ├── MainActivity.kt # Single activity, NavHost host
│ │ │ ├── ui/
│ │ │ │ ├── theme/ # MaterialTheme, Color, Type, Shape
│ │ │ │ ├── components/ # Shared design system composables
│ │ │ │ └── feature/
│ │ │ │ ├── home/
│ │ │ │ │ ├── HomeScreen.kt
│ │ │ │ │ ├── HomeViewModel.kt
│ │ │ │ │ └── HomeUiState.kt
│ │ │ ├── domain/
│ │ │ │ ├── model/ # Domain models (pure Kotlin, no Android deps)
│ │ │ │ ├── repository/ # Interfaces only
│ │ │ │ └── usecase/ # One class per use case
│ │ │ ├── data/
│ │ │ │ ├── remote/ # Retrofit services, DTOs, mappers
│ │ │ │ ├── local/ # Room DB, DAOs, entities
│ │ │ │ └── repository/ # Repository implementations
│ │ │ └── di/ # Hilt modules
│ └── test/ # Unit tests
│ └── androidTest/ # Instrumented tests
├── build.gradle.kts
└── proguard-rules.pro
```
## ViewModel Pattern
```kotlin
// UiState — sealed class for exhaustive when()
sealed class HomeUiState {
object Loading : HomeUiState()
data class Success(val items: List<Item>) : HomeUiState()
data class Error(val message: String) : HomeUiState()
}
// UiEvent — one-shot events (navigation, snackbars)
sealed class HomeUiEvent {
data class NavigateTo(val route: String) : HomeUiEvent()
data class ShowSnackbar(val message: String) : HomeUiEvent()
}
@HiltViewModel
class HomeViewModel @Inject constructor(
private val getItemsUseCase: GetItemsUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<HomeUiState>(HomeUiState.Loading)
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
private val _uiEvent = Channel<HomeUiEvent>()
val uiEvent = _uiEvent.receiveAsFlow()
init { loadItems() }
fun loadItems() {
viewModelScope.launch {
_uiState.value = HomeUiState.Loading
getItemsUseCase()
.onSuccess { _uiState.value = HomeUiState.Success(it) }
.onFailure { _uiState.value = HomeUiState.Error(it.message ?: "Unknown error") }
}
}
}
```
## Repository Pattern
```kotlin
// Interface in domain layer
interface ItemRepository {
fun observeItems(): Flow<List<Item>>
suspend fun refreshItems(): Result<Unit>
suspend fun getItemById(id: String): Result<Item>
}
// Implementation in data layer
class ItemRepositoryImpl @Inject constructor(
private val remoteSource: ItemRemoteDataSource,
private val localSource: ItemLocalDataSource,
private val mapper: ItemMapper
) : ItemRepository {
override fun observeItems(): Flow<List<Item>> =
localSource.observeAll().map { mapper.toDomain(it) }
override suspend fun refreshItems(): Result<Unit> = runCatching {
val dto = remoteSource.fetchItems()
localSource.insertAll(mapper.toEntity(dto))
}
override suspend fun getItemById(id: String): Result<Item> = runCatching {
// Example implementation fetching from local cache
val entity = localSource.getById(id) ?: throw Exception("Item not found")
mapper.toDomain(entity)
}
}
```
## Compose Screen
```kotlin
@Composable
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel(),
onNavigate: (String) -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
// One-shot event handling
LaunchedEffect(Unit) {
viewModel.uiEvent.collect { event ->
when (event) {
is HomeUiEvent.NavigateTo -> onNavigate(event.route)
is HomeUiEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.message)
}
}
}
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { padding ->
when (val state = uiState) {
is HomeUiState.Loading -> LoadingContent()
is HomeUiState.Success -> HomeContent(state.items, Modifier.padding(padding))
is HomeUiState.Error -> ErrorContent(state.message, onRetry = viewModel::loadItems)
}
}
}
```
## Room Database
```kotlin
@Entity(tableName = "items")
data class ItemEntity(
@PrimaryKey val id: String,
val title: String,
val updatedAt: Long = System.currentTimeMillis()
)
@Dao
interface ItemDao {
@Query("SELECT * FROM items ORDER BY updatedAt DESC")
fun observeAll(): Flow<List<ItemEntity>>
@Upsert
suspend fun upsertAll(items: List<ItemEntity>)
@Query("DELETE FROM items")
suspend fun deleteAll()
}
@Database(entities = [ItemEntity::class], version = 1, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
abstract fun itemDao(): ItemDao
}
```
## Hilt DI Setup
```kotlin
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides @Singleton
fun provideRetrofit(): Retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(buildOkHttpClient())
.build()
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds @Singleton
abstract fun bindItemRepository(impl: ItemRepositoryImpl): ItemRepository
}
```
## Key Dependencies (libs.versions.toml)
```toml
[versions]
kotlin = "2.0.0"
compose-bom = "2024.06.00"
hilt = "2.51"
room = "2.6.1"
retrofit = "2.11.0"
coroutines = "1.8.1"
lifecycle = "2.8.2"
[libraries]
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
```
## Testing Setup
```kotlin
// ViewModel unit test
@OptIn(ExperimentalCoroutinesApi::class)
class HomeViewModelTest {
@get:Rule val mainDispatcherRule = MainDispatcherRule()
private val getItemsUseCase = mockk<GetItemsUseCase>()
private lateinit var viewModel: HomeViewModel
@BeforeEach
fun setup() { viewModel = HomeViewModel(getItemsUseCase) }
@Test
fun `loadItems emits Success when use case succeeds`() = runTest {
val items = listOf(Item("1", "Test"))
coEvery { getItemsUseCase() } returns Result.success(items)
viewModel.uiState.test {
skipItems(1) // Loading
assertThat(awaitItem()).isEqualTo(HomeUiState.Success(items))
}
}
}
```
@@ -0,0 +1,242 @@
# React Native Reference (TypeScript)
## Project Structure
```
src/
├── app/
│ ├── App.tsx # Root component, providers
│ ├── navigation/ # React Navigation stacks + types
│ └── store/ # RTK store setup
├── features/
│ └── home/
│ ├── api/ # RTK Query endpoints
│ ├── components/ # Screen-specific components
│ ├── hooks/ # Feature-level custom hooks
│ ├── screens/ # Screen components
│ ├── store/ # Zustand slice or RTK slice
│ └── types.ts # Feature types
├── shared/
│ ├── components/ # Design system components
│ ├── hooks/ # Shared hooks
│ ├── theme/ # Colors, typography, spacing constants
│ └── utils/ # Utilities
└── services/
├── api/ # Axios/fetch client + interceptors
└── storage/ # MMKV wrapper
```
## Navigation Setup (React Navigation v7)
```typescript
export type RootStackParamList = {
Auth: undefined;
Home: undefined;
Detail: { id: string };
Settings: undefined;
};
export type RootStackScreenProps<T extends keyof RootStackParamList> =
NativeStackScreenProps<RootStackParamList, T>;
const Stack = createNativeStackNavigator<RootStackParamList>();
export const RootNavigator = () => {
const isLoggedIn = useAuthStore((s) => s.isLoggedIn);
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isLoggedIn ? (
<>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Detail" component={DetailScreen} />
</>
) : (
<Stack.Screen name="Auth" component={AuthScreen} />
)}
</Stack.Navigator>
);
};
```
## State Management (Zustand + React Query)
```typescript
// Client state — Zustand
interface AuthState {
token: string | null;
isLoggedIn: boolean;
setToken: (token: string) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
token: null,
isLoggedIn: false,
setToken: (token) => set({ token, isLoggedIn: true }),
logout: () => set({ token: null, isLoggedIn: false }),
}),
{ name: 'auth-storage', storage: createJSONStorage(() => mmkvStorage) }
)
);
// Server state — React Query
export const useItems = () =>
useQuery({
queryKey: ['items'],
queryFn: itemsApi.getAll,
staleTime: 5 * 60 * 1000, // 5 minutes
});
export const useRefreshItems = () =>
useMutation({
mutationFn: itemsApi.refresh,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
});
```
## Screen Pattern
```typescript
type HomeScreenProps = RootStackScreenProps<'Home'>;
export const HomeScreen: FC<HomeScreenProps> = ({ navigation }) => {
const { data: items, isLoading, isError, refetch } = useItems();
if (isLoading) return <LoadingView />;
if (isError) return <ErrorView onRetry={refetch} />;
return (
<SafeAreaView style={styles.container}>
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<ItemCard
item={item}
onPress={() => navigation.navigate('Detail', { id: item.id })}
/>
)}
ListEmptyComponent={<EmptyView />}
refreshControl={
<RefreshControl refreshing={isLoading} onRefresh={refetch} />
}
/>
</SafeAreaView>
);
};
```
## API Client (Axios with interceptors)
```typescript
const apiClient = axios.create({
baseURL: Config.API_BASE_URL,
timeout: 10_000,
headers: { 'Content-Type': 'application/json' },
});
// Auth token injection
apiClient.interceptors.request.use((config) => {
const token = useAuthStore.getState().token;
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// Token refresh on 401
apiClient.interceptors.response.use(
(res) => res,
async (error: AxiosError) => {
if (error.response?.status === 401) {
const newToken = await refreshToken();
if (newToken) {
useAuthStore.getState().setToken(newToken);
return apiClient(error.config!);
}
useAuthStore.getState().logout();
}
return Promise.reject(error);
}
);
```
## API Response Validation (Zod)
```typescript
const ItemSchema = z.object({
id: z.string(),
title: z.string(),
description: z.string().optional(),
createdAt: z.string().datetime(),
});
const ItemsResponseSchema = z.array(ItemSchema);
type Item = z.infer<typeof ItemSchema>;
const getItems = async (): Promise<Item[]> => {
const { data } = await apiClient.get('/items');
return ItemsResponseSchema.parse(data); // throws ZodError on invalid shape
};
```
## Key Dependencies
```json
{
"dependencies": {
"react-native": "0.74.x",
"@react-navigation/native": "^7.0.0",
"@react-navigation/native-stack": "^7.0.0",
"@tanstack/react-query": "^5.45.0",
"zustand": "^4.5.4",
"axios": "^1.7.2",
"zod": "^3.23.8",
"react-native-mmkv": "^2.12.2",
"react-native-safe-area-context": "^4.10.1",
"react-native-screens": "^3.32.0"
},
"devDependencies": {
"typescript": "^5.4.5",
"@testing-library/react-native": "^12.5.1",
"msw": "^2.3.1",
"jest": "^29.7.0"
}
}
```
## New Architecture (Bridgeless) Notes
- Enable New Architecture in `android/gradle.properties`: `newArchEnabled=true`
- Use TurboModules for native modules; avoid legacy NativeModules API
- Use Fabric for custom native views
- Test with Hermes JS engine always enabled
## Performance Tips
- Use `useCallback` + `memo` on `renderItem` / list item components
- `FlatList` `windowSize`, `initialNumToRender`, `maxToRenderPerBatch` tuned
- Avoid anonymous inline functions in JSX
- `InteractionManager.runAfterInteractions` for heavy post-navigation work
- `react-native-reanimated` for 60fps animations (runs on UI thread)
## Testing
```typescript
describe('HomeScreen', () => {
it('shows items when query succeeds', async () => {
server.use(
http.get(`${API_URL}/items`, () =>
HttpResponse.json([{ id: '1', title: 'Test Item' }])
)
);
const { getByText } = render(
<QueryClientProvider client={testQueryClient}>
<HomeScreen navigation={mockNavigation} route={mockRoute} />
</QueryClientProvider>
);
expect(await findByText('Test Item')).toBeTruthy();
});
});
```