Build a Flutter App with Offline Support using Hive and Connectivity

by Didin J. on Jun 29, 2025 Build a Flutter App with Offline Support using Hive and Connectivity

Learn how to build a Flutter app with offline support using Hive for local storage and Connectivity Plus to manage network status.

In today’s mobile-first world, users expect their apps to work seamlessly, online or offline. Whether it’s viewing cached content or making updates that sync later, offline support can significantly improve user experience, reliability, and performance.

In this tutorial, you’ll learn how to build a Flutter app that supports offline functionality using Hive, a lightweight and blazing-fast NoSQL database for Flutter, along with connectivity_plus to detect and respond to changes in network status. We’ll implement an offline-first logic to store and retrieve data locally, ensuring the app gracefully handles connectivity loss.

By the end of this guide, you’ll have a solid understanding of:

  • How to use Hive for local storage in Flutter

  • How to detect network connectivity with connectivity_plus

  • How to build a simple offline-capable Flutter app with real-time connectivity awareness

Let’s dive in and bring offline capability to your Flutter apps!

Project Setup

1. Create a New Flutter Project

If you haven’t already, create a new Flutter project:

flutter create flutter_offline_hive_app
cd flutter_offline_hive_app

2. Add Required Dependencies

Open pubspec.yaml and add the following packages:

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8
  hive: ^2.2.3
  hive_flutter: ^1.1.0
  path_provider: ^2.1.2
  connectivity_plus: ^6.1.4

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^6.0.0
  hive_generator: ^2.0.1
  build_runner: ^2.4.8

Then run:

flutter pub get

3. Initialize Hive

In main.dart, you need to initialize Hive and its storage path using path_provider.

import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final appDocumentDir = await getApplicationDocumentsDirectory();
  await Hive.initFlutter(appDocumentDir.path);
  await Hive.openBox('myBox'); // You can name it as needed

  runApp(const MyApp());
}

4. Create a Simple Data Model (Optional with Hive TypeAdapter)

If you want to store complex objects (not just primitives like String or int), define a model and generate a Hive adapter:

import 'package:hive/hive.dart';

part 'note.g.dart';

@HiveType(typeId: 0)
class Note {
  @HiveField(0)
  final String title;

  @HiveField(1)
  final String content;

  Note({required this.title, required this.content});
}

Then generate the adapter:

flutter pub run build_runner build

Register the adapter in main.dart:

Hive.registerAdapter(NoteAdapter());

Next, we can move on to building the UI, handling connectivity changes with connectivity_plusand writing the logic for reading and writing to Hive based on the network state.

Build the UI & Handle Connectivity

We’ll create a basic app to display a list of notes, with the ability to add new notes and detect connectivity status in real time.

1. Create the Home Page

Create a new file: lib/home_page.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:connectivity_plus/connectivity_plus.dart';

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  late Box box;
  late StreamSubscription<List<ConnectivityResult>> subscription;
  bool isOnline = true;

  final titleController = TextEditingController();
  final contentController = TextEditingController();

  @override
  void initState() {
    super.initState();
    box = Hive.box('myBox');
    checkConnectivity();
    listenToConnectivityChanges();
  }

  void checkConnectivity() async {
    final result = await Connectivity().checkConnectivity();
    setState(() {
      isOnline = result != ConnectivityResult.none;
    });
  }

  void listenToConnectivityChanges() {
    subscription = Connectivity().onConnectivityChanged.listen((result) {
      setState(() {
        isOnline = result != ConnectivityResult.none;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(isOnline ? 'Back Online' : 'You are Offline'),
          backgroundColor: isOnline ? Colors.green : Colors.red,
        ),
      );
    }) as StreamSubscription<ConnectivityResult>;
  }

  void addNote(String title, String content) {
    final note = {'title': title, 'content': content};
    box.add(note);
    titleController.clear();
    contentController.clear();
    setState(() {});
  }

  @override
  void dispose() {
    subscription.cancel();
    titleController.dispose();
    contentController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final notes = box.values.toList();

    return Scaffold(
      appBar: AppBar(
        title: const Text('Offline Notes App'),
        backgroundColor: isOnline ? Colors.green : Colors.grey,
      ),
      body: Column(
        children: [
          if (!isOnline)
            Container(
              color: Colors.red,
              width: double.infinity,
              padding: const EdgeInsets.all(8),
              child: const Text(
                'Offline Mode',
                style: TextStyle(color: Colors.white),
                textAlign: TextAlign.center,
              ),
            ),
          Expanded(
            child: ListView.builder(
              itemCount: notes.length,
              itemBuilder: (context, index) {
                final item = notes[index] as Map;
                return ListTile(
                  title: Text(item['title']),
                  subtitle: Text(item['content']),
                );
              },
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Column(
              children: [
                TextField(
                  controller: titleController,
                  decoration: const InputDecoration(labelText: 'Title'),
                ),
                TextField(
                  controller: contentController,
                  decoration: const InputDecoration(labelText: 'Content'),
                ),
                const SizedBox(height: 10),
                ElevatedButton(
                  onPressed: () {
                    if (titleController.text.isNotEmpty &&
                        contentController.text.isNotEmpty) {
                      addNote(titleController.text, contentController.text);
                    }
                  },
                  child: const Text('Add Note'),
                ),
              ],
            ),
          )
        ],
      ),
    );
  }
}

2. Update main.dart to Load the Home Page

import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'home_page.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final dir = await getApplicationDocumentsDirectory();
  await Hive.initFlutter(dir.path);
  await Hive.openBox('myBox');

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Offline Hive',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const HomePage(),
    );
  }
}

Next up, we can implement data sync logic when the device goes back online (if storing to a backend is part of the scope), or add data encryption, pagination, or persistent offline sync.

Sync Local Data to Remote Server When Back Online

We’ll simulate syncing by:

  • Storing notes locally with Hive while offline.

  • When back online, automatically send unsynced notes to a remote API.

  • Marking them as synced once successfully sent.

1. Update the Note Model to Track Sync Status

If you use a model class (and Hive TypeAdapters), update it like this:

@HiveType(typeId: 0)
class Note extends HiveObject {
  @HiveField(0)
  final String title;

  @HiveField(1)
  final String content;

  @HiveField(2)
  bool isSynced;

  Note({required this.title, required this.content, this.isSynced = false});
}

Re-run:

flutter pub run build_runner build --delete-conflicting-outputs

If you don’t use a model, and store plain Map objects instead, you can simply include an isSynced boolean:

final note = {
  'title': title,
  'content': content,
  'isSynced': false,
};

2. Create a Dummy API Endpoint for Sync

You can use a test server like reqres.in or a local Express/Django/Node server.

Assume this endpoint:

POST https://example.com/api/notes
Body: { title: "", content: "" }

3. Add HTTP Package

Add this to your pubspec.yaml:

dependencies:
  http: ^1.2.1

Then run:

flutter pub get

4. Sync Notes Function

Update your _HomePageState widget:

import 'package:http/http.dart' as http;
import 'dart:convert';

Future<void> syncUnsyncedNotes() async {
  final notes = box.values.toList();

  for (int i = 0; i < notes.length; i++) {
    final note = notes[i];

    if (note is Map && note['isSynced'] == false) {
      final response = await http.post(
        Uri.parse('https://example.com/api/notes'),
        headers: {'Content-Type': 'application/json'},
        body: jsonEncode({
          'title': note['title'],
          'content': note['content'],
        }),
      );

      if (response.statusCode == 200 || response.statusCode == 201) {
        // Update isSynced status
        final updatedNote = {
          'title': note['title'],
          'content': note['content'],
          'isSynced': true,
        };
        box.putAt(i, updatedNote);
      }
    }
  }
}

5. Trigger Sync When Back Online

Update the connectivity listener in _HomePageState:

void listenToConnectivityChanges() {
  subscription = Connectivity().onConnectivityChanged.listen((results) {
    final result = results.isNotEmpty ? results.first : ConnectivityResult.none;

    final nowOnline = result != ConnectivityResult.none;
    setState(() {
      isOnline = nowOnline;
    });

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(nowOnline ? 'Back Online' : 'You are Offline'),
        backgroundColor: nowOnline ? Colors.green : Colors.red,
      ),
    );

    if (nowOnline) {
      syncUnsyncedNotes();
    }
  });
}

Result

  • When offline, Notes are saved locally with isSynced: false.

  • When online, the app automatically detects reconnection and syncs unsynced notes to the server.

  • Once synced, Notes are updated locally to isSynced: true.

Add Visual Indicators for Sync Status

We'll update the note display in the ListView.builder to include a small icon next to each note title:

1. Update the Note Display UI

In your ListView.builder inside HomePage, update the ListTile like this:

ListView.builder(
  itemCount: notes.length,
  itemBuilder: (context, index) {
    final note = notes[index] as Map;
    final isSynced = note['isSynced'] == true;

    return ListTile(
      leading: Icon(
        isSynced ? Icons.cloud_done : Icons.cloud_off,
        color: isSynced ? Colors.green : Colors.grey,
      ),
      title: Text(note['title']),
      subtitle: Text(note['content']),
      trailing: isSynced
          ? const Text(
              'Synced',
              style: TextStyle(color: Colors.green, fontSize: 12),
            )
          : const Text(
              'Offline',
              style: TextStyle(color: Colors.red, fontSize: 12),
            ),
    );
  },
),

Optional: Add Tooltip for Better UX

You can wrap the icon in a Tooltip to provide more context:

leading: Tooltip(
  message: isSynced ? 'Synced to server' : 'Not yet synced',
  child: Icon(
    isSynced ? Icons.cloud_done : Icons.cloud_off,
    color: isSynced ? Colors.green : Colors.grey,
  ),
),

Result

  • Notes show a green cloud icon and “Synced” label if uploaded to the server.

  • Notes saved offline show a gray/red cloud icon with the “Offline” label.

  • Users visually understand which data is stored remotely and which is pending.

Build a Flutter App with Offline Support using Hive and Connectivity - app

Conclusion

In this tutorial, you learned how to build a Flutter app with offline support using Hive for local data storage and connectivity_plus to detect network changes in real-time. You implemented:

  • A local-first approach using Hive to store notes.

  • Real-time connectivity awareness to inform users of their network status.

  • Automatic syncing of unsynced notes to a remote server when the device comes back online.

  • Visual indicators to show which notes are synced and which are pending.

These features ensure that your app remains responsive, reliable, and user-friendly even when network connectivity is intermittent or unavailable—an essential capability for real-world mobile apps.

With this foundation, you can extend the app further by:

  • Supporting authentication and per-user syncing

  • Handling conflicts during sync

  • Encrypting local data using HiveAesCipher

  • Supporting background sync using workmanager or flutter_background

You can get the full working source code on our GitHub.

That's just the basics. If you need more deep learning about Flutter, Dart, or related, you can take the following cheap course:

Thanks!