### 📱 تاثیر در UI:
- iOS: Cupertino-style transitions
- Android: Material ripples
- Safe area handling مختلف
- Font rendering
---
## 📦 ۶. JSON کامل Initial Config
این JSON باید **یکبار هنگام load WebView** ارسال شود:
```json
{
"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
```json
{
"type": "LAYOUT_CHANGED",
"payload": {
"breakpoint": "tablet",
"screenWidth": 820,
"isMobile": false
}
}
```
### ۲. Keyboard Changed
```json
{
"type": "KEYBOARD_CHANGED",
"payload": {
"visible": true,
"height": 336,
"animationDuration": 250
}
}
```
### ۳. Safe Area Changed
```json
{
"type": "SAFE_AREA_CHANGED",
"payload": {
"top": 0,
"bottom": 0,
"left": 0,
"right": 0
}
}
```
### ۴. Orientation Changed
```json
{
"type": "ORIENTATION_CHANGED",
"payload": {
"orientation": "landscape",
"screenWidth": 812,
"screenHeight": 375
}
}
```
---
---
## 💻 کد Flutter برای ارسال اطلاعات به WebView
```dart
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 (
{/* محتوای چت */}
);
}
// نتیجه:
// 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
// نتیجه: دکمه زیر home indicator مخفی میشود!
// ✅ با safe area
// نتیجه: دکمه بالای 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
// مثال 3: تشخیص Dynamic Island
{platform.hasDynamicIsland && (
{/* فضا برای 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) │
└─────────────────────┴───────────────────┴────────────────────────────────────────────────┘