-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture Overview
Hakim edited this page Sep 18, 2025
·
3 revisions
Dokumentasi lengkap arsitektur Local YouTube Downloader Android, design patterns, dan struktur kode.
Aplikasi menggunakan MVVM (Model-View-ViewModel) dengan Clean Architecture principles untuk memastikan:
- Separation of Concerns
- Testability
- Maintainability
- Scalability
┌─────────────────────────────────────────┐
│ UI Layer │
│ (Jetpack Compose + Material Design 3) │
├─────────────────────────────────────────┤
│ ViewModel Layer │
│ (State Management + Business Logic) │
├─────────────────────────────────────────┤
│ Repository Layer │
│ (Data Access + Coordination) │
├─────────────────────────────────────────┤
│ Service Layer │
│ (External APIs + Download Engine) │
├─────────────────────────────────────────┤
│ Data Layer │
│ (Models + Utils + File System) │
└─────────────────────────────────────────┘
app/src/main/java/com/irnhakim/ytmp3/
├── 📱 ui/
│ ├── screen/
│ │ └── DownloadScreen.kt # Main UI screen
│ └── theme/
│ ├── Color.kt # Color definitions
│ ├── Theme.kt # App theme
│ └── Type.kt # Typography
├── 🧠 viewmodel/
│ ├── DownloadViewModel.kt # Main view model
│ └── CrashSafeDownloadViewModel.kt # Crash-safe wrapper
├── 🗄️ repository/
│ ├── DownloadRepository.kt # Main repository
│ └── CrashSafeDownloadRepository.kt # Crash-safe wrapper
├── 🔧 service/
│ ├── YouTubeExtractorService.kt # yt-dlp integration
│ └── CrashSafeYouTubeExtractorService.kt # Crash-safe wrapper
├── 📊 data/
│ ├── VideoInfo.kt # Video information model
│ ├── DownloadState.kt # Download state sealed class
│ ├── DownloadRequest.kt # Download request model
│ └── DownloadFormat.kt # Format enumeration
├── 🛠️ utils/
│ └── FileUtils.kt # File operations utilities
└── 📱 MainActivity.kt # Entry point
@Composable
fun DownloadScreen(
modifier: Modifier = Modifier,
viewModel: DownloadViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsState()
val downloadState by viewModel.downloadState.collectAsState()
// UI composition based on state
}- DownloadScreen: Main UI composable
- Material Design 3: Modern design system
- State-driven UI: Reactive to ViewModel state changes
- Compose Navigation: (Future enhancement)
data class DownloadUiState(
val url: String = "",
val selectedFormat: DownloadFormat = DownloadFormat.MP4,
val isLoading: Boolean = false,
val videoInfo: VideoInfo? = null,
val showVideoInfo: Boolean = false,
val errorMessage: String? = null
)class DownloadViewModel(application: Application) : AndroidViewModel(application) {
private val repository = DownloadRepository(application)
private val _uiState = MutableStateFlow(DownloadUiState())
val uiState: StateFlow<DownloadUiState> = _uiState.asStateFlow()
private val _downloadState = MutableStateFlow<DownloadState>(DownloadState.Idle)
val downloadState: StateFlow<DownloadState> = _downloadState.asStateFlow()
}- State Management: UI state dan download state
- Business Logic: Koordinasi antara UI dan Repository
- Error Handling: Transform errors untuk UI
- Lifecycle Management: Handle Android lifecycle
UI State Flow:
┌─────────┐ ┌──────────────┐ ┌─────────────┐
│ UI │◄───│ ViewModel │◄───│ Repository │
│(Compose)│ │ (StateFlow) │ │ (Flow) │
└─────────┘ └──────────────┘ └─────────────┘
class DownloadRepository(private val context: Context) {
private val youtubeExtractor = YouTubeExtractorService(context)
suspend fun getVideoInfo(url: String): Result<VideoInfo>
fun downloadVideo(request: DownloadRequest): Flow<DownloadState>
}- Data Coordination: Koordinasi antara services
- Business Rules: Implementasi business logic
- Error Transformation: Convert service errors
- Flow Management: Manage download progress flows
sealed class DownloadState {
object Idle : DownloadState()
object Loading : DownloadState()
data class Progress(val percentage: Int, val status: String) : DownloadState()
data class Success(val filePath: String, val fileName: String) : DownloadState()
data class Error(val message: String) : DownloadState()
}class YouTubeExtractorService(private val context: Context) {
suspend fun initializeYoutubeDL(): Result<Unit>
suspend fun extractVideoInfo(url: String): Result<VideoInfo>
suspend fun downloadVideo(url: String, outputDir: File, format: String, onProgress: (Float, Long, String) -> Unit): Result<String>
suspend fun downloadAudio(url: String, outputDir: File, onProgress: (Float, Long, String) -> Unit): Result<String>
}- yt-dlp Integration: YouTube video extraction
- FFmpeg Support: Audio/video processing
- Progress Callbacks: Real-time progress updates
- Fallback System: Multiple format attempts
- Error Recovery: Graceful error handling
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ URL Input │───►│ Video Info │───►│ Format │
│ Validation │ │ Extraction │ │ Selection │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Download │───►│ Progress │───►│ File │
│ Execution │ │ Tracking │ │ Validation │
└─────────────┘ └─────────────┘ └─────────────┘
data class VideoInfo(
val id: String,
val title: String,
val duration: String,
val thumbnail: String,
val uploader: String,
val url: String,
val formats: List<VideoFormat>
)
data class DownloadRequest(
val url: String,
val format: DownloadFormat
)
enum class DownloadFormat {
MP4, MP3
}object FileUtils {
fun getDownloadDirectory(context: Context): File
fun generateFileName(title: String, extension: String): String
fun getMimeType(filePath: String): String
fun getContentUri(context: Context, file: File): Uri
fun openFile(context: Context, filePath: String): Boolean
}┌─────────────┐
│ User │
│ Action │
└──────┬──────┘
│
▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ UI │───►│ ViewModel │───►│ Repository │
│ (Compose) │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
▲ ▲ │
│ │ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ State │◄───│ State │◄───│ Service │
│ Update │ │ Management │ │ Layer │
└─────────────┘ └─────────────┘ └─────────────┘
sequenceDiagram
participant UI as UI Layer
participant VM as ViewModel
participant Repo as Repository
participant Service as Service
participant FS as File System
UI->>VM: startDownload()
VM->>Repo: downloadVideo(request)
Repo->>Service: extractVideoInfo(url)
Service-->>Repo: VideoInfo
Repo->>Service: downloadVideo(url, format)
Service->>FS: Create file
Service-->>Repo: Progress updates
Repo-->>VM: DownloadState.Progress
VM-->>UI: State update
Service->>FS: Write file data
Service-->>Repo: DownloadState.Success
Repo-->>VM: Success with file path
VM-->>UI: Show success UI
sealed class AppError : Exception() {
data class NetworkError(override val message: String) : AppError()
data class ValidationError(override val message: String) : AppError()
data class FileSystemError(override val message: String) : AppError()
data class ServiceError(override val message: String) : AppError()
}Service Layer Error ──► Repository Layer ──► ViewModel ──► UI Layer
(Technical) (Business Logic) (User State) (User Message)
class CrashSafeDownloadRepository(
private val delegate: DownloadRepository
) {
suspend fun getVideoInfo(url: String): Result<VideoInfo> {
return try {
delegate.getVideoInfo(url)
} catch (e: Exception) {
Result.failure(AppError.ServiceError("Safe fallback: ${e.message}"))
}
}
}┌─────────────────┐
│ UI Tests │ ← Integration & E2E
│ (Instrumented) │
├─────────────────┤
│ Integration │ ← Repository + Service
│ Tests │
├─────────────────┤
│ Unit Tests │ ← ViewModel + Utils
│ (Local JVM) │
└─────────────────┘
app/src/test/java/com/irnhakim/ytmp3/
├── BasicFunctionalityTest.kt # Core functionality
├── DownloadRepositoryTest.kt # Repository layer
├── DownloadViewModelTest.kt # ViewModel layer
├── FileUtilsTest.kt # Utility functions
├── IntegrationTest.kt # Cross-layer integration
├── NetworkTest.kt # Network operations
├── RealDownloadTest.kt # Real download scenarios
└── YouTubeIntegrationTest.kt # YouTube API integration
class DownloadViewModel(application: Application) : AndroidViewModel(application) {
private val repository = DownloadRepository(application)
// Dependencies injected through constructor
}
class DownloadRepository(private val context: Context) {
private val youtubeExtractor = YouTubeExtractorService(context)
// Service dependencies created internally
}@HiltAndroidApp
class YTMPApplication : Application()
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideDownloadRepository(@ApplicationContext context: Context): DownloadRepository {
return DownloadRepository(context)
}
}<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>class DownloadViewModel : AndroidViewModel {
override fun onCleared() {
super.onCleared()
// Cleanup resources
viewModelScope.cancel()
}
}- StateFlow instead of LiveData for better performance
- Lazy initialization of heavy components
- Proper lifecycle management to prevent leaks
- Efficient Compose recomposition
- Coroutines for asynchronous operations
- IO Dispatcher for file operations
- Default Dispatcher for CPU-intensive tasks
- Main Dispatcher for UI updates
- Streaming downloads to avoid memory issues
- Progress callbacks for responsive UI
- Atomic file operations to prevent corruption
- Cleanup mechanisms for failed downloads
- Dependency Injection: Hilt/Dagger integration
- Room Database: Download history and metadata
- WorkManager: Background download queue
- Navigation Component: Multi-screen navigation
- Modularization: Feature-based modules
- Multi-module architecture for large teams
- Feature flags for gradual rollouts
- Plugin architecture for extensibility
- Microservices integration for cloud features
Architecture Documentation 📚 Complete overview of the application structure and design decisions.