Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Flutter

Application de Gestion de Notes Multi-Plateforme

Un cours complet pour maîtriser Application de Gestion de Notes Multi-Plateforme.

0 chapitres 0 QCM Accès gratuit
 
  1. 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
  2. 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
      );
      
  3. Implémentation Backend (Spring Boot)

    1. 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>
        
    2. 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 Set tags = new HashSet<>();
              
              // Getters and setters
          }
          
          public enum Visibility {
              PRIVATE, SHARED, PUBLIC
          }
    3. 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;
            }
        }
    4. 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));
            }
        }
  4. Implémentation Frontend (Flutter)

    1. 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
    2. Implémentation des principaux composants
      1. 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/
      2. Modèles de données

        • lib/models/note.dart
            import '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 List tags;
              
              @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.dart
            import '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(Map json) {
                return Tag(
                  id: json['id'],
                  label: json['label'],
                );
              }
              
              Map toJson() {
                return {
                  'id': id,
                  'label': label,
                };
              }
            }
      3. Services

        • lib/services/api_service.dart
            import '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;
              
              Future setToken(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.dart
            import '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';
              
              Future init() 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>(pendingOpsBox);
              }
              
              Future saveNote(Note note) async {
                final box = Hive.box(notesBox);
                await box.put(note.id, note);
              }
              
              Future> getNotes() async {
                final box = Hive.box(notesBox);
                return box.values.toList();
              }
              
              Future getNote(String id) async {
                final box = Hive.box(notesBox);
                return box.get(id);
              }
              
              Future deleteNote(String id) async {
                final box = Hive.box(notesBox);
                await box.delete(id);
              }
              
              Future addPendingOperation(String type, Map data) async {
                final box = Hive.box>(pendingOpsBox);
                final id = DateTime.now().millisecondsSinceEpoch.toString();
                await box.put(id, {'type': type, 'data': data, 'timestamp': DateTime.now()});
              }
              
              Future>> getPendingOperations() async {
                final box = Hive.box>(pendingOpsBox);
                return box.values.toList();
              }
              
              Future removePendingOperation(String id) async {
                final box = Hive.box>(pendingOpsBox);
                await box.delete(id);
              }
              
              Future clearAllData() async {
                final notesBox = Hive.box(notesBox);
                final pendingOpsBox = Hive.box>(pendingOpsBox);
                
                await notesBox.clear();
                await pendingOpsBox.clear();
              }
            }
      4. 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();
            }
          }
      5. É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,
                                              );
                                            },
                                          );
                                        },
                                      ),
                          ),
                        ),
                      ],
                    );
                  },
                ),
              );
            }
          }
      6. 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),
                ),
              );
            }
          }
      7. Utilitaires

        • lib/utils/constants.dart
            class 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.dart
            import 'package:connectivity_plus/connectivity_plus.dart';
            
            class NetworkUtils {
              static Future isConnected() 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(),
                    },
                  ),
                );
              }
            }
    3. Structure du projet Flutter