مرحبا بالجميع! أود اليوم أن أتحدث عن بنية تطبيقات أندرويد.
في الحقيقة ، أنا لا أحب حقًا التقارير والمقالات حول هذا الموضوع ، لكنني توصلت مؤخرًا إلى إدراك أنني أود مشاركتها.
عندما بدأت في التعرف على البنى لأول مرة ، سقطت عيني على MVP. أعجبتني البساطة وتوافر كمية هائلة من المواد التدريبية.
لكن مع مرور الوقت ، بدأت ألاحظ وجود شيء خاطئ. كان هناك شعور أنه من الممكن أفضل.
تبدو جميع التطبيقات التي رأيتها تقريبًا كالتالي: لدينا فئة مجردة نرث منها جميع مقدمي العروض.
class MoviePresenter(private val repository: Repository) : BasePresenter<MovieView>() { fun loadMovies() { coroutineScope.launch { when (val result = repository.loadMovies()) { is Either.Left -> view?.showError() is Either.Right -> view?.showMovies(result.value) } } } }
نحن أيضًا نصنع واجهة عرض لكل شاشة ، والتي سيعمل بها المقدم
interface MovieView : MvpView { fun showMovies(movies: List<Movie>) fun showError() }
دعونا نلقي نظرة على عيوب هذا النهج:
- يجب عليك إنشاء واجهة عرض لكل شاشة. في المشروعات الكبيرة ، سيكون لدينا الكثير من التعليمات البرمجية والملفات الإضافية التي تجعل التنقل في الحزم أمرًا صعبًا.
- من الصعب إعادة استخدام مقدم العرض ، لأنه مرتبط بالمنظر ، ويمكن أن يكون له طرق محددة.
- شرط معين مفقود. تخيل أننا نقدم طلبًا للشبكة ، وفي هذه اللحظة يموت نشاطنا ويتم إنشاء نشاط جديد. جاءت البيانات عند عدم ربط طريقة العرض مع مقدم العرض. هذا يطرح سؤال حول كيفية إظهار هذه البيانات عندما يكون العرض مرتبطًا بالمقدم؟ الجواب: العكازات فقط. Moxy ، على سبيل المثال ، لديه ViewState يخزن قائمة ViewCommand. يعمل هذا الحل ، لكن يبدو لي أن سحب الكود لحفظ حالة العرض أمر غير ضروري (multidex أقرب بكثير مما تعتقد. بالإضافة إلى ذلك ، سيبدأ التجميع في معالجة التعليقات التوضيحية ، مما سيجعله أطول ، نعم ، ستقول أننا لدينا الآن kapt تدريجي ، ولكن بعض الشروط ضرورية لتشغيلها). Plus ViewCommand غير قابلة للتجزئة أو متسلسلة ، مما يعني أنه لا يمكننا حفظها في حالة وفاة العملية. من المهم أن يكون لديك حالة مستمرة حتى لا تفقد أي شيء. أيضًا ، لا يسمح غياب حالة معينة بتغييره مركزيًا ، وقد يؤدي ذلك إلى صعوبة إعادة إنتاج الأخطاء.
دعونا نرى ما إذا كانت هذه المشاكل قد حلت في أبنية أخرى.
MVVM
class MovieViewModel(private val repository: Repository) { val moviesObservable: ObservableProperty<List<Movie>> = MutableObservableProperty() val errorObservable: ObservableProperty<Throwable> = MutableObservableProperty() fun loadMovies() { coroutineScope.launch { when (val result = repository.loadMovies()) { is Either.Left -> errorObservable.value = result.value is Either.Right -> moviesObservable.value = result.value } } } }
دعنا نذهب من خلال النقاط المذكورة أعلاه:
- في MVVM ، لم تعد VIew لها واجهة ، حيث تشترك ببساطة في الحقول التي يمكن ملاحظتها في ViewModel.
- يعد ViewModel أسهل في إعادة الاستخدام لأنه لا يعرف شيئًا عن View. (يلي من الفقرة الأولى)
- في MVVM ، يتم حل مشكلة الحالة ، ولكن ليس بالكامل. في هذا المثال ، لدينا خاصية في ViewModel ، حيث يأخذ View البيانات. عندما نقدم طلبًا إلى الشبكة ، سيتم حفظ البيانات في العقار ، وسيتلقى العرض بيانات صالحة عند الاشتراك (ولن يلزمك الرقص مع الدف). يمكننا أيضًا أن نجعل الخاصية مستمرة ، مما سيسمح لهم بالحفاظ عليها في حالة وفاة العملية.
MVI
تحديد الإجراءات ، الآثار الجانبية والدولة
sealed class Action { class LoadAction(val page: Int) : Action() class ShowResult(val result: List<Movie>) : Action() class ShowError(val error: Throwable) : Action() } sealed class SideEffect { class LoadMovies(val page: Int) : SideEffect() } data class State( val loading: Boolean = false, val data: List<Movie>? = null, val error: Throwable? = null )
التالي يأتي المخفض
val reducer = { state: State, action: Action -> when (action) { is Action.LoadAction -> state.copy(loading = true, data = null, error = null) to setOf( SideEffect.LoadMovies(action.page) ) is Action.ShowResult -> state.copy( loading = false, data = action.result, error = null ) to emptySet() is Action.ShowError -> state.copy( loading = false, data = null, error = action.error ) to emptySet() } }
و EffectHandler للتعامل مع الآثار الجانبية
class MovieEffectHandler(private val movieRepository: MovieRepository) : EffectHandler<SideEffect, Action> { override fun handle(sideEffect: SideEffect) = when (sideEffect) { is SideEffect.LoadMovies -> flow { when (val result = movieRepository.loadMovies(sideEffect.page)) { is Either.Left -> emit(Action.ShowError(result.value)) is Either.Right -> emit(Action.ShowResult(result.value)) } } } }
ماذا لدينا:
- في MVI ، لا نحتاج أيضًا إلى إنشاء مجموعة من العقود لـ View. تحتاج فقط إلى تحديد وظيفة التجسيد (الحالة).
- لإعادة استخدام هذا ، لسوء الحظ ، ليست بهذه البساطة ، لأن لدينا دولة ، والتي يمكن أن تكون محددة للغاية.
- في MVI ، لدينا حالة معينة يمكننا تغييرها مركزيًا من خلال وظيفة الاختزال. بفضل هذا ، يمكننا تتبع التغييرات الدولة. على سبيل المثال ، اكتب كل التغييرات في السجل. ثم يمكننا قراءة الحالة الأخيرة إذا تعطل التطبيق. يمكن أن يكون برنامج Plus State مستمرًا ، مما يسمح لك بالتعامل مع موت العملية.
يؤدي
MVVM يحل مشكلة الموت العملية. لكن لسوء الحظ ، لا تزال الحالة هنا غير مؤكدة ولا يمكن أن تتغير بشكل مركزي. هذا ، بالطبع ، ناقص ، لكن الوضع لا يزال أفضل بشكل واضح من MVP. تعمل MVI على حل مشكلة الحالة ، لكن النهج نفسه يمكن أن يكون معقدًا بعض الشيء. بالإضافة إلى ذلك ، هناك مشكلة في واجهة المستخدم ، حيث أن مجموعة أدوات واجهة المستخدم الحالية في android سيئة. في MVVM ، نقوم بتحديث واجهة المستخدم على شكل قطع ، وفي MVI نسعى جاهدين لتحديثها ككل. لذلك ، لواجهة مستخدم ملحة ، سوف MVVM تتصرف بشكل أفضل. إذا كنت ترغب في استخدام MVI ، فإنني أنصحك بأن تتعرف على نظرية DOM الظاهرية / الإضافية والمكتبات لنظام android: litho و anvil و jetpack (يجب عليك الانتظار). أو يمكنك أن تأخذ فرق بين يديك.
استنادًا إلى جميع البيانات الواردة أعلاه ، أنصحك بالاختيار بين MVVM و MVI عند تصميم التطبيق. لذا يمكنك الحصول على نهج أكثر حداثة ومريحة (لا سيما في واقع أندرويد).
المكتبات التي يمكن أن تساعد في تطبيق هذه الأساليب:
MVVM - https://github.com/Miha-x64/Lychee
MVI - https://github.com/egroden/mvico ، https://github.com/badoo/MVICore ، https://github.com/arkivanov/MVIDroid
شكرا لكم جميعا على اهتمامكم!