Back to Posts

Top 10 Must-Have Flutter Packages in 2024

9 min read

Flutter's ecosystem is rich with packages that can significantly speed up development and add powerful features to your apps. Here are the top 10 must-have Flutter packages in 2024:

1. Riverpod

Riverpod has become the de facto state management solution for Flutter. It's more flexible than Provider and offers better dependency injection.

final counterProvider = StateProvider<int>((ref) => 0);

class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Text('Count: $count');
  }
}

2. Go Router

For navigation, Go Router provides a declarative routing solution that's both powerful and easy to use.

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => HomeScreen(),
    ),
    GoRoute(
      path: '/details/:id',
      builder: (context, state) => DetailsScreen(id: state.params['id']!),
    ),
  ],
);

3. Freezed

Freezed is a code generation package that helps you create immutable classes with minimal boilerplate.

@freezed
class User with _$User {
  const factory User({
    required String name,
    required int age,
  }) = _User;
}

4. Flutter Hooks

Flutter Hooks provides a way to use hooks in Flutter, making state management and side effects more manageable.

class TimerWidget extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final controller = useAnimationController(
      duration: const Duration(seconds: 1),
    );
    // Use the controller...
  }
}

5. Dio

For HTTP requests, Dio is a powerful HTTP client that supports interceptors, form data, and more.

final dio = Dio();
final response = await dio.get('https://api.example.com/data');

6. Isar

Isar is a fast, lightweight, and easy-to-use NoSQL database for Flutter.

final isar = await Isar.open([UserSchema]);
final user = User()
  ..name = 'John'
  ..age = 30;
await isar.writeTxn(() async {
  await isar.users.put(user);
});

7. Flutter Bloc

For those who prefer the BLoC pattern, Flutter Bloc provides a predictable state management solution.

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
  }
}

8. Flutter Secure Storage

For securely storing sensitive data, Flutter Secure Storage is the go-to package.

final storage = FlutterSecureStorage();
await storage.write(key: 'token', value: 'your-token');
final token = await storage.read(key: 'token');

9. Cached Network Image

For efficient image loading and caching, Cached Network Image is essential.

CachedNetworkImage(
  imageUrl: 'https://example.com/image.jpg',
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
)

10. Flutter Local Notifications

For handling local notifications, this package provides a comprehensive solution.

final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
await flutterLocalNotificationsPlugin.show(
  0,
  'New Message',
  'You have a new message',
  NotificationDetails(
    android: AndroidNotificationDetails(
      'channel_id',
      'channel_name',
      importance: Importance.max,
    ),
  ),
);

Package Selection Criteria

When choosing packages for your Flutter project, consider these factors:

  1. Maintenance Status

    • Regular updates
    • Active community
    • Issue resolution rate
    • Documentation quality
  2. Performance Impact

    • Package size
    • Memory usage
    • Build time impact
    • Runtime performance
  3. Compatibility

    • Flutter version support
    • Platform support
    • Dependency conflicts
    • Migration path

Advanced Usage Patterns

1. Riverpod with Dependency Injection

// Dependency injection setup
final apiClientProvider = Provider<ApiClient>((ref) {
  final dio = ref.watch(dioProvider);
  return ApiClient(dio);
});

// State management with async operations
final userProvider = FutureProvider<User>((ref) async {
  final apiClient = ref.watch(apiClientProvider);
  return apiClient.getUser();
});

// Error handling
final userStateProvider = StateNotifierProvider<UserNotifier, AsyncValue<User>>((ref) {
  return UserNotifier(ref.watch(apiClientProvider));
});

class UserNotifier extends StateNotifier<AsyncValue<User>> {
  final ApiClient _apiClient;
  
  UserNotifier(this._apiClient) : super(const AsyncValue.loading());
  
  Future<void> fetchUser() async {
    state = const AsyncValue.loading();
    try {
      final user = await _apiClient.getUser();
      state = AsyncValue.data(user);
    } catch (error, stack) {
      state = AsyncValue.error(error, stack);
    }
  }
}

2. Go Router with Nested Navigation

final router = GoRouter(
  initialLocation: '/',
  routes: [
    ShellRoute(
      builder: (context, state, child) {
        return Scaffold(
          body: child,
          bottomNavigationBar: BottomNavigationBar(
            currentIndex: _calculateSelectedIndex(state),
            onTap: (index) => _onItemTapped(index, context),
            items: const [
              BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
              BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Settings'),
            ],
          ),
        );
      },
      routes: [
        GoRoute(
          path: '/',
          builder: (context, state) => const HomeScreen(),
          routes: [
            GoRoute(
              path: 'details/:id',
              builder: (context, state) => DetailsScreen(
                id: state.params['id']!,
              ),
            ),
          ],
        ),
        GoRoute(
          path: '/settings',
          builder: (context, state) => const SettingsScreen(),
        ),
      ],
    ),
  ],
);

3. Freezed with JSON Serialization

@freezed
class User with _$User {
  const factory User({
    required String name,
    required int age,
    @Default([]) List<String> tags,
    @JsonKey(name: 'created_at') required DateTime createdAt,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

// Usage with Dio
final response = await dio.get('/user');
final user = User.fromJson(response.data);

Package Maintenance Best Practices

1. Version Management

dependencies:
  flutter:
    sdk: flutter
  # Use specific versions for critical packages
  riverpod: 2.4.0
  go_router: 12.0.0
  # Use caret syntax for minor updates
  freezed: ^2.4.0
  # Use any for frequently updated packages
  flutter_hooks: any

2. Dependency Updates

flutter pub outdated

flutter pub upgrade

flutter pub upgrade package_name

3. Security Considerations

// Secure storage with encryption
final storage = FlutterSecureStorage(
  aOptions: AndroidOptions(
    encryptedSharedPreferences: true,
  ),
  iOptions: IOSOptions(
    accessibility: KeychainAccessibility.first_unlock,
  ),
);

// Secure API communication
final dio = Dio()
  ..interceptors.add(
    InterceptorsWrapper(
      onRequest: (options, handler) {
        options.headers['Authorization'] = 'Bearer $token';
        return handler.next(options);
      },
    ),
  );

Performance Optimization

1. Image Caching Strategy

CachedNetworkImage(
  imageUrl: 'https://example.com/image.jpg',
  memCacheWidth: 400,
  memCacheHeight: 400,
  maxWidthDiskCache: 400,
  maxHeightDiskCache: 400,
  placeholder: (context, url) => Shimmer.fromColors(
    baseColor: Colors.grey[300]!,
    highlightColor: Colors.grey[100]!,
    child: Container(
      color: Colors.white,
    ),
  ),
  errorWidget: (context, url, error) => Icon(Icons.error),
  fadeInDuration: Duration(milliseconds: 500),
  imageBuilder: (context, imageProvider) => Container(
    decoration: BoxDecoration(
      image: DecorationImage(
        image: imageProvider,
        fit: BoxFit.cover,
      ),
    ),
  ),
);

2. Database Optimization

// Isar database configuration
final isar = await Isar.open(
  [UserSchema, PostSchema],
  directory: getApplicationDocumentsDirectory().path,
  maxSizeMiB: 50,
  compactOnLaunch: CompactCondition(
    minFileSize: 50 * 1024 * 1024,
    minBytes: 10 * 1024 * 1024,
  ),
);

// Efficient queries
final users = await isar.users
    .where()
    .ageGreaterThan(18)
    .sortByAge()
    .limit(10)
    .findAll();

Testing Packages

1. Unit Testing

void main() {
  group('UserProvider Tests', () {
    late ProviderContainer container;
    late MockApiClient mockApiClient;

    setUp(() {
      container = ProviderContainer();
      mockApiClient = MockApiClient();
      container.registerProvider<ApiClient>((ref) => mockApiClient);
    });

    test('UserProvider loads user data', () async {
      when(mockApiClient.getUser()).thenAnswer(
        (_) async => User(name: 'Test', age: 25),
      );

      final user = await container.read(userProvider.future);
      expect(user.name, 'Test');
      expect(user.age, 25);
    });
  });
}

2. Widget Testing

void main() {
  testWidgets('CounterWidget increments counter', (tester) async {
    final container = ProviderContainer();
    
    await tester.pumpWidget(
      ProviderScope(
        parent: container,
        child: MaterialApp(
          home: CounterWidget(),
        ),
      ),
    );

    expect(find.text('Count: 0'), findsOneWidget);
    
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    expect(find.text('Count: 1'), findsOneWidget);
  });
}

Conclusion

These packages represent the foundation of modern Flutter development. By understanding their advanced usage patterns and following best practices, you can build robust, performant, and maintainable applications.

Remember to:

  • Choose packages carefully based on your project needs
  • Keep dependencies updated and secure
  • Implement proper testing
  • Monitor performance impact
  • Follow community best practices

Stay tuned for more detailed tutorials on how to use these packages effectively in your Flutter applications!