مرحبا بالجميع! في هذه المقالة ، أود أن أتحدث عن مكتبة جديدة تجلب نمط تصميم MVI إلى Android. تسمى هذه المكتبة MVIDroid ، مكتوبة بنسبة 100 ٪ في Kotlin ، خفيفة الوزن وتستخدم RxJava 2.x. أنا شخصياً مؤلف المكتبة ، كود المصدر متاح على GitHub ، ويمكنك توصيله من خلال JitPack (رابط إلى المستودع في نهاية المقالة). تتكون هذه المقالة من جزأين: وصف عام للمكتبة ومثال على استخدامها.
MVI
وهكذا ، كمقدمة ، اسمحوا لي أن أذكركم بما هو MVI. نموذج - عرض - القصد أو ، إذا كان باللغة الروسية ، نموذج - عرض - القصد. هذا هو نمط التصميم الذي يكون فيه النموذج مكونًا نشطًا يقبل Intents ويولد حالة. العرض (العرض) بدوره يقبل نماذج التمثيل (عرض النموذج) وينتج نفس النوايا. يتم تحويل الحالة إلى "نموذج عرض" باستخدام دالة تحويل (عرض نموذج المخطط). من الناحية التخطيطية ، يمكن تمثيل نمط MVI على النحو التالي:

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

المكونات الرئيسية لـ MVIDroid
نموذج
لنبدأ بالنموذج. في المكتبة ، تم توسيع مفهوم النموذج قليلاً ، حيث ينتج هنا ليس فقط الدول ولكن أيضًا العلامات. يتم استخدام التصنيفات لتوصيل النماذج مع بعضها البعض. يمكن تحويل تسميات بعض النماذج إلى نوايا نماذج أخرى باستخدام وظائف التحويل. من الناحية التخطيطية ، يمكن تمثيل النموذج على النحو التالي:

في MVIDroid ، يتم تمثيل النموذج بواجهة MviStore (اسم المتجر مستعار من Redux):
interface MviStore<State : Any, in Intent : Any, Label : Any> : (Intent) -> Unit, Disposable { @get:MainThread val state: State val states: Observable<State> val labels: Observable<Label> @MainThread override fun invoke(intent: Intent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean }
ولدينا:
- تحتوي الواجهة على ثلاث معلمات عامة: State - type of State، Intent - type of Intent and Label - type of Labels
- يحتوي على ثلاثة حقول: الحالة - الحالة الحالية للنموذج ، الحالات - الدول والملصقات القابلة للرصد - الملصقات القابلة للملاحظة. يوفر الحقلان الأخيران فرصة للاشتراك في التغييرات في الحالة والعلامات ، على التوالي.
- نية المستهلك
- يمكن التخلص منه ، مما يجعل من الممكن تدمير النموذج وإيقاف جميع العمليات التي تحدث فيه
لاحظ أنه يجب تنفيذ جميع طرق النموذج على الخيط الرئيسي. وينطبق الشيء نفسه على أي مكون آخر. بالطبع ، يمكنك تنفيذ مهام الخلفية باستخدام أدوات RxJava القياسية.
مكون
مكون في MVIDroid هو مجموعة من النماذج توحدها هدف مشترك. على سبيل المثال ، يمكنك تحديد جميع النماذج لشاشة في المكون. بمعنى آخر ، المكون هو واجهة للنماذج المرفقة به ويسمح لك بإخفاء تفاصيل التنفيذ (النماذج ، وظائف التحويل وعلاقاتها). دعونا نلقي نظرة على مخطط المكون:

كما ترى من الرسم البياني ، فإن للمكون وظيفة مهمة في تحويل الأحداث وإعادة توجيهها.
قائمة كاملة بوظيفة المكون هي كما يلي:
- إقران أحداث التمثيل والعلامات الواردة بكل نموذج باستخدام وظائف التحويل المقدمة
- إحضار تسميات النموذج الصادرة إلى الخارج
- يدمر جميع النماذج ويكسر كل الروابط عندما يتم تدمير أحد المكونات
يحتوي المكون أيضًا على واجهة خاصة به:
interface MviComponent<in UiEvent : Any, out States : Any> : (UiEvent) -> Unit, Disposable { @get:MainThread val states: States @MainThread override fun invoke(event: UiEvent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean }
خذ بعين الاعتبار واجهة المكون بمزيد من التفاصيل:
- يحتوي على معلمتين عامتين: UiEvent - نوع عرض الأحداث والولايات - نوع حالات النماذج
- يحتوي على حقل الحالات الذي يتيح الوصول إلى مجموعة حالات النموذج (على سبيل المثال ، كواجهة أو فئة بيانات)
- أحداث عرض المستهلك
- يمكن التخلص منه ، مما يجعل من الممكن تدمير المكون وجميع نماذجه
عرض
كما قد تخمن ، هناك حاجة إلى طريقة عرض لعرض البيانات. يتم تجميع بيانات كل طريقة عرض في نموذج عرض وعادة ما يتم تمثيلها على أنها فئة بيانات (Kotlin). خذ بعين الاعتبار واجهة العرض:
interface MviView<ViewModel : Any, UiEvent : Any> { val uiEvents: Observable<UiEvent> @MainThread fun subscribe(models: Observable<ViewModel>): Disposable }
كل شيء أبسط قليلاً هنا. معلمتان عامتان: ViewModel - نوع نموذج العرض و UiEvent - نوع عرض الأحداث. أحد مجالات uiEvents هو حدث العرض المرئي ، والذي يمكّن العملاء من الاشتراك في هذه الأحداث نفسها. وطريقة اشتراك واحدة () تتيح لك الاشتراك في عرض النماذج.
مثال للاستخدام
حان الوقت لتجربة شيء ما في الممارسة. أقترح القيام بشيء بسيط للغاية. شيء لا يتطلب الكثير من الجهد لفهمه ، وفي الوقت نفسه يعطي فكرة عن كيفية استخدام كل هذا وفي أي اتجاه للتحرك. فليكن مولد UUID: بلمسة زر ، سننشئ UUID ونعرضه على الشاشة.
التقديم
أولاً ، نصف نموذج العرض:
data class ViewModel(val text: String)
وعرض الأحداث:
sealed class UiEvent { object OnGenerateClick: UiEvent() }
ننفذ الآن طريقة العرض نفسها ، لذلك نحن بحاجة إلى الميراث من فئة MviAb abstractView المجردة:
class View(activity: Activity) : MviAbstractView<ViewModel, UiEvent>() { private val textView = activity.findViewById<TextView>(R.id.text) init { activity.findViewById<Button>(R.id.button).setOnClickListener { dispatch(UiEvent.OnGenerateClick) } } override fun subscribe(models: Observable<ViewModel>): Disposable = models.map(ViewModel::text).distinctUntilChanged().subscribe { textView.text = it } }
كل شيء بسيط للغاية: نحن نشترك في تغييرات UUID ونحدث TextView عندما نتلقى UUID جديدًا ، وعندما يتم النقر على الزر ، نرسل حدث OnGenerateClick.
نموذج
سيتألف النموذج من جزأين: الواجهة والتنفيذ.
الواجهة:
interface UuidStore : MviStore<State, Intent, Nothing> { data class State(val uuid: String? = null) sealed class Intent { object Generate : Intent() } }
كل شيء بسيط هنا: تمد واجهتنا واجهة MviStore ، مشيرة إلى أنواع الدولة (State) و Intent (Intent). نوع العلامات - لا شيء ، لأن نموذجنا لا ينتجها. تحتوي الواجهة أيضًا على فئتي State و Intent.
من أجل تنفيذ النموذج ، تحتاج إلى فهم كيفية عمله. عند إدخال النموذج ، يتم تلقي Intents ، والتي يتم تحويلها إلى إجراءات باستخدام وظيفة IntentToAction الخاصة. يتم إدخال الإجراءات إلى المنفذ ، الذي ينفذها وينتج النتيجة والتسمية. ثم تنتقل النتائج إلى Reducer ، الذي يحول الحالة الحالية إلى حالة جديدة.
جميع النماذج الأربعة المؤلفة:
- IntentToAction - وظيفة تحول النية إلى العمل
- MviExecutor - تنفيذ الإجراءات وإنتاج النتائج والعلامات
- MviReducer - يحول الأزواج (الحالة ، النتيجة) إلى ولايات جديدة
- MviBootstrapper هو مكون خاص يسمح لك بتهيئة النموذج. يعطي نفس الإجراءات التي تذهب أيضا إلى المنفذ. يمكنك تنفيذ إجراء لمرة واحدة ، أو يمكنك الاشتراك في مصدر بيانات وتنفيذ إجراءات على أحداث معينة. يبدأ Bootstrapper تلقائيًا عند إنشاء نموذج.
لإنشاء النموذج نفسه ، يجب عليك استخدام مصنع خاص للنماذج. ويمثلها واجهة MviStoreFactory وتنفيذها لـ MviDefaultStoreFactory. يقبل المصنع مكونات النموذج ويصدر نموذجًا جاهزًا للاستخدام.
سيبدو مصنع نموذجنا كما يلي:
class UuidStoreFactory(private val factory: MviStoreFactory) { fun create(factory: MviStoreFactory): UuidStore = object : UuidStore, MviStore<State, Intent, Nothing> by factory.create( initialState = State(), bootstrapper = Bootstrapper, intentToAction = { when (it) { Intent.Generate -> Action.Generate } }, executor = Executor(), reducer = Reducer ) { } private sealed class Action { object Generate : Action() } private sealed class Result { class Uuid(val uuid: String) : Result() } private object Bootstrapper : MviBootstrapper<Action> { override fun bootstrap(dispatch: (Action) -> Unit): Disposable? { dispatch(Action.Generate) return null } } private class Executor : MviExecutor<State, Action, Result, Nothing>() { override fun invoke(action: Action): Disposable? { dispatch(Result.Uuid(UUID.randomUUID().toString())) return null } } private object Reducer : MviReducer<State, Result> { override fun State.reduce(result: Result): State = when (result) { is Result.Uuid -> copy(uuid = result.uuid) } } }
يقدم هذا المثال جميع المكونات الأربعة للنموذج. أولاً ، يقوم المصنع بإنشاء طريقة ، ثم الإجراءات والنتائج ، يليه المقاول وفي النهاية المخفض.
مكون
يتم وصف حالات المكون (مجموعة الدولة) بواسطة فئة البيانات:
data class States(val uuidStates: Observable<UuidStore.State>)
عند إضافة نماذج جديدة إلى أحد المكونات ، يجب أيضًا إضافة حالتها إلى المجموعة.
وفي الواقع ، التنفيذ نفسه:
class Component(uuidStore: UuidStore) : MviAbstractComponent<UiEvent, States>( stores = listOf( MviStoreBundle( store = uuidStore, uiEventTransformer = UuidStoreUiEventTransformer ) ) ) { override val states: States = States(uuidStore.states) private object UuidStoreUiEventTransformer : (UiEvent) -> UuidStore.Intent? { override fun invoke(event: UiEvent): UuidStore.Intent? = when (event) { UiEvent.OnGenerateClick -> UuidStore.Intent.Generate } } }
لقد ورثنا فئة MviAb abstractComponent المجردة ، وحددنا أنواع الولايات وعرض الأحداث ، مررنا نموذجنا إلى الفئة الفائقة ، وطبّقنا حقل الولايات. بالإضافة إلى ذلك ، أنشأنا وظيفة تحويل ستحول عرض الأحداث إلى نوايا نموذجنا.
نماذج عرض الخرائط
لدينا نماذج الشروط والعرض التقديمي ، لقد حان الوقت لتحويل واحد إلى آخر. للقيام بذلك ، نقوم بتطبيق واجهة MviViewModelMapper:
object ViewModelMapper : MviViewModelMapper<States, ViewModel> { override fun map(states: States): Observable<ViewModel> = states.uuidStates.map { ViewModel(text = it.uuid ?: "None") } }
ملزم
وجود المكون والعرض التقديمي وحده لا يكفي. لكي يبدأ كل شيء في العمل ، يجب أن يكون متصلاً. حان الوقت لإنشاء نشاط:
class UuidActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_uuid) bind( Component(UuidStoreFactory(MviDefaultStoreFactory).create()), View(this) using ViewModelMapper ) } }
استخدمنا طريقة bind () ، التي تأخذ مكون ومجموعة من طرق العرض مع مصممي النماذج الخاصة بهم. هذه الطريقة هي طريقة امتداد عبر LifecycleOwner (وهي نشاط وجزء) وتستخدم DefaultLifecycleObserver من حزمة القوس ، والتي تتطلب توافق مصدر Java 8. إذا لم تتمكن من استخدام Java 8 لسبب ما ، فإن طريقة bind () الثانية مناسبة لك ، وهي ليست طريقة امتداد وترجع MviLifecyleObserver. في هذه الحالة ، سيكون عليك استدعاء أساليب دورة الحياة بنفسك.
المراجع
يمكن العثور على التعليمات البرمجية المصدر للمكتبة ، بالإضافة إلى التعليمات التفصيلية للاتصال والاستخدام على GitHub .