Flutter 11: To'liq Loyiha — Desktop & Mobile Kross-Platforma Dashboard & Store App
💙 Flutter — Mobile (iOS, Android) & Desktop (macOS, Windows)
Flutter 11: To'liq Loyiha — Desktop & Mobile Kross-Platforma Dashboard & Store App
Holat: 2026-yil. Nazariy bilimlarni mustahkamlashning eng yaxshi yo'li — bu barcha qismlarni (Dart 3, Clean Architecture, REST API, Dio, Riverpod, Desktop & Mobile moslashuvchan interfeys) birlashtirgan to'liq ishlab chiqarish (production-ready) loyihasini qurishdir. Ushbu darsda biz macOS, Windows, iOS va Android'da birdek mukammal ishlaydigan "DevStore Hub" — zamonaviy e-commerce dashboard ilovasini noldan quramiz.
Mundarija
- Loyiha konsepsiyasi va UI/UX xaritasi
- Loyihaning to'liq papkalar strukturasi (Clean Architecture)
- Domain Qatlami: Entities va Repository Shartnomasi
- Data Qatlami: Dio Client, Models va Repository Implementatsiyasi
- Presentation Qatlami: Riverpod Notifiers
- UI/UX Qatlami: Desktop va Mobile uchun Moslashuvchan Shell (App Shell)
- Mahsulotlar Katalogi va Master-Detail ko'rinishi
- Statistika Dashboardi va Responsive Grid
- Desktop xususiyatlari: Klaviatura Shortcuts va Context Menu integratsiyasi
- Testlash va Ishga tushirish yo'riqnomasi
- Xulosa va Keyingi Qadamlar
- Rasmiy Manbalar
1. Loyiha konsepsiyasi va UI/UX xaritasi {#loyixa-xaritasi}
Loyihamiz — bu tovarlar ro'yxatini real REST API orqali olib keluvchi, qidiruv, saralash, tahlil qilish va savatchaga qo'shish imkoniyatiga ega boshqaruv paneli (Dashboard) hisoblanadi.
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ DEVSTORE MULTIPLATFORM │
├────────────────────────────────┬─────────────────────────────────────────────────────────────────┤
│ DESKTOP (macOS & Windows) │ MOBILE (iOS & Android) │
├────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ • Chap tomonda qat'iy Sidebar │ • Yuqorida AppBar + Qidiruv maydoni │
│ • Klaviatura tezkor tugmalari: │ • Pastda zamonaviy NavigationBar │
│ - Cmd/Ctrl+K -> Qidiruv │ • Vertikal to'liq ekranli ro'yxat │
│ - Cmd/Ctrl+R -> Yangilash │ • Pull-to-refresh (barmog'i bilan pastga tortish) │
│ • Master-Detail (2 ta ustun) │ • Detail sahifasiga alohida ekran orqali o'tish (Push route) │
│ • Sichqoncha Hover va Context │ • Tizim haptikasi (titrash) │
└────────────────────────────────┴─────────────────────────────────────────────────────────────────┘
2. Loyihaning to'liq papkalar strukturasi (Clean Architecture) {#papka-strukturasi}
lib/
├── core/
│ ├── network/
│ │ ├── api_client.dart
│ │ └── api_result.dart
│ ├── theme/
│ │ └── app_theme.dart
│ └── utils/
│ └── responsive.dart
├── features/
│ ├── products/
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ │ └── product.dart
│ │ │ └── repositories/
│ │ │ └── product_repository.dart
│ │ ├── data/
│ │ │ ├── models/
│ │ │ │ └── product_dto.dart
│ │ │ └── repositories/
│ │ │ └── product_repository_impl.dart
│ │ └── presentation/
│ │ ├── providers/
│ │ │ └── product_providers.dart
│ │ └── widgets/
│ │ ├── product_card.dart
│ │ └── product_detail_view.dart
│ └── dashboard/
│ └── presentation/
│ └── dashboard_screen.dart
├── app_shell.dart
└── main.dart
3. Domain Qatlami: Entities va Repository Shartnomasi {#domain-qatlami}
lib/features/products/domain/entities/product.dart
class Product {
final int id;
final String title;
final double price;
final String description;
final String category;
final String image;
final double rating;
const Product({
required this.id,
required this.title,
required this.price,
required this.description,
required this.category,
required this.image,
required this.rating,
});
}
lib/features/products/domain/repositories/product_repository.dart
import '../../../../core/network/api_result.dart';
import '../entities/product.dart';
abstract interface class ProductRepository {
Future<ApiResult<List<Product>>> fetchProducts();
Future<ApiResult<Product>> fetchProductDetails(int id);
}
4. Data Qatlami: Dio Client, Models va Repository Implementatsiyasi {#data-qatlami}
lib/core/network/api_result.dart
sealed class ApiResult<T> {
const ApiResult();
}
final class ApiSuccess<T> extends ApiResult<T> {
final T data;
const ApiSuccess(this.data);
}
final class ApiFailure<T> extends ApiResult<T> {
final String message;
final int? statusCode;
const ApiFailure(this.message, {this.statusCode});
}
lib/features/products/data/models/product_dto.dart
import '../../domain/entities/product.dart';
class ProductDto extends Product {
const ProductDto({
required super.id,
required super.title,
required super.price,
required super.description,
required super.category,
required super.image,
required super.rating,
});
factory ProductDto.fromJson(Map<String, dynamic> json) {
return ProductDto(
id: json['id'] as int,
title: json['title'] as String? ?? 'Nomsiz',
price: (json['price'] as num?)?.toDouble() ?? 0.0,
description: json['description'] as String? ?? '',
category: json['category'] as String? ?? 'Umumiy',
image: json['image'] as String? ?? 'https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg',
rating: ((json['rating'] as Map<String, dynamic>?)?['rate'] as num?)?.toDouble() ?? 4.5,
);
}
}
lib/features/products/data/repositories/product_repository_impl.dart
import 'package:dio/dio.dart';
import '../../../../core/network/api_result.dart';
import '../../domain/entities/product.dart';
import '../../domain/repositories/product_repository.dart';
import '../models/product_dto.dart';
class ProductRepositoryImpl implements ProductRepository {
final Dio dio;
const ProductRepositoryImpl(this.dio);
@override
Future<ApiResult<List<Product>>> fetchProducts() async {
try {
// FakeStore API — ochiq test API
final response = await dio.get('https://fakestoreapi.com/products');
if (response.statusCode == 200) {
final rawList = response.data as List<dynamic>;
final products = rawList
.map((json) => ProductDto.fromJson(json as Map<String, dynamic>))
.toList();
return ApiSuccess(products);
}
return ApiFailure('Server xatosi: ${response.statusCode}', statusCode: response.statusCode);
} on DioException catch (e) {
if (e.type == DioExceptionType.connectionTimeout || e.type == DioExceptionType.receiveTimeout) {
return const ApiFailure('Server javob bermadi (Timeout)');
}
return const ApiFailure('Internetga ulanishni tekshiring');
} catch (e) {
return ApiFailure('Kutilmagan xato: $e');
}
}
@override
Future<ApiResult<Product>> fetchProductDetails(int id) async {
try {
final response = await dio.get('https://fakestoreapi.com/products/$id');
if (response.statusCode == 200) {
return ApiSuccess(ProductDto.fromJson(response.data as Map<String, dynamic>));
}
return ApiFailure('Mahsulot topilmadi: ${response.statusCode}');
} catch (e) {
return ApiFailure('Xatolik: $e');
}
}
}
5. Presentation Qatlami: Riverpod Notifiers {#state-management}
lib/features/products/presentation/providers/product_providers.dart
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/network/api_result.dart';
import '../../domain/entities/product.dart';
import '../../domain/repositories/product_repository.dart';
import '../../data/repositories/product_repository_impl.dart';
// Dio instansiyasi
final dioProvider = Provider<Dio>((ref) {
return Dio(
BaseOptions(
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
),
);
});
// Repository provayderi
final productRepositoryProvider = Provider<ProductRepository>((ref) {
return ProductRepositoryImpl(ref.watch(dioProvider));
});
// Tanlangan mahsulot (Master-Detail uchun)
final selectedProductProvider = StateProvider<Product?>((ref) => null);
// Qidiruv so'zi
final searchQueryProvider = StateProvider<String>((ref) => '');
// Mahsulotlar ro'yxatini yuklovchi FutureProvider
final productsProvider = FutureProvider<List<Product>>((ref) async {
final repository = ref.watch(productRepositoryProvider);
final result = await repository.fetchProducts();
return switch (result) {
ApiSuccess(:final data) => data,
ApiFailure(:final message) => throw Exception(message),
};
});
// Qidiruv bo'yicha filtrlangan mahsulotlar
final filteredProductsProvider = Provider<AsyncValue<List<Product>>>((ref) {
final productsAsync = ref.watch(productsProvider);
final query = ref.watch(searchQueryProvider).toLowerCase();
return productsAsync.whenData((products) {
if (query.isEmpty) return products;
return products.where((p) => p.title.toLowerCase().contains(query)).toList();
});
});
6. UI/UX Qatlami: Desktop va Mobile uchun Moslashuvchan Shell {#app-shell}
Ilovaning asosi — AppShell ekrani. U foydalanuvchining ekrani kengligini tekshirib, avtomatik ravishda navigatsiyani o'zgartiradi:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'features/products/presentation/providers/product_providers.dart';
import 'features/products/presentation/widgets/catalog_view.dart';
import 'features/dashboard/presentation/dashboard_screen.dart';
class AppShell extends ConsumerStatefulWidget {
const AppShell({super.key});
@override
ConsumerState<AppShell> createState() => _AppShellState();
}
class _AppShellState extends ConsumerState<AppShell> {
int _currentTab = 0;
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
final isDesktop = width >= 1024;
final isTablet = width >= 600 && width < 1024;
// Desktop Shortcuts (Cmd+K / Ctrl+K, Cmd+R / Ctrl+R)
return Shortcuts(
shortcuts: {
LogicalKeySet(
Platform.isMacOS ? LogicalKeyboardKey.meta : LogicalKeyboardKey.control,
LogicalKeyboardKey.keyR,
): const _RefreshIntent(),
},
child: Actions(
actions: {
_RefreshIntent: CallbackAction<_RefreshIntent>(
onInvoke: (_) => ref.refresh(productsProvider),
),
},
child: Focus(
autofocus: true,
child: Scaffold(
body: Row(
children: [
// DESKTOP VA TABLET YONBOSH MENYUSI
if (isDesktop || isTablet)
NavigationRail(
extended: isDesktop,
minExtendedWidth: 220,
selectedIndex: _currentTab,
onDestinationSelected: (val) => setState(() => _currentTab = val),
leading: Padding(
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.indigo,
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.storefront_rounded, color: Colors.white, size: 22),
),
if (isDesktop) ...[
const SizedBox(width: 12),
const Text(
'DevStore',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
],
],
),
),
destinations: const [
NavigationRailDestination(
icon: Icon(Icons.dashboard_outlined),
selectedIcon: Icon(Icons.dashboard),
label: Text('Boshqaruv'),
),
NavigationRailDestination(
icon: Icon(Icons.inventory_2_outlined),
selectedIcon: Icon(Icons.inventory_2),
label: Text('Katalog'),
),
NavigationRailDestination(
icon: Icon(Icons.shopping_cart_outlined),
selectedIcon: Icon(Icons.shopping_cart),
label: Text('Savatcha'),
),
],
),
if (isDesktop || isTablet) const VerticalDivider(width: 1, thickness: 1),
// ASOSIY SAHIFA
Expanded(
child: IndexedStack(
index: _currentTab,
children: const [
DashboardScreen(),
CatalogView(),
Center(child: Text('Savatcha bo\'limi')),
],
),
),
],
),
// MOBIL PASTKI MENYUSI (< 600px)
bottomNavigationBar: (!isDesktop && !isTablet)
? NavigationBar(
selectedIndex: _currentTab,
onDestinationSelected: (val) => setState(() => _currentTab = val),
destinations: const [
NavigationDestination(icon: Icon(Icons.dashboard_outlined), selectedIcon: Icon(Icons.dashboard), label: 'Statistika'),
NavigationDestination(icon: Icon(Icons.inventory_2_outlined), selectedIcon: Icon(Icons.inventory_2), label: 'Katalog'),
NavigationDestination(icon: Icon(Icons.shopping_cart_outlined), selectedIcon: Icon(Icons.shopping_cart), label: 'Savatcha'),
],
)
: null,
),
),
),
);
}
}
class _RefreshIntent extends Intent {
const _RefreshIntent();
}
7. Mahsulotlar Katalogi va Master-Detail ko'rinishi {#catalog-screen}
Ushbu qismda desktopda mahsulot bosilganda o'ng tomonda uning to'liq ma'lumoti chiqadi (Master-Detail). Mobilda esa bosilganda yangi sahifaga o'tiladi:
// lib/features/products/presentation/widgets/catalog_view.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/product.dart';
import '../providers/product_providers.dart';
class CatalogView extends ConsumerWidget {
const CatalogView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final width = MediaQuery.sizeOf(context).width;
final isDesktop = width >= 1024;
final productsAsync = ref.watch(filteredProductsProvider);
final selectedProduct = ref.watch(selectedProductProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Mahsulotlar katalogi'),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(60),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: SearchBar(
hintText: 'Mahsulot nomini qidiring...',
leading: const Icon(Icons.search),
onChanged: (val) => ref.read(searchQueryProvider.notifier).state = val,
),
),
),
actions: [
IconButton(
tooltip: 'Yangilash',
icon: const Icon(Icons.refresh),
onPressed: () => ref.refresh(productsProvider),
),
],
),
body: productsAsync.when(
loading: () => const Center(child: CircularProgressIndicator.adaptive()),
error: (err, _) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 12),
Text(err.toString()),
const SizedBox(height: 12),
ElevatedButton(onPressed: () => ref.refresh(productsProvider), child: const Text('Qayta urinish')),
],
),
),
data: (products) {
if (products.isEmpty) {
return const Center(child: Text('Hech qanday mahsulot topilmadi'));
}
if (!isDesktop) {
// MOBIL RO'YXAT
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: products.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final product = products[index];
return _buildProductTile(context, product, onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => ProductDetailPage(product: product)),
);
});
},
);
}
// DESKTOP MASTER-DETAIL (2 ustun)
return Row(
children: [
// Chap ustun: Ro'yxat
SizedBox(
width: 420,
child: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: products.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final product = products[index];
final isSelected = selectedProduct?.id == product.id;
return _buildProductTile(
context,
product,
isSelected: isSelected,
onTap: () => ref.read(selectedProductProvider.notifier).state = product,
);
},
),
),
const VerticalDivider(width: 1, thickness: 1),
// O'ng ustun: Tafsilotlar
Expanded(
child: selectedProduct == null
? const Center(child: Text('Mahsulotni tanlang'))
: ProductDetailContent(product: selectedProduct),
),
],
);
},
),
);
}
Widget _buildProductTile(BuildContext context, Product product, {bool isSelected = false, required VoidCallback onTap}) {
return Card(
elevation: isSelected ? 2 : 0,
color: isSelected ? Colors.indigo.shade50 : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: isSelected ? Colors.indigo : Colors.grey.shade300,
width: isSelected ? 2 : 1,
),
),
child: ListTile(
contentPadding: const EdgeInsets.all(12),
leading: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(product.image, width: 60, height: 60, fit: BoxFit.contain),
),
title: Text(product.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.w600)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(product.category.toUpperCase(), style: TextStyle(fontSize: 11, color: Colors.grey.shade600)),
const SizedBox(height: 4),
Text('\$${product.price}', style: const TextStyle(color: Colors.indigo, fontWeight: FontWeight.bold, fontSize: 15)),
],
),
onTap: onTap,
),
);
}
}
8. Statistika Dashboardi va Responsive Grid {#dashboard-screen}
Katta ekranda 4 ta, o'rtacha ekranda 2 ta, mobilda esa 1 ta ustunli bo'lib moslashuvchi (Responsive Grid) statistika bloki:
// lib/features/dashboard/presentation/dashboard_screen.dart
import 'package:flutter/material.dart';
class DashboardScreen extends StatelessWidget {
const DashboardScreen({super.key});
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
// Ustunlar sonini kenglikka qarab belgilaymiz
int crossAxisCount = 1;
if (width >= 1200) {
crossAxisCount = 4;
} else if (width >= 700) {
crossAxisCount = 2;
}
return Scaffold(
appBar: AppBar(title: const Text('Boshqaruv Paneli (Analytics)')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Kompaniya Ko\'rsatkichlari', style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: crossAxisCount,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 2.0,
children: const [
_StatCard(title: 'Jami Savdo', value: '\$128,450', icon: Icons.attach_money, color: Colors.green),
_StatCard(title: 'Buyurtmalar', value: '1,420 ta', icon: Icons.shopping_bag, color: Colors.blue),
_StatCard(title: 'Mijozlar', value: '8,920 nafar', icon: Icons.people, color: Colors.orange),
_StatCard(title: 'Qaytarilganlar', value: '12 ta', icon: Icons.replay, color: Colors.red),
],
),
],
),
),
);
}
}
class _StatCard extends StatelessWidget {
final String title;
final String value;
final IconData icon;
final Color color;
const _StatCard({required this.title, required this.value, required this.icon, required this.color});
@override
Widget build(BuildContext context) {
return Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16), side: BorderSide(color: Colors.grey.shade300)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
CircleAvatar(radius: 24, backgroundColor: color.withOpacity(0.12), child: Icon(icon, color: color)),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(title, style: TextStyle(color: Colors.grey.shade600, fontSize: 13)),
const SizedBox(height: 4),
Text(value, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
],
),
],
),
),
);
}
}
9. Detail sahifasi va Mobil/Desktop tafsilotlar komponenti {#details-component}
class ProductDetailContent extends StatelessWidget {
final Product product;
const ProductDetailContent({super.key, required this.product});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(product.image, height: 260, fit: BoxFit.contain),
),
),
const SizedBox(height: 24),
Chip(label: Text(product.category.toUpperCase()), backgroundColor: Colors.indigo.shade50),
const SizedBox(height: 12),
Text(product.title, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.star, color: Colors.amber, size: 20),
const SizedBox(width: 4),
Text('${product.rating} reyting', style: const TextStyle(fontWeight: FontWeight.w600)),
const Spacer(),
Text('\$${product.price}', style: const TextStyle(fontSize: 26, fontWeight: FontWeight.bold, color: Colors.indigo)),
],
),
const Divider(height: 32),
const Text('Tavsif', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(product.description, style: TextStyle(color: Colors.grey.shade700, height: 1.5)),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(backgroundColor: Colors.indigo, foregroundColor: Colors.white),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${product.title} savatchaga qo\'shildi!')),
);
},
icon: const Icon(Icons.add_shopping_cart),
label: const Text('Savatchaga qo\'shish', style: TextStyle(fontSize: 16)),
),
),
],
),
);
}
}
class ProductDetailPage extends StatelessWidget {
final Product product;
const ProductDetailPage({super.key, required this.product});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(product.title)),
body: ProductDetailContent(product: product),
);
}
}
10. Testlash va Ishga tushirish yo'riqnomasi {#run-project}
Loyihani istalgan platformada bitta buyruq bilan ishga tushirish mumkin:
# macOS Desktopda ishga tushirish:
flutter run -d macos
# Windows Desktopda ishga tushirish:
flutter run -d windows
# iOS Simulyatorda:
flutter run -d "iPhone 16 Pro"
# Android Emulyatorda:
flutter run -d android
11. Xulosa va Keyingi Qadamlar {#xulosa}
Siz ushbu dars orqali quyidagi muhim bosqichlarni o'zlashtirdingiz:
- Clean Architecture tamoyillari: Biznes mantiq (Domain) va server mijozini (Data) vizual interfeysdan (Presentation) butunlay ajratish.
- Kross-Platforma Dizayn:
MediaQuery,NavigationRailvaNavigationBaryordamida har bir ekranda native hissini berish. - Desktop xususiyatlari: Shortcuts, Master-Detail va sichqoncha kursorlari bilan professional darajada ishlash.
- Tarmoq va Xatolar: Dio va
ApiResultbilan ishonchli xatoliklar menejmenti.