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.
Quick Start
Section titled “Quick Start”Prerequisites
Section titled “Prerequisites”Before contributing to NAHPU, make sure you have the following installed:
- Git
- Dart SDK
- Flutter SDK
- Rust toolchain
- flutter_rust_bridge
- GitHub CLI (optional, but recommended)
Install Git.
Git comes pre-installed on macOS. Verify with:
Terminal window git --versionIf not installed, install it via Homebrew:
Terminal window brew install gitTerminal window # Debian/Ubuntusudo apt install git# Fedorasudo dnf install gitDownload and install Git from git-scm.com. During installation, keep the default options. Verify with:
Terminal window git --versionInstall GitHub CLI (optional, but recommended).
Terminal window brew install ghTerminal window # Debian/Ubuntusudo apt install gh# Fedorasudo dnf install ghTerminal window winget install --id GitHub.cliVerify the installation:
Terminal window gh --versionAuthenticate GitHub CLI with your GitHub account.
Terminal window gh auth loginFollow 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 statusConfigure 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.
Install the Flutter SDK.
Follow the official Flutter macOS guide. Install Android Studio for Android development, then run:
Terminal window flutter doctorFollow the official Flutter Linux guide. Install Android Studio for Android development, then run:
Terminal window flutter doctorFollow the official Flutter Windows guide. Install Android Studio for Android development, then run:
Terminal window flutter doctorResolve any issues reported before proceeding.
Install the Rust toolchain.
Terminal window curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shVerify the installation:
Terminal window rustc --versionDownload and run rustup-init.exe from the official site. Verify the installation:
Terminal window rustc --versionInstall flutter_rust_bridge dependencies.
Terminal window cargo install flutter_rust_bridge_codegenMake 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.Add
%USERPROFILE%\.cargo\binto your systemPATHvia System Properties → Environment Variables.See the flutter_rust_bridge documentation for more details.
Code Contribution
Section titled “Code Contribution”Fork the NAHPU repository on GitHub.
Go to hhandika/nahpu and click Fork.
Clone the forked repository to your local machine.
Terminal window git clone https://github.com/nahpu/nahpu.gitTerminal window gh repo clone nahpu/nahpuNavigate into the repository.
Terminal window cd nahpuCreate a new branch for your changes.
Terminal window git checkout -b your-branch-nameTerminal window git checkout -b your-branch-nameMake your changes and commit them.
Terminal window git add .git commit -m "Your commit message"Push your changes to your forked repository.
Terminal window git push origin your-branch-nameTerminal window git push origin your-branch-nameOpen a pull request to the main NAHPU repository.
Go to your fork on GitHub and click Compare & pull request.
Terminal window gh pr create --repo nahpu/nahpu --base main --fillIf 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.
Deep Dive
Section titled “Deep Dive”Technologies
Section titled “Technologies”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
Why These Technologies?
Section titled “Why These Technologies?”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.
Code Architecture
Section titled “Code Architecture”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.
User Interface and Design
Section titled “User Interface and Design”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.
Directory Structure
Section titled “Directory Structure”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
- …
Code Conventions
Section titled “Code Conventions”Dart and Flutter
Section titled “Dart and Flutter”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:
Use
constconstructors wherever possible. This tells Flutter to skip rebuilding widgets that do not change, improving performance.// Preferredconst Text('Hello, NAHPU!');// AvoidText('Hello, NAHPU!');Prefer
finalovervarfor variables that are assigned once.// Preferredfinal specimenId = 'NAHPU-001';// Avoidvar specimenId = 'NAHPU-001';Split large widgets into smaller ones rather than nesting deeply. Each widget should have a single, clear responsibility.
Name files using
snake_caseand classes usingPascalCase. For example,specimen_card.dartcontains theSpecimenCardwidget.Avoid business logic inside widgets. Keep widgets focused on rendering. Move logic to providers, services, or helper classes.
State Management with Riverpod
Section titled “State Management with Riverpod”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— likeNotifierProviderbut 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; preferAsyncNotifierProviderwhen 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@riverpodclass 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 ConsumerWidgetclass 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 parameterclass 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 changesfinal specimens = ref.watch(specimenListNotifierProvider);
// Reading inside a callback — no rebuild triggeredonPressed: () => 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:
Follow standard Rust naming conventions —
snake_casefor functions and variables,PascalCasefor types and structs,SCREAMING_SNAKE_CASEfor constants.Prefer returning
Result<T, E>over panicking. Panics in Rust code called from Flutter will crash the app. Always handle errors gracefully:// ✅ Preferredpub fn parse_specimen(data: &str) -> Result<Specimen, ParseError> {// ...}// ❌ Avoidpub fn parse_specimen(data: &str) -> Specimen {data.parse().unwrap() // panics on failure}Keep the Rust/Flutter boundary thin. Avoid passing complex Dart objects into Rust. Prefer primitive types or simple structs that
flutter_rust_bridgecan easily serialize.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> {// ...}Run
cargo clippybefore submitting to catch common mistakes and style issues:Terminal window cargo clippy --all-targets --all-features
Database Schema Changes
Section titled “Database Schema Changes”NAHPU uses Drift for SQLite database management. You can find the database schema definitions in the db_schemas/ directory.
Propose the schema change in a GitHub issue first.
Once the change is approved, add the new schema definition in the
/lib/services/database/table.driftfile.Bump the database version in the
/lib/services/database/database.dartfile.For example, to bump from version 6 to 7, update the
kSchemaVersionconstant:const int kSchemaVersion = 7;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}After updating the schema, run the code generator to update the generated Dart code:
Terminal window flutter pub run build_runner build --delete-conflicting-outputsDump the final schema to a new file in
db_schemas/with the next version number.Use the
the builtindrift_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.jsonRun test.
Terminal window flutter testCommit 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.
Troubleshooting
Section titled “Troubleshooting”flutter_rust_bridge_codegen not found
Section titled “flutter_rust_bridge_codegen not found”- Cause:
flutter_rust_bridge_codegenis not in your PATH. - Solution: Install it with
cargo install flutter_rust_bridge_codegenand ensure~/.cargo/binis 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 "Please supply one or more path/to/llvm..."”Cause: The code generator cannot find your LLVM installation.
Solution: Install LLVM with Homebrew:
Terminal window brew install llvm
Error on macOS: CocoaPods issues
Section titled “Error on macOS: CocoaPods issues”Cause: CocoaPods is outdated, missing, or incompatible with the current Xcode version, causing
pod installto fail during the Flutter build.Solution: Install or update CocoaPods and reinstall the pods:
Terminal window # Install or update CocoaPodssudo gem install cocoapods# Navigate to the iOS directory and reinstall podscd iospod deintegratepod installIf the issue persists, try clearing the CocoaPods cache:
Terminal window pod cache clean --allrm -rf ~/Library/Caches/CocoaPodsrm -rf ios/Podsrm -rf ios/Podfile.lockcd ios && pod installMake sure your Xcode command line tools are up to date:
Terminal window xcode-select --install
Error on Windows: linker errors or "nudget" failures
Section titled “Error on Windows: linker errors or "nudget" failures”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-staticlibtarget. Under Deployment, set Strip Linked Product toNoand Strip Style toDebugging 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.dbfrom your device or emulator to reset the schema. For production releases, create proper migration files and bump the version indb_schemas/schema_version.txt.Terminal window # Find the database filefind ~/Library/Containers -name "nahpu.db" 2>/dev/null# Delete the database filerm /path/to/nahpu.dbTerminal window # Find the database filefind ~/.local/share -name "nahpu.db" 2>/dev/null# or try:find ~/Documents -name "nahpu.db" 2>/dev/null# Delete the database filerm /path/to/nahpu.dbTerminal window # Find the database file# Find the database fileGet-ChildItem -Recurse -Path "$env:USERPROFILE\Documents" -Filter "nahpu.db"# Delete the database fileRemove-Item "$env:USERPROFILE\Documents\nahpu\nahpu.db"Terminal window adb shellcd /data/data/org.nahpu.app/databasesrm nahpu.dbTerminal window xcrun simctl spawn booted rm \/data/Containers/Data/Application/*/Library/Application\ Support/nahpu.db
Getting Help
Section titled “Getting Help”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.