Files
unyo-app/CONTRIBUTING.md
2026-05-12 22:04:29 +01:00

13 KiB

Contributing to Unyo

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.

Table of Contents


Code of Conduct

  • 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.

Development Environment

Prerequisites

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.

Repository Layout

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

Initial Setup

# 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 analyze

Project Structure

lib/
├── 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

Architecture Overview

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

State Management: Cubit + EffectMixin

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.

Dependency Injection: GetIt

All dependencies are registered in lib/core/di/locator.dart:

  • SingletonsLogger, bridge services (one instance, created eagerly).
  • Lazy singletons — Repositories, notifiers, HTTP services (created on first access).
  • FactoriesCubits only (new instance per screen).

Named instances are stored as const strings in lib/config/config.dart.

Navigation: AutoRoute

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.

Cross-Cubit Communication: Notifiers

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.


Getting Started

Running the App

# Debug build (Linux)
flutter run

# Release build
flutter build linux --release

Running Tests

flutter test

Static Analysis

flutter analyze

Always run flutter analyze before committing. The CI enforces this.


Adding a New Feature

The following workflow adds a complete feature end-to-end. For concrete code examples, see the relevant skill documents in .agents/skills/.

Step 1: Domain Layer (Entity + Repository Contract)

  1. Create the abstract entity + Freezed model in lib/domain/entities/<feature>.dart:

    • Abstract class defining the contract.
    • Freezed @freezed class FeatureModel implements Feature.
    • .empty() factory for default values.
    • fromJson / toJson for JSON serialization.
    • Custom JsonConverter if referencing other abstract entities.
  2. Add the repository interface in lib/domain/repositories/<feature>_repository.dart.

  3. Export both from the barrel files (entities.dart, repositories.dart).

Step 2: Data Layer (Model + Repository Implementation)

  1. 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.
  2. Create the repository implementation in lib/data/repositories/<feature>_repository_<source>.dart.

    • implements FeatureRepository.
    • Use RepositoryMixin for error handling.
    • Inject services via sl<T>().
  3. Export from barrel files.

Step 3: Application Layer (State + Cubit)

  1. Create the Freezed state in lib/application/states/<feature>_state.dart:

    • implements HasEffects.
    • @Default(<AppEffect>[]) List<AppEffect> effects.
    • stateEffects getter.
  2. Create the cubit in lib/application/cubits/<feature>_cubit.dart:

    • extends Cubit<State> with EffectMixin<State>.
    • Implement copyStateWithEffects and logger.
    • Constructor-inject all dependencies.
    • Private _init() for stream subscriptions.
    • Cancel subscriptions in close().

Step 4: Presentation Layer (Screen + Widgets)

  1. Create the screen in lib/presentation/screens/<feature>_screen.dart:

    • Follow the three-layer pattern:
      • ScreenBlocProvider creates the cubit.
      • ListenerBlocListener handles effects via AppEffectHandler.
      • ViewBlocBuilder renders UI.
    • Annotate the outer widget with @RoutePage().
  2. Add reusable widgets to lib/presentation/widgets/styled/ if needed.

Step 5: Routing

  1. Add the route in lib/core/router/app_router.dart under the correct parent (RootRoute or TabsRoute).
  2. Run code generation.

Step 6: Dependency Injection

  1. Register the repository (lazy singleton) and cubit (factory) in lib/core/di/locator.dart.
  2. Follow the registration order: services → notifiers → repositories → cubits.

Step 7: Code Generation

flutter pub run build_runner build --delete-conflicting-outputs

Step 8: Verification

flutter analyze
flutter test

Code Style & Conventions

General

  • No BuildContext in cubits — use effects for navigation/dialogs.
  • Immutable state only — always use state.copyWith(), never mutate.
  • Cancel stream subscriptions — always call .cancel() in cubit.close().
  • No manual edits to *.g.dart — these are generated. Run build_runner instead.

Naming

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

Generated Files

The following are auto-generated and must never be edited manually:

  • *.freezed.dart — Freezed code generation
  • *.g.dart — JSON serialization, Hive adapters
  • lib/generated/json/*.g.dart — JSON model generation
  • lib/hive_registrar.g.dart — Hive adapter registration
  • lib/core/router/app_router.gr.dart — AutoRoute generation

Effect Conventions

  • Use handleError("...", stackTrace: stackTrace) for errors (logs + snackbar).
  • Use pushRouteEffect(path: "/...") for navigation.
  • Use showWidgetDialogEffect(dialog: ...) for dialogs.
  • Always pass cubit.clearEffects to AppEffectHandler.handleEffects().

Build, Test & Analyze

Essential Commands

# 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 --release

Always run build_runner after flutter pub get or after modifying any codegen-related files.


Multi-Repository Setup

unyo (this repo)

The Flutter desktop app. Depends on unyo_lib as a local path dependency.

unyo_lib (sibling directory)

Flutter FFI package bridging to the JVM runtime via JNI:

cd ../unyo_lib
fvm flutter pub get
fvm flutter test

Key files:

  • lib/aniyomi_bridge.dart — main API
  • lib/jni_isolate.dart — JNI isolate worker
  • lib/jmodels/ — auto-generated JNI bindings (never edit manually)

unyo-core (sibling directory)

Java/Kotlin Maven multi-module project providing the JVM runtime for Aniyomi/Tachiyomi extensions:

cd ../unyo-core
mvn clean package -DskipTests

Output: 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.

Go Binding (Torrent Server)

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.


Submitting Changes

  1. Fork the repository.
  2. Create a branch: git checkout -b feature/your-feature-name.
  3. Make your changes following the conventions above.
  4. Run verification:
    flutter pub run build_runner build --delete-conflicting-outputs
    flutter analyze
    flutter test
    
  5. Commit with a descriptive message.
  6. Push to your fork.
  7. Open a Pull Request against main.

PR Checklist

  • flutter analyze passes 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 EffectMixin and do not hold BuildContext.
  • Stream subscriptions are canceled in close().
  • DI registrations follow the correct order and use the right registration type.

Getting Help

  • 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!