قالب MVI المعماري في Kotlin Multiplatform ، الجزء الأول



منذ حوالي عام ، أصبحت مهتمًا بتقنية Kotlin Multiplatform الجديدة. يسمح لك بكتابة كود مشترك وتجميعه لمنصات مختلفة ، مع إمكانية الوصول إلى API الخاص بهم. منذ ذلك الحين وأنا أجرب بنشاط في هذا المجال والترويج لهذه الأداة في شركتنا. إحدى النتائج ، على سبيل المثال ، هي مكتبة Reaktive - الامتدادات التفاعلية لـ Kotlin Multiplatform.

في تطبيقات Badoo و Bumble لتطوير Android ، نستخدم قالب MVI المعماري (لمزيد من التفاصيل حول هندستنا ، اقرأ مقالة Zsolt Kocsi: " هندسة MVI الحديثة القائمة على Kotlin"). بالعمل في مشاريع مختلفة ، أصبحت من المعجبين بهذا النهج. بالطبع ، لم أستطع تفويت فرصة تجربة MVI في Kotlin Multiplatform. علاوة على ذلك ، كانت الحالة مناسبة: كنا بحاجة إلى كتابة أمثلة لمكتبة Reaktive. بعد هذه التجارب الخاصة بي ، أصبحت أكثر إلهامًا من MVI.

أنا دائمًا أنتبه إلى كيفية استخدام المطورين Kotlin Multiplatform وكيفية بناء بنية هذه المشاريع. وفقًا لملاحظاتي ، فإن مطور Kotlin متعدد المنصات العادي هو في الواقع مطور Android يستخدم قالب MVVM في عمله لمجرد أنه معتاد عليه. بالإضافة إلى تطبيق بعض "العمارة النظيفة". ومع ذلك ، في رأيي ، MVI هو الأنسب لـ Kotlin Multiplatform ، و "العمارة النظيفة" هي تعقيد غير ضروري.

لذلك ، قررت كتابة هذه السلسلة المكونة من ثلاث مقالات حول الموضوعات التالية:

  1. وصف موجز لنموذج MVI وبيان المشكلة وإنشاء وحدة نمطية مشتركة باستخدام Kotlin Multiplatform.
  2. دمج الوحدة النمطية المشتركة في تطبيقات iOS و Android.
  3. اختبار الوحدة والتكامل.

فيما يلي المقالة الأولى في السلسلة. سيكون مفيدًا لكل من يستخدم بالفعل أو يخطط فقط لاستخدام Kotlin Multiplatform.

ألاحظ على الفور أن الغرض من هذه المقالة ليس لتعليمك كيفية العمل مع Kotlin Multiplatform نفسها. إذا كنت تشعر أنه ليس لديك ما يكفي من المعرفة في هذا المجال ، فإنني أوصيك بأن تتعرف أولاً على المقدمة والوثائق (خاصة قسمي " التزامن " و " الثبات " لفهم ميزات نموذج الذاكرة Kotlin / Native). في هذه المقالة ، لن أصف تكوين المشروع والوحدات وغيرها من الأشياء التي لا تتعلق بالموضوع.

MVI


أولاً ، لنتذكر ما هو MVI. يرمز الاختصار إلى Model-View-Intent. هناك مكونان رئيسيان فقط في النظام:

  • نموذج - طبقة من المنطق والبيانات (يخزن النموذج أيضًا الحالة الحالية للنظام) ؛
  • (View) — UI-, (states) (intents).

ربما يكون الرسم البياني التالي مألوفًا بالفعل للكثيرين:



لذا ، نرى تلك المكونات الأساسية للغاية: النموذج والعرض التقديمي. كل شيء آخر هو البيانات التي يتم تداولها بينهما.

من السهل رؤية أن البيانات تتحرك في اتجاه واحد فقط. تأتي الدول من النموذج وتندرج في العرض للعرض ، وتأتي النوايا من العرض وتذهب إلى النموذج للمعالجة. يسمى هذا التداول تدفق البيانات أحادي الاتجاه.

من الناحية العملية ، غالبًا ما يتم تمثيل النموذج بواسطة كيان يسمى Store (يتم استعارته من Redux). ومع ذلك ، لا يحدث هذا دائمًا. على سبيل المثال ، في مكتبة MVICore الخاصة بنا ، يسمى النموذج الميزة.

من الجدير بالذكر أيضًا أن MVI يرتبط ارتباطًا وثيقًا بالتفاعلية. يعد عرض تدفقات البيانات وتحويلها ، بالإضافة إلى إدارة دورة الحياة للاشتراكات مريحة للغاية للتنفيذ باستخدام مكتبات البرمجة التفاعلية. يتوفر عدد كبير إلى حد ما الآن ، ومع ذلك ، عند كتابة التعليمات البرمجية العامة في Kotlin Multiplatform ، يمكننا فقط استخدام مكتبات متعددة الأنظمة الأساسية. نحن بحاجة إلى تجريد لتدفقات البيانات ، نحتاج إلى القدرة على الاتصال بقطع المدخلات والمخرجات وفصلها ، وكذلك إجراء التحولات. في الوقت الحالي ، أعرف مكتبتين من هذا القبيل:

  • مكتبة Reaktive الخاصة بنا - تنفيذ الإضافات التفاعلية على منصة Kotlin المتعددة
  • Coroutines والتدفق - تنفيذ التيارات الباردة مع Corotines Kotlin.

صياغة المشكلة


الغرض من هذه المقالة هو إظهار كيفية استخدام قالب MVI في Kotlin Multiplatform وما هي مزايا وعيوب هذا النهج. لذلك ، لن أكون مرتبطًا بأي تنفيذ محدد لـ MVI. ومع ذلك ، سأستخدم Reaktive ، لأن تدفقات البيانات لا تزال مطلوبة. إذا رغبت في ذلك ، بعد أن فهمت الفكرة ، يمكن استبدال Reaktive بواسطة coroutines و Flow. بشكل عام ، سأحاول أن نجعل MVI لدينا بسيطًا قدر الإمكان ، دون مضاعفات غير ضرورية.

لتوضيح MVI ، سأحاول تنفيذ أبسط مشروع يلبي المتطلبات التالية:

  • دعم Android و iOS ؛
  • عرض العملية غير المتزامنة (المدخلات والمخرجات ومعالجة البيانات ، وما إلى ذلك) ؛
  • أكبر قدر ممكن من الرمز المشترك ؛
  • تنفيذ واجهة المستخدم مع الأدوات الأصلية لكل منصة ؛
  • نقص Rx على جانب المنصة (بحيث لا تضطر إلى تحديد التبعيات على Rx كـ "api").

كمثال ، اخترت تطبيقًا بسيطًا جدًا: شاشة واحدة بزر ، من خلال النقر على أي قائمة بها صور عشوائية للقطط سيتم تنزيلها وعرضها. لتحميل الصور ، سأستخدم واجهة برمجة التطبيقات المفتوحة: https://thecatapi.com . سيسمح لك هذا باستيفاء متطلبات التشغيل غير المتزامن ، حيث يتعين عليك تنزيل القوائم من الويب وتحليل ملف JSON.

يمكنك العثور على جميع التعليمات البرمجية المصدر للمشروع على GitHub .

الشروع في البدء: ملخصات MVI


نحتاج أولاً إلى تقديم بعض الملخصات عن MVI. سنحتاج إلى المكونات الأساسية للغاية - النموذج والمنظر - واثنين من أنماط الكتابة.

الأنماط


لمعالجة النوايا ، قدمنا ​​فاعلًا (ممثل) - وظيفة تقبل القصد والحالة الحالية وتعرض تيارًا من النتائج (التأثير):

Typealias الفاعل < State ، Intent ، Effect > = ( State ، Intent ) - > ملحوظ < Effect >
عرض الخام تمت استضافة MviKmpActor.kt مع ❤ بواسطة GitHub

نحتاج أيضًا إلى مخفض (Reducer) - وظيفة تسري والحالة الحالية وتعيد حالة جديدة:

Typealias Reducer < State ، Effect > = ( State ، Effect ) - > State
عرض الخام تمت استضافة MviKmpReducer.kt مع ❤ بواسطة GitHub

متجر


سيقدم المتجر نموذجًا من MVI. يجب أن يقبل النوايا ويعطي تيارًا من الدول. عند الاشتراك في تيار الدولة ، يجب إصدار الحالة الحالية.

دعونا نقدم الواجهة المناسبة:

واجهة المتجر < في نية : أي ، خارج الدولة : أي > : المستهلك < نوايايمكن ملاحظتها < الدولةيمكن التخلص منها
عرض الخام تمت استضافة MviKmpStoreInterface.kt مع ❤ بواسطة GitHub

لذلك ، يحتوي متجرنا على الخصائص التالية:

  • له معلمتان عامتان: هدف الإدخال وحالة الإخراج ؛
  • هو مستهلك للنوايا (المستهلك <Intent>) ؛
  • هو تيار الدولة (ملاحظته <الدولة>) ؛
  • إنه قابل للتدمير (يمكن التخلص منه).

نظرًا لأنه ليس من الملائم جدًا تنفيذ مثل هذه الواجهة في كل مرة ، فسنحتاج إلى مساعد معين:

الطبقة StoreHelper < في نية : أي ، خارج الدولة : أي ، في تأثير : أي > (
الحالة الأولية : الدولة ،
ممثل فال الخاص : الفاعل < State ، Intent ، Effect > ،
private val reducer: Reducer<State, Effect>
) : Observable<State>, DisposableScope by DisposableScope() {
init {
ensureNeverFrozen()
}
private val subject = BehaviorSubject(initialState)
fun onIntent(intent: Intent) {
actor(subject.value, intent).subscribeScoped(isThreadLocal = true, onNext = ::onEffect)
}
fun onEffect ( تأثير : تأثير ) {
subject.onNext (المخفض (value.value ، تأثير))
}}
تجاوز الاشتراك المرح ( المراقب : ObservableObserver < State >) {
موضوع الاشتراك (مراقب)
}}
}}
عرض الخام تمت استضافة MviKmpStoreHelper.kt مع ❤ بواسطة GitHub


StoreHelper عبارة عن فئة صغيرة ستسهل علينا إنشاء متجر. له الخصائص التالية:

  • لديه ثلاث معلمات عامة: القصد من المدخلات والتأثير وحالة الإخراج ؛
  • يقبل الحالة الأولية من خلال المنشئ والممثل وعلبة التروس ؛
  • هو تيار من الدول ؛
  • قابلة للتدمير (يمكن التخلص منها) ؛
  • عدم التجميد (بحيث لا يتم تجميد المشتركين أيضًا ) ؛
  • تنفذ DisposableScope (واجهة من Reaktive لإدارة الاشتراكات) ؛
  • يقبل ويعالج النوايا والآثار.

انظر إلى الرسم البياني لمتجرنا. الفاعل وعلبة التروس فيه تفاصيل التنفيذ:



لنفكر في طريقة onIntent بمزيد من التفاصيل:

  • يقبل النوايا كحجة ؛
  • يدعو الفاعل ويمرر النية والحالة الحالية إليه ؛
  • يشترك في دفق التأثيرات التي أرجعها الممثل ؛
  • يوجه كل الآثار إلى طريقة onEffect ؛
  • يتم الاشتراك في التأثيرات باستخدام علامة isThreadLocal (هذا يتجنب التجميد في Kotlin / Native).

الآن دعونا نلقي نظرة فاحصة على طريقة onEffect:

  • يأخذ الآثار كحجة ؛
  • استدعاء علبة التروس ونقل التأثير والحالة الحالية إليه ؛
  • يمرر الدولة الجديدة إلى BehaviorSubject ، مما يؤدي إلى استلام الدولة الجديدة من قبل جميع المشتركين.

رأي


الآن دعنا ندخل في العرض التقديمي. يجب أن يقبل نماذج للعرض وإعطاء تيار من الأحداث. سنقوم أيضًا بإنشاء واجهة منفصلة:

الواجهة MviView < في الموديل : Any ، out Event : Any > {
أحداث فال : ملحوظة < حدث >
تقديم المرح ( نموذج : نموذج )
}}
عرض الخام تمت استضافة MviKmpMviView.kt مع ❤ بواسطة GitHub


لطريقة العرض الخصائص التالية:

  • له معلمتان عامتان: نموذج الإدخال وحدث الإخراج ؛
  • يقبل نماذج للعرض باستخدام طريقة التقديم ؛
  • يرسل دفق الأحداث باستخدام خاصية الأحداث.

أضفت بادئة Mvi إلى اسم MviView لتجنب الخلط مع Android View. أيضًا ، لم أقوم بتوسيع واجهات المستهلك والملاحظة ، ولكن ببساطة استخدم الخاصية والطريقة. هذا حتى تتمكن من تعيين واجهة العرض التقديمي على نظام أساسي للتنفيذ (Android أو iOS) دون تصدير Rx كمُعالٍ "api". الحيلة هي أن العملاء لن يتفاعلوا بشكل مباشر مع خاصية "الأحداث" ، ولكنهم سينفذون واجهة MviView ، مما يوسع الفئة المجردة.

أضف هذه الفئة المجردة على الفور لتمثيل:

abstract class AbstractMviView<in Model : Any, Event : Any> : MviView<Model, Event> {
private val subject = PublishSubject<Event>()
override val events: Observable<Event> = subject
protected fun dispatch(event: Event) {
subject.onNext(event)
}
}
view raw تمت استضافة MviKmpAb abstractMviView.kt مع ❤ بواسطة GitHub

سيساعدنا هذا الفصل في إصدار الأحداث ، بالإضافة إلى إنقاذ النظام الأساسي من التفاعل مع Rx.

فيما يلي رسم تخطيطي يوضح كيفية عمل ذلك:



ينتج المتجر حالات يتم تحويلها إلى نماذج ويتم عرضها بواسطة العرض. ينتج هذا الأخير أحداثًا يتم تحويلها إلى نوايا وتسليمها إلى المتجر للمعالجة. يزيل هذا النهج التماسك بين المتجر والعرض التقديمي. ولكن في حالات بسيطة ، يمكن أن تعمل طريقة العرض بشكل مباشر مع الدول والنوايا.

هذا كل ما نحتاجه لتنفيذ MVI. لنكتب الرمز العام.

كود مشترك


خطة


  1. سنقوم بعمل وحدة عامة مهمتها تنزيل وعرض قائمة صور القطط.
  2. نقوم بتجريد الواجهة وسننقل تنفيذها للخارج.
  3. سنخفي تنفيذنا وراء واجهة مريحة.

Kittenstore


لنبدأ بالشيء الرئيسي - إنشاء KittenStore الذي سيحمل قائمة بالصور:

الواجهة الداخلية KittenStore : المتجر < Intent ، State > {
نية فئة مختومة {
تحديث الكائن : Intent ()
}}
حالة فئة البيانات (
val isLoading : Boolean = false ،
بيانات فال : البيانات = البيانات . الصور ()
) {
بيانات الفئة المختومة {
صور فئة البيانات ( عناوين url val : القائمة < سلسلة > = فارغة قائمة ()) : البيانات ()
خطأ في الكائن : البيانات ()
}}
}}
}}
عرض الخام تمت استضافة MviKmpKittenStoreInterface.kt مع ❤ بواسطة GitHub


لقد قمنا بتوسيع واجهة المتجر بأنواع intent وأنواع الحالة. يرجى ملاحظة ما يلي: تم إعلان الواجهة على أنها داخلية. KittenStore الخاص بنا هو تفاصيل تنفيذ الوحدة. نيتنا واحدة فقط - إعادة تحميل ، يتسبب في تحميل قائمة الصور. لكن الشرط يستحق النظر بمزيد من التفصيل:

  • تشير علامة isLoading إلى ما إذا كان التنزيل قيد التنفيذ حاليًا أم لا ؛
  • يمكن أن تأخذ خاصية البيانات أحد الخيارين:
    • الصور - قائمة روابط الصور ؛
    • خطأ - يعني حدوث خطأ.

الآن لنبدأ التنفيذ. سنفعل ذلك على مراحل. أولاً ، قم بإنشاء فئة KittenStoreImpl فارغة من شأنها تنفيذ واجهة KittenStore:

فئة داخلية KittenStoreImpl (
) : KittenStore ، DisposableScope بواسطة DisposableScope () {
تجاوز المرح onNext ( القيمة : Intent ) {
}}
تجاوز الاشتراك المرح ( المراقب : ObservableObserver < State >) {
}}
}}
عرض الخام تمت استضافة MviKmpKittenStoreImpl1.kt مع ❤ بواسطة GitHub


قمنا أيضًا بتطبيق واجهة DisposableScope المألوفة. هذا ضروري لإدارة الاشتراك المريحة.

سنحتاج إلى تنزيل قائمة الصور من الويب وتحليل ملف JSON. أعلن التبعيات المقابلة:

فئة داخلية KittenStoreImpl (
شبكة فال الخاصة : شبكة ،
محلل فال الخاص : محلل
) : KittenStore, DisposableScope by DisposableScope() {
override fun onNext(value: Intent) {
}
override fun subscribe(observer: ObservableObserver<State>) {
}
interface Network {
fun load(): Maybe<String>
}
interface Parser {
تحليل مرح ( json : String ) : ربما < List < String >>
}}
}}
عرض الخام تمت استضافة MviKmpKittenStoreImpl2.kt مع ❤ بواسطة GitHub

ستقوم الشبكة بتنزيل نص ملف JSON من الشبكة ، وسيقوم المحلل اللغوي بتحليل ملف JSON وإرجاع قائمة روابط الصورة. في حالة حدوث خطأ ، ربما ينتهي ببساطة دون نتيجة. في هذه المقالة ، لسنا مهتمين بنوع الخطأ.

أعلن الآن الآثار والمخفض:

فئة داخلية KittenStoreImpl (
شبكة فال الخاصة : شبكة ،
محلل فال الخاص : محلل
) : KittenStore, DisposableScope by DisposableScope() {
override fun onNext(value: Intent) {
}
override fun subscribe(observer: ObservableObserver<State>) {
}
private fun reduce(state: State, effect: Effect): State =
when (effect) {
is Effect.LoadingStarted -> state.copy(isLoading = true)
is Effect.LoadingFinished -> state.copy(isLoading = false, data = State.Data.Images(urls = effect.imageUrls))
is Effect.LoadingFailed -> state.copy(isLoading = false, data = State.Data.Error)
}
private sealed class Effect {
object LoadingStarted : Effect()
data class LoadingFinished(val imageUrls: List<String>) : Effect()
object LoadingFailed : Effect()
}
interface Network {
fun load(): Maybe<String>
}
interface Parser {
تحليل مرح ( json : String ) : ربما < List < String >>
}}
}}
عرض الخام تمت استضافة MviKmpKittenStoreImpl3.kt مع ❤ بواسطة GitHub

قبل بدء التنزيل ، نصدر تأثير LoadingStarted ، مما يؤدي إلى تعيين علامة isLoading. بعد اكتمال التنزيل ، نصدر إما LoadingFinished أو LoadingFailed. في الحالة الأولى ، نقوم بمسح علامة isLoading ونطبق قائمة الصور ، وفي الحالة الثانية ، نقوم أيضًا بمسح العلم وتطبيق حالة الخطأ. يرجى ملاحظة أن التأثيرات هي واجهة برمجة تطبيقات KittenStore الخاصة بنا.

الآن نقوم بتنفيذ التنزيل نفسه:

فئة داخلية KittenStoreImpl (
private val network: Network,
private val parser: Parser
) : KittenStore, DisposableScope by DisposableScope() {
override fun onNext(value: Intent) {
}
override fun subscribe(observer: ObservableObserver<State>) {
}
private fun reload(network: Network, parser: Parser): Observable<Effect> =
network
.load()
.flatMap(parser::parse)
.map(Effect::LoadingFinished)
.observeOn(mainScheduler)
.asObservable()
.defaultIfEmpty(Effect.LoadingFailed)
.startWithValue(Effect.LoadingStarted)
private fun reduce(state: State, effect: Effect): State =
// Omitted code
private sealed class Effect {
// Omitted code
}
interface Network {
fun load(): Maybe<String>
}
interface Parser {
fun parse(json: String): Maybe<List<String>>
}
}
view raw تمت استضافة MviKmpKittenStoreImpl4.kt مع ❤ بواسطة GitHub

هنا يجدر الانتباه إلى حقيقة أننا مررنا Network and Parser إلى وظيفة إعادة التحميل ، على الرغم من حقيقة أنها متاحة لنا بالفعل كممتلكات من المنشئ. يتم ذلك لتجنب الإشارات إلى هذا ، ونتيجة لذلك ، يتم تجميد KittenStore بالكامل.

حسنًا ، أخيرًا ، استخدم StoreHelper وإنهاء تطبيق KittenStore:

فئة داخلية KittenStoreImpl (
شبكة فال الخاصة : شبكة ،
محلل فال الخاص : محلل
) : KittenStore ، DisposableScope بواسطة DisposableScope () {
private val helper = StoreHelper(State(), ::handleIntent, ::reduce).scope()
override fun onNext(value: Intent) {
helper.onIntent(value)
}
override fun subscribe(observer: ObservableObserver<State>) {
helper.subscribe(observer)
}
private fun handleIntent(state: State, intent: Intent): Observable<Effect> =
when (intent) {
is Intent.Reload -> reload(network, parser)
}
private fun reload(network: Network, parser: Parser): Observable<Effect> =
// Omitted code
private fun reduce(state: State, effect: Effect): State =
// Omitted code
private sealed class Effect {
// Omitted code
}
interface Network {
تحميل المرح () : ربما < سلسلة >
}}
واجهة المحلل اللغوي {
تحليل مرح ( json : String ) : ربما < List < String >>
}}
}}
عرض الخام تمت استضافة MviKmpKittenStoreImpl5.kt مع ❤ بواسطة GitHub

متجر KittenStore جاهز! ننتقل إلى العرض التقديمي.

Kittenview


أعلن الواجهة التالية:

الواجهة KittenView : MviView < النموذج ، الحدث > {
نموذج فئة البيانات (
فال هو التحميل : منطقي ،
فال isError : منطقي ،
val imageUrls : قائمة < سلسلة >
)
حدث الفصل المختوم {
كائن RefreshTriggered : Event ()
}}
}}
عرض الخام تمت استضافة MviKmpKittenViewInterface.kt مع ❤ بواسطة GitHub

أعلنا عن نموذج عرض بعلامتي التحميل والخطأ وقائمة روابط الصورة. لدينا حدث واحد فقط - RefreshTriggered. يتم إصداره في كل مرة يقوم فيها المستخدم باستدعاء تحديث. KittenView هي واجهة برمجة التطبيقات العامة لوحدتنا.

KittenDataSource


تتمثل مهمة مصدر البيانات هذا في تنزيل نص لملف JSON من الويب. كالعادة ، أعلن عن الواجهة:

الواجهة الداخلية KittenDataSource {
حمل المرح ( الحد : Int ، الإزاحة : Int ) : ربما < String >
}}

سيتم تنفيذ تطبيقات مصدر البيانات لكل منصة على حدة. لذلك ، يمكننا أن نعلن عن طريقة المصنع باستخدام المتوقع / الفعلي:

توقع متعة داخلية KittenDataSource () : KittenDataSource

سيتم مناقشة تنفيذ مصدر البيانات في الجزء التالي ، حيث سننفذ تطبيقات لنظامي التشغيل iOS و Android.

دمج


المرحلة الأخيرة هي تكامل جميع المكونات.

تنفيذ واجهة الشبكة:

فئة داخلية KittenStoreNetwork (
بيانات فال الخاصة المصدر : KittenDataSource
) : KittenStoreImpl . شبكة {
تجاوز حمل المرح () : ربما < String > = dataSource.load (الحد = 50 ، الإزاحة = 0 )
}}
عرض الخام تمت استضافة KittenStoreNetwork.kt مع ❤ بواسطة GitHub


تنفيذ واجهة المحلل:

كائن داخلي KittenStoreParser : KittenStoreImpl . المحلل {
تجاوز تحليل المرح ( json : String ) : ربما < List < String >> =
ربما من الوظيفة {
سلمان ( JsonConfiguration . مستقر )
.parseJson (json)
.jsonArray
.map {it.jsonObject.getPrimitive ( " url " ) .content}
}}
.subscribeOn (حسابجدولة)
.onErrorComplete ()
}}
عرض الخام استضاف KittenStoreParser.kt مع ❤ بواسطة GitHub

استخدمنا هنا مكتبة kotlinx.serialization . يتم إجراء التحليل على جدولة حسابية لتجنب حظر سلسلة الرسائل الرئيسية.

تحويل الحالة لعرض النموذج:

دولة المرح الداخلي . toModel () : نموذج =
نموذج (
isLoading = isLoading ،
isError = عندما ( البيانات ) {
هي الدولة . البيانات . صور - > كاذبة
هي الدولة . البيانات . خطأ - > صحيح
} ،
imageUrls = عندما ( البيانات ) {
هي الدولة . البيانات . الصور - > بيانات .urls
هي الدولة . البيانات . خطأ - > blankList ()
}}
)
عرض الخام تمت استضافة MviKmpStateToModel.kt مع ❤ بواسطة GitHub

تحويل الأحداث إلى نوايا:

حدث ممتع داخلي . toIntent () : الهدف =
متى ( هذا ) {
هو الحدث . RefreshTriggered - > النية . إعادة تحميل
}}
عرض الخام تمت استضافة MviKmpEventToIntent.kt مع ❤ بواسطة GitHub

إعداد الواجهة:

class KittenComponent {
متعة onViewCreated ( عرض : KittenView ) {
}}
مرح onStart () {
}}
متعة onStop () {
}}
متعة onViewDestroyed () {
}}
متعة onDestroy () {
}}
}}
عرض الخام تمت استضافة MviKmpKittenComponent1.kt مع ❤ بواسطة GitHub

مألوفة لكثير من دورة حياة مطوري Android. إنه رائع لنظام iOS ، وحتى JavaScript. يبدو مخطط الانتقال بين حالات دورة حياة واجهتنا كما يلي:


سأشرح بإيجاز ما يحدث هنا:

  • أولاً ، تسمى طريقة onCreate ، بعدها - onViewCreated ، ثم - onStart: هذا يضع الواجهة في حالة عمل (بدء) ؛
  • في مرحلة ما بعد ذلك ، تسمى طريقة onStop: هذا يضع الواجهة في حالة توقف (توقف) ؛
  • في حالة التوقف ، يمكن استدعاء إحدى الطريقتين: onStart أو onViewDestroyed ، أي أنه يمكن بدء الواجهة مرة أخرى ، أو يمكن تدمير وجهة نظرها ؛
  • عندما يتم تدمير العرض ، إما أنه يمكن إنشاؤه مرة أخرى (onViewCreated) ، أو يمكن تدمير الواجهة بأكملها (onDestroy).

قد يبدو تنفيذ الواجهة كما يلي:

class KittenComponent {
خاص فال مخزن =
KittenStoreImpl (
شبكة = KittenStoreNetwork (dataSource = KittenDataSource ()) ،
محلل = KittenStoreParser
)
عرض فار خاص : KittenView؟ = فارغ
خاص var startStopScope : DisposableScope؟ = فارغ
متعة onViewCreated ( عرض : KittenView ) {
هذا. view = عرض
}}
fun onStart() {
val view = requireNotNull(view)
startStopScope = disposableScope {
store.subscribeScoped(onNext = view::render)
view.events.map(Event::toIntent).subscribeScoped(onNext = store::onNext)
}
}
fun onStop() {
startStopScope?.dispose()
}
fun onViewDestroyed() {
view = null
}
fun onDestroy() {
store.dispose()
}
}

كيف تعمل:

  • أولاً نقوم بإنشاء نسخة من KittenStore ؛
  • في طريقة onViewCreated نتذكر الارتباط بـ KittenView ؛
  • في onStart نوقع KittenStore و KittenView لبعضنا البعض ؛
  • في onStop ، نعكسها على بعضها البعض ؛
  • في onViewDestroyed نقوم بمسح الرابط للعرض ؛
  • في onDestroy ، قم بتدمير KittenStore.

استنتاج


كانت هذه هي المقالة الأولى في سلسلة MVI الخاصة بي في Kotlin Multiplatform. في ذلك نحن:

  • تذكر ما هو MVI وكيف يعمل ؛
  • جعل أبسط تنفيذ MVI على Kotlin Multiplatform باستخدام مكتبة Reaktive ؛
  • إنشاء وحدة مشتركة لتحميل قائمة الصور باستخدام MVI.

لاحظ أهم خصائص وحدتنا المشتركة:

  • تمكنا من وضع جميع التعليمات البرمجية في الوحدة النمطية متعددة المنصات باستثناء رمز واجهة المستخدم ؛ كل المنطق ، بالإضافة إلى الاتصالات والتحويلات بين المنطق وواجهة المستخدم شائعة ؛
  • المنطق وواجهة المستخدم غير مرتبطتين تمامًا ؛
  • تنفيذ واجهة المستخدم بسيط للغاية: ما عليك سوى عرض نماذج العرض الواردة وإلقاء الأحداث ؛
  • تكامل الوحدة بسيط أيضًا: كل ما تحتاج إليه هو:
    • تنفيذ واجهة KittenView (بروتوكول) ؛
    • إنشاء نسخة من KittenComponent ؛
    • استدعاء أساليب دورة حياته في الوقت المناسب ؛
  • يتجنب هذا النهج "تدفق" Rx (أو coroutine) في الأنظمة الأساسية ، مما يعني أنه ليس علينا إدارة أي اشتراكات على مستوى التطبيق ؛
  • يتم تلخيص جميع الفئات المهمة من خلال واجهات واختبارها.

في الجزء التالي ، سأوضح عمليًا كيف يبدو تكامل KittenComponent في تطبيقات iOS و Android.

تابعني على تويتر وابق على اتصال!

All Articles