Consistent code style makes AppFlowy’s codebase easier to read, maintain, and contribute to. This guide covers style conventions for both Dart/Flutter and Rust code.
Dart/Flutter Code Style
Dart Style Guide
AppFlowy follows the official Dart Style Guide .
Analysis Options
Linting rules are configured in analysis_options.yaml:
include : package:flutter_lints/flutter.yaml
linter :
rules :
- require_trailing_commas
- prefer_collection_literals
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- sized_box_for_whitespace
- use_decorated_box
- unnecessary_parenthesis
- avoid_unnecessary_containers
- always_declare_return_types
- sort_constructors_first
- unawaited_futures
Use dartfmt
Format code with dartfmt (built into flutter format): cd appflowy_flutter
flutter format .
Enable format on save
In VS Code (settings.json): {
"editor.formatOnSave" : true ,
"[dart]" : {
"editor.formatOnSave" : true
}
}
Key Conventions
Naming
Classes
Variables
Constants
Private
Use UpperCamelCase for class names: class DocumentBloc { }
class UserProfile { }
class AppFlowyEditor { }
Use lowerCamelCase for variables and functions: final documentId = '123' ;
void loadDocument () { }
Use lowerCamelCase for constants: const maxRetries = 3 ;
const defaultTimeout = Duration (seconds : 30 );
Prefix private members with underscore: class _PrivateClass { }
final _privateField = '' ;
void _privateMethod () { }
Trailing Commas
Always use trailing commas for better formatting:
Widget build ( BuildContext context) {
return Column (
children : [
Text ( 'Hello' ),
Text ( 'World' ),
], // trailing comma
);
}
Prefer Final
Use final for variables that don’t change:
final documentId = '123' ;
final userProfile = getUserProfile ();
Return Types
Always declare return types explicitly:
Future < Document > loadDocument ( String id) async {
// ...
}
Widget buildTitle () {
return Text ( 'Title' );
}
BLoC Pattern
AppFlowy uses the BLoC pattern for state management:
class DocumentBloc extends Bloc < DocumentEvent , DocumentState > {
DocumentBloc ({
required this .documentId,
}) : super ( DocumentState . initial ()) {
on < DocumentEvent > (_onDocumentEvent);
}
final String documentId;
Future < void > _onDocumentEvent (
DocumentEvent event,
Emitter < DocumentState > emit,
) async {
// Handle event
}
}
Key principles:
Events are immutable and describe actions
States are immutable and describe UI state
BLoCs handle business logic, not UI
Organize widgets consistently:
class MyWidget extends StatelessWidget {
const MyWidget ({
super .key,
required this .title,
this .subtitle,
});
// 1. Fields
final String title;
final String ? subtitle;
// 2. Build method
@override
Widget build ( BuildContext context) {
return Column (
children : [
_buildTitle (),
if (subtitle != null ) _buildSubtitle (),
],
);
}
// 3. Private helper methods
Widget _buildTitle () {
return Text (title);
}
Widget _buildSubtitle () {
return Text (subtitle ! );
}
}
File Organization
One class per file
Each file should contain one main public class.
File naming
Use snake_case for file names: document_bloc.dart
user_profile_widget.dart
Import ordering
Order imports as follows: // 1. Dart SDK imports
import 'dart:async' ;
import 'dart:convert' ;
// 2. Flutter imports
import 'package:flutter/material.dart' ;
import 'package:flutter/services.dart' ;
// 3. Third-party package imports
import 'package:flutter_bloc/flutter_bloc.dart' ;
import 'package:freezed_annotation/freezed_annotation.dart' ;
// 4. AppFlowy imports
import 'package:appflowy/workspace/domain/document.dart' ;
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart' ;
Rust Code Style
Rust Style Guide
AppFlowy follows the official Rust Style Guide .
Rustfmt Configuration
Formatting is configured in rust-lib/rustfmt.toml:
max_width = 100
tab_spaces = 2
newline_style = "Auto"
match_block_trailing_comma = true
use_field_init_shorthand = true
use_try_shorthand = true
reorder_imports = true
reorder_modules = true
remove_nested_parens = true
merge_derives = true
edition = "2024"
Use rustfmt
Format code with rustfmt:
Enable format on save
In VS Code (settings.json): {
"[rust]" : {
"editor.formatOnSave" : true ,
"editor.defaultFormatter" : "rust-lang.rust-analyzer"
}
}
Clippy Linting
Use Clippy for additional linting:
cargo clippy -- -D warnings
CI/CD pipelines enforce Clippy warnings. Fix all warnings before submitting PRs.
Key Conventions
Naming
Types
Functions
Constants
Lifetimes
Use UpperCamelCase for types: struct UserProfile { }
enum DocumentEvent { }
trait DocumentHandler { }
Use snake_case for functions and variables: fn load_document ( id : & str ) -> Document { }
let user_profile = get_profile ();
Use SCREAMING_SNAKE_CASE for constants: const MAX_RETRIES : usize = 3 ;
const DEFAULT_TIMEOUT : Duration = Duration :: from_secs ( 30 );
Use short, descriptive lifetime names: fn process <' a >( input : & ' a str ) -> & ' a str { }
Error Handling
Use Result for fallible operations:
use anyhow :: Result ;
fn load_document ( id : & str ) -> Result < Document > {
let doc = database . get ( id ) ? ;
Ok ( doc )
}
Option Handling
Prefer combinators over pattern matching:
let name = user . name . unwrap_or_default ();
let length = text . as_ref () . map ( | t | t . len ());
Struct Organization
pub struct Document {
// 1. Public fields
pub id : String ,
pub title : String ,
// 2. Private fields
content : String ,
metadata : Metadata ,
}
impl Document {
// 1. Constructor(s)
pub fn new ( id : String , title : String ) -> Self {
Self {
id ,
title ,
content : String :: new (),
metadata : Metadata :: default (),
}
}
// 2. Public methods
pub fn update_content ( & mut self , content : String ) {
self . content = content ;
}
// 3. Private methods
fn validate ( & self ) -> bool {
! self . content . is_empty ()
}
}
Module Organization
File naming
Use snake_case for module files: user_profile.rs
document_handler.rs
Module declaration
In lib.rs or mod.rs: mod user_profile ;
mod document_handler ;
pub use user_profile :: UserProfile ;
pub use document_handler :: DocumentHandler ;
Import ordering
// 1. Standard library
use std :: collections :: HashMap ;
use std :: sync :: Arc ;
// 2. External crates
use anyhow :: Result ;
use serde :: { Deserialize , Serialize };
// 3. Internal modules
use crate :: user :: UserProfile ;
use crate :: error :: FlowyError ;
Async Code
Use async/await for asynchronous operations:
use tokio :: time :: sleep;
pub async fn load_document ( id : & str ) -> Result < Document > {
// Simulate async operation
sleep ( Duration :: from_millis ( 100 )) . await ;
let doc = database . get ( id ) . await ? ;
Ok ( doc )
}
Testing
Organize tests clearly:
#[cfg(test)]
mod tests {
use super ::* ;
#[test]
fn test_document_creation () {
let doc = Document :: new ( "123" . to_string (), "Title" . to_string ());
assert_eq! ( doc . id, "123" );
}
#[tokio :: test]
async fn test_async_load () {
let doc = load_document ( "123" ) . await . unwrap ();
assert! ( ! doc . title . is_empty ());
}
}
General Best Practices
Keep Functions Small Functions should do one thing well. Aim for under 50 lines.
Avoid Deep Nesting Use early returns and helper functions to reduce nesting.
Write Tests Test new code and maintain existing test coverage.
Document Public APIs All public functions and types should have documentation.
Pre-commit Checks
Before committing, run:
cd appflowy_flutter
# Format code
flutter format .
# Analyze code
flutter analyze
# Run tests
flutter test
cd rust-lib
# Format code
cargo fmt
# Check formatting
cargo fmt -- --check
# Run Clippy
cargo clippy -- -D warnings
# Run tests
cargo test
CI/CD Enforcement
The following checks run automatically on all PRs:
Code formatting (Dart and Rust)
Linting (dartanalyzer, Clippy)
Unit tests
Integration tests
Build verification
PRs that fail these checks will not be merged. Fix all issues before requesting review.
Next Steps
Testing Learn about testing practices
Contributing Contribute to AppFlowy
Architecture Understand the architecture
Building Build AppFlowy from source