ביטול קוד ספגטי עם Clean Architecture לאנדרואיד
פרויקט אנדרואיד ללא ארכיטקטורה ברורה נראה צפוי: Activity עם 800 שורות, ממשק Retrofit שנקרא ישירות מ-onClick, Room DAO שמחזיר LiveData<List<User>> ישירות ל-Fragment. זה עובד עד לדרישה הראשונה: "הוסף קאשינג", "כתוב טסטים", "חלץ מודול משותף ל-Wear OS". ואז אתה מגלה שהכל מחובר בחוזקה. אופייני לאפליקציות שגדלו ללא רפקטורינג. Clean Architecture Android פותר את זה באמצעות היפוך תלות: שכבות פנימיות לא יודעות על חיצוניות. Retrofit ו-Room ניתנים להחלפה ללא שינוי בלוגיקה העסקית. הניסיון שלנו מראה שארכיטקטורה מוגדרת כראוי מקצרת את זמן יישום הפיצ'רים ב-30% בהשוואה לקוד מונוליטי. מאמץ הבדיקות יורד ב-50% — רוב הבדיקות רצות על JVM ללא אמולטור.
איך Clean Architecture מפריד בין שכבות
Domain — הליבה של האפליקציה. Kotlin טהור ללא ייבוא אנדרואיד. כאן תמצאו מודלים של Entity, ממשקי Repository, מחלקות UseCase. מודול זה מתקמפל כספריית JVM וניתן לבדיקה ללא אמולטור. היעדר תלות באנדרואיד הוא יתרון מרכזי.
Data — מימושים של רפוזיטוריז. DTO של Retrofit, Entities של Room, מיפוי DTO ל-Domain. UserRepositoryImpl מממש את UserRepository מ-Domain ויודע על שני מקורות הנתונים:
class UserRepositoryImpl @Inject constructor(
private val api: UserApi,
private val dao: UserDao,
private val mapper: UserMapper
) : UserRepository {
override fun getUser(id: String): Flow<User> = flow {
dao.getUser(id)?.let { emit(mapper.fromEntity(it)) }
try {
val remote = api.getUser(id)
dao.upsert(mapper.toEntity(remote))
emit(mapper.fromDto(remote))
} catch (e: HttpException) {
if (dao.getUser(id) == null) throw e
}
}
}אסטרטגיה: קודם החזר מהקאש, ואז עדכן מהשרת במקביל. אם הרשת נכשלת אבל הקאש קיים, המשתמש לא רואה שגיאה.
Presentation — ViewModel, UI (Compose או XML). תלוי רק ב-Domain: קורא ל-UseCases, מקבל Flow, הופך למצב UI. הוא לא יודע אם הנתונים מגיעים מ-Room או Retrofit.
מקרה בוחן: לאחרונה העברנו אפליקציית משלוחי מזון מונוליטית ל-Clean Architecture. הקוד המקורי הכיל Activity עם 1500 שורות שערבב בקשות HTTP, פעולות מסד נתונים ולוגיקת UI. אחרי הפרדה לשכבות, השגנו כיסוי בדיקות של 80% תוך חודשיים, ויישום פיצ'ר חדש (הוספת התראות push לסטטוס הזמנה) ירד משבוע ליומיים.
מתי UseCase נחוץ ומתי זה מוגזם?
UseCase מוצדק כאשר:
- הוא מתזמן מספר רפוזיטוריז
- הוא מכיל חוקים עסקיים לא טריוויאליים
- הוא בשימוש חוזר במספר ViewModels
class UserRepositoryImpl @Inject constructor( private val api: UserApi, private val dao: UserDao, private val mapper: UserMapper ) : UserRepository { override fun getUser(id: String): Flow<User> = flow { dao.getUser(id)?.let { emit(mapper.fromEntity(it)) } try { val remote = api.getUser(id) dao.upsert(mapper.toEntity(remote)) emit(mapper.fromDto(remote)) } catch (e: HttpException) { if (dao.getUser(id) == null) throw e } } } שעושה רק GetUserUseCase הוא שכבה מיותרת. אם ViewModel עובד עם רפוזיטורי יחיד ללא לוגיקה נוספת — הזרק את הרפוזיטורי ישירות. זה מפחית boilerplate ומפשט הבנה.
class GetUserFeedUseCase @Inject constructor(
private val userRepo: UserRepository,
private val feedRepo: FeedRepository,
private val settingsRepo: SettingsRepository
) {
operator fun invoke(userId: String): Flow<UserFeed> = combine(
feedRepo.getFeed(userId),
settingsRepo.getContentFilters()
) { feed, filters ->
feed.filter { filters.allows(it) }
}
}זהו UseCase אמיתי: הוא משלב שלושה מקורות ומפעיל פילטור.
איך Multi-Module מאיץ בניות
לאפליקציה קטנה, שלוש חבילות במודול אחד מספיקות. לפרויקט גדול (5+ פיצ'רים, מספר צוותים) אנחנו עוברים ל-multi-module:
:core:domain
:core:data
:feature:profile:domain (опционально)
:feature:profile:presentation
:feature:feed:presentation
:appMulti-module משפר בניות אינקרמנטליות ב-40%: שינויים ב-return userRepository.getUser(id) לא גורמים לבנייה מחדש של class GetUserFeedUseCase @Inject constructor( private val userRepo: UserRepository, private val feedRepo: FeedRepository, private val settingsRepo: SettingsRepository ) { operator fun invoke(userId: String): Flow<UserFeed> = combine( feedRepo.getFeed(userId), settingsRepo.getContentFilters() ) { feed, filters -> feed.filter { filters.allows(it) } } } . ניהול :core:domain :core:data :feature:profile:domain (опционально) :feature:profile:presentation :feature:feed:presentation :app מול :feature:profile ב-Gradle בין מודולים הוא נושא כוונון נפרד.
Hilt + Clean Architecture
Hilt מייצר את גרף Dagger מהאנוטציות. :feature:feed על Application, api על Activity/Fragment, implementation על ViewModel. קישורים בין ממשקי Domain למימושי Data:
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
}שגיאות סקופינג שגויות נתפסות בזמן קומפילציה, לא בזמן ריצה. לפרטים נוספים, ראו את מדריך הארכיטקטורה של אנדרואיד.
בדיקות לפי שכבה
| שכבה | כלים | תלות באנדרואיד |
|---|---|---|
| Domain UseCase | JUnit 5 + MockK | לא |
| Data Repository | JUnit 5 + MockK + MockWebServer | לא (מינימלי עם Room) |
| ViewModel | Turbine + Coroutines Test | לא |
| UI | Espresso / Compose UI Test | כן (אמולטור/מכשיר) |
רוב הבדיקות רצות על JVM — מהיר וזול. Clean Architecture הופך כתיבת בדיקות לפי 3 מהר יותר מאשר במונולית.
בעיות יישום אופייניות
- מודלי Domain עם
@HiltAndroidAppאו@AndroidEntryPoint— דליפה משכבת הנתונים. השתמשו ב-DTO נפרדים וב-mapper. - UseCases שמחזיקים
@HiltViewModel.@Module @InstallIn(SingletonComponent::class) abstract class RepositoryModule { @Binds @Singleton abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository }הוא תלות אנדרואיד. השתמשו באבסטרקציה כמו@Entityב-Domain עם מימוש ב-Presentation. - Flow ב-Domain שמשתמש בטיפוסי אנדרואיד.
@SerialNameב-Domain הוא הפרה. השתמשו רק ב-Context.
מה כלול בהתקנה
| רכיב | תיאור |
|---|---|
| דיאגרמת ארכיטקטורה | גרף שכבות ותלויות |
| הגדרת Hilt | מודולים, סקופים, קישורים |
| מודול פיצ'ר לדוגמה | UseCase + Repository + ViewModel + בדיקות |
| צינור CI/CD | אינטגרציה עם GitHub Actions / GitLab CI |
| תיעוד | README עם סקירת מבנה |
| הדרכת צוות | 1–2 מפגשים על Clean Architecture |
למה לבחור בנו
- 5+ שנות ניסיון בפיתוח אנדרואיד
- 20+ פרויקטים של Clean Architecture שיושמו בהצלחה
- מהנדסים מוסמכים של Google
- אחריות איכות: לפחות 70% כיסוי בדיקות
לוחות זמנים ומחירים
התקנה חדשה (פרויקט מודול יחיד): 3–5 ימים. Multi-module מאפס: 1–2 שבועות. העברת מונולית קיימת: 3–8 שבועות תלוי בהיקף. אנו מספקים הערכה מדויקת לאחר ניתוח הקוד שלכם.
צרו קשר כדי לדון בפרויקט שלכם. נעריך את הארכיטקטורה הנוכחית שלכם ונציע תוכנית פעולה. בקשו אודיט ראשוני.
דוגמה לתצורת build.gradle.kts במודול מרובה
// build.gradle.kts (root)
plugins {
id("com.android.application") version "8.1.0" apply false
}







