Table of Contents
How To Build A Windows Desktop App Using Flutter And Local Database
Flutter has revolutionized cross-platform development, and with its expanding desktop support, building Windows applications has never been easier. In this comprehensive guide, we’ll walk through creating a fully functional Windows desktop app with local database integration using Flutter’s powerful framework.
“Flutter’s single codebase approach reduces development time by 50% compared to traditional desktop application development methods.” – Google Developer Survey 2023
Why Choose Flutter For Windows Desktop Development?
Before diving into the technical implementation, let’s understand why Flutter stands out for Windows app development:
- Single Codebase: Build for Windows, macOS, and Linux from one codebase
- Native Performance: Compiles to native ARM and x64 code
- Rich Widget Library: Beautiful, customizable UI components
- Hot Reload: Instant preview of changes during development
- Growing Ecosystem: Mature package ecosystem including database solutions
Prerequisites For Flutter Windows Development
To follow along with this tutorial, you’ll need:
- Flutter SDK (version 3.0 or higher)
- Visual Studio with C++ development tools
- Windows 10/11 (64-bit)
- Basic understanding of Dart programming
Setting Up Your Flutter Development Environment
Let’s begin by configuring your system for Flutter desktop development:
Step 1: Install Flutter SDK
Download the latest stable Flutter SDK from the official website and extract it to your preferred location. Add the Flutter bin directory to your system PATH to access Flutter commands globally.
Step 2: Enable Desktop Support
Run the following command in your terminal to enable Windows desktop development:
flutter config --enable-windows-desktop
Step 3: Install Visual Studio
Install Visual Studio 2022 with the following workloads:
- “Desktop development with C++”
- Windows 10/11 SDK
Step 4: Verify Installation
Run the doctor command to check your setup:
flutter doctor
All checks should pass before proceeding. If you encounter issues, consult the Flutter documentation for troubleshooting.
Creating Your First Flutter Windows Project
With the environment ready, let’s create our application:
Initialize The Project
Create a new Flutter project with Windows support:
flutter create my_windows_app cd my_windows_app
Project Structure Overview
Your project contains several important directories:
- lib/: Main Dart code for your application
- windows/: Windows-specific runner and configuration
- pubspec.yaml: Project dependencies and metadata
Running The Default App
Launch the default Flutter counter app on Windows:
flutter run -d windows
This verifies your basic setup is working correctly before we add database functionality.
Integrating A Local Database In Your Flutter App
For data persistence in our Windows application, we’ll use SQLite through the sqflite
package, which works excellently for desktop applications.
Adding Database Dependencies
Open pubspec.yaml
and add these dependencies:
dependencies: sqflite: ^2.0.0+4 path: ^1.8.0 path_provider: ^2.0.11
Run flutter pub get
to install the packages.
Creating The Database Helper Class
Create a new file database_helper.dart
to manage database operations:
import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; class DatabaseHelper { static final DatabaseHelper instance = DatabaseHelper._init(); static Database? _database; DatabaseHelper._init(); Future<Database> get database async { if (_database != null) return _database!; _database = await _initDB('app_database.db'); return _database!; } Future<Database> _initDB(String filePath) async { final dbPath = await getDatabasesPath(); final path = join(dbPath, filePath); return await openDatabase(path, version: 1, onCreate: _createDB); } Future _createDB(Database db, int version) async { await db.execute(''' CREATE TABLE notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT NOT NULL ) '''); } }
Implementing CRUD Operations
Extend the DatabaseHelper
class with basic CRUD operations:
// Add to DatabaseHelper class Future<int> createNote(Map<String, dynamic> note) async { final db = await instance.database; return await db.insert('notes', note); } Future<List<Map<String, dynamic>>> readAllNotes() async { final db = await instance.database; return await db.query('notes', orderBy: 'created_at DESC'); } Future<int> updateNote(Map<String, dynamic> note) async { final db = await instance.database; return await db.update( 'notes', note, where: 'id = ?', whereArgs: [note['id']], ); } Future<int> deleteNote(int id) async { final db = await instance.database; return await db.delete( 'notes', where: 'id = ?', whereArgs: [id], ); }
Building The User Interface
With our database layer ready, let’s create a UI to interact with it.
Creating A Note Model
First, define a simple Note model in note_model.dart
:
class Note { int? id; String title; String content; DateTime createdAt; Note({ this.id, required this.title, required this.content, required this.createdAt, }); Map<String, dynamic> toMap() { return { 'title': title, 'content': content, 'created_at': createdAt.toIso8601String(), }; } factory Note.fromMap(Map<String, dynamic> map) { return Note( id: map['id'], title: map['title'], content: map['content'], createdAt: DateTime.parse(map['created_at']), ); } }
Building The Note List Screen
Create a screen to display all notes from the database:
class NotesListScreen extends StatefulWidget { @override _NotesListScreenState createState() => _NotesListScreenState(); } class _NotesListScreenState extends State<NotesListScreen> { late Future<List<Note>> notesFuture; @override void initState() { super.initState(); notesFuture = _fetchNotes(); } Future<List<Note>> _fetchNotes() async { final notesList = await DatabaseHelper.instance.readAllNotes(); return notesList.map((noteMap) => Note.fromMap(noteMap)).toList(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('My Notes App'), centerTitle: true, ), body: FutureBuilder<List<Note>>( future: notesFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); } else if (snapshot.hasError) { return Center(child: Text('Error loading notes')); } else if (!snapshot.hasData || snapshot.data!.isEmpty) { return Center(child: Text('No notes found')); } else { return ListView.builder( itemCount: snapshot.data!.length, itemBuilder: (context, index) { final note = snapshot.data![index]; return ListTile( title: Text(note.title), subtitle: Text(note.content), trailing: Text(DateFormat('MMM dd, yyyy').format(note.createdAt)), onTap: () => _navigateToNoteDetail(context, note), ); }, ); } }, ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () => _navigateToNoteDetail(context, null), ), ); } void _navigateToNoteDetail(BuildContext context, Note? note) async { final result = await Navigator.push( context, MaterialPageRoute( builder: (context) => NoteDetailScreen(note: note), ), ); if (result == true) { setState(() { notesFuture = _fetchNotes(); }); } } }
Creating The Note Detail Screen
Implement a screen to create/edit notes:
class NoteDetailScreen extends StatefulWidget { final Note? note; NoteDetailScreen({this.note}); @override _NoteDetailScreenState createState() => _NoteDetailScreenState(); } class _NoteDetailScreenState extends State<NoteDetailScreen> { final _formKey = GlobalKey<FormState>(); late TextEditingController _titleController; late TextEditingController _contentController; @override void initState() { super.initState(); _titleController = TextEditingController(text: widget.note?.title ?? ''); _contentController = TextEditingController(text: widget.note?.content ?? ''); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.note == null ? 'Add Note' : 'Edit Note'), actions: [ if (widget.note != null) IconButton( icon: Icon(Icons.delete), onPressed: _deleteNote, ), ], ), body: Padding( padding: EdgeInsets.all(16.0), child: Form( key: _formKey, child: Column( children: [ TextFormField( controller: _titleController, decoration: InputDecoration(labelText: 'Title'), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter a title'; } return null; }, ), SizedBox(height: 16), TextFormField( controller: _contentController, decoration: InputDecoration(labelText: 'Content'), maxLines: 5, validator: (value) { if (value == null || value.isEmpty) { return 'Please enter some content'; } return null; }, ), Spacer(), ElevatedButton( child: Text('Save'), onPressed: _saveNote, ), ], ), ), ), ); } Future_saveNote() async { if (_formKey.currentState!.validate()) { final note = Note( id: widget.note?.id, title: _titleController.text, content: _contentController.text, createdAt: widget.note?.createdAt ?? DateTime.now(), ); if (note.id == null) { await DatabaseHelper.instance.createNote(note.toMap()); } else { await DatabaseHelper.instance.updateNote(note.toMap()); } Navigator.pop(context, true); } } Future _deleteNote() async { final confirmed = await showDialog ( context: context, builder: (context) => AlertDialog( title: Text('Delete Note?'), content: Text('Are you sure you want to delete this note?'), actions: [ TextButton( child: Text('Cancel'), onPressed: () => Navigator.pop(context, false), ), TextButton( child: Text('Delete'), onPressed: () => Navigator.pop(context, true), ), ], ), ); if (confirmed == true && widget.note?.id != null) { await DatabaseHelper.instance.deleteNote(widget.note!.id!); Navigator.pop(context, true); } } @override void dispose() { _titleController.dispose(); _contentController.dispose(); super.dispose(); } }
Testing And Debugging Your Windows App
Before finalizing your application, thorough testing is essential.
Running The Application
Launch your app in debug mode:
flutter run -d windows
Common Issues And Solutions
- Database not initializing: Check file permissions and database path
- UI not updating: Ensure you’re calling setState() after database operations
- Performance issues: Consider using isolates for heavy database operations
Debugging Database Operations
Add logging to your database helper:
// Add to _initDB method print('Database initialized at: $path');
Building For Distribution
Once your app is ready, package it for Windows distribution.
Creating A Release Build
Generate an optimized build:
flutter build windows
Packaging Your Application
The build output is located in build\windows\runner\Release
. You can:
- Distribute the folder as a zip file
- Create an installer using WiX Toolset or Inno Setup
- Publish to the Microsoft Store
Advanced Features To Consider
Enhance your Windows application with these professional features:
Database Migrations
Implement versioned database schema changes for future updates.
Offline Synchronization
Add cloud sync capabilities when internet connection is available.
Native Windows Integration
Use the window_size
package to customize window behavior.
Conclusion
Building Windows desktop applications with Flutter offers a modern, efficient approach to cross-platform development. By combining Flutter’s powerful UI capabilities with local database persistence, you can create professional-grade applications with a single codebase.
This tutorial covered the complete process from setup to distribution, providing you with a solid foundation for your Flutter Windows development journey. The techniques learned here can be applied to various types of desktop applications requiring local data storage.
As you continue developing, explore additional Flutter packages that can enhance your Windows applications, such as file system access, system tray integration, and more advanced database solutions.
Be the first to write a comment.