portaldacalheta.pt
  • Κύριος
  • Τροποσ Ζωησ
  • Ζωή Σχεδιαστών
  • Kpi Και Analytics
  • Σχεδιασμός Μάρκας
Κινητό

Καλύτερες εφαρμογές Android χρησιμοποιώντας MVVM με καθαρή αρχιτεκτονική



Εάν δεν επιλέξετε τη σωστή αρχιτεκτονική για τη δική σας Android έργο, θα δυσκολευτείτε να το διατηρήσετε καθώς η βάση κώδικα σας μεγαλώνει και η ομάδα σας επεκτείνεται.

η στρατηγική είναι ο σχεδιασμός ενός επιχειρηματικού μοντέλου

Αυτό δεν είναι απλώς ένα σεμινάριο MVVM Android. Σε αυτό το άρθρο, θα συνδυάσουμε το MVVM (Model-View-ViewModel ή μερικές φορές στυλιζαρισμένο 'το πρότυπο ViewModel') με Καθαρή αρχιτεκτονική . Θα δούμε πώς μπορεί να χρησιμοποιηθεί αυτή η αρχιτεκτονική για τη σύνταξη αποσυνδεδεμένου, δοκιμή και διατηρήσιμου κώδικα.



Γιατί MVVM με καθαρή αρχιτεκτονική;

Το MVVM διαχωρίζει την προβολή σας (δηλαδή Activity s και Fragment s) από τη λογική της επιχείρησής σας. Το MVVM είναι αρκετό για μικρά έργα, αλλά όταν ο κώδικας βάσης δεδομένων σας γίνει τεράστιος, το ViewModel σας αρχίζει να φουσκώνει. Ο διαχωρισμός των ευθυνών γίνεται δύσκολος.



Το MVVM με καθαρή αρχιτεκτονική είναι αρκετά καλό σε τέτοιες περιπτώσεις. Προχωρεί ένα βήμα παραπέρα στον διαχωρισμό των ευθυνών της βάσης κώδικα. Αφαιρεί με σαφήνεια τη λογική των ενεργειών που μπορούν να εκτελεστούν στην εφαρμογή σας.



Σημείωση: Μπορείτε επίσης να συνδυάσετε την καθαρή αρχιτεκτονική με την αρχιτεκτονική μοντέλο-view-presenter (MVP). Αλλά από τότε Στοιχεία αρχιτεκτονικής Android ήδη παρέχει ένα ενσωματωμένο ViewModel τάξη, πηγαίνουμε με MVVM μέσω MVP - δεν απαιτείται πλαίσιο MVVM!

Πλεονεκτήματα της χρήσης καθαρής αρχιτεκτονικής

  • Ο κωδικός σας μπορεί να ελεγχθεί ακόμα πιο εύκολα από ό, τι με τον απλό MVVM.
  • Ο κωδικός σας αποσυνδέεται περαιτέρω (το μεγαλύτερο πλεονέκτημα.)
  • Η δομή του πακέτου είναι ακόμα πιο εύκολη στην πλοήγηση.
  • Το έργο είναι ακόμη πιο εύκολο στη συντήρηση.
  • Η ομάδα σας μπορεί να προσθέσει νέες δυνατότητες ακόμα πιο γρήγορα.

Μειονεκτήματα της καθαρής αρχιτεκτονικής

  • Έχει μια ελαφρώς απότομη καμπύλη μάθησης. Ο τρόπος με τον οποίο λειτουργούν όλα τα επίπεδα μπορεί να χρειαστεί λίγος χρόνος για να καταλάβουμε, ειδικά αν προέρχεστε από μοτίβα όπως το απλό MVVM ή το MVP.
  • Προσθέτει πολλά επιπλέον μαθήματα, επομένως δεν είναι ιδανικό για έργα χαμηλής πολυπλοκότητας.

Η ροή δεδομένων μας θα έχει την εξής μορφή:



Η ροή δεδομένων του MVVM με καθαρή αρχιτεκτονική. Τα δεδομένα ρέουν από το View στο ViewModel στο Domain to Data Repository και, στη συνέχεια, σε μια πηγή δεδομένων (τοπική ή απομακρυσμένη.)

Η επιχειρηματική μας λογική αποσυνδέεται πλήρως από τη διεπαφή χρήστη. Κάνει τον κώδικα μας πολύ εύκολο στη συντήρηση και τον έλεγχο.



Το παράδειγμα που θα δούμε είναι αρκετά απλό. Επιτρέπει στους χρήστες να δημιουργούν νέες αναρτήσεις και να βλέπουν μια λίστα αναρτήσεων που έχουν δημιουργήσει. Δεν χρησιμοποιώ βιβλιοθήκη τρίτων (όπως το Dagger, το RxJava κ.λπ.) σε αυτό το παράδειγμα για λόγους απλότητας.

Τα επίπεδα του MVVM με καθαρή αρχιτεκτονική

Ο κωδικός χωρίζεται σε τρία ξεχωριστά επίπεδα:



  1. Επίπεδο παρουσίασης
  2. Επίπεδο τομέα
  3. Επίπεδο δεδομένων

Θα αναφερθούμε σε περισσότερες λεπτομέρειες σχετικά με κάθε επίπεδο παρακάτω. Προς το παρόν, η προκύπτουσα δομή πακέτου μοιάζει με αυτήν:

MVVM με δομή πακέτου Clean Architecture.



Ακόμα και στην αρχιτεκτονική εφαρμογών 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 με καθαρή αρχιτεκτονική: Ένας συμπαγής συνδυασμός

Σκοπός μας με αυτό το έργο ήταν να κατανοήσουμε το MVVM με την καθαρή αρχιτεκτονική, οπότε παραλείψαμε μερικά πράγματα που μπορείτε να προσπαθήσετε να το βελτιώσετε περαιτέρω:

  1. Χρησιμοποιήστε το LiveData ή το RxJava για να αφαιρέσετε τις επιστροφές κλήσεων και να το κάνετε λίγο πιο όμορφο.
  2. Χρησιμοποιήστε καταστάσεις για να αντιπροσωπεύσετε το περιβάλλον χρήστη σας. (Για αυτό, ρίξτε μια ματιά αυτή η καταπληκτική ομιλία του Τζέικ Γουάρτον .)
  3. Χρησιμοποιήστε το Dagger 2 για να κάνετε ένεση εξαρτήσεων.

Αυτή είναι μια από τις καλύτερες και πιο επεκτάσιμες αρχιτεκτονικές για εφαρμογές Android. Ελπίζω να σας άρεσε αυτό το άρθρο και ανυπομονώ να ακούσω πώς έχετε χρησιμοποιήσει αυτήν την προσέγγιση στις δικές σας εφαρμογές!

Σχετίζεται με: Xamarin Forms, MVVMCross και SkiaSharp: The Holy Trinity of Cross-Platform App Development

Κατανόηση των βασικών

Τι είναι η αρχιτεκτονική Android;

Η αρχιτεκτονική Android είναι ο τρόπος με τον οποίο διαμορφώνετε τον κώδικα έργου Android, έτσι ώστε ο κώδικάς σας να είναι επεκτάσιμος και να διατηρείται εύκολα. Οι προγραμματιστές αφιερώνουν περισσότερο χρόνο για τη συντήρηση ενός έργου από το αρχικό του, επομένως είναι λογικό να ακολουθήσουμε ένα σωστό αρχιτεκτονικό σχέδιο.

Ποια είναι η διαφορά μεταξύ MVC και MVVM;

Στο Android, το MVC αναφέρεται στο προεπιλεγμένο μοτίβο όπου μια δραστηριότητα ενεργεί ως ελεγκτής και τα αρχεία XML είναι προβολές. Το MVVM αντιμετωπίζει τόσο τις κατηγορίες δραστηριότητας όσο και τα αρχεία XML ως προβολές και τα μαθήματα ViewModel είναι εκεί όπου γράφετε την επιχειρηματική σας λογική. Διαχωρίζει εντελώς τη διεπαφή χρήστη μιας εφαρμογής από τη λογική της.

Ποια είναι η διαφορά μεταξύ MVP και MVVM;

Στο MVP, ο παρουσιαστής γνωρίζει για την προβολή και η προβολή γνωρίζει για τον παρουσιαστή. Αλληλεπιδρούν μεταξύ τους μέσω μιας διεπαφής. Στο MVVM, μόνο η προβολή γνωρίζει για το μοντέλο προβολής. Το μοντέλο προβολής δεν έχει ιδέα για την προβολή.

Ποια είναι τα βασικά στοιχεία της αρχιτεκτονικής Android;

Το ένα είναι ο διαχωρισμός των ανησυχιών, δηλαδή η λογική της επιχείρησής σας, η διεπαφή χρήστη και τα μοντέλα δεδομένων πρέπει να ζουν σε διαφορετικά μέρη. Ένα άλλο είναι η αποσύνδεση του κώδικα: Κάθε κομμάτι του κώδικα θα πρέπει να λειτουργεί ως μαύρο κουτί, ώστε η αλλαγή οτιδήποτε σε μια τάξη να μην έχει καμία επίδραση σε άλλο μέρος της βάσης κώδικα.

τι είναι το appexchange στο salesforce

Τι είναι η καθαρή αρχιτεκτονική;

Το 'Clean Architecture' του Robert C. Martin είναι ένα μοτίβο που σας επιτρέπει να διακόψετε την αλληλεπίδρασή σας με δεδομένα σε απλούστερες οντότητες που ονομάζονται 'περιπτώσεις χρήσης'. Είναι υπέροχο για τη σύνταξη αποσυνδεδεμένου κώδικα.

Τι είναι τα αποθετήρια Android;

Οι περισσότερες εφαρμογές αποθηκεύουν και ανακτούν δεδομένα, είτε από τοπικό χώρο αποθήκευσης είτε από απομακρυσμένο διακομιστή. Τα αποθετήρια Android είναι τάξεις που αποφασίζουν εάν τα δεδομένα πρέπει να προέρχονται από διακομιστή ή τοπικό χώρο αποθήκευσης, αποσυνδέοντας τη λογική αποθήκευσης από εξωτερικές τάξεις.

Το νέο κύμα της επιχειρηματικότητας

Τεχνολογία

Το νέο κύμα της επιχειρηματικότητας
Η απόλυτη λίστα με τα 50 από τα καλύτερα πρόσθετα σκίτσων

Η απόλυτη λίστα με τα 50 από τα καλύτερα πρόσθετα σκίτσων

Εργαλεία Και Σεμινάρια

Δημοφιλείς Αναρτήσεις
Μάθετε αυτές τις δημοφιλείς τάσεις με αυτά τα μαθήματα του Photoshop
Μάθετε αυτές τις δημοφιλείς τάσεις με αυτά τα μαθήματα του Photoshop
Ενεργοποιήστε το Angular 2: Αναβάθμιση από 1.5
Ενεργοποιήστε το Angular 2: Αναβάθμιση από 1.5
Απεριόριστη κλίμακα και δωρεάν φιλοξενία ιστοσελίδων με σελίδες GitHub και Cloudflare
Απεριόριστη κλίμακα και δωρεάν φιλοξενία ιστοσελίδων με σελίδες GitHub και Cloudflare
Διευθυντής λειτουργιών Marketplace
Διευθυντής λειτουργιών Marketplace
Εισαγωγή στο λειτουργικό σύστημα ρομπότ: Το απόλυτο πλαίσιο εφαρμογής ρομπότ
Εισαγωγή στο λειτουργικό σύστημα ρομπότ: Το απόλυτο πλαίσιο εφαρμογής ρομπότ
 
Μεγάλα πρόσφατα παραδείγματα επιτυχημένης ανταγωνιστικής στρατηγικής
Μεγάλα πρόσφατα παραδείγματα επιτυχημένης ανταγωνιστικής στρατηγικής
Lighter and Faster - Ένας οδηγός για το Svelte Framework
Lighter and Faster - Ένας οδηγός για το Svelte Framework
Ένα σεμινάριο για τις φιλοδοξίες του Google Glass Developers: Δημιουργία της πρώτης σας εφαρμογής Glass
Ένα σεμινάριο για τις φιλοδοξίες του Google Glass Developers: Δημιουργία της πρώτης σας εφαρμογής Glass
The Statistic Edge: Βελτιώστε τις μετρήσεις σας με τη μέθοδο αναλογιστικής αποτίμησης
The Statistic Edge: Βελτιώστε τις μετρήσεις σας με τη μέθοδο αναλογιστικής αποτίμησης
Είναι οι αγορές εκατομμυρίων δολαρίων καλύτερες από τις αγορές δισεκατομμυρίων δολαρίων;
Είναι οι αγορές εκατομμυρίων δολαρίων καλύτερες από τις αγορές δισεκατομμυρίων δολαρίων;
Δημοφιλείς Αναρτήσεις
  • εκεί vs μόνος εναντίον εμένα
  • αμοιβές επιτυχίας για άντληση κεφαλαίων
  • δημιουργία εγγράφου word από το xml
  • κατανόηση κώδικα c++
  • πόσο αξίζει η βιομηχανία ομορφιάς το 2018
Κατηγορίες
  • Τροποσ Ζωησ
  • Ζωή Σχεδιαστών
  • Kpi Και Analytics
  • Σχεδιασμός Μάρκας
  • © 2022 | Ολα Τα Δικαιώματα Διατηρούνται

    portaldacalheta.pt