Architecture du Projet
-
notes-app/
├── backend/# API Spring Boot
│ ├── src/main/java/com/notes/
│ │ ├── controller/ # Contrôleurs REST
│ │ ├── service/ # Logique métier
│ │ ├── repository/ # Accès aux données
│ │ ├── model/ # Entités JPA
│ │ ├── security/ # Configuration JWT
│ │ └── dto/ # Objets de transfert
│ ├── src/main/resources/
│ │ ├── application.properties
│ │ └── data.sql # Données initiales
│ ├── Dockerfile
│ └── pom.xml
├── frontend/ # Application Flutter
│ ├── lib/
│ │ ├── models/ # Modèles de données
│ │ ├── services/ # Services API et base locale
│ │ ├── providers/ # State management
│ │ ├── screens/ # Écrans de l'application
│ │ ├── widgets/ # Composants réutilisables
│ │ └── utils/ # Utilitaires
│ ├── assets/
│ ├── pubspec.yaml
│ └── Dockerfile
├── docker-compose.yml # Configuration multi-conteneurs
└── README.md # Documentation
-
Implémentation Backend (Spring Boot)
CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() ); CREATE TABLE notes ( id BIGSERIAL PRIMARY KEY, owner_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, title TEXT NOT NULL, content_md TEXT, visibility VARCHAR(10) NOT NULL CHECK (visibility IN ('PRIVATE','SHARED','PUBLIC')), created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() ); CREATE TABLE tags ( id BIGSERIAL PRIMARY KEY, label VARCHAR(100) UNIQUE NOT NULL ); CREATE TABLE note_tag ( note_id BIGINT NOT NULL REFERENCES notes(id) ON DELETE CASCADE, tag_id BIGINT NOT NULL REFERENCES tags(id) ON DELETE CASCADE, PRIMARY KEY (note_id, tag_id) ); CREATE TABLE shares ( id BIGSERIAL PRIMARY KEY, note_id BIGINT NOT NULL REFERENCES notes(id) ON DELETE CASCADE, shared_with_user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, permission VARCHAR(10) NOT NULL CHECK (permission IN ('READ')) ); CREATE TABLE public_links ( id BIGSERIAL PRIMARY KEY, note_id BIGINT NOT NULL REFERENCES notes(id) ON DELETE CASCADE, url_token VARCHAR(128) UNIQUE NOT NULL, expires_at TIMESTAMP WITH TIME ZONE NULL ); -
Implémentation Backend (Spring Boot)
-
Configuration des dépendances (pom.xml)
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies> -
Modèles de données
User.java@Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String email; @Column(nullable = false) private String passwordHash; private LocalDateTime createdAt; // Getters and setters }Note.java@Entity @Table(name = "notes") public class Note { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String title; @Column(columnDefinition = "TEXT") private String contentMd; @Enumerated(EnumType.STRING) private Visibility visibility = Visibility.PRIVATE; private LocalDateTime createdAt; private LocalDateTime updatedAt; @ManyToOne @JoinColumn(name = "owner_id", nullable = false) private User owner; @ManyToMany @JoinTable( name = "note_tags", joinColumns = @JoinColumn(name = "note_id"), inverseJoinColumns = @JoinColumn(name = "tag_id") ) private Settags = new HashSet<>(); // Getters and setters } public enum Visibility { PRIVATE, SHARED, PUBLIC }
Authentification JWT
JwtAuthenticationFilter.java
public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = getJwtFromRequest(request); if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) { Long userId = jwtTokenProvider.getUserIdFromJWT(jwt); UserDetails userDetails = customUserDetailsService.loadUserById(userId); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception ex) { logger.error("Could not set user authentication in security context", ex); } filterChain.doFilter(request, response); } private String getJwtFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } }Contrôleur d'authentification
AuthController.java
@RestController @RequestMapping("/api/auth") public class AuthController { @PostMapping("/login") public ResponseEntity> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getEmail(), loginRequest.getPassword() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = jwtTokenProvider.generateToken(authentication); return ResponseEntity.ok(new JwtAuthenticationResponse(jwt)); } @PostMapping("/register") public ResponseEntity> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) { if (userRepository.existsByEmail(signUpRequest.getEmail())) { return ResponseEntity.badRequest().body(new ApiResponse(false, "Email is already taken!")); } User user = new User(); user.setEmail(signUpRequest.getEmail()); user.setPasswordHash(passwordEncoder.encode(signUpRequest.getPassword())); user.setCreatedAt(LocalDateTime.now()); userRepository.save(user); Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( signUpRequest.getEmail(), signUpRequest.getPassword() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = jwtTokenProvider.generateToken(authentication); return ResponseEntity.ok(new JwtAuthenticationResponse(jwt)); } }
-
-
Implémentation Frontend (Flutter)
Structure du projet Flutter
-
frontend/
├── lib/
│ ├── main.dart
│ ├── models/
│ │ ├── note.dart
│ │ ├── tag.dart
│ │ ├── user.dart
│ │ ├── share.dart
│ │ └── api_response.dart
│ ├── services/
│ │ ├── api_service.dart
│ │ ├── auth_service.dart
│ │ ├── local_db_service.dart
│ │ └── sync_service.dart
│ ├── providers/
│ │ ├── auth_provider.dart
│ │ ├── notes_provider.dart
│ │ └── sync_provider.dart
│ ├── screens/
│ │ ├── auth/
│ │ │ ├── login_screen.dart
│ │ │ └── register_screen.dart
│ │ ├── notes/
│ │ │ ├── notes_list_screen.dart
│ │ │ ├── note_detail_screen.dart
│ │ │ ├── note_edit_screen.dart
│ │ │ └── share_info_screen.dart
│ │ └── splash_screen.dart
│ ├── widgets/
│ │ ├── note_card.dart
│ │ ├── tag_chip.dart
│ │ ├── markdown_preview.dart
│ │ ├── search_bar.dart
│ │ └── offline_indicator.dart
│ └── utils/
│ ├── constants.dart
│ ├── network_utils.dart
│ └── date_utils.dart
├── assets/
├── pubspec.yaml
└── Dockerfile
Implémentation des principaux composants
Configuration du pubspec.yaml
name: notes_app description: A multi-platform note management application version: 1.0.0+1 environment: sdk: '>=3.0.0 <4.0.0' dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 http: ^1.1.0 shared_preferences: ^2.2.2 flutter_markdown: ^0.6.15 hive: ^2.2.3 hive_flutter: ^1.1.0 path_provider: ^2.0.15 provider: ^6.0.5 pull_to_refresh: ^2.0.0 connectivity_plus: ^4.0.2 intl: ^0.18.1 dev_dependencies: flutter_test: sdk: flutter hive_generator: ^1.1.3 build_runner: ^2.4.6 flutter: uses-material-design: true assets: - assets/Modèles de données
lib/models/note.dartimport 'package:hive/hive.dart'; part 'note.g.dart'; @HiveType(typeId: 0) class Note { @HiveField(0) final String id; @HiveField(1) final String ownerId; @HiveField(2) final String title; @HiveField(3) final String contentMd; @HiveField(4) final String visibility; // PRIVATE, SHARED, PUBLIC @HiveField(5) final DateTime createdAt; @HiveField(6) final DateTime updatedAt; @HiveField(7) final Listtags; @HiveField(8) final bool isSynced; @HiveField(9) final String? localId; // For offline operations Note({ required this.id, required this.ownerId, required this.title, required this.contentMd, required this.visibility, required this.createdAt, required this.updatedAt, this.tags = const [], this.isSynced = true, this.localId, }); // Copy with method for updates Note copyWith({ String? id, String? ownerId, String? title, String? contentMd, String? visibility, DateTime? createdAt, DateTime? updatedAt, List ? tags, bool? isSynced, String? localId, }) { return Note( id: id ?? this.id, ownerId: ownerId ?? this.ownerId, title: title ?? this.title, contentMd: contentMd ?? this.contentMd, visibility: visibility ?? this.visibility, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, tags: tags ?? this.tags, isSynced: isSynced ?? this.isSynced, localId: localId ?? this.localId, ); } // Convert from JSON factory Note.fromJson(Map json) { return Note( id: json['id'], ownerId: json['ownerId'], title: json['title'], contentMd: json['contentMd'], visibility: json['visibility'], createdAt: DateTime.parse(json['createdAt']), updatedAt: DateTime.parse(json['updatedAt']), tags: (json['tags'] as List ?) ?.map((tag) => Tag.fromJson(tag)) .toList() ?? [], isSynced: true, ); } // Convert to JSON Map toJson() { return { 'id': id, 'ownerId': ownerId, 'title': title, 'contentMd': contentMd, 'visibility': visibility, 'createdAt': createdAt.toIso8601String(), 'updatedAt': updatedAt.toIso8601String(), 'tags': tags.map((tag) => tag.toJson()).toList(), }; } } lib/models/tag.dartimport 'package:hive/hive.dart'; part 'tag.g.dart'; @HiveType(typeId: 1) class Tag { @HiveField(0) final String id; @HiveField(1) final String label; Tag({ required this.id, required this.label, }); factory Tag.fromJson(Mapjson) { return Tag( id: json['id'], label: json['label'], ); } Map toJson() { return { 'id': id, 'label': label, }; } }
Services
lib/services/api_service.dartimport 'dart:convert'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import '../models/note.dart'; import '../models/tag.dart'; import '../utils/constants.dart'; class ApiService { static final ApiService _instance = ApiService._internal(); factory ApiService() => _instance; ApiService._internal(); String? _token; FuturesetToken(String token) async { _token = token; final prefs = await SharedPreferences.getInstance(); await prefs.setString('auth_token', token); } Future getToken() async { if (_token != null) return _token; final prefs = await SharedPreferences.getInstance(); _token = prefs.getString('auth_token'); return _token; } Future clearToken() async { _token = null; final prefs = await SharedPreferences.getInstance(); await prefs.remove('auth_token'); } Map _getHeaders() { return { 'Content-Type': 'application/json', 'Authorization': 'Bearer $_token', }; } Future - > getNotes({String? search, List
? tags}) async { final token = await getToken(); if (token == null) throw Exception('Not authenticated'); final url = Uri.parse('${Constants.apiBaseUrl}/notes'); final response = await http.get(url, headers: _getHeaders()); if (response.statusCode == 200) { final List data = json.decode(response.body); return data.map((json) => Note.fromJson(json)).toList(); } else { throw Exception('Failed to load notes'); } } Future getNote(String id) async { final token = await getToken(); if (token == null) throw Exception('Not authenticated'); final url = Uri.parse('${Constants.apiBaseUrl}/notes/$id'); final response = await http.get(url, headers: _getHeaders()); if (response.statusCode == 200) { return Note.fromJson(json.decode(response.body)); } else { throw Exception('Failed to load note'); } } Future createNote(Note note) async { final token = await getToken(); if (token == null) throw Exception('Not authenticated'); final url = Uri.parse('${Constants.apiBaseUrl}/notes'); final response = await http.post( url, headers: _getHeaders(), body: json.encode(note.toJson()), ); if (response.statusCode == 201) { return Note.fromJson(json.decode(response.body)); } else { throw Exception('Failed to create note'); } } Future updateNote(Note note) async { final token = await getToken(); if (token == null) throw Exception('Not authenticated'); final url = Uri.parse('${Constants.apiBaseUrl}/notes/${note.id}'); final response = await http.put( url, headers: _getHeaders(), body: json.encode(note.toJson()), ); if (response.statusCode == 200) { return Note.fromJson(json.decode(response.body)); } else { throw Exception('Failed to update note'); } } Future deleteNote(String id) async { final token = await getToken(); if (token == null) throw Exception('Not authenticated'); final url = Uri.parse('${Constants.apiBaseUrl}/notes/$id'); final response = await http.delete(url, headers: _getHeaders()); if (response.statusCode != 204) { throw Exception('Failed to delete note'); } } Future - > getTags() async {
final token = await getToken();
if (token == null) throw Exception('Not authenticated');
final url = Uri.parse('${Constants.apiBaseUrl}/tags');
final response = await http.get(url, headers: _getHeaders());
if (response.statusCode == 200) {
final List
data = json.decode(response.body); return data.map((json) => Tag.fromJson(json)).toList(); } else { throw Exception('Failed to load tags'); } } } lib/services/local_db_service.dartimport 'package:hive/hive.dart'; import 'package:path_provider/path_provider.dart'; import '../models/note.dart'; import '../models/tag.dart'; class LocalDbService { static final LocalDbService _instance = LocalDbService._internal(); factory LocalDbService() => _instance; LocalDbService._internal(); static const String notesBox = 'notes'; static const String pendingOpsBox = 'pending_operations'; Futureinit() async { final appDocumentDir = await getApplicationDocumentsDirectory(); Hive.init(appDocumentDir.path); // Register adapters Hive.registerAdapter(NoteAdapter()); Hive.registerAdapter(TagAdapter()); // Open boxes await Hive.openBox (notesBox); await Hive.openBox
Providers (State Management)
lib/providers/notes_provider.dart
import 'package:flutter/foundation.dart'; import '../models/note.dart'; import '../services/api_service.dart'; import '../services/local_db_service.dart'; import '../utils/network_utils.dart'; class NotesProvider with ChangeNotifier { final ApiService _apiService = ApiService(); final LocalDbService _localDbService = LocalDbService(); List_notes = []; bool _isLoading = false; String _error = ''; bool _isOffline = false; List get notes => _notes; bool get isLoading => _isLoading; String get error => _error; bool get isOffline => _isOffline; NotesProvider() { _checkConnectivity(); loadNotes(); } Future _checkConnectivity() async { _isOffline = !(await NetworkUtils.isConnected()); notifyListeners(); } Future loadNotes() async { _isLoading = true; notifyListeners(); try { if (await NetworkUtils.isConnected()) { // Online: fetch from API _notes = await _apiService.getNotes(); // Save to local DB for (final note in _notes) { await _localDbService.saveNote(note); } _isOffline = false; } else { // Offline: load from local DB _notes = await _localDbService.getNotes(); _isOffline = true; } _error = ''; } catch (e) { _error = e.toString(); // Fallback to local DB if online fetch fails _notes = await _localDbService.getNotes(); } _isLoading = false; notifyListeners(); } Future refreshNotes() async { await loadNotes(); } Future createNote(Note note) async { try { Note createdNote; if (await NetworkUtils.isConnected()) { // Online: create via API createdNote = await _apiService.createNote(note); await _localDbService.saveNote(createdNote); } else { // Offline: save locally and queue operation final localId = DateTime.now().millisecondsSinceEpoch.toString(); final localNote = note.copyWith( id: '', // Will be assigned by server later isSynced: false, localId: localId, ); await _localDbService.saveNote(localNote); await _localDbService.addPendingOperation( 'create', note.toJson(), ); createdNote = localNote; _isOffline = true; } _notes.insert(0, createdNote); notifyListeners(); return createdNote; } catch (e) { _error = e.toString(); notifyListeners(); rethrow; } } Future updateNote(Note note) async { try { Note updatedNote; if (await NetworkUtils.isConnected()) { // Online: update via API updatedNote = await _apiService.updateNote(note); await _localDbService.saveNote(updatedNote); } else { // Offline: update locally and queue operation final localNote = note.copyWith(isSynced: false); await _localDbService.saveNote(localNote); await _localDbService.addPendingOperation( 'update', note.toJson(), ); updatedNote = localNote; _isOffline = true; } final index = _notes.indexWhere((n) => n.id == note.id); if (index != -1) { _notes[index] = updatedNote; } notifyListeners(); return updatedNote; } catch (e) { _error = e.toString(); notifyListeners(); rethrow; } } Future deleteNote(String id) async { try { final note = _notes.firstWhere((n) => n.id == id); if (await NetworkUtils.isConnected()) { // Online: delete via API await _apiService.deleteNote(id); await _localDbService.deleteNote(id); } else { // Offline: mark for deletion and queue operation await _localDbService.addPendingOperation( 'delete', {'id': id}, ); _isOffline = true; } _notes.removeWhere((n) => n.id == id); notifyListeners(); } catch (e) { _error = e.toString(); notifyListeners(); rethrow; } } List searchNotes(String query) { if (query.isEmpty) return _notes; return _notes.where((note) { return note.title.toLowerCase().contains(query.toLowerCase()) || note.contentMd.toLowerCase().contains(query.toLowerCase()); }).toList(); } List filterNotesByTag(String tagLabel) { return _notes.where((note) { return note.tags.any((tag) => tag.label == tagLabel); }).toList(); } } Écrans principaux
lib/screens/auth/login_screen.dart
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../providers/auth_provider.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({Key? key}) : super(key: key); @override _LoginScreenState createState() => _LoginScreenState(); } class _LoginScreenState extends State{ final _formKey = GlobalKey (); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); bool _isLoading = false; Future _login() async { if (!_formKey.currentState!.validate()) return; setState(() => _isLoading = true); try { await Provider.of (context, listen: false).login( _emailController.text, _passwordController.text, ); // Navigate to notes list on success Navigator.pushReplacementNamed(context, '/notes'); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Login failed: $e')), ); } setState(() => _isLoading = false); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Login')), body: Padding( padding: const EdgeInsets.all(16.0), child: Form( key: _formKey, child: Column( children: [ TextFormField( controller: _emailController, decoration: const InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress, validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your email'; } return null; }, ), TextFormField( controller: _passwordController, decoration: const InputDecoration(labelText: 'Password'), obscureText: true, validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your password'; } return null; }, ), const SizedBox(height: 20), _isLoading ? const CircularProgressIndicator() : ElevatedButton( onPressed: _login, child: const Text('Login'), ), TextButton( onPressed: () { Navigator.pushNamed(context, '/register'); }, child: const Text('Create an account'), ), ], ), ), ), ); } } lib/screens/notes/notes_list_screen.dart
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import '../../providers/notes_provider.dart'; import '../../widgets/note_card.dart'; import '../../widgets/offline_indicator.dart'; import '../../widgets/search_bar.dart'; class NotesListScreen extends StatefulWidget { const NotesListScreen({Key? key}) : super(key: key); @override _NotesListScreenState createState() => _NotesListScreenState(); } class _NotesListScreenState extends State{ final RefreshController _refreshController = RefreshController(); String _searchQuery = ''; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { Provider.of (context, listen: false).loadNotes(); }); } Future _onRefresh() async { try { await Provider.of (context, listen: false).refreshNotes(); _refreshController.refreshCompleted(); } catch (e) { _refreshController.refreshFailed(); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('My Notes'), actions: [ IconButton( icon: const Icon(Icons.add), onPressed: () { Navigator.pushNamed(context, '/edit-note'); }, ), ], ), body: Consumer ( builder: (context, notesProvider, child) { final notes = _searchQuery.isEmpty ? notesProvider.notes : notesProvider.searchNotes(_searchQuery); return Column( children: [ const OfflineIndicator(), SearchBar( onSearch: (query) { setState(() => _searchQuery = query); }, ), Expanded( child: SmartRefresher( controller: _refreshController, onRefresh: _onRefresh, child: notesProvider.isLoading ? const Center(child: CircularProgressIndicator()) : notes.isEmpty ? const Center(child: Text('No notes found')) : ListView.builder( itemCount: notes.length, itemBuilder: (context, index) { final note = notes[index]; return NoteCard( note: note, onTap: () { Navigator.pushNamed( context, '/note-detail', arguments: note.id, ); }, ); }, ), ), ), ], ); }, ), ); } } Widgets
lib/widgets/note_card.dart
import 'package:flutter/material.dart'; import '../models/note.dart'; import 'tag_chip.dart'; class NoteCard extends StatelessWidget { final Note note; final VoidCallback onTap; const NoteCard({ Key? key, required this.note, required this.onTap, }) : super(key: key); @override Widget build(BuildContext context) { return Card( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: ListTile( onTap: onTap, title: Text( note.title, maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( note.contentMd, maxLines: 2, overflow: TextOverflow.ellipsis, ), if (note.tags.isNotEmpty) ...[ const SizedBox(height: 4), Wrap( spacing: 4, children: note.tags .map((tag) => TagChip(label: tag.label)) .toList(), ), ], ], ), trailing: !note.isSynced ? const Icon(Icons.sync_disabled, size: 16) : null, ), ); } }lib/widgets/markdown_preview.dart
import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; class MarkdownPreview extends StatelessWidget { final String text; const MarkdownPreview({Key? key, required this.text}) : super(key: key); @override Widget build(BuildContext context) { return Markdown( data: text, styleSheet: MarkdownStyleSheet( p: const TextStyle(fontSize: 16, height: 1.5), h1: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), h2: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold), h3: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), ); } }Utilitaires
lib/utils/constants.dartclass Constants { static const String apiBaseUrl = 'http://your-backend-url:8080/api'; // Hive type IDs static const int noteTypeId = 0; static const int tagTypeId = 1; }lib/utils/network_utils.dartimport 'package:connectivity_plus/connectivity_plus.dart'; class NetworkUtils { static FutureisConnected() async { final connectivityResult = await Connectivity().checkConnectivity(); return connectivityResult != ConnectivityResult.none; } } Point d'entrée principal
lib/main.dart
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'providers/auth_provider.dart'; import 'providers/notes_provider.dart'; import 'screens/auth/login_screen.dart'; import 'screens/auth/register_screen.dart'; import 'screens/notes/notes_list_screen.dart'; import 'screens/notes/note_detail_screen.dart'; import 'screens/notes/note_edit_screen.dart'; import 'screens/notes/share_info_screen.dart'; import 'screens/splash_screen.dart'; import 'services/local_db_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize Hive await LocalDbService().init(); runApp(const NotesApp()); } class NotesApp extends StatelessWidget { const NotesApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => AuthProvider()), ChangeNotifierProvider(create: (_) => NotesProvider()), ], child: MaterialApp( title: 'Notes App', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), initialRoute: '/splash', routes: { '/splash': (context) => const SplashScreen(), '/login': (context) => const LoginScreen(), '/register': (context) => const RegisterScreen(), '/notes': (context) => const NotesListScreen(), '/note-detail': (context) => const NoteDetailScreen(), '/edit-note': (context) => const NoteEditScreen(), '/share-info': (context) => const ShareInfoScreen(), }, ), ); } }
Structure du projet Flutter