Posts tagged with “flutter”

Why We Shouldn't Unconditionally Load Data in `didChangeDependencies

Flutter’s didChangeDependencies lifecycle method is a common source of redundant network requests and data reloads. Based on real-world code examples from a notes app, let’s explore why this happens and how to fix it.


When Does didChangeDependencies Trigger?

didChangeDependencies in Flutter triggers in these scenarios:

  • Immediately after initState()
  • When an InheritedWidget ancestor changes
  • When the widget's dependencies change

In the provided HomePage code:

@override  
void didChangeDependencies() {  
  super.didChangeDependencies();  
  navigateToPage(currentPageNumber); // ❌ Unconditional call  
}  

This causes duplicate API calls every time the event being triggerd.


The Fix: Initialize-Once Flag

class HomePageState extends State<HomePage> {  
  ...
  bool _isInitialized = false; // Add flag  

  @override  
  void didChangeDependencies() {  
    super.didChangeDependencies();  
    if (!_isInitialized) {  
      _isInitialized = true;  
      navigateToPage(currentPageNumber); // ✅ Only first load  
    }  
  }  
  ...
}  

Why This Works

  1. First Load: Initializes data once
  2. Subsequent Route Changes: Skips reload unless explicitly refreshed
  3. Memory Efficiency: Prevents duplicate API calls (evident from the NotesService cache-less implementation)

Key Takeaways

  1. didChangeDependencies isn’t just for initialization
  2. Always guard data-loading logic with flags

[Solution] The argument type 'Consumer' can't be assigned to the parameter type 'PreferredSizeWidget?'.

The error occurs because AppBar expects a widget that implements the PreferredSizeWidget interface, but Consumer<NoteModel> does not directly implement this interface. To solve this, you need to return an AppBar from within the Consumer builder method.

Here’s how you can do it:

Scaffold(
  appBar: PreferredSize(
    preferredSize: Size.fromHeight(kToolbarHeight),
    child: Consumer<NoteModel>(
      builder: (context, noteModel, child) {
        return AppBar(
          title: Text(
            'Your Title',
            style: TextStyle(
              color: noteModel.isPrivate ? Colors.red : Colors.green,
            ),
          ),
        );
      },
    ),
  ),
  body: // Your other widgets,
);

In this approach, I wrapped the Consumer<NoteModel> inside a PreferredSize widget to ensure it adheres to the PreferredSizeWidget interface required by appBar.

This should resolve the error while allowing you to update only the AppBar based on changes in your NoteModel.

Glory to ChatGPT!

Build command for deploying your flutter web app to cloudflare pages

set -x && if cd flutter; then git pull && cd .. ; else git clone https://github.com/flutter/flutter.git; (cd flutter && git fetch --tags && git checkout 3.22.3); fi && ls && flutter/bin/flutter doctor && flutter/bin/flutter clean && flutter/bin/flutter config --enable-web && cp .env.production .env && sed -i "s/VERSION_PLACEHOLDER/`git rev-parse --short HEAD`/" .env && flutter/bin/flutter build web --web-renderer html --base-href="/" --release

Dart `...` Spread Operator: Simplifying Conditional Widget Addition in Flutter

The Dart ... spread operator is a powerful feature that allows you to insert multiple elements from a collection into another collection. This is particularly useful when building Flutter widget trees, as it can make your code more concise and readable.

What is the Spread Operator ...?

The ... operator is used to expand elements of a collection and add them to another collection. For example:

List<int> list1 = [1, 2, 3];
List<int> list2 = [0, ...list1, 4];
print(list2); // Output: [0, 1, 2, 3, 4]

Using the Spread Operator in Flutter Widgets

In Flutter, you can use the ... operator to conditionally add multiple widgets to a widget list, making the code cleaner and more maintainable.

Without the Spread Operator

If you don't use the ... operator, you might end up writing repetitive conditional checks like this:

AppBar(
  title: const Text('Note Details'),
  actions: [
    if (widget.note.userId == UserSession().id)
      IconButton(
        icon: const Icon(Icons.edit),
        onPressed: _enterEditingMode,
      ),
    if (widget.note.userId == UserSession().id)
      IconButton(
        icon: const Icon(Icons.delete),
        onPressed: () async {
          await DialogService.showConfirmDialog(
            context,
            title: 'Delete note',
            text: 'Each note is a story, are you sure you want to delete it?',
            yesCallback: () => _controller.deleteNote(context, widget.note.id),
          );
        },
      ),
  ],
)

This approach involves duplicating the condition check for each widget, making the code verbose and harder to maintain.

With the Spread Operator

Using the ... spread operator, you can simplify the code by grouping the widgets under a single conditional check:

AppBar(
  title: const Text('Note Details'),
  actions: [
    if (widget.note.userId == UserSession().id) ...[
      IconButton(
        icon: const Icon(Icons.edit),
        onPressed: _enterEditingMode,
      ),
      IconButton(
        icon: const Icon(Icons.delete),
        onPressed: () async {
          await DialogService.showConfirmDialog(
            context,
            title: 'Delete note',
            text: 'Each note is a story, are you sure you want to delete it?',
            yesCallback: () => _controller.deleteNote(context, widget.note.id),
          );
        },
      ),
    ],
  ],
)

This method is more concise and only requires a single condition check, reducing redundancy and improving readability.

Benefits of Using the Spread Operator

  1. Reduces Code Duplication: The spread operator allows you to write less code by removing the need for multiple conditional checks.
  2. Improves Readability and Maintainability: With fewer lines of code and clearer structure, your code is easier to read and maintain.
  3. Simplifies Conditional Addition of Multiple Elements: When you need to add multiple elements based on a condition, the spread operator provides a clean and efficient way to do so.

Using RouteObserver to Refresh a widget when you go back

In a recent project, I ran into an issue where I needed to refresh a page when the user navigated back to it. After searching on google and asking ChatGPT, I found this simple and clean solution.

What is RouteObserver?

RouteObserver is part of the Flutter framework that helps you track navigation events in your app. It allows you to listen to changes in the route stack, such as when a route is pushed or popped. This is incredibly useful for scenarios where you need to refresh or update your UI based on navigation.

Setting Up RouteObserver

First, I created a UserSession class to hold a singleton instance of RouteObserver. Here’s the gist:

// user_session.dart
import 'package:flutter/material.dart';

class UserSession {
  static final UserSession _instance = UserSession._internal();
  static final routeObserver = RouteObserver<ModalRoute>();

  UserSession._internal();
}

By doing this, I could easily access routeObserver from anywhere in my app. My UserSession also holds other user session data, maybe a bettway is to create a separate file for RouteObserver. But for now, we just use the UserSession class.

Adding RouteObserver to Your App

Next, I needed to register this RouteObserver with my app’s navigator. I did this in the main app widget:

// main.dart
import 'package:flutter/material.dart';
import 'package:happy_notes/screens/account/user_session.dart';

class HappyNotesApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const InitialPage(),
      navigatorObservers: [UserSession.routeObserver],
    );
  }
}

By adding UserSession.routeObserver to navigatorObservers, we’re now tracking route changes across the entire app.

Using RouteObserver in Screens

To use RouteObserver, I extended my state classes with RouteAware and subscribed to the routeObserver in the initState method. Here’s an example from the Memories screen:

// memories.dart
import 'package:flutter/material.dart';
import '../account/user_session.dart';

class MemoriesState extends State<Memories> with RouteAware {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      UserSession.routeObserver.subscribe(this, ModalRoute.of(context)!);
    });
  }

  @override
  void didPopNext() {
    // Called when the top route has been popped off, and this route shows up
    _fetchMemories();
    setState(() {});
  }

  @override
  void dispose() {
    UserSession.routeObserver.unsubscribe(this);
    super.dispose();
  }
  
  void _fetchMemories() {
    // Logic to refresh memories
  }
}

Here’s what’s happening:

  1. Subscription: In initState, the screen subscribes to routeObserver.
  2. Route Events: The didPopNext method is triggered when the screen becomes visible again after another screen is popped off. This is where I refreshed the screen's data by calling _fetchMemories().
  3. Unsubscription: It’s important to unsubscribe in the dispose method to avoid memory leaks.

Conclusion

Using RouteObserver in Flutter allows you to manage your app’s navigation state effectively. By listening to route changes, you can ensure that your UI stays in sync with user actions, providing a seamless experience.