L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Développement · SKILL.md

mobile-patterns

mobile-patterns

Use this skill when the user asks about mobile architecture patterns, MVVM, MVI, Clean Architecture, Coordinator, Navigator, Repository pattern, UseCase, or dependency injection in mobile apps.

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: mobile-patterns description: > Use this skill when the user asks about mobile architecture patterns, MVVM, MVI, Clean Architecture, Coordinator, Navigator, Repository pattern, UseCase, or dependency injection in mobile apps. version: "1.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [mobile, patterns, phase-4, universal]

Mobile Architecture Patterns

Purpose

Select and implement cross-platform mobile architecture patterns including MVVM, MVI, Clean Architecture, Coordinator, and Repository patterns.

Agent Protocol

Trigger

User request includes: mobile pattern, mobile architecture, mvvm, mvi, clean architecture mobile, coordinator, navigator pattern, mobile project structure, mobile folder structure, mobile di.

Input Context

  • Platform (iOS, Android, Flutter, React Native)
  • Architecture approach (MVVM, MVI, Clean, TCA)
  • DI framework (Hilt, Swinject, GetIt, manual)

Output Artifact

A markdown document containing:

  • Architecture pattern selection guide
  • Folder structure matching the pattern
  • Data flow diagram (text)
  • Key code snippets for the chosen pattern

Response Format

No preamble. No postamble. No explanations.

Max Response Length

4096 tokens

Decision Trees

Architecture Pattern Selection

App complexity?
├── Simple CRUD, list/detail, <10 screens
│   └── MVVM + Repository (minimal abstraction, well-known)
├── Complex state (real-time, multi-step, concurrent updates)
│   └── MVI (unidirectional, predictable, sealed intents/state)
├── Large codebase, 5+ teams, shared domain logic
│   └── Clean Architecture (strict boundaries, modular)
└── Swift-only, complex state, composable
    └── TCA (The Composable Architecture, SwiftUI-native)

DI Framework Selection

Platform?
├── Android
│   ├── Hilt (standard, Dagger wrapper, @HiltViewModel)
│   └── Koin (lightweight, Kotlin DSL, no annotation processing)
├── iOS
│   ├── Swinject (runtime DI, container, assembly)
│   └── Factory (Swift macros, compile-time safe)
├── Flutter
│   ├── GetIt (service locator, simple, fast)
│   └── Riverpod (declarative, auto-dispose, testable)
└── React Native
    ├── InversifyJS (decorators, container)
    └── Manual context-based DI (React Context + hooks)

Navigation Pattern Selection

Navigation complexity?
├── Simple linear flow (login → home → detail)
│   └── Built-in navigation (NavigationStack, NavHost, Stack)
├── Deep linking, conditional flows, auth state
│   └── Coordinator pattern (navigation extracted from views)
├── Tab-based with nested stacks
│   └── ShellRoute (Flutter), Tab Navigator (RN)
└── Complex onboarding/wizard with branching
    └── Coordinator with child coordinators

Workflow

Step 1: Identify Architecture Requirements

Assess team size, app complexity, testing requirements, and platform constraints.

Step 2: Select Core Pattern

Choose MVVM for standard apps, MVI for complex state, Clean Architecture for large multi-team codebases.

Step 3: Define Layer Boundaries

Establish dependency rules: presentation depends on domain, domain is pure business logic, data implements domain interfaces.

Step 4: Separate Navigation

Extract navigation logic into Coordinator/Navigator — views know nothing about other screens.

Step 5: Set Up Dependency Injection

Configure DI framework with explicit scopes and clear module organization per layer.

Cross-Platform Pattern Mapping

PatterniOSAndroidFlutterReact Native
MVVMObservableObject + @PublishedViewModel + StateFlowChangeNotifier + RiverpoduseState + Zustand
MVICombine + enum IntentMVI + StateFlow + sealed classBLoC (Event → Bloc → State)useReducer + dispatch
Clean ArchSPM modules per layerGradle modulesDart packagesWorkspace packages
CoordinatorCoordinator protocol + childNavHost + sealed routeGoRouter ShellRouteReact Navigation nesting

Rules

  • Domain layer must have zero framework dependencies — pure business logic only
  • Data layer depends on domain layer — never the reverse
  • MVVM: View observes ViewModel state; ViewModel never holds View reference
  • MVI: sealed class for intents, single sealed state class, reducer function
  • Clean Architecture: outer layers depend on inner layers, never inward
  • Coordinator owns navigation — views call coordinator callbacks
  • Repository is the single source of truth
  • Choose architecture based on app complexity, not trends

MVVM Implementation

Android — Jetpack Compose

// ViewModel
@HiltViewModel
class OrderViewModel @Inject constructor(
  private val getOrdersUseCase: GetOrdersUseCase
) : ViewModel() {
  private val _state = MutableStateFlow(OrderUiState())
  val state: StateFlow<OrderUiState> = _state.asStateFlow()

  init { loadOrders() }

  fun onAction(action: OrderAction) {
    when (action) {
      is OrderAction.Refresh -> loadOrders()
      is OrderAction.SelectOrder -> selectOrder(action.id)
    }
  }

  private fun loadOrders() {
    viewModelScope.launch {
      _state.update { it.copy(isLoading = true) }
      getOrdersUseCase().fold(
        onSuccess = { orders ->
          _state.update { it.copy(orders = orders, isLoading = false) }
        },
        onFailure = { error ->
          _state.update { it.copy(error = error.message, isLoading = false) }
        }
      )
    }
  }
}

data class OrderUiState(
  val orders: List<Order> = emptyList(),
  val isLoading: Boolean = false,
  val error: String? = null
)

sealed interface OrderAction {
  data object Refresh : OrderAction
  data class SelectOrder(val id: String) : OrderAction
}

// Composable
@Composable
fun OrderScreen(viewModel: OrderViewModel = hiltViewModel()) {
  val state by viewModel.state.collectAsStateWithLifecycle()
  OrderContent(state = state, onAction = viewModel::onAction)
}

iOS — SwiftUI

// ViewModel
@MainActor
@Observable
final class OrderViewModel {
  private let getOrdersUseCase: GetOrdersUseCaseProtocol
  var orders: [Order] = []
  var isLoading = false
  var error: String?

  init(getOrdersUseCase: GetOrdersUseCaseProtocol) {
    self.getOrdersUseCase = getOrdersUseCase
  }

  func loadOrders() async {
    isLoading = true
    do {
      orders = try await getOrdersUseCase()
      error = nil
    } catch {
      self.error = error.localizedDescription
    }
    isLoading = false
  }
}

// View
struct OrderView: View {
  @State private var vm: OrderViewModel

  var body: some View {
    List(vm.orders) { order in
      OrderRow(order: order)
    }
    .overlay { if vm.isLoading { ProgressView() } }
    .task { await vm.loadOrders() }
    .refreshable { await vm.loadOrders() }
  }
}

Flutter — Riverpod

// Provider
@riverpod
class OrderList extends _$OrderList {
  @override
  Future<List<Order>> build() => _fetchOrders();

  Future<List<Order>> _fetchOrders() async {
    final repo = ref.read(orderRepositoryProvider);
    return repo.getOrders();
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => _fetchOrders());
  }
}

// View
class OrderScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final ordersAsync = ref.watch(orderListProvider);
    return ordersAsync.when(
      data: (orders) => ListView.builder(
        itemCount: orders.length,
        itemBuilder: (_, i) => OrderRow(order: orders[i]),
      ),
      loading: () => const CircularProgressIndicator(),
      error: (e, _) => Text('Error: $e'),
    );
  }
}

React Native — Zustand

// Store
import { create } from 'zustand';

interface OrderState {
  orders: Order[];
  isLoading: boolean;
  error: string | null;
  loadOrders: () => Promise<void>;
  refresh: () => Promise<void>;
}

export const useOrderStore = create<OrderState>((set) => ({
  orders: [],
  isLoading: false,
  error: null,
  loadOrders: async () => {
    set({ isLoading: true, error: null });
    try {
      const orders = await orderService.getOrders();
      set({ orders, isLoading: false });
    } catch (e) {
      set({ error: (e as Error).message, isLoading: false });
    }
  },
  refresh: async () => {
    set({ isLoading: true });
    try {
      const orders = await orderService.getOrders();
      set({ orders, isLoading: false });
    } catch (e) {
      set({ isLoading: false });
    }
  },
}));

// Component
function OrderScreen() {
  const { orders, isLoading, loadOrders } = useOrderStore();
  useEffect(() => { loadOrders(); }, []);
  return (
    <FlatList
      data={orders}
      renderItem={({ item }) => <OrderRow order={item} />}
      refreshing={isLoading}
      onRefresh={loadOrders}
    />
  );
}

MVI Implementation

Android — MVI with StateFlow

// Intent (user actions)
sealed interface OrderIntent {
  data object Load : OrderIntent
  data class Search(val query: String) : OrderIntent
  data class Delete(val id: String) : OrderIntent
}

// State (single source of truth)
data class OrderListState(
  val orders: List<Order> = emptyList(),
  val searchQuery: String = "",
  val isLoading: Boolean = false,
  val error: String? = null
)

// Reducer (pure function)
fun OrderListState.reduce(intent: OrderIntent): OrderListState {
  return when (intent) {
    is OrderIntent.Load -> copy(isLoading = true, error = null)
    is OrderIntent.Search -> copy(searchQuery = intent.query)
    is OrderIntent.Delete -> copy(
      orders = orders.filter { it.id != intent.id }
    )
  }
}

// ViewModel processes intents, emits states
class OrderListViewModel : ViewModel() {
  private val _state = MutableStateFlow(OrderListState())
  val state: StateFlow<OrderListState> = _state.asStateFlow()

  fun process(intent: OrderIntent) {
    _state.update { it.reduce(intent) }
    when (intent) {
      is OrderIntent.Load -> loadOrders()
      is OrderIntent.Search -> searchOrders(intent.query)
      is OrderIntent.Delete -> deleteOrder(intent.id)
    }
  }
}

Flutter — BLoC

// Event
abstract class OrderEvent {}
class LoadOrders extends OrderEvent {}
class SearchOrders extends OrderEvent { final String query; }

// State
abstract class OrderState {}
class OrderInitial extends OrderState {}
class OrderLoading extends OrderState {}
class OrderLoaded extends OrderState { final List<Order> orders; }
class OrderError extends OrderState { final String message; }

// Bloc
class OrderBloc extends Bloc<OrderEvent, OrderState> {
  final OrderRepository repo;
  OrderBloc(this.repo) : super(OrderInitial()) {
    on<LoadOrders>((event, emit) async {
      emit(OrderLoading());
      try {
        final orders = await repo.getOrders();
        emit(OrderLoaded(orders: orders));
      } catch (e) {
        emit(OrderError(message: e.toString()));
      }
    });
  }
}

Clean Architecture

Folder Structure

project/
├── domain/                    # Pure business logic — zero framework deps
│   ├── model/
│   │   └── Order.kt
│   ├── repository/            # Interfaces only
│   │   └── OrderRepository.kt
│   └── usecase/
│       ├── GetOrdersUseCase.kt
│       └── CreateOrderUseCase.kt
├── data/                      # Implements domain interfaces
│   ├── repository/
│   │   └── OrderRepositoryImpl.kt
│   ├── remote/
│   │   └── OrderApi.kt
│   └── local/
│       ├── OrderDao.kt
│       └── OrderEntity.kt
├── presentation/              # UI framework specific
│   ├── viewmodel/
│   │   └── OrderViewModel.kt
│   ├── screen/
│   │   └── OrderScreen.kt
│   └── navigation/
│       └── OrderNavGraph.kt
└── di/                        # Dependency injection modules
    ├── NetworkModule.kt
    ├── DatabaseModule.kt
    └── RepositoryModule.kt

Dependency Rule

presentation/ → domain/ ← data/
Outer layers depend on inner layers. Inner never knows outer.
Domain has no imports from Android, iOS, or platform SDKs.

UseCase Pattern

// domain/usecase/GetOrdersUseCase.kt
class GetOrdersUseCase(
  private val repository: OrderRepository
) {
  suspend operator fun invoke(): Result<List<Order>> {
    return repository.getOrders()
  }
}

// domain/usecase/CreateOrderUseCase.kt
class CreateOrderUseCase(
  private val repository: OrderRepository,
  private val validator: OrderValidator
) {
  suspend operator fun invoke(order: Order): Result<Order> {
    if (!validator.validate(order)) {
      return Result.failure(ValidationException("Invalid order"))
    }
    return repository.createOrder(order)
  }
}

Coordinator / Navigator Pattern

iOS — Coordinator

protocol Coordinator: AnyObject {
  var navigationController: UINavigationController { get }
  var childCoordinators: [Coordinator] { get set }
  func start()
}

extension Coordinator {
  func addChild(_ coordinator: Coordinator) {
    childCoordinators.append(coordinator)
  }
  func removeChild(_ coordinator: Coordinator) {
    childCoordinators.removeAll { $0 === coordinator }
  }
}

final class AppCoordinator: Coordinator {
  let navigationController: UINavigationController
  var childCoordinators: [Coordinator] = []

  init(navigationController: UINavigationController) {
    self.navigationController = navigationController
  }

  func start() {
    let vm = OrderListViewModel(onOrderSelected: { [weak self] id in
      self?.showOrderDetail(id)
    })
    let vc = OrderListViewController(viewModel: vm)
    navigationController.pushViewController(vc, animated: false)
  }

  private func showOrderDetail(_ id: String) {
    let child = OrderDetailCoordinator(
      navigationController: navigationController,
      orderId: id
    )
    addChild(child)
    child.onFinish = { [weak self, weak child] in
      guard let child else { return }
      self?.removeChild(child)
    }
    child.start()
  }
}

Android — Navigation Compose

sealed class Route(val route: String) {
  data object OrderList : Route("orders")
  data class OrderDetail(val id: String) : Route("orders/{id}") {
    companion object {
      const val ROUTE = "orders/{id}"
      fun createRoute(id: String) = "orders/$id"
    }
  }
}

@Composable
fun AppNavGraph(navController: NavHostController = rememberNavController()) {
  NavHost(navController, startDestination = Route.OrderList.route) {
    composable(Route.OrderList.route) {
      OrderListScreen(onOrderClick = { id ->
        navController.navigate(Route.OrderDetail.createRoute(id))
      })
    }
    composable(
      route = Route.OrderDetail.ROUTE,
      arguments = listOf(navArgument("id") { type = NavType.StringType })
    ) {
      OrderDetailScreen()
    }
  }
}

Flutter — GoRouter

final router = GoRouter(
  initialLocation: '/orders',
  routes: [
    ShellRoute(
      builder: (context, state, child) => AppShell(child: child),
      routes: [
        GoRoute(
          path: '/orders',
          builder: (_, __) => const OrderListScreen(),
          routes: [
            GoRoute(
              path: ':id',
              builder: (_, state) => OrderDetailScreen(
                id: state.pathParameters['id']!,
              ),
            ),
          ],
        ),
        GoRoute(path: '/settings', builder: (_, __) => const SettingsScreen()),
      ],
    ),
  ],
);

Repository Pattern

// Repository interface (domain layer)
interface OrderRepository {
  fun getOrders(): Flow<List<Order>>
  suspend fun refreshOrders()
  suspend fun createOrder(order: Order): Order
  suspend fun deleteOrder(id: String)
}

// Repository implementation (data layer)
class OrderRepositoryImpl(
  private val api: OrderApi,
  private val dao: OrderDao,
  private val connectivity: ConnectivityMonitor
) : OrderRepository {
  override fun getOrders(): Flow<List<Order>> {
    return dao.observeAll().map { entities ->
      entities.map { it.toDomain() }
    }
  }

  override suspend fun refreshOrders() {
    if (!connectivity.isOnline()) return
    val remote = api.getOrders()
    dao.upsertAll(remote.map { it.toEntity() })
  }

  override suspend fun createOrder(order: Order): Order {
    val entity = order.toEntity()
    dao.insert(entity)
    if (connectivity.isOnline()) {
      return api.createOrder(order).also { dao.update(it.toEntity()) }
    }
    return order  // Will sync later
  }

  override suspend fun deleteOrder(id: String) {
    dao.delete(id)
    if (connectivity.isOnline()) {
      api.deleteOrder(id)
    }
  }
}

State Management Patterns

Loading/Error/Success (LES) Wrapper

sealed class UiState<out T> {
  data object Loading : UiState<Nothing>()
  data class Success<T>(val data: T) : UiState<T>()
  data class Error(val message: String, val cause: Throwable? = null) : UiState<Nothing>()
}

Optimistic UI Updates

// Zustand — optimistic update with rollback
const useOrderStore = create<OrderState>((set, get) => ({
  deleteOrder: async (id: string) => {
    const previousOrders = get().orders;
    // Optimistic remove
    set((state) => ({
      orders: state.orders.filter((o) => o.id !== id),
    }));
    try {
      await api.deleteOrder(id);  // Confirm on server
    } catch {
      set({ orders: previousOrders });  // Rollback on failure
      throw new Error('Failed to delete order');
    }
  },
}));

Error Boundary Pattern

enum AsyncResult<T> {
  case idle
  case loading
  case success(T)
  case failure(Error)

  var data: T? {
    if case .success(let value) = self { return value }
    return nil
  }

  var error: Error? {
    if case .failure(let error) = self { return error }
    return nil
  }

  var isLoading: Bool {
    if case .loading = self { return true }
    return false
  }
}

Dependency Injection Setup

Android — Hilt

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
  @Provides @Singleton
  fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .build()

  @Provides @Singleton
  fun provideOrderApi(client: OkHttpClient): OrderApi =
    Retrofit.Builder().client(client).build().create(OrderApi::class.java)
}

@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {
  @Provides @Singleton
  fun provideOrderRepository(api: OrderApi, dao: OrderDao): OrderRepository =
    OrderRepositoryImpl(api, dao)
}

iOS — Swinject

import Swinject

let container = Container()
container.register(OrderApi.self) { _ in OrderApi() }.inObjectScope(.container)
container.register(OrderDao.self) { _ in OrderDao() }.inObjectScope(.container)
container.register(OrderRepository.self) { r in
  OrderRepositoryImpl(api: r.resolve(OrderApi.self)!, dao: r.resolve(OrderDao.self)!)
}.inObjectScope(.container)
container.register(GetOrdersUseCase.self) { r in
  GetOrdersUseCase(repository: r.resolve(OrderRepository.self)!)
}
container.register(OrderViewModel.self) { r in
  OrderViewModel(getOrdersUseCase: r.resolve(GetOrdersUseCase.self)!)
}

Flutter — GetIt

final sl = GetIt.instance;

void setupDI() {
  sl.registerLazySingleton<OrderApi>(() => OrderApi());
  sl.registerLazySingleton<OrderDao>(() => OrderDao());
  sl.registerLazySingleton<OrderRepository>(() =>
    OrderRepositoryImpl(api: sl(), dao: sl()));
  sl.registerFactory(() => OrderViewModel(repo: sl()));
}

Anti-Patterns

  • Massive ViewModel: Thousands of lines with all logic. Decompose into UseCases, separate concerns
  • Leaking framework into domain: Room annotations, Retrofit types in domain layer. Domain must be pure
  • View holding ViewModel reference: Prevents garbage collection. ViewModel survives config changes
  • Navigator called from View: Ties screen to navigation. Use Coordinator callback or navigation event
  • One architecture for every screen: Different screens need different complexity. Simple screens can use simpler patterns
  • Data layer knowing about presentation: Returns UI-specific models. Data returns domain models only
  • No UseCase when logic is simple: Adds boilerplate with no benefit. UseCases only when logic is shared or complex
  • State exposed as mutable object: Multiple sources of mutation cause race conditions. Expose immutable state only
  • DI creates circular dependencies: Two classes depending on each other via DI. Extract shared dependency
  • Singletons everywhere: Global state makes testing impossible. Scope DI to appropriate lifecycle
  • Repository leaking cache details: Callers should not know about cache strategy. Repository abstracts it

Testing Patterns

ViewModel Test (Android)

class OrderViewModelTest {
  private val useCase = mockk<GetOrdersUseCase>()
  private val vm = OrderViewModel(useCase)

  @Test
  fun `load orders emits success state`() = runTest {
    val orders = listOf(Order("1", "Test"))
    coEvery { useCase() } returns Result.success(orders)
    vm.onAction(OrderAction.Refresh)
    val state = vm.state.first { !it.isLoading }
    assertEquals(orders, state.orders)
  }
}

ViewModel Test (iOS)

@Testable import MyApp

final class OrderViewModelTests: XCTestCase {
  func testLoadOrders() async {
    let mockUseCase = MockGetOrdersUseCase()
    mockUseCase.stubResult = [Order(id: "1", title: "Test")]
    let vm = await OrderViewModel(getOrdersUseCase: mockUseCase)
    await vm.loadOrders()
    let orders = await vm.orders
    XCTAssertEqual(orders.count, 1)
  }
}

Performance Considerations

  • ViewModel state should be as flat as possible (minimize data class copies)
  • Use distinctUntilChanged / skipRepeats to avoid unnecessary recomposition
  • Debounce search inputs by 300ms before dispatching intents
  • Unsubscribe from observers in onCleared / deinit / dispose to prevent leaks
  • Prefer StateFlow over LiveData on Android for better lifecycle handling
  • Use @Stable annotations on Compose / equatable on Flutter to reduce recomposition
  • Lazy-load feature modules to reduce cold start time
  • Keep UseCases stateless — inject state through parameters
  • Profile recomposition counts in debug builds to detect unnecessary redraws

References

  • references/architecture-patterns.md — Mobile Architecture Patterns
  • references/clean-arch.md — Clean Architecture for Mobile
  • references/coordinator.md — Coordinator / Navigator
  • references/design-patterns.md — Mobile Design Patterns
  • references/mobile-architecture-patterns.md — Mobile Architecture Patterns
  • references/mvvm-mvi.md — MVVM vs MVI

Handoff

After architecture selection, hand off to:

  • mobile/universal/testing — Architecture testability, unit testing patterns
  • mobile/universal/networking — Repository + API layer integration
  • mobile/universal/offline-first — Offline repository patterns
  • mobile/universal/performance — Architecture reactivity performance
  • mobile/android — Jetpack Compose, Hilt, Navigation
  • mobile/ios — SwiftUI, Combine, Coordinator
  • mobile/flutter — Riverpod, GoRouter, BLoC
  • mobile/react-native — Zustand, React Navigation
Skills enregistrées