|
| 1 | +# Developer Guide for markdownr |
| 2 | + |
| 3 | +This document provides guidelines for agentic coding agents working on this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +markdownr is a Flutter Android app that converts URLs to Markdown format and allows sharing to other apps. It uses the readability algorithm to extract content from web pages. |
| 8 | + |
| 9 | +## Tech Stack |
| 10 | + |
| 11 | +- **Flutter**: 3.32Android only.0+ () |
| 12 | +- **Dart**: 3.0.0+ |
| 13 | +- **Key dependencies**: http, html2md, flutter_markdown_plus, share_plus, receive_sharing_intent, readability |
| 14 | +- **Testing**: flutter_test, mockito, build_runner |
| 15 | +- **Linting**: flutter_lints |
| 16 | + |
| 17 | +## Build / Lint / Test Commands |
| 18 | + |
| 19 | +### Running Tests |
| 20 | + |
| 21 | +```bash |
| 22 | +# Run all tests |
| 23 | +flutter test |
| 24 | + |
| 25 | +# Run a single test file |
| 26 | +flutter test test/converter_test.dart |
| 27 | + |
| 28 | +# Run a specific test by name |
| 29 | +flutter test --name "convert should handle exceptions" |
| 30 | +``` |
| 31 | + |
| 32 | +### Linting & Formatting |
| 33 | + |
| 34 | +```bash |
| 35 | +# Format code (required before committing) |
| 36 | +dart format --set-exit-if-changed . |
| 37 | + |
| 38 | +# Run static analysis |
| 39 | +flutter analyze |
| 40 | +``` |
| 41 | + |
| 42 | +### Building |
| 43 | + |
| 44 | +```bash |
| 45 | +# Build debug APK |
| 46 | +flutter build apk |
| 47 | + |
| 48 | +# Build release APK |
| 49 | +flutter build apk --release |
| 50 | + |
| 51 | +# Get dependencies |
| 52 | +flutter pub get |
| 53 | +``` |
| 54 | + |
| 55 | +### Code Generation |
| 56 | + |
| 57 | +```bash |
| 58 | +# Generate mock classes (required after modifying mocks) |
| 59 | +dart run build_runner build |
| 60 | +``` |
| 61 | + |
| 62 | +### Taskfile Commands |
| 63 | + |
| 64 | +The project includes a Taskfile.yaml with common tasks: |
| 65 | + |
| 66 | +```bash |
| 67 | +# Run tests |
| 68 | +task test |
| 69 | + |
| 70 | +# Run lint and format checks |
| 71 | +task lint |
| 72 | + |
| 73 | +# Generate mocks |
| 74 | +task build-mocks |
| 75 | +``` |
| 76 | + |
| 77 | +## Code Style Guidelines |
| 78 | + |
| 79 | +### General Conventions |
| 80 | + |
| 81 | +- Follow Flutter/Dart best practices |
| 82 | +- Use `analysis_options.yaml` which includes `package:flutter_lints/flutter.yaml` |
| 83 | +- Enable strict null safety (Dart 3.0+) |
| 84 | +- Prefer immutability: use `const` constructors where possible |
| 85 | + |
| 86 | +### Imports |
| 87 | + |
| 88 | +Organize imports in the following order: |
| 89 | +1. Dart core libraries (`dart:io`, `dart:async`, etc.) |
| 90 | +2. Package imports (`package:flutter/...`) |
| 91 | +3. Relative imports (`package:markdownr/...`) |
| 92 | + |
| 93 | +```dart |
| 94 | +import 'dart:async'; |
| 95 | +
|
| 96 | +import 'package:flutter/material.dart'; |
| 97 | +import 'package:flutter/services.dart'; |
| 98 | +import 'package:markdownr/converter.dart'; |
| 99 | +import 'package:markdownr/settings.dart'; |
| 100 | +``` |
| 101 | + |
| 102 | +### Naming Conventions |
| 103 | + |
| 104 | +- **Classes**: PascalCase (`Url2MdConverter`, `HomePage`) |
| 105 | +- **Functions/Methods**: camelCase (`convertPage`, `_buildAppBar`) |
| 106 | +- **Private members**: prefix with underscore (`_httpClient`, `_controller`) |
| 107 | +- **Constants**: camelCase or SCREAMING_SNAKE_CASE depending on context |
| 108 | +- **Files**: snake_case (`converter_test.dart`, `httpclient.dart`) |
| 109 | + |
| 110 | +### Class Structure |
| 111 | + |
| 112 | +```dart |
| 113 | +class MyClass { |
| 114 | + // Final fields first |
| 115 | + final String _someField; |
| 116 | +
|
| 117 | + // Constructor |
| 118 | + MyClass({required String someField}) : _someField = someField; |
| 119 | +
|
| 120 | + // Public methods |
| 121 | + Future<void> doSomething() async { } |
| 122 | +
|
| 123 | + // Private methods |
| 124 | + void _helperMethod() { } |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +### Error Handling |
| 129 | + |
| 130 | +- Use try-catch blocks for operations that may fail |
| 131 | +- Show user-friendly error messages via notification service |
| 132 | +- Return sensible defaults on error (see `converter.dart:66-75`) |
| 133 | + |
| 134 | +```dart |
| 135 | +try { |
| 136 | + var html = await _httpClient.getPage(url); |
| 137 | + // process... |
| 138 | +} catch (e) { |
| 139 | + _notificationService.showToast("$e"); |
| 140 | + return defaultValue; |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +### Testing Conventions |
| 145 | + |
| 146 | +- Test files: `test/<feature>_test.dart` |
| 147 | +- Use mockito for mocking dependencies |
| 148 | +- Generate mocks with `@GenerateNiceMocks` annotation |
| 149 | +- Run `dart run build_runner build` after creating/updating mocks |
| 150 | +- Group tests with `group()` and use `test()` for individual cases |
| 151 | + |
| 152 | +```dart |
| 153 | +@GenerateNiceMocks([ |
| 154 | + MockSpec<HttpClient>(), |
| 155 | + MockSpec<NotificationService>(), |
| 156 | +]) |
| 157 | +import 'my_test.mocks.dart'; |
| 158 | +
|
| 159 | +void main() { |
| 160 | + group('MyClass', () { |
| 161 | + late MockHttpClient mockHttpClient; |
| 162 | + late MyClass myClass; |
| 163 | +
|
| 164 | + setUp(() { |
| 165 | + mockHttpClient = MockHttpClient(); |
| 166 | + myClass = MyClass(httpClient: mockHttpClient); |
| 167 | + }); |
| 168 | +
|
| 169 | + test('should do something', () async { |
| 170 | + when(mockHttpClient.getPage(any)).thenAnswer((_) async => 'result'); |
| 171 | + final result = await myClass.doSomething(); |
| 172 | + expect(result, 'expected'); |
| 173 | + }); |
| 174 | + }); |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +### Flutter-Specific Guidelines |
| 179 | + |
| 180 | +- Use `const` for widgets and constructors when possible |
| 181 | +- Follow Flutter's widget composition patterns |
| 182 | +- Use `setState` for local state management (simple app) |
| 183 | +- Handle async operations in `initState` properly |
| 184 | +- Check `context.mounted` before using context in async callbacks |
| 185 | + |
| 186 | +```dart |
| 187 | +// Good: Check mounted before using context |
| 188 | +Future.delayed(Duration.zero, () { |
| 189 | + if (context.mounted) { |
| 190 | + ScaffoldMessenger.of(context).showSnackBar(...); |
| 191 | + } |
| 192 | +}); |
| 193 | +``` |
| 194 | + |
| 195 | +### Widget Build Methods |
| 196 | + |
| 197 | +- Extract UI parts into private methods starting with `_build` |
| 198 | +- Keep `build()` method clean and readable |
| 199 | +- Use descriptive names: `_buildAppBar()`, `_buildUrlInput()` |
| 200 | + |
| 201 | +## Project Structure |
| 202 | + |
| 203 | +- `lib`: contains the flutter code of the app |
| 204 | +- `test`: contains the tests |
| 205 | + |
| 206 | +## CI/CD |
| 207 | + |
| 208 | +The project uses GitHub Actions to build, test and publish the application. |
| 209 | + |
| 210 | +## Common Issues & Solutions |
| 211 | + |
| 212 | +1. **Mocks not found**: Run `dart run build_runner build` to regenerate mocks |
| 213 | +2. **Lint errors**: Run `dart format . && flutter analyze` before committing |
| 214 | +3. **Missing dependencies**: Run `flutter pub get` after updating pubspec.yaml |
0 commit comments