476
docs/ARCHITECTURE.md
Archivo normal
476
docs/ARCHITECTURE.md
Archivo normal
@@ -0,0 +1,476 @@
|
||||
# Architecture Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
My ActivityPub follows the **MVVM (Model-View-ViewModel)** architecture pattern combined with **Repository pattern** for clean separation of concerns and testability.
|
||||
|
||||
## Architecture Layers
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ UI Layer (View) │
|
||||
│ Jetpack Compose Composables │
|
||||
└──────────────────┬──────────────────────────────┘
|
||||
│ observes StateFlow
|
||||
↓
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ ViewModel Layer │
|
||||
│ Business Logic & State Management │
|
||||
└──────────────────┬──────────────────────────────┘
|
||||
│ calls methods
|
||||
↓
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Repository Layer │
|
||||
│ Data Operations Abstraction │
|
||||
└──────────────────┬──────────────────────────────┘
|
||||
│ makes API calls
|
||||
↓
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Data Source Layer │
|
||||
│ API Service (Retrofit) │
|
||||
└──────────────────┬──────────────────────────────┘
|
||||
│ HTTP requests
|
||||
↓
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Remote Server │
|
||||
│ Mastodon/ActivityPub API │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Layer Details
|
||||
|
||||
### 1. UI Layer (View)
|
||||
|
||||
**Location**: `ui/components/`, `MainActivity.kt`
|
||||
|
||||
**Responsibilities**:
|
||||
- Display data to the user
|
||||
- Capture user interactions
|
||||
- Observe ViewModel state
|
||||
- Render UI using Jetpack Compose
|
||||
|
||||
**Key Components**:
|
||||
|
||||
#### MainActivity.kt
|
||||
The entry point of the application. Sets up the Compose UI, dependency injection (manual), and hosts the main composable.
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun MyActivityPubApp() {
|
||||
// Setup dependencies
|
||||
val repository = MastodonRepository(apiService)
|
||||
val viewModel: TimelineViewModel = viewModel { TimelineViewModel(repository) }
|
||||
|
||||
// Observe state
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
// Render UI based on state
|
||||
when (uiState) {
|
||||
is TimelineUiState.Loading -> LoadingUI()
|
||||
is TimelineUiState.Success -> TimelineUI(statuses)
|
||||
is TimelineUiState.Error -> ErrorUI(message)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### StatusCard.kt
|
||||
Reusable composable component for displaying individual status posts.
|
||||
|
||||
**Features**:
|
||||
- User avatar and information
|
||||
- HTML content rendering
|
||||
- Media attachment display
|
||||
- Interaction buttons (reply, boost, favorite)
|
||||
- Timestamp formatting
|
||||
|
||||
### 2. ViewModel Layer
|
||||
|
||||
**Location**: `ui/viewmodel/`
|
||||
|
||||
**Responsibilities**:
|
||||
- Manage UI state
|
||||
- Handle business logic
|
||||
- Coordinate data operations
|
||||
- Expose data streams to UI
|
||||
- Survive configuration changes
|
||||
|
||||
**Key Components**:
|
||||
|
||||
#### TimelineViewModel.kt
|
||||
|
||||
Manages the timeline screen state and operations.
|
||||
|
||||
**State Management**:
|
||||
```kotlin
|
||||
sealed class TimelineUiState {
|
||||
object Loading : TimelineUiState()
|
||||
data class Success(val statuses: List<Status>) : TimelineUiState()
|
||||
data class Error(val message: String) : TimelineUiState()
|
||||
}
|
||||
```
|
||||
|
||||
**Flow**:
|
||||
1. UI subscribes to `uiState` StateFlow
|
||||
2. ViewModel fetches data from repository
|
||||
3. ViewModel updates state based on result
|
||||
4. UI recomposes with new state
|
||||
|
||||
**Benefits**:
|
||||
- Survives configuration changes (screen rotation)
|
||||
- Separates UI logic from business logic
|
||||
- Makes testing easier
|
||||
- Provides lifecycle awareness
|
||||
|
||||
### 3. Repository Layer
|
||||
|
||||
**Location**: `data/repository/`
|
||||
|
||||
**Responsibilities**:
|
||||
- Abstract data sources
|
||||
- Provide clean API to ViewModel
|
||||
- Handle data operations
|
||||
- Implement error handling
|
||||
- Coordinate multiple data sources (if needed)
|
||||
|
||||
**Key Components**:
|
||||
|
||||
#### MastodonRepository.kt
|
||||
|
||||
Provides data operations for Mastodon API.
|
||||
|
||||
**Methods**:
|
||||
- `getPublicTimeline()`: Fetch public timeline
|
||||
- `getInstanceInfo()`: Get instance information
|
||||
|
||||
**Pattern**:
|
||||
```kotlin
|
||||
suspend fun getPublicTimeline(limit: Int, local: Boolean): Result<List<Status>> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val response = apiService.getPublicTimeline(limit, local)
|
||||
if (response.isSuccessful) {
|
||||
Result.success(response.body() ?: emptyList())
|
||||
} else {
|
||||
Result.failure(Exception("Error: ${response.code()}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Single source of truth
|
||||
- Easy to test with mock data
|
||||
- Can add caching layer
|
||||
- Switches between local/remote data
|
||||
|
||||
### 4. Data Source Layer
|
||||
|
||||
**Location**: `data/api/`, `data/models/`
|
||||
|
||||
**Responsibilities**:
|
||||
- Define API endpoints
|
||||
- Make network requests
|
||||
- Parse JSON responses
|
||||
- Define data models
|
||||
|
||||
**Key Components**:
|
||||
|
||||
#### MastodonApiService.kt
|
||||
|
||||
Retrofit interface defining API endpoints.
|
||||
|
||||
```kotlin
|
||||
interface MastodonApiService {
|
||||
@GET("api/v1/timelines/public")
|
||||
suspend fun getPublicTimeline(
|
||||
@Query("limit") limit: Int = 20,
|
||||
@Query("local") local: Boolean = false
|
||||
): Response<List<Status>>
|
||||
}
|
||||
```
|
||||
|
||||
#### Data Models
|
||||
|
||||
**Status.kt**: Represents a post/toot
|
||||
**Account.kt**: Represents a user account
|
||||
**MediaAttachment.kt**: Represents media files
|
||||
**Instance.kt**: Represents server instance info
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Loading Timeline Example
|
||||
|
||||
```
|
||||
1. User opens app
|
||||
↓
|
||||
2. MainActivity creates ViewModel
|
||||
↓
|
||||
3. ViewModel initializes and calls loadTimeline()
|
||||
↓
|
||||
4. ViewModel sets state to Loading
|
||||
↓
|
||||
5. UI shows loading indicator
|
||||
↓
|
||||
6. ViewModel calls repository.getPublicTimeline()
|
||||
↓
|
||||
7. Repository calls apiService.getPublicTimeline()
|
||||
↓
|
||||
8. Retrofit makes HTTP request to Mastodon API
|
||||
↓
|
||||
9. API returns JSON response
|
||||
↓
|
||||
10. Gson parses JSON to List<Status>
|
||||
↓
|
||||
11. Repository returns Result.success(statuses)
|
||||
↓
|
||||
12. ViewModel updates state to Success(statuses)
|
||||
↓
|
||||
13. UI recomposes with status list
|
||||
↓
|
||||
14. StatusCard components render each status
|
||||
```
|
||||
|
||||
### Error Handling Flow
|
||||
|
||||
```
|
||||
Network Error occurs
|
||||
↓
|
||||
Repository catches exception
|
||||
↓
|
||||
Returns Result.failure(exception)
|
||||
↓
|
||||
ViewModel updates state to Error(message)
|
||||
↓
|
||||
UI shows error message and retry button
|
||||
↓
|
||||
User clicks retry
|
||||
↓
|
||||
Flow starts again from step 3
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### UI State Pattern
|
||||
|
||||
The app uses **unidirectional data flow** with sealed classes for state:
|
||||
|
||||
```kotlin
|
||||
sealed class TimelineUiState {
|
||||
object Loading : TimelineUiState()
|
||||
data class Success(val statuses: List<Status>) : TimelineUiState()
|
||||
data class Error(val message: String) : TimelineUiState()
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Type-safe state representation
|
||||
- Exhaustive when expressions
|
||||
- Clear state transitions
|
||||
- Easy to add new states
|
||||
|
||||
### StateFlow vs LiveData
|
||||
|
||||
The app uses **StateFlow** instead of LiveData because:
|
||||
- Better Kotlin coroutines integration
|
||||
- More powerful operators
|
||||
- Lifecycle-aware collection in Compose
|
||||
- Can be used outside Android
|
||||
|
||||
## Dependency Management
|
||||
|
||||
### Current Implementation
|
||||
|
||||
The app uses **manual dependency injection** in `MainActivity`:
|
||||
|
||||
```kotlin
|
||||
val client = OkHttpClient.Builder()
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.build()
|
||||
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl("https://mastodon.social/")
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
|
||||
val apiService = retrofit.create(MastodonApiService::class.java)
|
||||
val repository = MastodonRepository(apiService)
|
||||
val viewModel = TimelineViewModel(repository)
|
||||
```
|
||||
|
||||
### Future Improvements
|
||||
|
||||
For larger apps, consider:
|
||||
- **Hilt**: Dependency injection framework
|
||||
- **Koin**: Lightweight DI for Kotlin
|
||||
- **Manual DI with modules**: Create separate DI container classes
|
||||
|
||||
## Threading Model
|
||||
|
||||
### Coroutines and Dispatchers
|
||||
|
||||
The app uses Kotlin coroutines for async operations:
|
||||
|
||||
**Dispatchers.Main**: UI operations
|
||||
```kotlin
|
||||
viewModelScope.launch { // Main by default
|
||||
_uiState.value = TimelineUiState.Loading
|
||||
}
|
||||
```
|
||||
|
||||
**Dispatchers.IO**: Network and disk I/O
|
||||
```kotlin
|
||||
withContext(Dispatchers.IO) {
|
||||
apiService.getPublicTimeline()
|
||||
}
|
||||
```
|
||||
|
||||
**Dispatcher.Default**: CPU-intensive work
|
||||
```kotlin
|
||||
withContext(Dispatchers.Default) {
|
||||
// Heavy computation
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling Strategy
|
||||
|
||||
### Result Pattern
|
||||
|
||||
The repository uses Kotlin's `Result` type:
|
||||
|
||||
```kotlin
|
||||
suspend fun getPublicTimeline(): Result<List<Status>> {
|
||||
return try {
|
||||
Result.success(data)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Consumption in ViewModel**:
|
||||
```kotlin
|
||||
repository.getPublicTimeline().fold(
|
||||
onSuccess = { data -> /* handle success */ },
|
||||
onFailure = { error -> /* handle error */ }
|
||||
)
|
||||
```
|
||||
|
||||
### Error Types
|
||||
|
||||
1. **Network Errors**: No internet, timeout
|
||||
2. **HTTP Errors**: 4xx, 5xx status codes
|
||||
3. **Parse Errors**: Invalid JSON
|
||||
4. **Unknown Errors**: Unexpected exceptions
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Testing
|
||||
|
||||
**Repository Layer**:
|
||||
```kotlin
|
||||
@Test
|
||||
fun `getPublicTimeline returns success with valid data`() = runTest {
|
||||
val mockApi = mock<MastodonApiService>()
|
||||
val repository = MastodonRepository(mockApi)
|
||||
|
||||
// Test implementation
|
||||
}
|
||||
```
|
||||
|
||||
**ViewModel Layer**:
|
||||
```kotlin
|
||||
@Test
|
||||
fun `loadTimeline updates state to Success`() = runTest {
|
||||
val mockRepository = mock<MastodonRepository>()
|
||||
val viewModel = TimelineViewModel(mockRepository)
|
||||
|
||||
// Test implementation
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
|
||||
Test complete flows from ViewModel to Repository to API.
|
||||
|
||||
### UI Testing
|
||||
|
||||
Use Compose Testing to test UI components:
|
||||
```kotlin
|
||||
@Test
|
||||
fun statusCard_displays_content() {
|
||||
composeTestRule.setContent {
|
||||
StatusCard(status = testStatus)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Test content")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Image Loading
|
||||
|
||||
**Coil** library handles:
|
||||
- Async loading
|
||||
- Caching (memory + disk)
|
||||
- Placeholder/error images
|
||||
- Automatic lifecycle management
|
||||
|
||||
### List Performance
|
||||
|
||||
**LazyColumn** provides:
|
||||
- Lazy loading (only visible items rendered)
|
||||
- Recycling (item reuse)
|
||||
- Efficient scrolling
|
||||
|
||||
### Memory Management
|
||||
|
||||
- ViewModels survive configuration changes
|
||||
- Images are cached efficiently
|
||||
- Lists use lazy loading
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **HTTPS Only**: All API calls use HTTPS
|
||||
2. **No Cleartext Traffic**: Disabled in manifest
|
||||
3. **Input Validation**: Validate all user input
|
||||
4. **Rate Limiting**: Respect API rate limits
|
||||
5. **Error Messages**: Don't expose sensitive info in errors
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Recommended Additions
|
||||
|
||||
1. **Authentication**: OAuth 2.0 implementation
|
||||
2. **Database**: Room for offline caching
|
||||
3. **Pagination**: Implement infinite scroll
|
||||
4. **Deep Linking**: Handle Mastodon URLs
|
||||
5. **Search**: Add search functionality
|
||||
6. **Notifications**: Push notifications support
|
||||
7. **Multiple Accounts**: Account switching
|
||||
8. **Dark Theme**: Complete theme support
|
||||
9. **Accessibility**: Enhanced accessibility features
|
||||
10. **Testing**: Comprehensive test coverage
|
||||
|
||||
### Architecture Evolution
|
||||
|
||||
As the app grows:
|
||||
- Move to multi-module architecture
|
||||
- Implement Clean Architecture layers
|
||||
- Add use cases/interactors
|
||||
- Separate feature modules
|
||||
- Add shared UI components module
|
||||
|
||||
## References
|
||||
|
||||
- [Guide to app architecture (Android)](https://developer.android.com/topic/architecture)
|
||||
- [ViewModel Overview](https://developer.android.com/topic/libraries/architecture/viewmodel)
|
||||
- [Repository Pattern](https://developer.android.com/codelabs/basic-android-kotlin-training-repository-pattern)
|
||||
- [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html)
|
||||
- [Jetpack Compose Architecture](https://developer.android.com/jetpack/compose/architecture)
|
||||
Referencia en una nueva incidencia
Block a user