Thank you for your interest in contributing to Unyo! This guide covers everything you need to know to get started, from setting up your environment to understanding our architecture and submitting changes.
- Code of Conduct
- Development Environment
- Project Structure
- Architecture Overview
- Getting Started
- Adding a New Feature
- Code Style & Conventions
- Build, Test & Analyze
- Multi-Repository Setup
- Submitting Changes
- Be respectful and constructive in all interactions.
- Keep discussions focused on technical merit.
- Opening issues with suggestions or bug reports is contributing.
- Contributing means pull requests, issues, and constructive feedback — not rebranding the app under a new repository.
| Tool | Version | Notes |
|---|---|---|
| Flutter | 3.38.1 | Pinned via .fvmrc. Use fvm flutter if you manage multiple versions. |
| Dart | Bundled with Flutter | |
| Java | 17+ | Required for local Aniyomi/Tachiyomi extensions. |
| Maven | 3.x | Required to build unyo-core. |
| Go | 1.21+ | Required to build the torrent server binding. |
Unyo is split across three sibling repositories:
~/Projects/
├── unyo/ # This Flutter app
├── unyo_lib/ # Flutter FFI package (JNI bindings to unyo-core)
└── unyo-core/ # Java/Kotlin JVM runtime for Aniyomi extensions
All three must exist as sibling directories. unyo depends on unyo_lib via a local path (../unyo_lib).
# 1. Clone all three repositories
git clone https://github.com/K3vinb5/Unyo.git unyo
git clone https://github.com/K3vinb5/unyo_lib.git unyo_lib
git clone https://github.com/K3vinb5/unyo-core.git unyo-core
# 2. Install Flutter dependencies
cd unyo
flutter pub get
# 3. Generate code (Freezed, Hive, AutoRoute, JSON)
flutter pub run build_runner build --delete-conflicting-outputs
# 4. Verify analysis passes
flutter analyzelib/
├── application/
│ ├── cubits/ # Business logic (Cubit + EffectMixin)
│ ├── states/ # Freezed state classes
│ └── effects/ # Effect type definitions
├── core/
│ ├── di/ # GetIt service locator (locator.dart)
│ ├── enums/ # App-wide enums
│ ├── notification/ # RxDart notifiers (cross-cubit communication)
│ ├── router/ # AutoRoute configuration
│ └── services/ # API, HTTP, GraphQL, video services
├── data/
│ ├── models/ # Concrete Freezed models (API → domain mapping)
│ ├── repositories/ # Repository implementations
│ └── adapters/ # Hive type adapters
├── domain/
│ ├── entities/ # Abstract domain entities + Freezed implementations
│ └── repositories/ # Repository interfaces (contracts)
├── generated/ # Auto-generated JSON serialization code
├── presentation/
│ ├── screens/ # Route screens (@RoutePage)
│ ├── dialogs/ # Modal dialogs
│ ├── drawers/ # Slide-in drawers
│ └── widgets/ # Reusable UI components
└── main.dart
Unyo follows Clean Architecture with strict layer separation:
Presentation → Application → Domain ← Data
| Layer | Responsibility | Key Packages |
|---|---|---|
| Presentation | Screens, widgets, effect handling | flutter_bloc, auto_route, flutter_screenutil |
| Application | Business logic, state management | Cubit + EffectMixin, rxdart notifiers |
| Domain | Entities and repository contracts | Pure Dart, no external deps |
| Data | API calls, JSON mapping, local persistence | dio, graphql, hive_ce, json_serializable |
Every feature uses a Cubit with a custom EffectMixin for side effects:
- State — Immutable Freezed class carrying all UI data + a list of
AppEffects. - Cubit — Business logic that emits states; never holds
BuildContext. - Effects — Side effects (navigation, dialogs, snackbars) flow through state to the UI layer.
Cubits express intent (e.g., "navigate to anime details") via effects. The UI layer (BlocListener) executes them via AppEffectHandler.
All dependencies are registered in lib/core/di/locator.dart:
- Singletons —
Logger, bridge services (one instance, created eagerly). - Lazy singletons — Repositories, notifiers, HTTP services (created on first access).
- Factories — Cubits only (new instance per screen).
Named instances are stored as const strings in lib/config/config.dart.
Routes are declared in lib/core/router/app_router.dart. Code generation creates app_router.gr.dart. Navigation from cubits happens through effects, not direct BuildContext access.
RxDart BehaviorSubject notifiers in lib/core/notification/ share state across cubits without coupling them. Example: AnimeNotifier lets the home screen and anime details screen share the currently selected anime.
# Debug build (Linux)
flutter run
# Release build
flutter build linux --releaseflutter testflutter analyzeAlways run
flutter analyzebefore committing. The CI enforces this.
The following workflow adds a complete feature end-to-end. For concrete code examples, see the relevant skill documents in .agents/skills/.
-
Create the abstract entity + Freezed model in
lib/domain/entities/<feature>.dart:- Abstract class defining the contract.
- Freezed
@freezedclassFeatureModel implements Feature. .empty()factory for default values.fromJson/toJsonfor JSON serialization.- Custom
JsonConverterif referencing other abstract entities.
-
Add the repository interface in
lib/domain/repositories/<feature>_repository.dart. -
Export both from the barrel files (
entities.dart,repositories.dart).
-
Create the concrete model in
lib/data/models/<source>_<feature>_model.dart(e.g.,anilist_studio_model.dart).implements Feature(the abstract class).- Named factories mapping from API DTOs.
-
Create the repository implementation in
lib/data/repositories/<feature>_repository_<source>.dart.implements FeatureRepository.- Use
RepositoryMixinfor error handling. - Inject services via
sl<T>().
-
Export from barrel files.
-
Create the Freezed state in
lib/application/states/<feature>_state.dart:implements HasEffects.@Default(<AppEffect>[]) List<AppEffect> effects.stateEffectsgetter.
-
Create the cubit in
lib/application/cubits/<feature>_cubit.dart:extends Cubit<State> with EffectMixin<State>.- Implement
copyStateWithEffectsandlogger. - Constructor-inject all dependencies.
- Private
_init()for stream subscriptions. - Cancel subscriptions in
close().
-
Create the screen in
lib/presentation/screens/<feature>_screen.dart:- Follow the three-layer pattern:
- Screen —
BlocProvidercreates the cubit. - Listener —
BlocListenerhandles effects viaAppEffectHandler. - View —
BlocBuilderrenders UI.
- Screen —
- Annotate the outer widget with
@RoutePage().
- Follow the three-layer pattern:
-
Add reusable widgets to
lib/presentation/widgets/styled/if needed.
- Add the route in
lib/core/router/app_router.dartunder the correct parent (RootRouteorTabsRoute). - Run code generation.
- Register the repository (lazy singleton) and cubit (factory) in
lib/core/di/locator.dart. - Follow the registration order: services → notifiers → repositories → cubits.
flutter pub run build_runner build --delete-conflicting-outputsflutter analyze
flutter test- No
BuildContextin cubits — use effects for navigation/dialogs. - Immutable state only — always use
state.copyWith(), never mutate. - Cancel stream subscriptions — always call
.cancel()incubit.close(). - No manual edits to
*.g.dart— these are generated. Runbuild_runnerinstead.
| Concept | Convention | Example |
|---|---|---|
| Abstract entity | Noun |
Anime, User |
| Freezed model | NounModel |
AnimeModel, UserModel |
| Data model | SourceNounModel |
AnilistAnimeModel |
| Repository interface | NounRepository |
AnimeRepository |
| Repository impl | NounRepositorySource |
AnimeRepositoryAnilist |
| Cubit | FeatureCubit |
HomeCubit, AnimeDetailsCubit |
| State | FeatureState |
HomeState, AnimeDetailsState |
| Screen | FeatureScreen |
HomeScreen, AnimeDetailsScreen |
| Notifier | NounNotifier |
AnimeNotifier, UserNotifier |
| Barrel file | nouns.dart |
animes.dart, entities.dart |
The following are auto-generated and must never be edited manually:
*.freezed.dart— Freezed code generation*.g.dart— JSON serialization, Hive adapterslib/generated/json/*.g.dart— JSON model generationlib/hive_registrar.g.dart— Hive adapter registrationlib/core/router/app_router.gr.dart— AutoRoute generation
- Use
handleError("...", stackTrace: stackTrace)for errors (logs + snackbar). - Use
pushRouteEffect(path: "/...")for navigation. - Use
showWidgetDialogEffect(dialog: ...)for dialogs. - Always pass
cubit.clearEffectstoAppEffectHandler.handleEffects().
# Install dependencies
flutter pub get
# Generate code after any Freezed/JSON/Hive/AutoRoute changes
flutter pub run build_runner build --delete-conflicting-outputs
# Run static analysis
flutter analyze
# Run tests
flutter test
# Build release (Linux example)
flutter build linux --releaseAlways run
build_runnerafterflutter pub getor after modifying any codegen-related files.
The Flutter desktop app. Depends on unyo_lib as a local path dependency.
Flutter FFI package bridging to the JVM runtime via JNI:
cd ../unyo_lib
fvm flutter pub get
fvm flutter testKey files:
lib/aniyomi_bridge.dart— main APIlib/jni_isolate.dart— JNI isolate workerlib/jmodels/— auto-generated JNI bindings (never edit manually)
Java/Kotlin Maven multi-module project providing the JVM runtime for Aniyomi/Tachiyomi extensions:
cd ../unyo-core
mvn clean package -DskipTestsOutput: aniyomi-bridge/target/aniyomiBridge-1.0.0.jar
After building, copy the JAR to unyo_lib/assets/unyo-core.jar so the Flutter app picks it up.
The torrent server (libmtorrentserver) lives in a separate Go module (go/binding/desktop/). It must be compiled per-platform as a c-shared library. The CI handles this during releases.
- Fork the repository.
- Create a branch:
git checkout -b feature/your-feature-name. - Make your changes following the conventions above.
- Run verification:
flutter pub run build_runner build --delete-conflicting-outputs flutter analyze flutter test - Commit with a descriptive message.
- Push to your fork.
- Open a Pull Request against
main.
-
flutter analyzepasses with no new errors. - Codegen files are up to date (
*.g.dart,*.freezed.dart). - New features follow the layer pattern (Domain → Data → Application → Presentation).
- Cubits use
EffectMixinand do not holdBuildContext. - Stream subscriptions are canceled in
close(). - DI registrations follow the correct order and use the right registration type.
- Open an issue for bugs or feature requests.
- Check existing skills in
.agents/skills/for detailed workflow documentation. - Review existing code in the same layer you're modifying — the codebase is the best reference.
Thank you for contributing to Unyo!