477 líneas
12 KiB
Markdown
477 líneas
12 KiB
Markdown
# 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)
|