Skip to content

Code

NAHPU is more than just a digital catalog app for natural history fieldwork. It is a software system that supports specimen digitization right at the point of collection. This page explains how to contribute to the main NAHPU app — the digital catalog itself. See the developer tools section for details on contributing to supporting software.

Before contributing to NAHPU, make sure you have the following installed:

  1. Install Git.

    Git comes pre-installed on macOS. Verify with:

    Terminal window
    git --version

    If not installed, install it via Homebrew:

    Terminal window
    brew install git
  2. Install GitHub CLI (optional, but recommended).

    Terminal window
    brew install gh

    Verify the installation:

    Terminal window
    gh --version
  3. Authenticate GitHub CLI with your GitHub account.

    Terminal window
    gh auth login

    Follow the prompts: select GitHub.com as the host, HTTPS as the protocol, and confirm when asked to authenticate via browser. Verify once complete:

    Terminal window
    gh auth status
  4. Configure Git with your name and email.

    Terminal window
    git config --global user.name "Your Name"
    git config --global user.email "your@email.com"

    Use the same email address associated with your GitHub account.

  5. Install the Flutter SDK.

    Follow the official Flutter macOS guide. Install Android Studio for Android development, then run:

    Terminal window
    flutter doctor

    Resolve any issues reported before proceeding.

  6. Install the Rust toolchain.

    Terminal window
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

    Verify the installation:

    Terminal window
    rustc --version
  7. Install flutter_rust_bridge dependencies.

    Terminal window
    cargo install flutter_rust_bridge_codegen

    Make sure your cargo bin directory is in your PATH:

    Terminal window
    export PATH="$HOME/.cargo/bin:$PATH"

    Add this line to your ~/.bashrc, ~/.zshrc, or equivalent shell config to make it permanent.

    See the flutter_rust_bridge documentation for more details.

  1. Fork the NAHPU repository on GitHub.

    Go to hhandika/nahpu and click Fork.

  2. Clone the forked repository to your local machine.

    Terminal window
    git clone https://github.com/nahpu/nahpu.git
  3. Navigate into the repository.

    Terminal window
    cd nahpu
  4. Create a new branch for your changes.

    Terminal window
    git checkout -b your-branch-name
  5. Make your changes and commit them.

    Terminal window
    git add .
    git commit -m "Your commit message"
  6. Push your changes to your forked repository.

    Terminal window
    git push origin your-branch-name
  7. Open a pull request to the main NAHPU repository.

    Go to your fork on GitHub and click Compare & pull request.

    If your changes affect the user interface, include screenshots or a short demo video. See the NAHPU GitHub issues for examples of well-documented UI contributions.

NAHPU is built with the following core technologies:

  • Dart — Programming language for the Flutter app
  • Flutter — Cross-platform UI framework
  • Riverpod — State management library for Flutter
  • Drift — SQLite database library for Dart
  • Rust — Systems programming language for performance-critical components
  • flutter_rust_bridge — Bridge between Flutter and Rust

Dart and Flutter allow NAHPU to run natively on Android, iOS, Linux, macOS, and Windows from a single codebase. Flutter's widget system and Material Design 3 integration make it straightforward to build a consistent, accessible UI across all supported platforms without maintaining separate codebases for each operating system.

Riverpod is a robust, compile-safe state management solution for Flutter. It simplifies managing complex async state. NAHPU extensively uses AsyncNotifier for database-driven state.

Drift is a type-safe SQLite library for Dart that generates query code atcompile time. This eliminates entire classes of runtime SQL errors and makes database schema changes easier to manage and migrate. Because fieldwork happens in remote areas without internet access, SQLite's offline-first approach is fundamental to NAHPU's architecture.

Rust provides memory safety and near-native performance for compute-intensive operations such as parsing large datasets, file I/O, and complex data transformations. In the NAHPU codebase, Rust acts as a wrapper layer. The core implementations live in the separate nahpu_api repository.

flutter_rust_bridge generates the FFI bindings that allow Flutter and Rust to communicate safely and efficiently. It handles type conversions between Dart and Rust automatically, letting us call Rust functions from Dart as if the were native async Dart functions.

NAHPU is a cross-platform application built with Flutter, a high-performance framework for building natively compiled applications across mobile, desktop, and web from a single codebase. Flutter uses the Dart programming language, optimized for building fast, responsive user interfaces.

Data storage relies heavily on SQLite for local, offline-first storage. Because fieldwork often happens in remote areas without internet access, robust local data management is critical to NAHPU's design.

Rust integration — since early 2024, we have been migrating compute-heavy operations to Rust. By integrating Rust with Flutter via flutter_rust_bridge, we achieve the best of both worlds:

  • Flutter/Dart handles state management, routing, and rendering the UI smoothly at 60–120 fps.
  • Rust handles the heavy lifting: parsing large datasets, complex data transformations, file I/O, and specialized algorithms that would otherwise bottleneck the Dart isolate.

NAHPU uses the Material Design 3 system as the foundation for its visual identity.

Consistency — Material Design is deeply integrated with Flutter. Whenever possible, use standard Material widgets to ensure a consistent, accessible, and intuitive experience across all platforms.

Responsiveness — NAHPU is frequently used on field tablets and mobile devices. When contributing UI components, always consider responsive layouts. Use LayoutBuilder, MediaQuery, or flexible grid systems to ensure your changes look good on small phone screens as well as large tablet and desktop displays.

Accessibility — ensure that text has sufficient contrast, touch targets are at least 48×48 logical pixels, and semantic labels are provided for screen readers.

  • Directoryandroid/ Android platform-specific code and build configuration
  • Directoryassets/ Static assets such as images, fonts, and icons
  • Directorydb_schemas/ Database schema definitions and migration files
  • Directoryinstaller/ Installer scripts and configuration for packaging the app
  • Directoryintegration_test/ End-to-end integration tests
  • Directoryios/ iOS platform-specific code and build configuration
  • Directorylib/ Main Dart source code for the Flutter app
  • Directorylinux/ Linux platform-specific code and build configuration
  • Directorymacos/ macOS platform-specific code and build configuration
  • Directoryrust/ Rust source code for native performance-critical components
  • Directoryrust_builder/ Build scripts and configuration for compiling Rust code
  • Directoryscripts/ Utility scripts for development and automation tasks
  • Directorysnap/ Snap package configuration for Linux distribution
  • Directorytest/ Unit and widget tests
  • Directorytest_driver/ Flutter driver tests for automated UI testing
  • Directoryweb/ Web platform-specific code and build configuration
  • Directorywindows/ Windows platform-specific code and build configuration

NAHPU follows standard Dart style guidelines. If you are new to Dart, the official Dart language tour is the best place to start. Key conventions to follow:

  1. Use const constructors wherever possible. This tells Flutter to skip rebuilding widgets that do not change, improving performance.

    // Preferred
    const Text('Hello, NAHPU!');
    // Avoid
    Text('Hello, NAHPU!');
  2. Prefer final over var for variables that are assigned once.

    // Preferred
    final specimenId = 'NAHPU-001';
    // Avoid
    var specimenId = 'NAHPU-001';
  3. Split large widgets into smaller ones rather than nesting deeply. Each widget should have a single, clear responsibility.

  4. Name files using snake_case and classes using PascalCase. For example, specimen_card.dart contains the SpecimenCard widget.

  5. Avoid business logic inside widgets. Keep widgets focused on rendering. Move logic to providers, services, or helper classes.

NAHPU uses Riverpod for state management. If you are unfamiliar with Riverpod, read the official Riverpod documentation before contributing to state-related code. Key concepts you need to know:

  • Provider — exposes a read-only value or service.
  • StateProvider — exposes a simple mutable value.
  • NotifierProvider — exposes a class-based state with methods to mutate it. Preferred for complex synchronous state.
  • AsyncNotifierProvider — like NotifierProvider but for async state. Used extensively in NAHPU for database operations and data loading.
  • FutureProvider — exposes a one-time async value. Use this for simple read-only async data; prefer AsyncNotifierProvider when you also need to mutate state.
  • StreamProvider — exposes a stream of values.

AsyncNotifier is the primary pattern for database-backed state in NAHPU. It combines async data loading with mutation methods in a single class, keeping your providers clean and testable:

// ✅ Preferred pattern for database-backed state
@riverpod
class SpecimenListNotifier extends _$SpecimenListNotifier {
@override
Future<List<Specimen>> build() async {
// Initial data load from the database
return ref.read(databaseProvider).getAllSpecimens();
}
Future<void> addSpecimen(Specimen specimen) async {
// Optimistically set loading state
state = const AsyncLoading();
state = await AsyncValue.guard(
() async {
await ref.read(databaseProvider).insertSpecimen(specimen);
return ref.read(databaseProvider).getAllSpecimens();
},
);
}
Future<void> deleteSpecimen(String id) async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() async {
await ref.read(databaseProvider).deleteSpecimen(id);
return ref.read(databaseProvider).getAllSpecimens();
},
);
}
}

Handle the three async states — data, loading, and error — explicitly in your widgets using .when():

final specimens = ref.watch(specimenListNotifierProvider);
return specimens.when(
data: (data) => SpecimenListView(specimens: data),
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
);

Prefer ConsumerWidget over passing ref down the widget tree. Passing ref as a constructor parameter couples widgets to Riverpod unnecessarily and makes them harder to test and reuse. Instead, make the widget a ConsumerWidget and read the provider directly inside build:

// ✅ Preferred — use ConsumerWidget
class SpecimenList extends ConsumerWidget {
const SpecimenList({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final specimens = ref.watch(specimenListNotifierProvider);
return specimens.when(
data: (data) => ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) => SpecimenTile(specimen: data[index]),
),
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
);
}
}
// ❌ Avoid — passing ref as a parameter
class SpecimenList extends StatelessWidget {
const SpecimenList({super.key, required this.ref});
final WidgetRef ref;
@override
Widget build(BuildContext context) {
final specimens = ref.watch(specimenListNotifierProvider);
...
}
}

Use ref.watch to reactively rebuild the widget when provider state changes. Use ref.read only inside callbacks and event handlers where you do not want to trigger a rebuild:

// Watching a provider — rebuilds when state changes
final specimens = ref.watch(specimenListNotifierProvider);
// Reading inside a callback — no rebuild triggered
onPressed: () => ref.read(specimenListNotifierProvider.notifier).addSpecimen(data),

NAHPU uses Rust for performance-critical operations via flutter_rust_bridge. In the NAHPU codebase, Rust is used only as a wrapper — it exposes functionality to Flutter but does not contain core implementations. All Rust implementations live in the nahpu_api repository. If your contribution involves Rust logic, head over there instead.

If you are new to Rust, the Rust Book is the recommended starting point. Key conventions to follow when working on the Rust wrapper:

  1. Follow standard Rust naming conventionssnake_case for functions and variables, PascalCase for types and structs, SCREAMING_SNAKE_CASE for constants.

  2. Prefer returning Result<T, E> over panicking. Panics in Rust code called from Flutter will crash the app. Always handle errors gracefully:

    // ✅ Preferred
    pub fn parse_specimen(data: &str) -> Result<Specimen, ParseError> {
    // ...
    }
    // ❌ Avoid
    pub fn parse_specimen(data: &str) -> Specimen {
    data.parse().unwrap() // panics on failure
    }
  3. Keep the Rust/Flutter boundary thin. Avoid passing complex Dart objects into Rust. Prefer primitive types or simple structs that flutter_rust_bridge can easily serialize.

  4. Document public API functions with doc comments (///), especially functions exposed to Dart via the bridge:

    /// Parses raw specimen data and returns a structured [Specimen].
    ///
    /// Returns an error if the input is malformed or missing required fields.
    pub fn parse_specimen(data: &str) -> Result<Specimen, ParseError> {
    // ...
    }
  5. Run cargo clippy before submitting to catch common mistakes and style issues:

    Terminal window
    cargo clippy --all-targets --all-features

NAHPU uses Drift for SQLite database management. You can find the database schema definitions in the db_schemas/ directory.

  1. Propose the schema change in a GitHub issue first.

  2. Once the change is approved, add the new schema definition in the /lib/services/database/table.drift file.

  3. Bump the database version in the /lib/services/database/database.dart file.

    For example, to bump from version 6 to 7, update the kSchemaVersion constant:

    const int kSchemaVersion = 7;
  4. Write a new migration function in the same file to handle the schema change. For example:

    Future<void> _migrateFromVersion7(Migrator m) async {
    await m.addColumn(specimens, specimens.newColumn);
    // Add any additional migration steps here
    }
  5. After updating the schema, run the code generator to update the generated Dart code:

    Terminal window
    flutter pub run build_runner build --delete-conflicting-outputs
  6. Dump the final schema to a new file in db_schemas/ with the next version number.

    Use the the builtin drift_dev` command to generate the schema dump:

    Terminal window
    dart run drift_dev schema dump lib/services/database/database.dart db_schemas/drift_schema_v7.json
  7. Run test.

    Terminal window
    flutter test
  8. Commit your changes and open a pull request with a clear description of the schema change, the reason for it, and any potential impacts on existing data.

  • Cause: flutter_rust_bridge_codegen is not in your PATH.
  • Solution: Install it with cargo install flutter_rust_bridge_codegen and ensure ~/.cargo/bin is in your PATH.

Error on macOS: flutter_rust_bridge_codegen fails with "Please supply one or more path/to/llvm..."

Section titled “Error on macOS: flutter_rust_bridge_codegen fails with &quot;Please supply one or more path/to/llvm...&quot;”
  • Cause: The code generator cannot find your LLVM installation.

  • Solution: Install LLVM with Homebrew:

    Terminal window
    brew install llvm
  • Cause: CocoaPods is outdated, missing, or incompatible with the current Xcode version, causing pod install to fail during the Flutter build.

  • Solution: Install or update CocoaPods and reinstall the pods:

    Terminal window
    # Install or update CocoaPods
    sudo gem install cocoapods
    # Navigate to the iOS directory and reinstall pods
    cd ios
    pod deintegrate
    pod install

    If the issue persists, try clearing the CocoaPods cache:

    Terminal window
    pod cache clean --all
    rm -rf ~/Library/Caches/CocoaPods
    rm -rf ios/Pods
    rm -rf ios/Podfile.lock
    cd ios && pod install

    Make sure your Xcode command line tools are up to date:

    Terminal window
    xcode-select --install
  • Cause: MSVC build tools or the Windows SDK are missing or misconfigured, causing the Rust toolchain to fail during linking.

  • Solution: Install the Visual Studio Build Tools and select the Desktop development with C++ workload. Then switch to the MSVC Rust toolchain:

    Terminal window
    rustup default stable-x86_64-pc-windows-msvc

Error on Android: android context was not initialized

Section titled “Error on Android: android context was not initialized”
  • Cause: The Dart VM loads the Rust library differently than a standard Android JVM application, skipping NDK context initialization.
  • Solution: This is a known quirk. Ensure your setup follows the official flutter_rust_bridge examples, which include workarounds for this issue.

Error on iOS TestFlight: store_dart_post_cobject not found

Section titled “Error on iOS TestFlight: store_dart_post_cobject not found”
  • Cause: Xcode may strip necessary symbols from the final release build.
  • Solution: In Xcode, navigate to the build settings for the native-staticlib target. Under Deployment, set Strip Linked Product to No and Strip Style to Debugging Symbols.

App stuck on home screen after schema changes

Section titled “App stuck on home screen after schema changes”
  • Cause: Schema changes were made without bumping the database version or creating a new migration file.

  • Solution: Delete the existing nahpu.db from your device or emulator to reset the schema. For production releases, create proper migration files and bump the version in db_schemas/schema_version.txt.

    Terminal window
    # Find the database file
    find ~/Library/Containers -name "nahpu.db" 2>/dev/null
    # Delete the database file
    rm /path/to/nahpu.db

If you get stuck setting up the environment or understanding the architecture, don't hesitate to reach out. Check the NAHPU GitHub Issues for ongoing discussions, or open a new issue if you have a bug report or architectural proposal.