الجوال والشاشات
شاشات العرض (Okta Screen)
الهاتف يفتحه شخص؛ الشاشة لا يفتحها أحد. تلفاز معلّق في الفصل، لوحة في ردهة المدرسة، شاشة عند باب المجمّع — جهاز يعمل طول اليوم بلا مستخدم أمامه، ويُدار من تطبيق مستقلّ اسمه Okta Screen (Android TV / Google TV، وWindows). يدخل بالاقتران فقط: لا حساب ولا كلمة سرّ، بل رمز يُنشئه مسؤول الجهة من سجلّ الأجهزة ويكتبه على الشاشة مرّة واحدة.
وما يميّز الشاشة عن الهاتف هو مكانها لا صاحبها: عند الاقتران يختار المسؤول أين تقف — الجهة كاملة (ردهة، ممرّ)، أو صفوف بعينها، أو فصول بعينها. تطبيقك يُعلن لأيّ الأماكن يقدّم سطحاً، والمنصّة تعرضه على الشاشات التي تقف هناك.
والمكان مجموعة لا واحداً. تلفازٌ في رأس ممرّ يخدم «أول ثانوي ١» و«ثاني ثانوي ٣» معاً؛ ولوحةٌ في جناح تخدم صفّين. الشاشة تُعطى مجموعةً من نوع واحد — فصولاً معاً أو صفوفاً معاً، لا خليطاً — وتصلك مفصولةً فترسمها كما تشاء (عمودين، تبويبين، قائمة مدموجة).
الإعلان — mobile.screen
صفّ لكل مكان، ولكل صفّ ملف دخوله:
"mobile": {
"screen": {
"enabled": true,
"title": "نداء الفصل",
"title_en": "Class roll call",
"min_contract": 21,
"auto_launch": true,
"places": [
{ "scope": "section", "entry": "okta_app/native/screen/lib/main.dart" },
{ "scope": "tenant", "entry": "okta_app/native/screen_lobby/lib/main.dart",
"title": "لوحة الإعلانات", "title_en": "Notice board" }
]
}
}
| المفتاح | الوصف |
|---|---|
places[] |
إلزامي. صفّ أو صفّان — واحد لكل مكان تخدمه. |
places[].scope |
إلزامي. "section" (فصل) أو "tenant" (الجهة كاملة). كل مكان مرّة واحدة؛ المكرّر يُبقي الصفّ الأول ويُرفض الثاني بالاسم. ولا يوجد "grade" في البيان — راجع الفقرة التالية. |
places[].entry |
إلزامي لكل صفّ. ملف Dart تحت okta_app/native/<حزمة>/lib/، في حزمة مستقلة. يكشف Widget main(). |
places[].title / title_en |
اختياري. يتجاوز عنوان الكتلة لهذا المكان وحده — «نداء الفصل» في الفصل، «لوحة الإعلانات» في الردهة. |
title / title_en |
العنوان الافتراضي الذي يرثه كل صفّ لم يكتب عنوانه. كل مكان لا بدّ أن ينتهي إلى عنوان — عنوانه أو هذا — وإلا رُفض الإعلان. |
min_contract |
اختياري. أرضية عقد المضيف لكود الشاشة؛ تُترك لتُؤخذ من mobile.minContract. على مستوى الكتلة لأنها تصف التطبيق لا المكان. |
auto_launch |
اختياري، افتراضه true: الشاشة التي لا تجد في كتالوجها غير تطبيقك تفتحه بنفسها. على مستوى الكتلة أيضاً. |
لماذا ملف دخول لكل مكان. الحزمة تُترجَم على الجهاز: الشاشة تنزّل
مصدر الحزمة التي أُعلنت لمكانها وتترجمه عندها. فلو تشارك المكانان ملفاً
واحداً لصار كل تلفاز فصل ينزّل كود الردهة ويترجمه ليرسم نداءً لا علاقة
له به. شاشة الفصل وشاشة الردهة شاشتان مختلفتان، والإعلان يقول ذلك: مكان
واحد = صفّ واحد = حزمة واحدة. القالب يشحن الحزمتين جاهزتين —
okta_app/native/screen/ (نداء الفصل) وokta_app/native/screen_lobby/
(لوحة الإعلانات).
ويجوز أن يشترك المكانان في ملف واحد إن أردت حزمةً واحدة تتفرّع على
Okta.context()['screen']: اكتب المسار نفسه في الصفّين. عندها هي حزمة
واحدة باختيارك لا بحكم الصيغة — والمنصّة تعرف أنها ناتج واحد لا اثنان.
والشاشة على صفّ يخدمها إعلان section — ولا تُعلن grade إطلاقاً.
المسؤول يستطيع أن يضع الشاشة على صفوف بعينها (شاشة الجناح)، لكن الإعلان
لا يعرف كلمة grade ولن يعرفها: لا بيان منشور اليوم يحملها، فاشتراطها
كان يشحن المكان ميّتاً — كل تطبيق شاشة قائم يُرفض على أول شاشة جناح. فشاشة
الصفّ تأخذ حزمة section نفسها، لأن ما تعرضه هو ما تعرضه شاشة الفصل
على نطاق أوسع قليلاً.
وإعلان tenant لا يخدمها: لوحة الردهة تخاطب المدرسة كلّها، وعرضها على
شاشة جناح يضع المبنى الخطأ على الحائط. فإن أردت أن تخدم تطبيقك شاشات
الأجنحة، أعلِن section.
الصيغة القديمة ما زالت تُقرأ. الشكل الأول كان entry واحداً
مع scopes[] مسطّحة؛ إن كان بيانك ما زال عليه فهو يُقرأ صفّاً لكل مكان
مذكور، كلّها بذلك الملف — بيانٌ كان يُنشَر بنجاح يبقى يُنشَر. لكنه لا
يُكتَب أبداً في الاتجاه الآخر: المخزَّن والمنشور دائماً places[].
الكتلة مستقلّة عن mobile.supported. تطبيق يخدم شاشة الفصل ولا
يقدّم شيئاً على الهاتف يُعلن screen وحدها ويترك supported: false —
وتصله المزامنة والمحرّر وأداة MCP كلّها بلا أن يُمحى. وهي للتطبيقات
المدمجة فقط: الشاشة تُترجم كودك من حزمة مصدرك الموقّعة، والتطبيق
external لا يشحن كوداً.
ما تصله الشاشة — Okta.context()
الصندوق الرملي هو صندوق التطبيق المصغّر نفسه: نفس Okta.*، نفس
okta_kit، نفس الثيم. الشاشة تضيف ثلاثة مفاتيح إلى Okta.context()
ولا رمزاً جديداً — فلا رفع عقد ولا minContract أعلى:
final ctx = Okta.context();
final String scope = '${ctx['screen']}'; // 'section' أو 'grade' أو 'tenant'
// الأماكن كلّها، مفصولةً وبالترتيب الذي رتّبها به المسؤول:
final List ids = (ctx['section_ids'] as List?) ?? const [];
final List names = (ctx['section_names'] as List?) ?? const [];
// وعلى شاشة جناح، الصفوف بدلاً منها:
final List gradeIds = (ctx['grade_ids'] as List?) ?? const [];
final List gradeNames = (ctx['grade_names'] as List?) ?? const [];
// والمفرد باقٍ يحمل **أوّل** مكان — لمن كتب تطبيقه قبل المجموعة:
final dynamic sectionId = ctx['section_id'];
final dynamic sectionName = ctx['section_name'];
اقرأ القوائم لا المفرد. الشاشة قد تقف في فصلين، وsection_id يسمّي
أولهما وحده — فتطبيقٌ يقرؤه يعرض نصف ما على الحائط ولا شيء يقول له ذلك.
والقوائم تغيب حين لا يكون للشاشة أماكن من ذلك النوع (لوحة الردهة،
أو مضيف أقدم من المجموعة)، فاقرأها كما في السطر أعلاه: as List? ثم
افتراض فارغ. ولا تشقّ نصّاً على فاصل لاستخراجها — اسم فصل قد يحوي
الفاصل نفسه.
والمفرد غائب تماماً على شاشة الجناح، لا null: تلك تقف على صفوف لا
فصول، فلا section_id ولا section_name في الخريطة أصلاً (والعكس على
شاشة الفصل: لا grade_id). فنداء دالّة على المفقود مباشرةً —
ctx['section_id'].toString() وأمثاله — يقتل تطبيقك في المحرّك
بـtype 'Null' is not a subtype of type '$Instance' in type cast، وإن
وقع في initState تكرّر مع كل إطار. وهذا وقع فعلاً على حائط مدرسة. اقرأ
القوائم، أو افحص المفرد قبل استعماله.
ولن تحتاج أن تسمّي الأماكن بنفسك في أغلب الحالات: كل صفّ طالب يصلك
من /api/apps/education/students يحمل فصله وصفّه، فتجمّعهم منه مباشرةً.
القوائم هنا لترسم العناوين وترتّب الأعمدة، لا لتُرشّح البيانات — الترشيح
يقع على الخادم (انظر أدناه).
['screen'] يصلك كما كان — لكنه لم يعد آلية التوجيه: المنصّة تختار
لكل شاشة ملف الدخول المُعلَن لمكانها، فحزمة الفصل لا تعمل إلا في فصل.
يبقى مفيداً لملفٍ اختَرت أن يخدم المكانين معاً (تتفرّع عليه)، ولحارسٍ
يقول «هذه لوحة ردهة عُلِّقت على شاشة فصل» بدل أن يرسم لوحاً فارغاً.
وقيمته 'grade' على شاشة الجناح — وهي تصل حزمة section كما قيل أعلاه،
فإن رسمت عنواناً باسم الفصل فاقرأ grade_names هناك.
اقرأ المفاتيح بـ ['…'] كما تقرأ locale، وتحقّق من null صراحةً ولا
تُدخل القيمة في نصّ قبل ذلك (راجع درس '$value' في قسم native).
tenant_id وlocale وis_dark تصل كما هي؛ وrole_id فارغ لأن الشاشة
بلا دور.
الشاشة ترى أماكنها فقط — بحكم الخادم
هذه ليست توصية لكودك. شاشة مقترنة بفصول تصل بيانات الطلاب عبر
/api/apps/education/students مثبَّتةً على أماكنها في الخادم:
- بلا أي مرشّح → طلاب فصولها هي، لا المدرسة.
?section_id=يسمّي فصلاً خارجها → صفحة فارغة./students/{ulid}لطالب خارجها → 404، كما لو لم يوجد./education/sections→ فصولها هي وحدها.
وشاشة الجناح مثلها بخطوة أوسع: طلاب فصول صفوفها، ومرشّح يسمّي صفّاً أو فصلاً خارجها يُفرِغ الصفحة.
فتطبيق النداء على شاشة الفصل لا يستطيع سرد المدرسة ولو طلب. والمرشّح الذي يسمّي مكاناً خارج الشاشة يُجيب صفحةً فارغة لا خطأً — وهو نفس ما يُجيبه فصلٌ غير موجود، عمداً: التفريق بينهما كان يجعل مشي المُعرّفات كشفاً لما في المدرسة.
وشاشة الجهة (tenant) ترى ما يراه تطبيقك على الهاتف، بالنطاقات نفسها
الممنوحة له عند التثبيت — لا نطاقات خاصة بالشاشات.
ولا نطاق جديد ولا رفع عقد في هذا كلّه: الأماكن تصل في Okta.context()
وهي خريطة يُسلسِلها المضيف كما هي، والترشيح يقع على الخادم من صفّ الجهاز.
تطبيقك يعمل كما هو ولا يُعلن شيئاً جديداً.
مثال: نداء الفصل (okta_app/native/screen/lib/main.dart)
import 'package:flutter/material.dart';
import 'package:okta_host/okta_host.dart';
import 'package:okta_kit/okta_kit.dart';
Widget main() => const RollCallScreen();
class RollCallScreen extends StatefulWidget {
const RollCallScreen({super.key});
@override
State<RollCallScreen> createState() => _RollCallScreenState();
}
class _RollCallScreenState extends State<RollCallScreen> {
List<dynamic> students = [];
@override
void initState() {
super.initState();
load();
}
Future<void> load() async {
// لا مرشّح: الخادم يثبّت الطلب على الفصل الذي تقف فيه الشاشة.
final res = await Okta.get('/api/apps/education/students?per_page=100');
if (res.ok) {
// body يبقى dynamic؛ OktaJson.rows تفكّ غلاف {data: [...]}.
final dynamic body = res.body;
final List<dynamic> rows = OktaJson.rows(body);
setState(() { students = rows; });
}
}
@override
Widget build(BuildContext context) {
final dynamic ctx = Okta.context();
final dynamic section = ctx['section_name'];
final palette = OktaPalette.of(Okta.isDark());
final List<Widget> rows = <Widget>[];
for (final dynamic student in students) {
rows.add(Padding(
padding: const EdgeInsets.all(12),
child: Text(OktaJson.strOr(student, 'full_name', '—'),
style: TextStyle(fontSize: 36, color: palette.textStrong)),
));
}
return Scaffold(
backgroundColor: palette.surface,
appBar: OktaAppBar.build(section == null ? 'نداء' : 'نداء الفصل $section', palette),
body: ListView(children: rows),
);
}
}
مثال: لوحة إعلانات الردهة (okta_app/native/screen_lobby/lib/main.dart)
حزمة أخرى، وملف آخر — لا فرع داخل الملف الأول. ولاحظ أنها لا تسأل عن
طالب: الردهة يمرّ بها الجميع، فالقالب يقرأ من نقطة تطبيقك أنت
(/api/<slug>/notices) ولا يحتاج أي نطاق منصّة.
import 'package:flutter/material.dart';
import 'package:okta_host/okta_host.dart';
import 'package:okta_kit/okta_kit.dart';
Widget main() => const LobbyBoardScreen();
class LobbyBoardScreen extends StatefulWidget {
const LobbyBoardScreen({super.key});
@override
State<LobbyBoardScreen> createState() => _LobbyBoardScreenState();
}
class _LobbyBoardScreenState extends State<LobbyBoardScreen> {
List<dynamic> notices = [];
@override
void initState() {
super.initState();
load();
}
Future<void> load() async {
final res = await Okta.get('/api/my-app/notices');
if (res.ok) {
final dynamic body = res.body;
final List<dynamic> rows = OktaJson.rows(body);
setState(() { notices = rows; });
}
}
@override
Widget build(BuildContext context) {
final palette = OktaPalette.of(Okta.isDark());
final List<Widget> rows = <Widget>[];
for (final dynamic notice in notices) {
rows.add(Padding(
padding: const EdgeInsets.all(12),
child: Text(OktaJson.strOr(notice, 'title', '—'),
style: TextStyle(fontSize: 36, color: palette.textStrong)),
));
}
return Scaffold(
backgroundColor: palette.surface,
appBar: OktaAppBar.build('لوحة الإعلانات', palette),
body: ListView(children: rows),
);
}
}
النسختان الكاملتان — بحلقة تحديث وإيقافها في dispose — تصلانك جاهزتين
في القالب تحت okta_app/native/screen/ وokta_app/native/screen_lobby/.
نصائح للشاشة الكبيرة: خطّ أكبر ممّا تعتاده على الهاتف (يُقرأ من آخر
الفصل)، بلا لمس — لا تعتمد على إيماءات أو حقول كتابة — وحدّث بياناتك
على جدول لا بسحبٍ للتحديث. الشاشة تُبقي تطبيقك مفتوحاً ساعات؛ وTimer
غير مجسور على هذا المحرّك (راجع البند ١٤ في قسم native)، فحلقة
التحديث استدعاء ذاتي بعد await Future.delayed(…) — لا دالّة تمرّرها إليها —
مع علم إيقاف تضبطه في dispose، وبفاصل طويل — كل دورة رحلة إلى المضيف. وشغّل
flutter test tool/validate.dart داخل كل حزمة شاشة تشحنها: الفحص
يترجم حزمة واحدة، فحزمة فصل خضراء لا تقول شيئاً عن الردهة.
دعم مفاتيح الاتجاه (D-Pad) ومفتاح القائمة
لا لمس على الشاشة أبداً: التحكّم بريموت تلفاز (مفاتيح اتجاه + OK) أو بلوحة مفاتيح على Windows (أسهم + Enter). وما يلي مَقيس على السندبوكس نفسه لا مستنتَج من Flutter عموماً — والفرق بين الاثنين هنا كبير.
العناصر القياسية تدعم الريموت تلقائياً وبلا سطر إضافي. ElevatedButton
وTextButton وListTile: الأسهم/Tab تنقل التركيز إليها، وOK/Enter
(وزرّ A على يد التحكّم) يُشغّل onPressed/onTap داخل كودك المُترجَم على
الجهاز. استعملها لأي شيء يُفترض أن يصله الريموت ولا تحتاج أكثر.
وبطاقتك أنت تصلها كذلك — منذ العقد 25 (package:okta_focus). هذا هو
الجديد، وقبله لم يكن ممكناً بأي حيلة:
import 'package:okta_focus/okta_focus.dart';
OktaFocus.first( // ← تحمل التركيز عند أول رسم
myBigCard(student), // ← أي شيء رسمتَه: Container، Row، صورة…
() => _call(student), // ← يُشغَّل على OK/Enter، وعلى اللمس أيضاً
)
OktaFocus.item(child, onSelect)— العادية، والأولى التي تُستعمل.OktaFocus.first(child, onSelect)— نفسها وتحمل التركيز أول رسم. واحدة لكل شاشة: اثنتان تطلبان التركيز الأول سؤالٌ له جوابان، وأيّهما يفوز ليس شيئاً يُبنى عليه.OktaFocus.watched(child, onSelect, onFocusChange)وwatchedFirst(...)— تُخبرك بدخول التركيز وخروجه لتكبّر البطاقة أو تلوّنها.OktaFocus.reachable(child)— يصله السهم ولا شيء يُضغَط: لصفٍّ في قائمة طويلة ينبغي أن تمرّ الحلقة خلاله بدل أن تقفز فوق كتلة من المحتوى.
والحلقة (focus ring) يرسمها المضيف بلون الغلاف، وعرضها محجوز دائماً فلا
يزحزح نيلُ التركيز الصفَّ الذي تجلس فيه بطاقتك. لا ترسمها ولا تستطيع إخفاءها:
لوحةٌ على حائط بلا حلقة مرئية لوحةٌ لا يستطيع أحد في الغرفة التنقّل فيها.
استعمل watched لتضيف تمييزك فوقها، لا بدلاً منها.
وautofocus: true على زرّ قياسي ما زالت لا تعمل، والسبب أن الزرّ المجسور
لا يملك ذلك الوسيط أصلاً. OktaFocus.first هي الجواب.
Focus وFocusNode وFocusTraversalGroup وKeyboardListener وPopScope
غير موجودة أصلاً — أيٌّ منها يفشل بخطأ ترجمة (Could not find declaration)، لا بخطأ تشغيل. وهذا يقرّر شيئين:
GestureDetectorوحده لا يصله الريموت أبداً. العنصر يصير هدفاً للتنقّل حين يملكFocusNode، ولا سبيل لمنحه واحداً بلاFocus. فاللمس على Windows يعمل عليه وريموت التلفاز لا يصله إطلاقاً. لُفّه بـOktaFocus.item(أو ضع الفعل علىListTile) — ولا تترك فعلاً علىGestureDetectorمجرّد على سطح الشاشة أبداً.- لا تستطيع ابتلاع مفتاح القائمة ولو أردت، و
OktaFocusلم تغيّر ذلك. اعتراض حدث لوحة المفاتيح الخام مستحيل من جهتك بنيوياً، لا «غير موصى به»: المفردة الوحيدة التي تصل كودك هي مفتاح الاختيار، ولاKeyEventيعبر إلى داخل الصندوق إطلاقاً. الأسهم تبقى تنقّل التركيز، ومفتاح القائمة (Menu على الريموت، أو F1/Escape على لوحة المفاتيح) ومفتاح الرجوع يبقيان للمضيف ويفتحان قائمته (اللانشر، إعادة تحميل تطبيقك، الإعدادات) — وهي المخرج الوحيد من تطبيق وحيد يعمل بوضع kiosk (auto_launch)، حيث لا «رجوع» لأن لا شيء آخر تعود إليه.
والشاشة السلبية بلا أزرار شكل مشروع تماماً (نداء فصل، لوحة إعلانات): مفتاح القائمة يبقى يعمل لأن المضيف يحمل تركيزاً احتياطياً حين لا يملك شيء في صفحتك التركيز.
لا MediaQuery ولا LayoutBuilder — كلاهما غير متاح (الأول غير مُجسَّر،
والثاني يفشل بخطأ ترجمة كالبقية أعلاه). أي أن شاشتك لا تستطيع قياس لوحتها
إطلاقاً، فصمِّم على القياس الثابت مباشرة: تلفاز 1080p بكثافة ~320 يُبلَّغ
منطقياً بـ960×540 تقريباً لا 1920×1080 — ومن هنا خطوط أكبر من الهاتف
(36 نقطة في الأمثلة أعلاه ليست تعسّفية).
auto_launch وصف نيّتك، لا ضمان. مدير الشاشة يملك مفتاحاً على جهازه
("اعرض الشاشة الرئيسية دائماً") يُبطل auto_launch لكل التطبيقات على
تلك الشاشة بعينها، فتفتح دائماً على شبكة التطبيقات المثبَّتة ولو كان
تطبيقك الوحيد المُعلَن هناك. لا تفترض أن تطبيقك سيُفتح بلا تدخّل: ابنِ أول
شاشة فيه بحيث تُفهَم لو دخلها المستخدم بضغطة من الشبكة لا تلقائياً — وهذا
هو حال الاختبار من المحاكي والمراجعة على أي حال.
تصميم واجهة الشاشة — القياسات والتخطيط وزرّ الرجوع
الشاشة تُرى من بعيد وبعيون كثيرة تمرّ عليها لا عين واحدة تحملها قريبة — هذا يقلب أولويات التصميم المعتادة على الهاتف:
- القياس المرجعي 960×540 منطقياً (تلفاز 1080p بكثافة ~320)، لا الهاتف مكبّراً. صمِّم على هذا الأساس مباشرة بدل تصميم لوحة هاتف ثم تكبير كل شيء بمعامل ثابت — النسب التي تبدو متوازنة على الهاتف تصير فارغة أو مزدحمة على شاشة بنسبة عرض مختلفة كلياً (16:9 عريضة قصيرة).
- هامش أمان من الحواف (Overscan): بعض شاشات التلفاز الحقيقية — خصوصاً الأقدم منها — تقصّ بضع بكسلات من كل حافة عرضاً لا برمجياً. لا تُلصق نصاً أو عنصراً تفاعلياً بحافة الشاشة مباشرة؛ اترك هامشاً لا يقلّ عن 24-32dp حول كامل المحتوى.
- خطوط أكبر بكثير مما تعتاده على الهاتف — تُقرأ من آخر الفصل أو الردهة
لا من مسافة الذراع. 36 نقطة في أمثلة هذا القسم أرضية معقولة للنصّ العادي،
والعناوين أكبر من ذلك بوضوح. تباين لون قويّ بين النص والخلفية (لا درجات
رمادية متقاربة) — استخدم
OktaPalette.of(Okta.isDark())بدل ألوان ثابتة حتى يتبع تطبيقك وضع الشاشة (فاتح/داكن) الذي يضبطه مدير الجهاز من إعداداته هو، لا تطبيقك. - عناصر تفاعلية كبيرة وقليلة، لا شبكة كثيفة صغيرة: التنقّل كلّه بمفاتيح
الاتجاه على الريموت، فكل عنصر إضافي في الشبكة هو ضغطة إضافية على مستخدم
لا يملك لمساً ليقفز مباشرة إليه. فضِّل قائمة قصيرة من بطاقات كبيرة على
شبكة مزدحمة من عناصر صغيرة، والحلقة تُرسَم لك — Flutter على العناصر
القياسية، والمضيف على بلاطات
OktaFocus— فلا تستبدلها بتلوين يدوي خافت يصعب تمييزه من آخر الغرفة. - كثافة معلومات منخفضة عموماً: الشاشة عرضٌ سلبي غالباً (نداء، لوحة إعلانات، إحصاء حيّ) يُلقى عليه نظر عابر، لا لوحة تحكّم يُدقَّق فيها. رسالة واحدة واضحة أو قائمة واحدة مقروءة أفضل من لوحة بأربع بطاقات إحصاء وجدول ورسم بياني في آن.
زرّ الرجوع على الريموت — يملكه المضيف لا تطبيقك. المفتاح الفيزيائي "رجوع" (Back على ريموت التلفاز، أو Escape على لوحة مفاتيح Windows) يُعترَض على مستوى المضيف بالكامل قبل أن يصل شجرة widgets الخاصة بك، وسلوكه ثابت بحكم عدد التطبيقات لا بحكم كودك:
- تطبيق واحد مثبَّت (kiosk): الرجوع يُبتلَع تماماً — لا شيء يحدث، عمداً، حتى لا تنزلق شاشة عرض سلبية عن حالتها بضغطة عابرة. المخرج الوحيد هو مفتاح القائمة (راجع القسم السابق).
- أكثر من تطبيق: الرجوع يعيد المستخدم إلى شبكة التطبيقات (اللانشر) مباشرة — شجرتك لا تُستشار ولا تحصل على أي إشعار بهذا الحدث.
لذلك لا تبنِ تنقّلاً داخلياً (قائمة ← تفصيل مثلاً) يعتمد على زرّ الرجوع
الفيزيائي للصعود مستوى — هذا الحدث لا يصل كودك أبداً بالمعنى الذي تتوقّعه
تطبيقات الهاتف العادية. إن احتجت مساراً للخروج من تفصيل إلى القائمة داخل
شاشتك، ضع زرّ "رجوع" مرئياً على الشاشة نفسها — زرّاً قياسياً أو ListTile
أو أي شيء رسمتَه ملفوفاً بـOktaFocus.item (راجع قاعدة الوصول بالريموت
أعلاه)، وأولاً في ترتيب القراءة أو حاملاً OktaFocus.first — وتحكّم فيه بحالتك الداخلية
(setState) لا بـNavigator.
تجربة الشاشة على الجهاز الافتراضي (المحاكي)
لا تحتاج تلفازاً لتجرّب: «الجهاز الافتراضي» في البوّابة (تبويب التطبيق، أو
/dashboard/simulator) يركّب سطح الشاشة نفسه في المتصفح — نفس محرّك
dart_eval الذي تُترجَم عليه على الشاشة، ونفس حزمة المصدر من فرعك.
- السطح «شاشة عرض (Okta Screen)» يظهر في اختيار السطح حين يُعلن الإصدار
mobile.screen، ويُختار تلقائياً لتطبيق لا سطح هاتف له. - المكان أولاً: صفّ لكل مكان مُعلَن (شاشة الجهة / شاشة الفصل)، وكلٌّ يسمّي ملف دخوله. المحاكي يترجم ملف المكان المختار وحده — كما تفعل الشاشة — فتطبيقٌ يخدم المكانين يُجرَّب مرتين، لكل مكان حزمته.
- الإطار تلفاز 960×540 منطقياً (dpr 2، بلا حواف آمنة) — الحجم الذي يبلّغه Android TV فعلاً، لا هاتف مقلوب. لوحة الجهاز تتيح مقاسات أخرى للمقارنة، لكن الحكم على التخطيط يكون على هذا المقاس.
Okta.context()كما تكتبه الشاشة، مفتاحاً بمفتاح:screen(tenant|section)، وللفصلsection_idوsection_name، وrole_idفارغ لأن لا أحد يدخل إلى شاشة. الفصل تجريبي (sim-section/ «الفصل التجريبي») — ومن لوحة «الشخصية» تكتب ULID فصلٍ حقيقي في السندبوكس واسمه إن أردت أن تتّبع بياناته. وتغيير المكان أو الفصل إعادة إطلاق لا إعادة رسم، لأن السياق يُحقَن عند البداية على الشاشة أيضاً.- المصدر فرع Git فقط: خيار «المثبَّت على السندبوكس» معطَّل للشاشة — لا جهاز شاشة مقترن بالسندبوكس لتُنزَّل حزمته منه.
- الريموت = لوحة مفاتيح الحاسوب: الأسهم وTab تنقل التركيز، وEnter هو OK. ما لا يصله السهم في المحاكي لا يصله الريموت على التلفاز (راجع قسم D-Pad).
- حدّ يجب أن تعرفه — البيانات لا تُقيَّد بالفصل هنا. نداءات
/api/appsفي المحاكي تمرّ بمقعد السندبوكس (حساب مدير) لا بتوكن جهاز شاشة، فيعودeducation/studentsبطلاب المدرسة كلّها لا طلاب الفصل. على الشاشة الحقيقية يثبّت الخادم الفصل على السياق (sectionScopeId) فلا يمرّ غيره — ضمانةُ خادم لا تعتمد على كودك، ولا تُختبَر إلا على شاشة مقترنة. في المحاكي رشّح بـsection_idبنفسك إن أردت أن ترى ما سيراه الفصل. - من MCP:
simulator_start {slug, surface: "screen", screen_scope: "section"}(أو"tenant")، ثمsimulator_screenshot/simulator_ui/simulator_tapكأي جلسة — والأداة تطبع في ردّها المكان والفصل التجريبي وحدّ البيانات أعلاه.
حين يتعطّل تطبيقك على الشاشة — رمز العطل وتبليغ المنصّة
شاشةٌ لا يقف خلفها أحد لا يجوز أن تبيضّ بصمت ولا أن تعرض شجرة خطأ حمراء
لفصلٍ كامل. فإن فشل تطبيقك — خطأ ترجمة، استثناء عند الرسم، انتهاء مهلة
التحميل — تعرض الشاشة شاشةَ حالة تحمل رمز عطل بصيغة MA-XXXXXXX
(MA = تطبيق مصغّر، ثم بصمة مشتقّة من نوع الخطأ ونصّه — نفس خوارزمية رموز
okta-app، فالرمز نفسه لنفس العطل على كل جهاز). المشغّل يقرؤه من على
التلفاز ويبلّغك به.
- الرمز يصل المنصّة تلقائياً: الشاشة تبلّغ okta-web بالعطل مرة واحدة لكل
رمز في الجلسة (
POST /api/device/client-errorsبتوكن الجهاز المقترن) مع الرسالة والمكدّس ومعرّف تطبيقك (module_slug) وهوية الجهاز — فيُنسَب العطل إلى تطبيقك لا إلى الشاشة. - تقرؤه أنت من صفحة «الأخطاء» على تطبيقك في البوّابة، أو من أدوات MCP:
recent_errorsللأحدث،app_errorsلقائمة الفرز، وget_errorبالرمز الذي قرأه المشغّل — من رمزٍ على تلفاز إلى المكدّس الكامل بلا وسيط. - التكرار يُطوى: الأعطال ذات البصمة نفسها تسقط على صفّ واحد بعدّاد
(
occurrences_count)، فمئة شاشة تعرض العطل نفسه صفٌّ واحد لا مئة بلاغ. - ما لا يُبلَّغ: تطبيق يعمل ويعرض شيئاً خاطئاً لا تعرفه المنصّة — البلاغ للفشل الذي يمنع الرسم. والمخرج على الجهاز نفسه هو قائمة المضيف (مفتاح القائمة ← إعادة تحميل التطبيق).
من المحرّر وMCP
- المحاكي: السطح «شاشة عرض» في الجهاز الافتراضي (القسم أعلاه)، ومن MCP
simulator_startبـsurface: "screen"وscreen_scope. - المحرّر: بطاقة «خدمات داخل تطبيق أوكتا» ← قسم شاشات العرض — مربّع لكل مكان يفتح ملف دخوله وعنوانه الخاص، وظاهر حتى لو كان سطح الهاتف مُطفأً.
- MCP:
set_mobile_screenتكتب الكتلة (استبدال كامل، أوnullللإزالة) وتشغّل فحوص النشر مبكراً؛get_mobile_surfaceتقرؤها وتطبع سطراً لكل مكان بملفه. - المزامنة من المستودع (
sync_from_manifest) تحملها كما تحملdashboard.