Posts in category “Programming”

Maintaining Scroll Position in Flutter's ListView: A Practical Guide

If you’ve worked with Flutter’s ListView, you might have encountered a scenario where you navigate between pages, and upon returning, the scroll position isn’t where you expected it to be. Recently, I faced a similar challenge with a note-taking app. Every time I moved between pages, I ended up at the bottom of the new page. Not the best user experience, right?

The Problem

In my note-taking app, I wanted the NoteList widget to always start at the top position whenever it’s built or updated. This way, users wouldn’t have to manually scroll up to see the latest notes. The challenge was to encapsulate the scroll behavior within the NoteList widget itself, without having to manage the scroll status from its parent widget. Here’s how we achieved that.

The Initial Implementation

Initially, NoteList was a simple StatelessWidget that displayed a list of notes grouped by their creation date. However, it didn’t retain or reset the scroll position upon navigation or page refreshes, which led to an inconsistent and frustrating user experience. Here’s a simplified version of the original NoteList:

class NoteList extends StatelessWidget {
  final List<Note> notes;
  final Function(Note) onTap;

  const NoteList({required this.notes, required this.onTap});

  @override
  Widget build(BuildContext context) {
    // Group notes by date
    final notesByDate = <String, List<Note>>{};
    for (var note in notes) {
      final dateKey = note.createDate!;
      notesByDate[dateKey] = notesByDate[dateKey] ?? [];
      notesByDate[dateKey]!.add(note);
    }

    return ListView.builder(
      itemCount: notesByDate.keys.length,
      itemBuilder: (context, index) {
        final dateKey = notesByDate.keys.elementAt(index);
        final dayNotes = notesByDate[dateKey]!;
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: dayNotes.map((note) => GestureDetector(
              onTap: () => onTap(note),
              child: Text(note.content),
            )).toList(),
        );
      },
    );
  }
}

The Solution

To solve this, we needed to ensure that every time NoteList was built, it would start at the top of the list. We introduced a ScrollController to manage the scroll position and added a simple logic to reset the scroll position to the top whenever the widget is built or updated.

Here's how we transformed the NoteList:

  1. Added a ScrollController: This allows us to control the scroll position programmatically.
  2. Reset Scroll Position: Using WidgetsBinding.instance.addPostFrameCallback, we ensure the scroll is reset after the widget's frame is built.

Here's the updated NoteList with these improvements:

class NoteList extends StatelessWidget {
  final List<Note> notes;
  final Function(Note) onTap;
+  final ScrollController _scrollController = ScrollController();

  NoteList({required this.notes, required this.onTap}) {
+    // Scroll to the top whenever the widget is built or updated
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      _scrollController.jumpTo(0);
+    });
  }

  @override
  Widget build(BuildContext context) {
    // Group notes by date
    final notesByDate = <String, List<Note>>{};
    for (var note in notes) {
      final dateKey = note.createDate!;
      notesByDate[dateKey] = notesByDate[dateKey] ?? [];
      notesByDate[dateKey]!.add(note);
    }

    return ListView.builder(
+      controller: _scrollController,
      itemCount: notesByDate.keys.length,
      itemBuilder: (context, index) {
        final dateKey = notesByDate.keys.elementAt(index);
        final dayNotes = notesByDate[dateKey]!;
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: dayNotes.map((note) => GestureDetector(
              onTap: () => onTap(note),
              child: Text(note.content),
            )).toList(),
        );
      },
    );
  }
}

Key Takeaways

  1. Encapsulation: By managing the scroll state within NoteList, we keep the parent widget cleaner and more focused on data management.
  2. Consistent User Experience: Resetting the scroll position ensures users always start at the top, providing a predictable and pleasant experience.
  3. Simple Yet Effective: Sometimes, small changes like adding a ScrollController can significantly enhance the usability of your app.

This solution should help other developers facing similar issues with managing scroll positions in Flutter. By following this approach, you can ensure that your list-based components offer a consistent and user-friendly experience.

By the way, my simple note taking app has been released at https://happynotes.shukebeta.com, and I'll improve it day by day. It is an free web application, and it has an android version on GitHub.

Feel free to use it and give me feedback on GitHub!

Navigation in Flutter: `pushReplacement` vs `pushAndRemoveUntil`

pushReplacement

Imagine you're replacing an old road sign with a new one. The old one is gone, and there's no way to know it was ever there. That's what pushReplacement does in Flutter.

Usage:

  • Navigator.pushReplacement(context, newRoute)

What It Does:

  • Replaces the current route with a new route.
  • The current route is removed from the stack, making way for the new route.
  • The removed route is completely gone—pressing the back button won’t bring it back.

When to Use It:

  • Ideal for scenarios like replacing a login screen with a home screen after a user logs in. You don’t want them to go back to the login screen, right?

Code Example:

Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (context) => HomeScreen()),
);

pushAndRemoveUntil

Now, think of pushAndRemoveUntil as clearing a cluttered desk, keeping only the essentials. You decide which items stay and which go. This function allows more control over which routes to remove.

Usage:

  • Navigator.pushAndRemoveUntil(context, newRoute, (route) => condition)

What It Does:

  • Pushes a new route and removes routes below it until a specified condition is met.
  • You provide a condition to determine which routes to keep or discard.

When to Use It:

  • Perfect for situations like logging out, where you want to return to a clean slate (the login screen) and remove all previous screens.

Code Example:

Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (context) => LoginScreen()),
  (Route<dynamic> route) => false, // Remove all previous routes.
);

Key Differences

  1. Scope of Operation:

    • pushReplacement swaps only the top route.
    • pushAndRemoveUntil can clear out multiple routes based on your condition.
  2. Stack Control:

    • pushReplacement provides straightforward, one-route replacement.
    • pushAndRemoveUntil allows for a more flexible navigation reset.
  3. Use Case Scenarios:

    • Use pushReplacement for simple replacement needs.
    • Use pushAndRemoveUntil for complex navigation flows requiring cleanup of multiple routes.

Two practical ORACLE procedures for adding/dropping fields

DECLARE
    -- Procedure to add a column if it does not exist
    PROCEDURE add_column_if_not_exists (
        p_owner       VARCHAR2,
        p_table_name  VARCHAR2,
        p_column_name VARCHAR2,
        p_column_type VARCHAR2
    ) IS
        l_sql    VARCHAR2(1024);
        l_count  NUMBER;
    BEGIN
        -- Check if column exists
        SELECT COUNT(*)
        INTO l_count
        FROM ALL_TAB_COLUMNS
        WHERE OWNER = p_owner
          AND TABLE_NAME = p_table_name
          AND COLUMN_NAME = p_column_name;

        -- Add column if it does not exist
        IF l_count = 0 THEN
            l_sql := 'ALTER TABLE ' || p_owner || '.' || p_table_name || ' ADD (' || p_column_name || ' ' || p_column_type || ')';
            DBMS_OUTPUT.PUT_LINE(l_sql);
            EXECUTE IMMEDIATE l_sql;
        END IF;
    END;

    -- Procedure to drop a column if it does exist
    PROCEDURE drop_column_if_exists (
        p_owner       VARCHAR2,
        p_table_name  VARCHAR2,
        p_column_name VARCHAR2
    ) IS
        l_sql    VARCHAR2(1024);
        l_count  NUMBER;
    BEGIN
        -- Check if column exists
        SELECT COUNT(*)
        INTO l_count
        FROM ALL_TAB_COLUMNS
        WHERE OWNER = p_owner
          AND TABLE_NAME = p_table_name
          AND COLUMN_NAME = p_column_name;

        -- drop column if it does exist
        IF l_count = 1 THEN
            l_sql := 'ALTER TABLE ' || p_owner || '.' || p_table_name || ' DROP COLUMN ' || p_column_name;
            DBMS_OUTPUT.PUT_LINE(l_sql);
            EXECUTE IMMEDIATE l_sql;
        END IF;
    END;
BEGIN
...
END;
/

Sharing pre-request script among Postman collections

I tried a few ways to write a script that can be reused among a few of postman request collections and finally I got a solution I like.

  1. Using const or let keywords to define internal used objects or utility containers. For example
const stpCommon = {
  randomCharacters: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split(''),
  nonUsdCurrencies: 'GBP,AUD,NZD,CAD,JPY,EUR'.split(','),
  getCurrencyPair: (currency) => `${currency}/USD`,
  getRandomRate: () => (Math.random() * 0.9 + 0.8).toFixed(5),
  getRandomElement: (array) => {
    if (array.length === 0) {
      throw new Error('Cannot get a random element from an empty array');
    }
    const randomIndex = Math.floor(Math.random() * array.length);
    return array[randomIndex];
  },
  getNextWeekday: function (date) {
    const clonedDate = new Date(date);
    const day = clonedDate.getDay();
    if (day === 6) {
      clonedDate.setDate(clonedDate.getDate() + 2);
    } else if (day === 0) {
      clonedDate.setDate(clonedDate.getDate() + 1);
    }
  },
  getBasicRequest: () => {
    const fixedAmountCurrency = stpCommon.getFixedAmountCurrency()
    return {
      currencyPair: stpCommon.getCurrencyPair(fixedAmountCurrency),
      rate: stpCommon.getRandomRate(),
      side: "2",
      fixedAmount: stpCommon.getFixedAmount(),
      ...
    }
...
  1. Using object name itself in its member methods. don't use this keyword, see the last method in above example.
  2. At the end of your script, using a conditional block to expose the object you want in Postman environment, normally one object is enough. For instance:
// the only way to expose an object in postman, is to define a object without any declaration keywords like var/let/const
// the following is a hack to expose the object in postman without troubling Node.js as in Node.js, we are not allowed to
// define a variable without any declaration keywords like var/let/const
if (typeof(pm) !== 'undefined') {
  stpUtils = _stpUtils
}
  1. At the end of your script, utilize conditional exports the objects that you want to use in Node.js environment, this step enables writing test cases for it. For example
const utilsA = {...}
const utilsB = {...}
const _utilsC = {something using utilsA & utilsB}
// provided you want to use utilsC in postman
if (typeof(pm) !== 'undefined') {
  utilsC = _utilsC
}
// and you want to expose all objects in your test cases
if (typeof (module) !== 'undefined') {
  module.exports = {
    utilsA, utilsB, _utilsC
  }
}

This way, you can paste all the script body into the postman scripts tab without trouble, while in Node.js env, you can freely use Jest or other unit test framework to write test cases for your shard library. What a great solution!

Adding Auto-Focus to TextFields in Flutter

When building user interfaces in Flutter, it's often desirable to have the keyboard automatically pop up and focus set on a specific TextField when navigating to a new page or rendering a widget. This auto-focus feature can greatly improve the user experience by allowing users to start typing immediately without having to manually tap on the TextField to focus it.

In this blog post, we'll explore how to implement the auto-focus feature for TextFields in Flutter using FocusNodes.

Step 1: Create a FocusNode

The first step is to create a FocusNode instance, which represents the focus state of a particular widget. You can create a FocusNode in the initState method of your StatefulWidget:

late FocusNode myFocusNode;

@override
void initState() {
  super.initState();
  myFocusNode = FocusNode();
}

Step 2: Associate the FocusNode with the TextField

Next, you need to associate the FocusNode with the TextField you want to focus. You can do this by passing the FocusNode to the focusNode property of the TextField:

TextField(
  focusNode: myFocusNode,
  // other properties
)

Step 3: Request Focus on the FocusNode

To set the focus on the TextField, you need to request focus on the FocusNode. The best place to do this is after the widget has been rendered, which you can achieve using the WidgetsBinding.instance.addPostFrameCallback method:

@override
void initState() {
  super.initState();
  myFocusNode = FocusNode();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    myFocusNode.requestFocus();
  });
}

The addPostFrameCallback method ensures that the focus request is made after the widget has been rendered, which is necessary to avoid any potential issues with focus management.

Example Implementation

Here's an example implementation of a StatefulWidget that demonstrates the auto-focus feature for a TextField:

class NewNoteState extends State<NewNote> {
  final TextEditingController _noteController = TextEditingController();
  final FocusNode _noteFocusNode = FocusNode();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _noteFocusNode.requestFocus();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('New Note'),
      ),
      body: Column(
        children: [
          TextField(
            controller: _noteController,
            focusNode: _noteFocusNode,
            keyboardType: TextInputType.multiline,
            // other properties
          ),
          // other widgets
        ],
      ),
    );
  }

  @override
  void dispose() {
    _noteController.dispose();
    _noteFocusNode.dispose();
    super.dispose();
  }
}

In this example, we create a FocusNode called _noteFocusNode and associate it with the TextField. In the initState method, we use WidgetsBinding.instance.addPostFrameCallback to request focus on the _noteFocusNode after the widget has been rendered. This will automatically set the focus on the TextField when the NewNote widget is rendered.

Conclusion

Adding the auto-focus feature to TextFields in Flutter can greatly enhance the user experience by allowing users to start typing immediately without having to manually tap on the TextField to focus it. By creating a FocusNode, associating it with the TextField, and requesting focus on the FocusNode after the widget has been rendered, you can easily implement this feature in your Flutter applications.

Remember to dispose of the FocusNode when it's no longer needed to avoid memory leaks. Additionally, be mindful of potential issues with focus management and use the appropriate methods and callbacks to ensure smooth focus handling in your application.