initial commit

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2026-01-24 17:45:29 +01:00
commit 8c7417f913
Se han modificado 73 ficheros con 6362 adiciones y 0 borrados

476
docs/ARCHITECTURE.md Archivo normal
Ver fichero

@@ -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)