Azimjon Abdurasulov

Flutter 9: Desktop (macOS & Windows) — Window Management, Menu, Shortcuts va Moslashuvchan UI

💙 Flutter — Mobile (iOS, Android) & Desktop (macOS, Windows)

Flutter 9: Desktop (macOS & Windows) — Window Management, Menu, Shortcuts va Moslashuvchan UI

Holat: 2026-yil. Flutter endi faqat mobil ramka emas — u macOS, Windows va Linux uchun to'laqonli 60/120 FPS native ishlovchi desktop platformasidir. Desktop ilovalar mobil ilovalardan tubdan farq qiladi: sichqoncha bilan hover, klaviatura tugmalari (Shortcuts), oyna o'lchamlarini o'zgartirish (Window Resizing), ko'p ustunli interfeys (Master-Detail) va tizim menyulari (Menu Bar). Ushbu darsda Flutter bilan desktop ilovalar yaratishning barcha nozikliklarini amaliy kodlar bilan ko'rib chiqamiz.


Mundarija

  1. Desktop va Mobile o'rtasidagi asosiy farqlar
  2. Desktop platformalari arxitekturasi va Runner tuzilishi
  3. Window Management: window_manager bilan oynani boshqarish
  4. Tizim menyusi: PlatformMenuBar va Context Menu
  5. Klaviatura Shortcuts va Actions tizimi
  6. Sichqoncha, Hover effektlar va Kursorni boshqarish
  7. Desktop Scrollbar va Wheel aylantirish qoidalari
  8. Moslashuvchan (Adaptive & Responsive) Dizayn: Breakpointlar
  9. To'liq Desktop & Mobile moslashuvchan kod namunasi
  10. Build va Distribution: macOS Sandbox va Windows Installer
  11. Xulosa va Maslahatlar
  12. Rasmiy Manbalar

1. Desktop va Mobile o'rtasidagi asosiy farqlar {#farqlar}

Mobil ilovani shunchaki desktop ekraniga cho'zib qo'yish — bu eng katta xato (anti-pattern) hisoblanadi.

┌─────────────────────────────────┐      ┌────────────────────────────────────────────────────────┐
│        MOBILE INTERFACE         │      │                   DESKTOP INTERFACE                    │
├─────────────────────────────────┤      ├────────────────────────────────────────────────────────┤
│ • Kichik sensorli ekran (Touch) │      │ • Katta displey, sichqoncha (Mouse/Trackpad) + Klaviat.│
│ • Bitta oyna, to'liq ekran      │      │ • Erkin o'lchamli oyna (Resizable, Min/Max/Restore)   │
│ • BottomNavigationBar (pastda)  │      │ • NavigationRail / Sidebar (chapda) + Split View       │
│ • Orqaga surish (Swipe back)    │      │ • Klaviatura tugmalari (Cmd/Ctrl+Z, Esc, Enter)        │
│ • Yengil dialoglar (ActionSheet)│      │ • Sichqonchaning o'ng tugmasi (Context Menu) + Hover   │
│ • Virtual klaviatura            │      │ • Native Menu Bar (macOS Apple menyusi, Win titlebar) │
└─────────────────────────────────┘      └────────────────────────────────────────────────────────┘
XususiyatMobil (iOS / Android)Desktop (macOS / Windows)
Kiritish qurilmasiBarmoq teginishi (Touch, Gestures)Kursor, g'ildirak (Wheel), klaviatura
Hover holatiMavjud emasJuda muhim (tugmalar, havolalar, ro'yxatlar)
NavigatsiyaBottomNavigationBar yoki DrawerNavigationRail yoki doimiy Sidebar
Oyna o'lchamiRuxsat etilgan qat'iy ekranFoydalanuvchi erkin o'zgartiradi
Ko'rinish zichligiKatta teginish maydonlari (min 44-48px)Yilqiq, zich ma'lumotlar (VisualDensity.compact)
ScrollbarSukut bo'yicha yashirin, surilganda chiqadiHar doim ko'rinib turishi maqsadga muvofiq

2. Desktop platformalari arxitekturasi va Runner tuzilishi {#arxitektura}

Flutter Desktop ilovasi qanday ishga tushadi?

  • macOS: Ilova macos/ papkasidagi Cocoa (Objective-C/Swift) NSApplication konteyneri ichida ishlaydi. Metal / Impeller grafik dvigateli orqali bevosita GPU ga render qilinadi.
  • Windows: Ilova windows/ papkasidagi Win32 (C++) ilovasi ichida ishlaydi. DirectX 11/12 (yoki Angle/Skia) orqali render qilinadi.

Loyihada desktopni faollashtirish:

flutter config --enable-macos-desktop
flutter config --enable-windows-desktop
flutter create --platforms=macos,windows,ios,android .

3. Window Management: window_manager bilan oynani boshqarish {#window-manager}

Desktop ilovada oyna boshlang'ich o'lchami, uning markazda ochilishi, sarlavhasi (title) va minimal/maksimal cheklovlarni boshqarish uchun window_manager paketi ishlatiladi:

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  window_manager: ^0.4.3

main.dart da oynani ishga tushirish:

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:window_manager/window_manager.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Faqat Desktop platformalarda oynani sozlaymiz
  if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
    await windowManager.ensureInitialized();

    const windowOptions = WindowOptions(
      size: Size(1100, 750),
      minimumSize: Size(650, 500),
      center: true,
      backgroundColor: Colors.transparent,
      skipTaskbar: false,
      title: 'DevStudio Desktop',
      titleBarStyle: TitleBarStyle.normal, // yoki TitleBarStyle.hidden macOS sarlavhasi uchun
    );

    await windowManager.waitUntilReadyToShow(windowOptions, () async {
      await windowManager.show();
      await windowManager.focus();
    });
  }

  runApp(const MultiplatformApp());
}

4. Tizim menyusi: PlatformMenuBar va Context Menu {#menu-bar}

macOS da ekranning eng yuqorisida (Apple logotipi yonida) tizim menyusi mavjud. Flutter buni PlatformMenuBar orqali native macOS menyusiga ulaydi:

class MultiplatformApp extends StatelessWidget {
  const MultiplatformApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.indigo,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: PlatformMenuBar(
        menus: [
          PlatformMenu(
            label: 'DevStudio',
            menus: [
              PlatformMenuItem(
                label: 'Dastur haqida...',
                onSelected: () => debugPrint('Haqida bosildi'),
              ),
              const PlatformMenuItemGroup(
                members: [
                  PlatformProvidedMenuItem(type: PlatformProvidedMenuItemType.quit),
                ],
              ),
            ],
          ),
          PlatformMenu(
            label: 'Fayl',
            menus: [
              PlatformMenuItem(
                label: 'Yangi loyiha',
                shortcut: const SingleActivator(LogicalKeyboardKey.keyN, meta: true),
                onSelected: () => debugPrint('Yangi loyiha ochildi'),
              ),
              PlatformMenuItem(
                label: 'Saqlash',
                shortcut: const SingleActivator(LogicalKeyboardKey.keyS, meta: true),
                onSelected: () => debugPrint('Saqlandi'),
              ),
            ],
          ),
        ],
        child: const HomeScreen(),
      ),
    );
  }
}

Sichqonchaning o'ng tugmasi (Context Menu):

Ixtiyoriy element ustiga o'ng tugma bosilganda kontekst menyu chiqarish:

GestureDetector(
  onSecondaryTapUp: (details) {
    showMenu(
      context: context,
      position: RelativeRect.fromLTRB(
        details.globalPosition.dx,
        details.globalPosition.dy,
        details.globalPosition.dx + 1,
        details.globalPosition.dy + 1,
      ),
      items: const [
        PopupMenuItem(value: 'copy', child: Text('Nusxa olish')),
        PopupMenuItem(value: 'edit', child: Text('Tahrirlash')),
        PopupMenuDivider(),
        PopupMenuItem(value: 'delete', child: Text('O\'chirish', style: TextStyle(color: Colors.red))),
      ],
    );
  },
  child: const Card(child: ListTile(title: Text('O\'ng tugmani bosing'))),
)

5. Klaviatura Shortcuts va Actions tizimi {#shortcuts}

Desktop foydalanuvchisi sichqonchaga teginmasdan klaviatura yordamida ishlashni afzal ko'radi (masalan: Cmd+K yoki Ctrl+K bilan qidiruv ochish, Esc bilan yopish).

Flutter'da bu Shortcuts + Actions + Intent modeli orqali ishlaydi:

// 1. Intent e'lon qilamiz
class SearchIntent extends Intent {
  const SearchIntent();
}

class RefreshIntent extends Intent {
  const RefreshIntent();
}

// 2. Widget ichida bog'laymiz
Widget buildKeyboardShortcutsWrapper({required Widget child, required BuildContext context}) {
  return Shortcuts(
    shortcuts: <ShortcutActivator, Intent>{
      // Cmd+K (macOS) yoki Ctrl+K (Windows)
      LogicalKeySet(
        Platform.isMacOS ? LogicalKeyboardKey.meta : LogicalKeyboardKey.control,
        LogicalKeyboardKey.keyK,
      ): const SearchIntent(),
      // Cmd+R yoki Ctrl+R
      LogicalKeySet(
        Platform.isMacOS ? LogicalKeyboardKey.meta : LogicalKeyboardKey.control,
        LogicalKeyboardKey.keyR,
      ): const RefreshIntent(),
    },
    child: Actions(
      actions: <Type, Action<Intent>>{
        SearchIntent: CallbackAction<SearchIntent>(
          onInvoke: (intent) => openSearchDialog(context),
        ),
        RefreshIntent: CallbackAction<RefreshIntent>(
          onInvoke: (intent) => refreshData(),
        ),
      },
      child: Focus(
        autofocus: true,
        child: child,
      ),
    ),
  );
}

6. Sichqoncha, Hover effektlar va Kursorni boshqarish {#mouse-hover}

Desktopda element ustiga kursor olib kelinganda foydalanuvchiga fikr-mulohaza (feedback) berish zarur:

  • Kursor shakli o'zgarishi (SystemMouseCursors.click)
  • Fon rangi yoki soya o'zgarishi (Hover highlight)
class DesktopInteractiveCard extends StatefulWidget {
  final String title;
  final VoidCallback onTap;

  const DesktopInteractiveCard({super.key, required this.title, required this.onTap});

  @override
  State<DesktopInteractiveCard> createState() => _DesktopInteractiveCardState();
}

class _DesktopInteractiveCardState extends State<DesktopInteractiveCard> {
  bool _isHovered = false;

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      cursor: SystemMouseCursors.click,
      onEnter: (_) => setState(() => _isHovered = true),
      onExit: (_) => setState(() => _isHovered = false),
      child: GestureDetector(
        onTap: widget.onTap,
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 200),
          curve: Curves.easeOutCubic,
          padding: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: _isHovered ? Colors.indigo.shade50 : Colors.white,
            borderRadius: BorderRadius.circular(12),
            border: Border.all(
              color: _isHovered ? Colors.indigo : Colors.grey.shade300,
              width: _isHovered ? 1.5 : 1,
            ),
            boxShadow: _isHovered
                ? [BoxShadow(color: Colors.indigo.withOpacity(0.12), blurRadius: 16, offset: const Offset(0, 6))]
                : [],
          ),
          child: Row(
            children: [
              Icon(Icons.dashboard_rounded, color: _isHovered ? Colors.indigo : Colors.grey.shade700),
              const SizedBox(width: 12),
              Text(
                widget.title,
                style: TextStyle(
                  fontWeight: _isHovered ? FontWeight.w600 : FontWeight.w500,
                  color: _isHovered ? Colors.indigo : Colors.black87,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

7. Desktop Scrollbar va Wheel aylantirish qoidalari {#scrollbars}

Mobil telefonda scrollbar odatda yashirin bo'ladi. Ammo Windows va macOS da uzun ro'yxatlarda scrollbar doimo ko'rinib turishi yoki hover qilinganda qalinlashishi shart.

Desktop uchun global Scrollbar sozlamasi:

MaterialApp(
  theme: ThemeData(
    scrollbarTheme: ScrollbarThemeData(
      thumbVisibility: WidgetStateProperty.all(true), // Har doim ko'rinsin
      thickness: WidgetStateProperty.resolveWith((states) {
        if (states.contains(WidgetState.hovered)) return 10.0;
        return 6.0;
      }),
      radius: const Radius.circular(8),
      thumbColor: WidgetStateProperty.all(Colors.grey.shade400),
    ),
  ),
);

Har bir ListView yoki SingleChildScrollView ga ScrollController bog'lash shart, aks holda Flutter ScrollController not attached ogohlantirishini beradi:

final scrollController = ScrollController();

Scrollbar(
  controller: scrollController,
  thumbVisibility: true,
  child: ListView.builder(
    controller: scrollController,
    itemCount: 100,
    itemBuilder: (context, index) => ListTile(title: Text('Element #$index')),
  ),
);

8. Moslashuvchan (Adaptive & Responsive) Dizayn: Breakpointlar {#adaptive-layout}

Bitta kod bazasidan foydalangan holda ilova mobil telefonda ham, planshetda ham, 4K monitorli Mac va Windows kompyuterda ham mukammal ko'rinishi kerak:

┌────────────────────────────────────────────────────────────────────────┐
│                        RESPONSIVE BREAKPOINTS                          │
├──────────────────┬───────────────────────┬─────────────────────────────┤
│ Ekran kengligi   │ Qurilma turi          │ Tavsiya etilgan navigatsiya │
├──────────────────┼───────────────────────┼─────────────────────────────┤
│ < 600px          │ Smartfon (Mobile)     │ BottomNavigationBar         │
│ 600px - 1024px   │ Planshet / Kichik Win │ NavigationRail (ixcham)     │
│ > 1024px         │ Desktop / Katta Mac   │ Doimiy Sidebar + Split View │
└──────────────────┴───────────────────────┴─────────────────────────────┘

Breakpointlarni aniqlash klassi:

enum ScreenType { mobile, tablet, desktop }

class ResponsiveHelper {
  static ScreenType getScreenType(BuildContext context) {
    final width = MediaQuery.sizeOf(context).width;
    if (width < 600) return ScreenType.mobile;
    if (width < 1024) return ScreenType.tablet;
    return ScreenType.desktop;
  }

  static bool isMobile(BuildContext context) => getScreenType(context) == ScreenType.mobile;
  static bool isDesktop(BuildContext context) => getScreenType(context) == ScreenType.desktop;
}

9. To'liq Desktop & Mobile moslashuvchan kod namunasi {#full-code}

Quyidagi kod — ham iOS/Android'da (Bottom Bar), ham macOS/Windows'da (Yonbosh Sidebar + Master-Detail ko'rinish) moslashib ishlaydigan mukammal shablondir:

import 'package:flutter/material.dart';

class AdaptiveDashboardScreen extends StatefulWidget {
  const AdaptiveDashboardScreen({super.key});

  @override
  State<AdaptiveDashboardScreen> createState() => _AdaptiveDashboardScreenState();
}

class _AdaptiveDashboardScreenState extends State<AdaptiveDashboardScreen> {
  int _selectedIndex = 0;
  int? _selectedItemId = 1; // Master-detail uchun tanlangan element

  final List<String> _navigationDestinations = [
    'Bosh sahifa',
    'Buyurtmalar',
    'Statistika',
    'Sozlamalar',
  ];

  @override
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.sizeOf(context).width;
    final isDesktop = screenWidth >= 1024;
    final isTablet = screenWidth >= 600 && screenWidth < 1024;

    // MOBIL KO'RINISH (< 600px)
    if (screenWidth < 600) {
      return Scaffold(
        appBar: AppBar(title: Text(_navigationDestinations[_selectedIndex])),
        body: _buildContentArea(isMasterDetail: false),
        bottomNavigationBar: NavigationBar(
          selectedIndex: _selectedIndex,
          onDestinationSelected: (idx) => setState(() => _selectedIndex = idx),
          destinations: const [
            NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Asosiy'),
            NavigationDestination(icon: Icon(Icons.shopping_bag_outlined), selectedIcon: Icon(Icons.shopping_bag), label: 'Buyurtma'),
            NavigationDestination(icon: Icon(Icons.bar_chart_outlined), selectedIcon: Icon(Icons.bar_chart), label: 'Tahlil'),
            NavigationDestination(icon: Icon(Icons.settings_outlined), selectedIcon: Icon(Icons.settings), label: 'Sozlama'),
          ],
        ),
      );
    }

    // DESKTOP VA TABLET KO'RINISH (>= 600px)
    return Scaffold(
      body: Row(
        children: [
          // YONBOSH MENYU (NavigationRail yoki Sidebar)
          NavigationRail(
            extended: isDesktop, // Desktopda yozuvlari bilan kengayadi
            minExtendedWidth: 200,
            selectedIndex: _selectedIndex,
            onDestinationSelected: (idx) => setState(() => _selectedIndex = idx),
            leading: Padding(
              padding: const EdgeInsets.symmetric(vertical: 20),
              child: Row(
                children: [
                  const Icon(Icons.bolt, color: Colors.indigo, size: 28),
                  if (isDesktop) ...[
                    const SizedBox(width: 8),
                    const Text('DEVSTUDIO', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
                  ],
                ],
              ),
            ),
            destinations: const [
              NavigationRailDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: Text('Asosiy')),
              NavigationRailDestination(icon: Icon(Icons.shopping_bag_outlined), selectedIcon: Icon(Icons.shopping_bag), label: Text('Buyurtmalar')),
              NavigationRailDestination(icon: Icon(Icons.bar_chart_outlined), selectedIcon: Icon(Icons.bar_chart), label: Text('Statistika')),
              NavigationRailDestination(icon: Icon(Icons.settings_outlined), selectedIcon: Icon(Icons.settings), label: Text('Sozlamalar')),
            ],
          ),
          const VerticalDivider(width: 1, thickness: 1),

          // ASOSIY TARKIB (MASTER-DETAIL VIEW DESKTOPDA)
          Expanded(
            child: _buildContentArea(isMasterDetail: isDesktop),
          ),
        ],
      ),
    );
  }

  Widget _buildContentArea({required bool isMasterDetail}) {
    if (!isMasterDetail) {
      // Yagona ustunli ro'yxat (Mobil va planshet)
      return ListView.separated(
        padding: const EdgeInsets.all(16),
        itemCount: 20,
        separatorBuilder: (_, __) => const SizedBox(height: 8),
        itemBuilder: (context, i) => ListTile(
          tileColor: Colors.grey.shade100,
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
          title: Text('Loyiha #$i'),
          subtitle: const Text('Status: Faol ishlov jarayonida'),
          trailing: const Icon(Icons.chevron_right),
          onTap: () {
            // Mobilda alohida sahifaga o'tiladi
          },
        ),
      );
    }

    // Desktopda ikkita ustun: Chapda Ro'yxat (Master), O'ngda Tafsilot (Detail)
    return Row(
      children: [
        // Master ustun (350px kenglik)
        SizedBox(
          width: 360,
          child: ListView.builder(
            padding: const EdgeInsets.all(16),
            itemCount: 20,
            itemBuilder: (context, i) {
              final isSelected = _selectedItemId == i;
              return Padding(
                padding: const EdgeInsets.only(bottom: 8),
                child: ListTile(
                  selected: isSelected,
                  selectedTileColor: Colors.indigo.shade50,
                  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
                  title: Text('Mijoz loyihasi #$i', style: TextStyle(fontWeight: isSelected ? FontWeight.bold : FontWeight.normal)),
                  subtitle: Text('Jami mablag\': \$${(i + 1) * 250}'),
                  onTap: () => setState(() => _selectedItemId = i),
                ),
              );
            },
          ),
        ),
        const VerticalDivider(width: 1, thickness: 1),

        // Detail ustun (Qolgan barcha bo'sh joy)
        Expanded(
          child: Container(
            color: Colors.white,
            padding: const EdgeInsets.all(32),
            child: _selectedItemId == null
                ? const Center(child: Text('Tafsilotlarni ko\'rish uchun chapdan elementni tanlang'))
                : Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('Loyiha #$_selectedItemId tafsilotlari', style: Theme.of(context).textTheme.headlineMedium),
                      const SizedBox(height: 16),
                      const Text('Ushbu qismda loyihaga oid barcha API ma\'lumotlari, jadvallar va boshqaruv tugmalari joylashadi.'),
                      const SizedBox(height: 24),
                      Wrap(
                        spacing: 12,
                        children: [
                          ElevatedButton.icon(onPressed: () {}, icon: const Icon(Icons.check), label: const Text('Tasdiqlash')),
                          OutlinedButton.icon(onPressed: () {}, icon: const Icon(Icons.download), label: const Text('Eksport PDF')),
                        ],
                      ),
                    ],
                  ),
          ),
        ),
      ],
    );
  }
}

10. Build va Distribution: macOS Sandbox va Windows Installer {#packaging}

Desktop ilovani tayyorlab foydalanuvchiga taqdim etishda ikki muhim qoida mavjud:

10.1 macOS App Sandbox & Tarmoq ruxsati

macOS da ilovangiz internetdan ma'lumot olishi (REST API bilan ishlashi) uchun macos/Runner/DebugProfile.entitlements va Release.entitlements fayllariga tarmoq ruxsatini qo'shish shart:

<key>com.apple.security.network.client</key>
<true/>

*Agar bu qo'shilmasa, ilova simulyatorda ishlashi mumkin, ammo release qilinganda barcha API so'rovlari SocketException bilan qulab tushadi!*

10.2 Windows EXE va Installer

Windows uchun yig'ish:

flutter build windows --release

Natija build/windows/x64/runner/Release/ papkasida hosil bo'ladi. Uni foydalanuvchilarga tarqatish uchun Inno Setup yoki MSIX Packaging Tool vositasidan foydalaniladi:

dart run msix:create

11. Xulosa va Maslahatlar {#xulosa}

  1. Universal layout qoidasi: Har doim kenglikni MediaQuery.sizeOf(context).width orqali tekshiring va 600px hamda 1024px nuqtalarida interfeysni avtomatik qayta quring.
  2. Kursorlarni unutmang: Har qanday bosiladigan card yoki tugmaga MouseRegion(cursor: SystemMouseCursors.click) yoki InkWell bering.
  3. Klaviatura bilan do'st bo'ling: Qidiruv, saqlash, yopish kabi eng ko'p bajariladigan amallarga Shortcuts bog'lang.

12. Rasmiy Manbalar {#manbalar}

Markdown manba

iOS Lessons