소스 정보
- 저장소
- tomevault-io/skills-registry
- 최근 소스 활동
- 2026년 7월 3일 19:45
- 감지된 SKILL.md 언어
- 영어
- 스타
- 0
- 포크
- 0
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/tomevault-io/skills-registry --skill mobile-design-patterns명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SOC 직업 분류 기준
SKILL.md 표시 중
| name | mobile-design-patterns |
| description | > Use when this capability is needed. |
You are a senior mobile architect. Help the user understand, compare, choose, or implement mobile design patterns with clear examples and tradeoff analysis.
| Question | Why It Matters |
|---|---|
| Are you choosing for a new project or understanding existing code? | Greenfield vs. migration |
| What platform? (Flutter, Android, iOS, or all) | Platform idioms differ |
| What is the app complexity? | Simple apps don't need complex patterns |
| What is the team size? | More developers = more need for strict boundaries |
| What is the team's experience? | Familiar patterns ship faster |
| What are the testing requirements? | Some patterns are far more testable |
| Pattern | Separation | Testability | Complexity | Learning Curve | Best For |
|---|---|---|---|---|---|
| MVC | Low | Low | Low | Easy | Small apps, prototypes, legacy |
| MVP | Medium | Medium-High | Medium | Moderate | UIKit/XML apps needing testable presenters |
| MVVM | High | High | Medium | Moderate | Most apps — the modern default |
| MVI | Very High | Very High | High | Steep | Complex state, undo/redo, time-travel debugging |
| Clean Architecture | Very High | Very High | High | Steep | Large apps, long-lived codebases, multiple teams |
| VIPER | Very High | Very High | Very High | Steep | Large UIKit apps (less relevant for SwiftUI) |
| TCA | Very High | Very High | High | Steep | SwiftUI apps, composability, exhaustive testing |
| BLoC | High | Very High | Medium-High | Moderate | Flutter apps, event-driven, stream-based |
Data flow:
User Action → Controller → updates Model
→ updates View
Model change → Controller → updates View
Roles:
| Component | Responsibility |
|---|---|
| Model | Data and business logic |
| View | Display UI, receive user input |
| Controller | Mediates between Model and View, handles user actions |
The problem: In mobile, the Controller (Activity/UIViewController) often becomes the View AND Controller — the "Massive View Controller" anti-pattern. Business logic, navigation, networking, and UI all end up in one class.
// Activity acts as both View and Controller (typical MVC issue)
public class ProductListActivity extends AppCompatActivity {
private ProductRepository repo;
private RecyclerView recyclerView;
private ProductAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_product_list);
recyclerView = findViewById(R.id.recycler_view);
adapter = new ProductAdapter();
recyclerView.setAdapter(adapter);
// Controller logic mixed with View
repo.getProducts(new Callback<List<Product>>() {
@Override
public void onSuccess(List<Product> products) {
adapter.setProducts(products); // View update
}
@Override
public void onError(Exception e) {
Toast.makeText(ProductListActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
}
// UIViewController acts as both View and Controller
@implementation ProductListViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell"];
// Controller logic mixed with View
[self.repository getProductsWithCompletion:^(NSArray<Product *> *products, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
[self showAlert:error.localizedDescription];
} else {
self.products = products;
[self.tableView reloadData];
}
});
}];
}
// Also acts as data source (more Controller+View mixing)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
cell.textLabel.text = self.products[indexPath.row].name;
return cell;
}
@end
When to use: Prototypes, very simple apps, learning. Avoid for production apps with more than a few screens.
Data flow:
User Action → View → Presenter → Model
↓
View ← Presenter ← Model (result)
Roles:
| Component | Responsibility |
|---|---|
| Model | Data and business logic |
| View | Display UI, delegates all actions to Presenter (passive) |
| Presenter | All presentation logic, updates View through interface |
Key difference from MVC: The View is passive (implements an interface). The Presenter holds no reference to Android/iOS framework classes, making it unit-testable.
// View interface
public interface ProductListView {
void showProducts(List<Product> products);
void showError(String message);
void showLoading();
}
// Presenter — fully testable, no Android imports
public class ProductListPresenter {
private final ProductListView view;
private final ProductRepository repo;
public ProductListPresenter(ProductListView view, ProductRepository repo) {
this.view = view;
this.repo = repo;
}
public void loadProducts() {
view.showLoading();
repo.getProducts(new Callback<List<Product>>() {
@Override
public void onSuccess(List<Product> products) {
view.showProducts(products);
}
@Override
public void onError(Exception e) {
view.showError(e.getMessage());
}
});
}
}
// Activity implements the View interface
public class ProductListActivity {
ProductListPresenter presenter;
{
.onCreate(savedInstanceState);
presenter = (, ());
presenter.loadProducts();
}
{
adapter.setProducts(products);
}
{
Toast.makeText(, message, Toast.LENGTH_SHORT).show();
}
{ }
}
// View protocol
protocol ProductListViewProtocol: AnyObject {
func showProducts(_ products: [Product])
func showError(_ message: String)
func showLoading()
}
// Presenter — no UIKit imports
class ProductListPresenter {
weak var view: ProductListViewProtocol?
private let repo: ProductRepository
init(repo: ProductRepository) {
self.repo = repo
}
func loadProducts() {
view?.showLoading()
repo.getProducts { [weak self] result in
switch result {
case .success(let products):
self?.view?.showProducts(products)
case .failure(let error):
self?.view?.showError(error.localizedDescription)
}
}
}
}
// ViewController implements View protocol
class : , {
presenter:
( : []) {
.products products
tableView.reloadData()
}
}
When to use: UIKit or XML-based apps where you need testable presentation logic. Less relevant for Compose/SwiftUI where MVVM is more natural.
Data flow:
User Action → View → ViewModel → Model / Repository
↓
View ← (observes) ← ViewModel ← Model (result)
Roles:
| Component | Responsibility |
|---|---|
| Model | Data, business logic, repository |
| View | Display UI, bind to ViewModel observables |
| ViewModel | Expose state as observable streams, handle user intents |
Key difference from MVP: The View observes the ViewModel reactively (data binding) rather than the Presenter calling View methods imperatively. The ViewModel does NOT hold a reference to the View.
// ViewModel (Riverpod notifier)
@riverpod
class ProductListViewModel extends _$ProductListViewModel {
@override
Future<List<Product>> build() async {
return ref.read(productRepoProvider).getAll();
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => ref.read(productRepoProvider).getAll(),
);
}
}
// View — observes ViewModel reactively
class ProductListScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(productListViewModelProvider);
return state.when(
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
data: (products) => ListView.builder(
itemCount: products.length,
itemBuilder: (_, i) => ProductTile(product: products[i]),
),
);
}
}
// ViewModel — exposes state as StateFlow
class ProductListViewModel(
private val repo: ProductRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<List<Product>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<Product>>> = _uiState.asStateFlow()
init { loadProducts() }
fun loadProducts() {
viewModelScope.launch {
_uiState.value = UiState.Loading
_uiState.value = try {
UiState.Success(repo.getAll())
} catch (e: Exception) {
UiState.Error(e.message ?: "Unknown error")
}
}
}
}
// View — observes ViewModel
@Composable
fun ProductListScreen(viewModel: ProductListViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (val state = uiState) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Error -> Text("Error: ${state.message}")
is UiState.Success -> LazyColumn {
items(state.data) { product -> ProductCard(product) }
}
}
}
// ViewModel — exposes state as LiveData
public class ProductListViewModel extends ViewModel {
private final MutableLiveData<List<Product>> products = new MutableLiveData<>();
private final MutableLiveData<Boolean> loading = new MutableLiveData<>(false);
private final ProductRepository repo;
public LiveData<List<Product>> getProducts() { return products; }
public LiveData<Boolean> isLoading() { return loading; }
public void loadProducts() {
loading.setValue(true);
repo.getAll(result -> {
loading.postValue(false);
products.postValue(result);
});
}
}
// Activity — observes LiveData
public class ProductListActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ProductListViewModel vm = new ().get(ProductListViewModel.class);
vm.getProducts().observe(, products -> {
adapter.submitList(products);
});
vm.isLoading().observe(, loading -> {
progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
});
vm.loadProducts();
}
}
// ViewModel — observable state
@Observable // iOS 17+
class ProductListViewModel {
var products: [Product] = []
var isLoading = false
var errorMessage: String?
private let repo: ProductRepository
func loadProducts() async {
isLoading = true
defer { isLoading = false }
do {
products = try await repo.getAll()
} catch {
errorMessage = error.localizedDescription
}
}
}
// View — automatic observation
struct ProductListView: View {
@State private var viewModel = ProductListViewModel(repo: .live)
var body: some View {
Group {
if viewModel.isLoading {
ProgressView()
} else {
List(viewModel.products) { product in
ProductRow(product: product)
}
}
}
.task { viewModel.loadProducts() }
}
}
class ProductListViewModel {
@Published private(set) var products: [Product] = []
@Published private(set) var isLoading = false
private var cancellables = Set<AnyCancellable>()
func loadProducts() {
isLoading = true
repo.getAll()
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] _ in self?.isLoading = false },
receiveValue: { [weak self] in self?.products = $0 }
)
.store(in: &cancellables)
}
}
// UIViewController binds via Combine
class ProductListVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
viewModel.$products
.sink { [weak self] _ .tableView.reloadData() }
.store(in: cancellables)
}
}
When to use: The default choice for most modern mobile apps. Works naturally with reactive frameworks (Compose, SwiftUI, Flutter).
Data flow:
User Action → Intent (Event) → Reducer → New State → View renders
↕
Side Effects
Roles:
| Component | Responsibility |
|---|---|
| Model | Immutable state object representing the entire screen |
| View | Renders state, emits user Intents (events) |
| Intent | User actions as explicit events (sealed class/enum) |
| Reducer | Pure function: (State, Intent) → State |
| Side Effects | Async operations triggered by Intents |
Key difference from MVVM: State is a single immutable object. All changes go through a reducer. This makes state transitions predictable, debuggable, and testable.
// State — single immutable object
@freezed
class ProductListState with _$ProductListState {
const factory ProductListState({
@Default([]) List<Product> products,
@Default(false) bool isLoading,
String? error,
@Default('') String searchQuery,
}) = _ProductListState;
}
// Events (Intents)
@freezed
class ProductListEvent with _$ProductListEvent {
const factory ProductListEvent.loadRequested() = _LoadRequested;
const factory ProductListEvent.searchChanged(String query) = _SearchChanged;
const factory ProductListEvent.productDeleted(String id) = _ProductDeleted;
}
// BLoC — reducer + side effects
class ProductListBloc extends Bloc<ProductListEvent, ProductListState> {
ProductListBloc(this._repo) : super(const ProductListState()) {
on<_LoadRequested>(_onLoadRequested);
on<_SearchChanged>(_onSearchChanged, transformer: debounce(300.ms));
}
Future<void> _onLoadRequested(event, emit) async {
emit(state.copyWith(isLoading: true, error: null));
try {
final products = await _repo.getAll();
emit(state.copyWith(isLoading: false, products: products));
} catch (e) {
emit(state.copyWith(isLoading: false, error: e.toString()));
}
}
}
// State
data class ProductListState(
val products: List<Product> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null,
val searchQuery: String = ""
)
// Intent (Event)
sealed interface ProductListIntent {
data object LoadRequested : ProductListIntent
data class SearchChanged(val query: String) : ProductListIntent
data class ProductDeleted(val id: String) : ProductListIntent
}
// ViewModel as state machine
class ProductListViewModel(private val repo: ProductRepository) : ViewModel() {
private val _state = MutableStateFlow(ProductListState())
val state: StateFlow<ProductListState> = _state.asStateFlow()
fun onIntent(intent: ProductListIntent) {
when (intent) {
is ProductListIntent.LoadRequested -> loadProducts()
is ProductListIntent.SearchChanged -> search(intent.query)
is ProductListIntent.ProductDeleted -> delete(intent.id)
}
}
private fun {
viewModelScope.launch {
_state.update { it.copy(isLoading = , error = ) }
{
products = repo.getAll()
_state.update { it.copy(isLoading = , products = products) }
} (e: Exception) {
_state.update { it.copy(isLoading = , error = e.message) }
}
}
}
}
@Reducer
struct ProductListFeature {
@ObservableState
struct State: Equatable {
var products: [Product] = []
var isLoading = false
var searchQuery = ""
@Presents var alert: AlertState<Action.Alert>?
}
enum Action {
case loadRequested
case productsLoaded(Result<[Product], Error>)
case searchChanged(String)
case alert(PresentationAction<Alert>)
enum Alert: Equatable {}
}
@Dependency(\.productClient) var productClient
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .loadRequested:
state.isLoading = true
return .run { send in
send(.productsLoaded( {
productClient.getAll()
}))
}
.productsLoaded(.success( products)):
state.isLoading
state.products products
.none
.productsLoaded(.failure( error)):
state.isLoading
state.alert { (error.localizedDescription) }
.none
.searchChanged( query):
state.searchQuery query
.none
.alert:
.none
}
}
}
}
When to use: Complex screens with many interacting state fields, undo/redo, time-travel debugging, or when state predictability is critical.
Layer structure:
┌─────────────────────┐
│ Presentation │ ← UI, ViewModels, Widgets
├─────────────────────┤
│ Domain │ ← Use Cases, Entities, Repository Interfaces
├─────────────────────┤
│ Data │ ← Repository Impls, APIs, Database, DTOs
└─────────────────────┘
Dependency rule: outer layers depend on inner layers, NEVER the reverse.
Domain layer has ZERO external dependencies.
Roles:
| Layer | Contains | Depends On |
|---|---|---|
| Domain | Entities, Use Cases, Repository interfaces | Nothing (pure language) |
| Data | Repository impls, API clients, DB, DTOs, mappers | Domain (implements interfaces) |
| Presentation | UI, ViewModels/BLoCs, navigation | Domain (uses Use Cases) |
Clean Architecture applies ON TOP of any presentation pattern (MVVM, MVI, BLoC).
lib/
features/
products/
domain/
entities/product.dart # Pure Dart class
repositories/product_repo.dart # Abstract class (interface)
usecases/get_products.dart # Single-purpose use case
data/
models/product_model.dart # DTO with fromJson/toJson
datasources/product_api.dart # HTTP client
datasources/product_local.dart # Local DB
repositories/product_repo_impl.dart # Implements domain interface
presentation/
bloc/product_list_bloc.dart # BLoC using use cases
pages/product_list_page.dart # Widget
widgets/product_card.dart # Reusable widget
app/
features/
products/
domain/
model/Product.kt # Domain entity (no annotations)
repository/ProductRepository.kt # Interface
usecase/GetProductsUseCase.kt # Single-purpose use case
data/
model/ProductDto.kt # API/DB model with annotations
mapper/ProductMapper.kt # DTO ↔ Domain mapping
remote/ProductApi.kt # Retrofit interface
local/ProductDao.kt # Room DAO
repository/ProductRepositoryImpl.kt
presentation/
ProductListViewModel.kt # Uses use cases, exposes StateFlow
ProductListScreen.kt # Compose UI
App/
Features/
Products/
Domain/
Entities/Product.swift # Plain Swift struct
Repositories/ProductRepository.swift # Protocol
UseCases/GetProductsUseCase.swift
Data/
DTOs/ProductDTO.swift # Codable struct
DataSources/ProductAPI.swift # URLSession/Alamofire
DataSources/ProductLocalDB.swift
Repositories/ProductRepositoryImpl.swift
Presentation/
ProductListViewModel.swift # @Observable, uses use cases
ProductListView.swift # SwiftUI view
When to use: Large apps with multiple developers, long-lived codebases, or when strict testability and separation are required. Overkill for small apps.
Components:
| Component | Responsibility |
|---|---|
| View | Display UI (passive, like MVP) |
| Interactor | Business logic (like Use Case) |
| Presenter | Presentation logic, mediates View ↔ Interactor |
| Entity | Data models |
| Router | Navigation logic |
When to use: Large UIKit apps. Largely superseded by Clean Architecture + MVVM for modern development. Avoid for SwiftUI/Compose (too many abstractions for declarative UI).
Is the app small (< 5 screens)?
→ MVVM (simple ViewModel per screen)
Is the app medium (5-20 screens)?
→ MVVM with Repository pattern
Is the app large (20+ screens, multiple devs)?
→ Clean Architecture + MVVM or MVI
Does state management need strict predictability?
→ MVI (BLoC for Flutter, TCA for iOS)
Is this a legacy UIKit/XML app?
→ MVP (if refactoring from MVC) or MVVM with UIKit binding
Is this a legacy Objective-C/Java app?
→ MVC (existing) → migrate to MVP → then MVVM as you modernize
| From | To | Strategy |
|---|---|---|
| MVC → MVP | Extract Presenter from Controller, define View interface | Screen by screen |
| MVC → MVVM | Extract ViewModel, add reactive bindings | Screen by screen |
| MVP → MVVM | Replace View interface with reactive observation | Replace Presenter with ViewModel |
| MVVM → MVI | Unify state into single object, add event/intent layer | Feature by feature |
| Any → Clean Architecture | Add domain layer (entities + use cases + interfaces), then refactor data layer | Layer by layer, domain first |
Rule: Never rewrite. Always migrate incrementally — one screen or one feature at a time.
## Pattern Recommendation
- **Pattern:** [MVVM / MVI / Clean Architecture + MVVM / etc.]
- **Platform:** [Flutter / Android / iOS]
- **Rationale:** [Why this pattern for this context]
## Architecture Diagram
[Layer diagram with data flow arrows]
## Example Implementation
[Key classes and their relationships]
## Testing Strategy
[What is testable and how]
## Migration Plan (if applicable)
[Step-by-step migration from current pattern]
Source: ashutoshsrivastava17/skill-library — distributed by TomeVault.