You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

31 KiB

📱 تاثیر در UI:

  • iOS: Cupertino-style transitions
  • Android: Material ripples
  • Safe area handling مختلف
  • Font rendering

📦 ۶. JSON کامل Initial Config

این JSON باید یکبار هنگام load WebView ارسال شود:

{
  "version": "1.0.0",
  "timestamp": 1234567890,
  
  "layout": {
    "breakpoint": "mobile",
    "screenWidth": 375,
    "screenHeight": 812,
    "isMobile": true,
    "isTablet": false,
    "isDesktop": false
  },
  
  "safeArea": {
    "top": 44,
    "bottom": 34,
    "left": 0,
    "right": 0
  },
  
  "viewInsets": {
    "top": 0,
    "bottom": 0,
    "left": 0,
    "right": 0
  },
  
  "designSize": {
    "mobile": { "width": 375, "height": 814 },
    "tablet": { "width": 834, "height": 1194 }
  },
  
  "platform": {
    "os": "ios",
    "version": "17.2",
    "model": "iPhone 14 Pro",
    "isPhysicalDevice": true,
    "hasNotch": true,
    "hasDynamicIsland": true
  },
  
  "locale": {
    "languageCode": "en",
    "countryCode": "US",
    "isRtl": false
  },
  
  "theme": {
    "mode": "light",
    "primaryColor": "#FFB236"
  }
}

🔄 ۷. Events که باید به WebView ارسال شوند

۱. Layout Changed

{
  "type": "LAYOUT_CHANGED",
  "payload": {
    "breakpoint": "tablet",
    "screenWidth": 820,
    "isMobile": false
  }
}

۲. Keyboard Changed

{
  "type": "KEYBOARD_CHANGED",
  "payload": {
    "visible": true,
    "height": 336,
    "animationDuration": 250
  }
}

۳. Safe Area Changed

{
  "type": "SAFE_AREA_CHANGED",
  "payload": {
    "top": 0,
    "bottom": 0,
    "left": 0,
    "right": 0
  }
}

۴. Orientation Changed

{
  "type": "ORIENTATION_CHANGED",
  "payload": {
    "orientation": "landscape",
    "screenWidth": 812,
    "screenHeight": 375
  }
}


💻 کد Flutter برای ارسال اطلاعات به WebView

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class TalkWebViewBridge {
  final WebViewController controller;
  BuildContext context;
  
  TalkWebViewBridge(this.controller, this.context) {
    _sendInitialConfig();
    _listenToChanges();
  }
  
  // ارسال اولیه config
  void _sendInitialConfig() {
    final config = jsonEncode({
      'version': '1.0.0',
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      
      'layout': {
        'breakpoint': _getBreakpoint(),
        'screenWidth': MediaQuery.of(context).size.width,
        'screenHeight': MediaQuery.of(context).size.height,
        'isMobile': context.isMobile,
        'isTablet': context.isTablet,
        'isDesktop': context.isDesktop,
      },
      
      'safeArea': {
        'top': MediaQuery.of(context).padding.top,
        'bottom': MediaQuery.of(context).padding.bottom,
        'left': MediaQuery.of(context).padding.left,
        'right': MediaQuery.of(context).padding.right,
      },
      
      'viewInsets': {
        'top': MediaQuery.of(context).viewInsets.top,
        'bottom': MediaQuery.of(context).viewInsets.bottom,
        'left': MediaQuery.of(context).viewInsets.left,
        'right': MediaQuery.of(context).viewInsets.right,
      },
      
      'designSize': {
        'mobile': {'width': 375, 'height': 814},
        'tablet': {'width': 834, 'height': 1194},
      },
      
      'platform': {
        'os': Platform.operatingSystem,
        'version': Platform.operatingSystemVersion,
      },
      
      'locale': {
        'languageCode': context.languageCode,
        'isRtl': context.isRtl,
      },
    });
    
    controller.runJavaScript('''
      window.dispatchEvent(new CustomEvent('flutterConfig', {
        detail: $config
      }));
    ''');
  }
  
  // لیسن به تغییرات keyboard
  void _listenToChanges() {
    // در هر frame چک می‌کنیم که آیا viewInsets تغییر کرده
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final currentInsets = MediaQuery.of(context).viewInsets;
      
      if (_lastInsets?.bottom != currentInsets.bottom) {
        _sendKeyboardChanged(currentInsets.bottom);
        _lastInsets = currentInsets;
      }
      
      _listenToChanges(); // تکرار
    });
  }
  
  EdgeInsets? _lastInsets;
  
  void _sendKeyboardChanged(double height) {
    final event = jsonEncode({
      'type': 'KEYBOARD_CHANGED',
      'payload': {
        'visible': height > 0,
        'height': height,
        'animationDuration': 250,
      },
    });
    
    controller.runJavaScript('''
      window.dispatchEvent(new CustomEvent('flutterEvent', {
        detail: $event
      }));
    ''');
  }
  
  String _getBreakpoint() {
    if (context.isMobile) return 'mobile';
    if (context.isTablet) return 'tablet';
    return 'desktop';
  }
}

📱 توضیح کامل با مثال‌های واقعی

🎯 Initial Config - ارسال یکبار هنگام Load

این JSON فقط یکبار زمانی که WebView لود می‌شود ارسال می‌شود تا React بداند در چه محیطی اجرا می‌شود.

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

1️⃣ layout - اطلاعات صفحه

"layout": { "breakpoint": "mobile", // نوع دستگاه "screenWidth": 375, // عرض صفحه (پیکسل) "screenHeight": 812, // ارتفاع صفحه (پیکسل) "isMobile": true, "isTablet": false, "isDesktop": false }

📱 مثال عملی:

// در React استفاده می‌شود برای:

function ChatPage({ layout }) { return ( <div style={{ padding: layout.isMobile ? '16px' : '64px' // موبایل = 16، تبلت = 64 }}> {/* محتوای چت */} ); }

// نتیجه: // iPhone (mobile): padding = 16px // iPad (tablet): padding = 64px

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

2️⃣ safeArea - فضای امن صفحه

"safeArea": { "top": 44, // ارتفاع status bar / notch "bottom": 34, // ارتفاع home indicator (خط سفید iPhone X) "left": 0, "right": 0 }

📱 چرا مهم است؟

مثال عملی:

// بدون safe area <button style={{ position: 'fixed', bottom: '16px' }}> Send Message // نتیجه: دکمه زیر home indicator مخفی می‌شود!

// با safe area <button style={{ position: 'fixed', bottom: ${16 + safeArea.bottom}px // 16 + 34 = 50px }}> Send Message // نتیجه: دکمه بالای home indicator نمایش داده می‌شود

مثال واقعی از کد فلاتر:

// فلاتر: bottom: 16 + context.padding.bottom, // 16 + 34 = 50px

// باید در React شود: bottom: 16 + safeArea.bottom // 16 + 34 = 50px

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

3️⃣ viewInsets - فضای اشغال شده توسط سیستم

"viewInsets": { "top": 0, "bottom": 0, // این عدد تغییر می‌کند! "left": 0, "right": 0 }

📱 کاربرد اصلی: Keyboard Height

┌─────────────────┐ │ Chat Messages │ ← محتوا │ │ │ │ ├─────────────────┤ │ TextField │ ← ورودی متن ├─────────────────┤ │ │ │ Keyboard │ ← viewInsets.bottom = 336px │ ⌨️ │ └─────────────────┘

مثال عملی:

// هنگام load اولیه: viewInsets.bottom = 0 // کیبورد بسته است

// کاربر روی TextField کلیک می‌کند: // Flutter event می‌فرستد: { "type": "KEYBOARD_CHANGED", "payload": { "bottom": 336 } }

// React باید TextField را بالا ببرد:

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

4️⃣ designSize - اندازه طراحی Figma

"designSize": { "mobile": { "width": 375, "height": 814 }, "tablet": { "width": 834, "height": 1194 } }

📱 کاربرد: Responsive Scaling

// اگر طراح در Figma با عرض 375 طراحی کرده: const figmaWidth = 375; const figmaFontSize = 16;

// دستگاه واقعی: const deviceWidth = 414; // iPhone 14 Pro Max

// محاسبه font size: const scaleFactor = deviceWidth / figmaWidth; // 414 / 375 = 1.104 const actualFontSize = figmaFontSize * scaleFactor; // 16 * 1.104 = 17.7px

// نتیجه: فونت در گوشی بزرگتر به نسبت اندازه صفحه بزرگتر می‌شود

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

5️⃣ platform - اطلاعات دستگاه

"platform": { "os": "ios", // ios | android "version": "17.2", "model": "iPhone 14 Pro", "isPhysicalDevice": true, // true = گوشی واقعی، false = emulator "hasNotch": true, // دارای notch "hasDynamicIsland": true // دارای Dynamic Island }

📱 کاربرد:

// مثال 1: تنظیم safe area مختلف if (platform.hasNotch) { safeAreaTop = 44; } else { safeAreaTop = 20; }

// مثال 2: استایل مختلف برای iOS/Android <button className={platform.os === 'ios' ? 'ios-style' : 'material-style'}> Send

// مثال 3: تشخیص Dynamic Island {platform.hasDynamicIsland && ( <div style={{ paddingTop: '59px' }}> {/* فضا برای Dynamic Island */} {/* محتوا */}

)}

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

6️⃣ locale - تنظیمات زبان

"locale": { "languageCode": "en", // en | fa | ar "countryCode": "US", "isRtl": false // راست به چپ؟ }

📱 کاربرد:

// مثال 1: تنظیم direction

{/* محتوا */}

// مثال 2: متن مناسب زبان const messages = { en: "Hello", fa: "سلام", ar: "مرحبا" };

{messages[locale.languageCode]}

// مثال 3: فرمت تاریخ const date = new Date(); date.toLocaleDateString(locale.languageCode); // en-US | fa-IR

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

7️⃣ theme - تنظیمات تم

"theme": { "mode": "light", // light | dark "primaryColor": "#FFB236" }

📱 کاربرد:

// تنظیم رنگ‌ها const colors = { light: { bg: '#F5F5F5', text: '#202227' }, dark: { bg: '#1A1A1A', text: '#FFFFFF' } };

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

🔄 Events - ارسال Real-time

این eventها هر بار که چیزی تغییر می‌کند ارسال می‌شوند.

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

1️⃣ SCREEN_SIZE_CHANGED - تغییر اندازه صفحه

📱 چه موقع رخ می‌دهد؟

┌─────────────┐ ┌──────────────────────┐ │ Portrait │ │ Landscape │ │ │ │ │ │ iPhone │ چرخش گوشی │ iPhone │ │ │ ──────────> │ │ │ 📱 │ │ 📱 │ │ │ │ │ └─────────────┘ └──────────────────────┘ 375 × 812 812 × 375

Event ارسالی:

{ "type": "SCREEN_SIZE_CHANGED", "payload": { "width": 812, "height": 375, "orientation": "landscape", "breakpoint": "tablet" // ممکن است تغییر کند! } }

React چه کار باید بکند:

useEffect(() => { window.addEventListener('message', (event) => { if (event.data.type === 'SCREEN_SIZE_CHANGED') { const { width, orientation, breakpoint } = event.data.payload;

    // 1. تغییر layout
    if (breakpoint === 'tablet') {
      setPadding(64);  // در landscape = tablet
    } else {
      setPadding(16);  // در portrait = mobile
    }
    
    // 2. تنظیم مجدد video player
    if (orientation === 'landscape') {
      setVideoPlayerFullWidth(true);
    }
  }
});

}, []);

مثال واقعی:

کاربر گوشی را می‌چرخاند (portrait → landscape) → عرض صفحه: 375 → 812 → breakpoint تغییر می‌کند: mobile → tablet → padding چت باید تغییر کند: 16px → 64px

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

2️⃣ KEYBOARD_CHANGED - باز/بسته شدن کیبورد

📱 چه موقع رخ می‌دهد؟

┌─────────────────┐ ┌─────────────────┐ │ Chat │ │ Chat │ │ Messages │ │ Messages │ │ │ │ (کمتر فضا) │ │ │ ├─────────────────┤ │ │ کلیک → │ TextField │ ├─────────────────┤ ├─────────────────┤ │ TextField │ │ Keyboard │ └─────────────────┘ │ ⌨️ │ └─────────────────┘

Event ارسالی:

// کیبورد باز شد: { "type": "KEYBOARD_CHANGED", "payload": { "visible": true, "height": 336, // ارتفاع کیبورد "animationDuration": 250 // مدت زمان انیمیشن (ms) } }

// کیبورد بسته شد: { "type": "KEYBOARD_CHANGED", "payload": { "visible": false, "height": 0, "animationDuration": 250 } }

React چه کار باید بکند:

const [keyboardHeight, setKeyboardHeight] = useState(0);

useEffect(() => { window.addEventListener('message', (event) => { if (event.data.type === 'KEYBOARD_CHANGED') { const { visible, height, animationDuration } = event.data.payload;

    // با انیمیشن smooth
    setKeyboardHeight(visible ? height : 0);
    
    // Scroll به آخرین پیام
    if (visible) {
      setTimeout(() => {
        chatListRef.current?.scrollToBottom();
      }, animationDuration);
    }
  }
});

}, []);

// در UI:

مثال واقعی:

کاربر روی TextField کلیک می‌کند → کیبورد باز می‌شود (336px) → لیست چت باید 336px بالا برود → با انیمیشن smooth (250ms)

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

3️⃣ SAFE_AREA_CHANGED - تغییر فضای امن

📱 چه موقع رخ می‌دهد؟

┌─────────────────┐ ┌─────────────────┐ │ Status Bar 44px │ │ (مخفی شده) │ ├─────────────────┤ ├─────────────────┤ │ │ │ │ │ Full Screen │ تمام صفحه│ Video/Game │ │ Video │ ──────→ │ Full Screen │ │ │ │ │ ├─────────────────┤ ├─────────────────┤ │ Home Bar 34px │ │ (مخفی شده) │ └─────────────────┘ └─────────────────┘

Event ارسالی:

// حالت عادی: { "type": "SAFE_AREA_CHANGED", "payload": { "top": 44, "bottom": 34, "left": 0, "right": 0 } }

// حالت full screen (video player): { "type": "SAFE_AREA_CHANGED", "payload": { "top": 0, // status bar مخفی شد "bottom": 0, // home indicator مخفی شد "left": 0, "right": 0 } }

React چه کار باید بکند:

useEffect(() => { window.addEventListener('message', (event) => { if (event.data.type === 'SAFE_AREA_CHANGED') { const { top, bottom } = event.data.payload;

    // اگر safe area = 0 → یعنی full screen شده
    if (top === 0 && bottom === 0) {
      setIsFullScreen(true);
      hideHeader();
      hideBottomNav();
    } else {
      setIsFullScreen(false);
      showHeader();
      showBottomNav();
    }
  }
});

}, []);

مثال واقعی:

کاربر video call را full screen می‌کند → status bar و home indicator مخفی می‌شوند → safe area: top=44 → 0, bottom=34 → 0 → ویدیو تمام صفحه را می‌پوشاند

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

4️⃣ ORIENTATION_CHANGED - تغییر orientation

📱 چه موقع رخ می‌دهد؟

Portrait → Landscape → Portrait 📱📱📱

Event ارسالی:

{ "type": "ORIENTATION_CHANGED", "payload": { "orientation": "landscape", // portrait | landscape "screenWidth": 812, "screenHeight": 375 } }

React چه کار باید بکند:

useEffect(() => { window.addEventListener('message', (event) => { if (event.data.type === 'ORIENTATION_CHANGED') { const { orientation } = event.data.payload;

    if (orientation === 'landscape') {
      // ویدیو پلیر را بزرگتر کن
      setVideoPlayerSize('large');
      
      // دکمه‌ها را در کنار قرار بده (row)
      setButtonsLayout('horizontal');
    } else {
      // ویدیو پلیر را عادی کن
      setVideoPlayerSize('normal');
      
      // دکمه‌ها را زیر هم قرار بده (column)
      setButtonsLayout('vertical');
    }
  }
});

}, []);

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

📊 مثال کامل: سناریوی واقعی

سناریو: کاربر در حال چت است و گوشی را می‌چرخاند.

// 1. Initial Load { "layout": { "screenWidth": 375, "isMobile": true }, "safeArea": { "top": 44, "bottom": 34 } } → Chat با padding 16px نمایش داده می‌شود

// 2. کاربر روی TextField کلیک می‌کند { "type": "KEYBOARD_CHANGED", "payload": { "visible": true, "height": 336 } } → Chat list 336px بالا می‌رود

// 3. کاربر گوشی را می‌چرخاند (landscape) { "type": "ORIENTATION_CHANGED", "payload": { "orientation": "landscape", "screenWidth": 812 } } → Layout تغییر می‌کند

{ "type": "SCREEN_SIZE_CHANGED", "payload": { "width": 812, "breakpoint": "tablet" } } → Padding از 16px به 64px تغییر می‌کند

{ "type": "KEYBOARD_CHANGED", "payload": { "visible": true, "height": 220 } // کیبورد کوچکتر در landscape } → Keyboard height جدید

// 4. کاربر کیبورد را می‌بندد { "type": "KEYBOARD_CHANGED", "payload": { "visible": false, "height": 0 } } → Chat list به حالت عادی برمی‌گردد

خلاصه

┌─────────────────────┬───────────────────┬────────────────────────────────────────────────┐ │ Event │ چه موقع؟ │ چرا مهم است؟ │ ├─────────────────────┼───────────────────┼────────────────────────────────────────────────┤ │ Initial Config │ یکبار هنگام load │ تنظیمات اولیه (breakpoint, safeArea, platform) │ ├─────────────────────┼───────────────────┼────────────────────────────────────────────────┤ │ SCREEN_SIZE_CHANGED │ چرخش گوشی │ تغییر padding, layout │ ├─────────────────────┼───────────────────┼────────────────────────────────────────────────┤ │ KEYBOARD_CHANGED │ باز/بسته کیبورد │ جا دادن TextField، scroll به پیام │ ├─────────────────────┼───────────────────┼────────────────────────────────────────────────┤ │ SAFE_AREA_CHANGED │ full screen video │ مخفی/نمایش header/footer │ ├─────────────────────┼───────────────────┼────────────────────────────────────────────────┤ │ ORIENTATION_CHANGED │ چرخش گوشی │ تغییر UI (horizontal/vertical) │ └─────────────────────┴───────────────────┴────────────────────────────────────────────────┘