Εάν δεν επιλέξετε τη σωστή αρχιτεκτονική για τη δική σας Android έργο, θα δυσκολευτείτε να το διατηρήσετε καθώς η βάση κώδικα σας μεγαλώνει και η ομάδα σας επεκτείνεται.
η στρατηγική είναι ο σχεδιασμός ενός επιχειρηματικού μοντέλου
Αυτό δεν είναι απλώς ένα σεμινάριο MVVM Android. Σε αυτό το άρθρο, θα συνδυάσουμε το MVVM (Model-View-ViewModel ή μερικές φορές στυλιζαρισμένο 'το πρότυπο ViewModel') με Καθαρή αρχιτεκτονική . Θα δούμε πώς μπορεί να χρησιμοποιηθεί αυτή η αρχιτεκτονική για τη σύνταξη αποσυνδεδεμένου, δοκιμή και διατηρήσιμου κώδικα.
Το MVVM διαχωρίζει την προβολή σας (δηλαδή Activity
s και Fragment
s) από τη λογική της επιχείρησής σας. Το MVVM είναι αρκετό για μικρά έργα, αλλά όταν ο κώδικας βάσης δεδομένων σας γίνει τεράστιος, το ViewModel
σας αρχίζει να φουσκώνει. Ο διαχωρισμός των ευθυνών γίνεται δύσκολος.
Το MVVM με καθαρή αρχιτεκτονική είναι αρκετά καλό σε τέτοιες περιπτώσεις. Προχωρεί ένα βήμα παραπέρα στον διαχωρισμό των ευθυνών της βάσης κώδικα. Αφαιρεί με σαφήνεια τη λογική των ενεργειών που μπορούν να εκτελεστούν στην εφαρμογή σας.
Σημείωση: Μπορείτε επίσης να συνδυάσετε την καθαρή αρχιτεκτονική με την αρχιτεκτονική μοντέλο-view-presenter (MVP). Αλλά από τότε Στοιχεία αρχιτεκτονικής Android ήδη παρέχει ένα ενσωματωμένο ViewModel
τάξη, πηγαίνουμε με MVVM μέσω MVP - δεν απαιτείται πλαίσιο MVVM!
Η ροή δεδομένων μας θα έχει την εξής μορφή:
Η επιχειρηματική μας λογική αποσυνδέεται πλήρως από τη διεπαφή χρήστη. Κάνει τον κώδικα μας πολύ εύκολο στη συντήρηση και τον έλεγχο.
Το παράδειγμα που θα δούμε είναι αρκετά απλό. Επιτρέπει στους χρήστες να δημιουργούν νέες αναρτήσεις και να βλέπουν μια λίστα αναρτήσεων που έχουν δημιουργήσει. Δεν χρησιμοποιώ βιβλιοθήκη τρίτων (όπως το Dagger, το RxJava κ.λπ.) σε αυτό το παράδειγμα για λόγους απλότητας.
Ο κωδικός χωρίζεται σε τρία ξεχωριστά επίπεδα:
Θα αναφερθούμε σε περισσότερες λεπτομέρειες σχετικά με κάθε επίπεδο παρακάτω. Προς το παρόν, η προκύπτουσα δομή πακέτου μοιάζει με αυτήν:
Ακόμα και στην αρχιτεκτονική εφαρμογών Android που χρησιμοποιούμε, υπάρχουν πολλοί τρόποι για τη δομή της ιεραρχίας αρχείων / φακέλων. Μου αρέσει να ομαδοποιώ αρχεία έργων βάσει χαρακτηριστικών. Το βρίσκω τακτοποιημένο και συνοπτικό. Είστε ελεύθεροι να επιλέξετε οποιαδήποτε δομή έργου σας ταιριάζει.
Αυτό περιλαμβάνει τα Activity
s, Fragment
s και ViewModel
s. Ένα Activity
πρέπει να είναι όσο πιο χαζός γίνεται. Ποτέ μην τοποθετείτε τη λογική της επιχείρησής σας σε Activity
s.
Ένα Activity
θα μιλήσει σε ένα ViewModel
και a ViewModel
θα μιλήσει στο επίπεδο τομέα για να εκτελέσει ενέργειες. Α ViewModel
ποτέ δεν μιλά απευθείας στο επίπεδο δεδομένων.
Εδώ περνάμε ένα UseCaseHandler
και δύο UseCase
s στο ViewModel
. Θα το συζητήσουμε με περισσότερες λεπτομέρειες σύντομα, αλλά σε αυτήν την αρχιτεκτονική, ένα UseCase
είναι μια ενέργεια που καθορίζει πώς ένα ViewModel
αλληλεπιδρά με το επίπεδο δεδομένων.
Εδώ είναι πώς μας Κωδικός Kotlin φαίνεται:
class PostListViewModel( val useCaseHandler: UseCaseHandler, val getPosts: GetPosts, val savePost: SavePost): ViewModel() { fun getAllPosts(userId: Int, callback: PostDataSource.LoadPostsCallback) { val requestValue = GetPosts.RequestValues(userId) useCaseHandler.execute(getPosts, requestValue, object : UseCase.UseCaseCallback { override fun onSuccess(response: GetPosts.ResponseValue) { callback.onPostsLoaded(response.posts) } override fun onError(t: Throwable) { callback.onError(t) } }) } fun savePost(post: Post, callback: PostDataSource.SaveTaskCallback) { val requestValues = SavePost.RequestValues(post) useCaseHandler.execute(savePost, requestValues, object : UseCase.UseCaseCallback { override fun onSuccess(response: SavePost.ResponseValue) { callback.onSaveSuccess() } override fun onError(t: Throwable) { callback.onError(t) } }) } }
Το επίπεδο τομέα περιέχει όλα τα περιπτώσεις χρήσης της αίτησής σας. Σε αυτό το παράδειγμα, έχουμε UseCase
, μια αφηρημένη τάξη. Όλα τα UseCase
s θα επεκτείνουν αυτήν την τάξη.
abstract class UseCase { var requestValues: Q? = null var useCaseCallback: UseCaseCallback? = null internal fun run() { executeUseCase(requestValues) } protected abstract fun executeUseCase(requestValues: Q?) /** * Data passed to a request. */ interface RequestValues /** * Data received from a request. */ interface ResponseValue interface UseCaseCallback { fun onSuccess(response: R) fun onError(t: Throwable) } }
Και UseCaseHandler
χειρίζεται την εκτέλεση ενός UseCase
. Δεν πρέπει ποτέ να αποκλείουμε το περιβάλλον εργασίας χρήστη κατά τη λήψη δεδομένων από τη βάση δεδομένων ή τον απομακρυσμένο διακομιστή μας. Αυτό είναι το μέρος όπου αποφασίζουμε να εκτελέσουμε το UseCase
σε ένα νήμα φόντου και λάβετε την απάντηση στο κύριο νήμα.
class UseCaseHandler(private val mUseCaseScheduler: UseCaseScheduler) { fun execute( useCase: UseCase, values: T, callback: UseCase.UseCaseCallback) { useCase.requestValues = values useCase.useCaseCallback = UiCallbackWrapper(callback, this) mUseCaseScheduler.execute(Runnable { useCase.run() }) } private fun notifyResponse(response: V, useCaseCallback: UseCase.UseCaseCallback) { mUseCaseScheduler.notifyResponse(response, useCaseCallback) } private fun notifyError( useCaseCallback: UseCase.UseCaseCallback, t: Throwable) { mUseCaseScheduler.onError(useCaseCallback, t) } private class UiCallbackWrapper( private val mCallback: UseCase.UseCaseCallback, private val mUseCaseHandler: UseCaseHandler) : UseCase.UseCaseCallback { override fun onSuccess(response: V) { mUseCaseHandler.notifyResponse(response, mCallback) } override fun onError(t: Throwable) { mUseCaseHandler.notifyError(mCallback, t) } } companion object { private var INSTANCE: UseCaseHandler? = null fun getInstance(): UseCaseHandler { if (INSTANCE == null) { INSTANCE = UseCaseHandler(UseCaseThreadPoolScheduler()) } return INSTANCE!! } } }
Όπως υποδηλώνει το όνομά του, το GetPosts
UseCase
είναι υπεύθυνη για τη λήψη όλων των δημοσιεύσεων ενός χρήστη.
class GetPosts(private val mDataSource: PostDataSource) : UseCase() { protected override fun executeUseCase(requestValues: GetPosts.RequestValues?) { mDataSource.getPosts(requestValues?.userId ?: -1, object : PostDataSource.LoadPostsCallback { override fun onPostsLoaded(posts: List) { val responseValue = ResponseValue(posts) useCaseCallback?.onSuccess(responseValue) } override fun onError(t: Throwable) { // Never use generic exceptions. Create proper exceptions. Since // our use case is different we will go with generic throwable useCaseCallback?.onError(Throwable('Data not found')) } }) } class RequestValues(val userId: Int) : UseCase.RequestValues class ResponseValue(val posts: List) : UseCase.ResponseValue }
Ο σκοπός του UseCase
s είναι να είστε διαμεσολαβητής μεταξύ των ViewModel
s και Repository
s.
Ας υποθέσουμε ότι στο μέλλον θα αποφασίσετε να προσθέσετε μια λειτουργία 'επεξεργασία ανάρτησης'. Το μόνο που έχετε να κάνετε είναι να προσθέσετε ένα νέο EditPost
UseCase
και όλος ο κώδικάς του θα είναι εντελώς ξεχωριστός και αποσυνδεδεμένος από άλλους UseCase
s. Όλοι το έχουμε δει πολλές φορές: Παρουσιάζονται νέες λειτουργίες και κατά λάθος σπάζουν κάτι στον προϋπάρχοντα κώδικα. Δημιουργία ξεχωριστού UseCase
βοηθά πάρα πολύ στην αποφυγή αυτού.
Φυσικά, δεν μπορείτε να εξαλείψετε αυτήν τη δυνατότητα 100 τοις εκατό, αλλά σίγουρα μπορείτε να την ελαχιστοποιήσετε. Αυτό διαχωρίζει την καθαρή αρχιτεκτονική από άλλα μοτίβα: Ο κώδικας είναι τόσο αποσυνδεδεμένος που μπορείτε να αντιμετωπίζετε κάθε στρώμα ως μαύρο κουτί.
Αυτό έχει όλα τα αποθετήρια που μπορεί να χρησιμοποιήσει το επίπεδο τομέα. Αυτό το επίπεδο εκθέτει ένα API προέλευσης δεδομένων σε εξωτερικές κλάσεις:
interface PostDataSource { interface LoadPostsCallback { fun onPostsLoaded(posts: List) fun onError(t: Throwable) } interface SaveTaskCallback { fun onSaveSuccess() fun onError(t: Throwable) } fun getPosts(userId: Int, callback: LoadPostsCallback) fun savePost(post: Post) }
PostDataRepository
υλοποιεί PostDataSource
. Αποφασίζει εάν θα ανακτήσουμε δεδομένα από μια τοπική βάση δεδομένων ή από έναν απομακρυσμένο διακομιστή.
class PostDataRepository private constructor( private val localDataSource: PostDataSource, private val remoteDataSource: PostDataSource): PostDataSource { companion object { private var INSTANCE: PostDataRepository? = null fun getInstance(localDataSource: PostDataSource, remoteDataSource: PostDataSource): PostDataRepository { if (INSTANCE == null) { INSTANCE = PostDataRepository(localDataSource, remoteDataSource) } return INSTANCE!! } } var isCacheDirty = false override fun getPosts(userId: Int, callback: PostDataSource.LoadPostsCallback) { if (isCacheDirty) { getPostsFromServer(userId, callback) } else { localDataSource.getPosts(userId, object : PostDataSource.LoadPostsCallback { override fun onPostsLoaded(posts: List) { refreshCache() callback.onPostsLoaded(posts) } override fun onError(t: Throwable) { getPostsFromServer(userId, callback) } }) } } override fun savePost(post: Post) { localDataSource.savePost(post) remoteDataSource.savePost(post) } private fun getPostsFromServer(userId: Int, callback: PostDataSource.LoadPostsCallback) { remoteDataSource.getPosts(userId, object : PostDataSource.LoadPostsCallback { override fun onPostsLoaded(posts: List) { refreshCache() refreshLocalDataSource(posts) callback.onPostsLoaded(posts) } override fun onError(t: Throwable) { callback.onError(t) } }) } private fun refreshLocalDataSource(posts: List) { posts.forEach { localDataSource.savePost(it) } } private fun refreshCache() { isCacheDirty = false } }
Ο κωδικός είναι κυρίως αυτονόητος. Αυτή η τάξη έχει δύο μεταβλητές, localDataSource
και remoteDataSource
. Ο τύπος τους είναι PostDataSource
, οπότε δεν μας ενδιαφέρει πώς εφαρμόζονται πραγματικά κάτω από την κουκούλα.
Στην προσωπική μου εμπειρία, αυτή η αρχιτεκτονική αποδείχθηκε πολύτιμη. Σε μια από τις εφαρμογές μου, ξεκίνησα με το Firebase στο πίσω μέρος, κάτι που είναι ιδανικό για τη γρήγορη δημιουργία της εφαρμογής σας. Ήξερα ότι τελικά θα έπρεπε να μεταφερθώ στον δικό μου διακομιστή.
Όταν το έκανα, το μόνο που έπρεπε να κάνω ήταν να αλλάξω την εφαρμογή στο RemoteDataSource
. Δεν χρειάστηκε να αγγίξω άλλη τάξη ακόμα και μετά από μια τόσο μεγάλη αλλαγή. Αυτό είναι το πλεονέκτημα του αποσυνδεδεμένου κώδικα. Η αλλαγή οποιασδήποτε κατηγορίας δεν πρέπει να επηρεάζει άλλα μέρη του κώδικα σας.
Μερικές από τις επιπλέον κατηγορίες που έχουμε είναι:
interface UseCaseScheduler { fun execute(runnable: Runnable) fun notifyResponse(response: V, useCaseCallback: UseCase.UseCaseCallback) fun onError( useCaseCallback: UseCase.UseCaseCallback, t: Throwable) } class UseCaseThreadPoolScheduler : UseCaseScheduler { val POOL_SIZE = 2 val MAX_POOL_SIZE = 4 val TIMEOUT = 30 private val mHandler = Handler() internal var mThreadPoolExecutor: ThreadPoolExecutor init { mThreadPoolExecutor = ThreadPoolExecutor(POOL_SIZE, MAX_POOL_SIZE, TIMEOUT.toLong(), TimeUnit.SECONDS, ArrayBlockingQueue(POOL_SIZE)) } override fun execute(runnable: Runnable) { mThreadPoolExecutor.execute(runnable) } override fun notifyResponse(response: V, useCaseCallback: UseCase.UseCaseCallback) { mHandler.post { useCaseCallback.onSuccess(response) } } override fun onError( useCaseCallback: UseCase.UseCaseCallback, t: Throwable) { mHandler.post { useCaseCallback.onError(t) } } }
UseCaseThreadPoolScheduler
είναι υπεύθυνη για την εκτέλεση εργασιών ασύγχρονα χρησιμοποιώντας ThreadPoolExecuter
.
class ViewModelFactory : ViewModelProvider.Factory { override fun create(modelClass: Class): T { if (modelClass == PostListViewModel::class.java) { return PostListViewModel( Injection.provideUseCaseHandler() , Injection.provideGetPosts(), Injection.provideSavePost()) as T } throw IllegalArgumentException('unknown model class $modelClass') } companion object { private var INSTANCE: ViewModelFactory? = null fun getInstance(): ViewModelFactory { if (INSTANCE == null) { INSTANCE = ViewModelFactory() } return INSTANCE!! } } }
Αυτό είναι το ViewModelFactory
. Πρέπει να το δημιουργήσετε για να μεταβιβάσετε ορίσματα στο ViewModel
κατασκευαστής.
Θα εξηγήσω μια ένεση εξάρτησης με ένα παράδειγμα. Αν κοιτάξετε το PostDataRepository
κλάση, έχει δύο εξαρτήσεις, LocalDataSource
και RemoteDataSource
. Χρησιμοποιούμε το Injection
τάξη για την παροχή αυτών των εξαρτήσεων στο PostDataRepository
τάξη.
Η εξάρτηση από την έγχυση έχει δύο κύρια πλεονεκτήματα. Το ένα είναι ότι μπορείτε να ελέγχετε την παρουσία αντικειμένων από ένα κεντρικό μέρος αντί να το διαδίδετε σε ολόκληρη τη βάση κώδικα. Ένα άλλο είναι ότι αυτό θα μας βοηθήσει να γράψουμε τεστ μονάδας για PostDataRepository
γιατί τώρα μπορούμε να περάσουμε απλώς πλαστές εκδόσεις του LocalDataSource
και RemoteDataSource
στο PostDataRepository
κατασκευαστής αντί για πραγματικές τιμές.
object Injection { fun providePostDataRepository(): PostDataRepository { return PostDataRepository.getInstance(provideLocalDataSource(), provideRemoteDataSource()) } fun provideViewModelFactory() = ViewModelFactory.getInstance() fun provideLocalDataSource(): PostDataSource = LocalDataSource.getInstance() fun provideRemoteDataSource(): PostDataSource = RemoteDataSource.getInstance() fun provideGetPosts() = GetPosts(providePostDataRepository()) fun provideSavePost() = SavePost(providePostDataRepository()) fun provideUseCaseHandler() = UseCaseHandler.getInstance() }
Σημείωση: Προτιμώ να χρησιμοποιώ το Dagger 2 για έγχυση εξάρτησης σε σύνθετα έργα. Αλλά με την εξαιρετικά απότομη καμπύλη μάθησης, είναι πέρα από το πεδίο εφαρμογής αυτού του άρθρου. Επομένως, αν σας ενδιαφέρει να πάτε πιο βαθιά, σας συνιστώ ανεπιφύλακτα Εισαγωγή του Hari Vignesh Jayapalan στο Dagger 2 .
Σκοπός μας με αυτό το έργο ήταν να κατανοήσουμε το MVVM με την καθαρή αρχιτεκτονική, οπότε παραλείψαμε μερικά πράγματα που μπορείτε να προσπαθήσετε να το βελτιώσετε περαιτέρω:
Αυτή είναι μια από τις καλύτερες και πιο επεκτάσιμες αρχιτεκτονικές για εφαρμογές Android. Ελπίζω να σας άρεσε αυτό το άρθρο και ανυπομονώ να ακούσω πώς έχετε χρησιμοποιήσει αυτήν την προσέγγιση στις δικές σας εφαρμογές!
Σχετίζεται με: Xamarin Forms, MVVMCross και SkiaSharp: The Holy Trinity of Cross-Platform App DevelopmentΗ αρχιτεκτονική Android είναι ο τρόπος με τον οποίο διαμορφώνετε τον κώδικα έργου Android, έτσι ώστε ο κώδικάς σας να είναι επεκτάσιμος και να διατηρείται εύκολα. Οι προγραμματιστές αφιερώνουν περισσότερο χρόνο για τη συντήρηση ενός έργου από το αρχικό του, επομένως είναι λογικό να ακολουθήσουμε ένα σωστό αρχιτεκτονικό σχέδιο.
Στο Android, το MVC αναφέρεται στο προεπιλεγμένο μοτίβο όπου μια δραστηριότητα ενεργεί ως ελεγκτής και τα αρχεία XML είναι προβολές. Το MVVM αντιμετωπίζει τόσο τις κατηγορίες δραστηριότητας όσο και τα αρχεία XML ως προβολές και τα μαθήματα ViewModel είναι εκεί όπου γράφετε την επιχειρηματική σας λογική. Διαχωρίζει εντελώς τη διεπαφή χρήστη μιας εφαρμογής από τη λογική της.
Στο MVP, ο παρουσιαστής γνωρίζει για την προβολή και η προβολή γνωρίζει για τον παρουσιαστή. Αλληλεπιδρούν μεταξύ τους μέσω μιας διεπαφής. Στο MVVM, μόνο η προβολή γνωρίζει για το μοντέλο προβολής. Το μοντέλο προβολής δεν έχει ιδέα για την προβολή.
Το ένα είναι ο διαχωρισμός των ανησυχιών, δηλαδή η λογική της επιχείρησής σας, η διεπαφή χρήστη και τα μοντέλα δεδομένων πρέπει να ζουν σε διαφορετικά μέρη. Ένα άλλο είναι η αποσύνδεση του κώδικα: Κάθε κομμάτι του κώδικα θα πρέπει να λειτουργεί ως μαύρο κουτί, ώστε η αλλαγή οτιδήποτε σε μια τάξη να μην έχει καμία επίδραση σε άλλο μέρος της βάσης κώδικα.
τι είναι το appexchange στο salesforce
Το 'Clean Architecture' του Robert C. Martin είναι ένα μοτίβο που σας επιτρέπει να διακόψετε την αλληλεπίδρασή σας με δεδομένα σε απλούστερες οντότητες που ονομάζονται 'περιπτώσεις χρήσης'. Είναι υπέροχο για τη σύνταξη αποσυνδεδεμένου κώδικα.
Οι περισσότερες εφαρμογές αποθηκεύουν και ανακτούν δεδομένα, είτε από τοπικό χώρο αποθήκευσης είτε από απομακρυσμένο διακομιστή. Τα αποθετήρια Android είναι τάξεις που αποφασίζουν εάν τα δεδομένα πρέπει να προέρχονται από διακομιστή ή τοπικό χώρο αποθήκευσης, αποσυνδέοντας τη λογική αποθήκευσης από εξωτερικές τάξεις.