Home Instagram like Listview in Flutter
Post
Cancel

Instagram like Listview in Flutter

In this article, we will simulate an Instagram search page. We will use a snap Listview for this task.

1
Flutter (Channel stable, 2.5.3)

Setup a new project

1
flutter create list_view_snap

After removing some code we don’t use, we have a clear main.dart and an empty page. Let’s start from here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(debugShowCheckedModeBanner: false, home: MyHomePage());
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Container());
  }
}

Add a List View

Let’s add a random color method for better UI:

1
2
3
import 'dart:math';

Color get randomColor => Color((Random().nextDouble() * 0xFFFFFF).toInt() << 0).withOpacity(1.0);

Build a vertical listview:

1
2
3
4
5
6
7
8
9
10
11
12
ListView.builder(
  scrollDirection: Axis.vertical,
  itemCount: 20,
  itemBuilder: (context, index) {
    return Container(
      width: double.infinity,
      height: 400,
      color: randomColor,
      margin: const EdgeInsets.all(20.0),
    );
  },
)

The full code of main.dart:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import 'dart:math';

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(debugShowCheckedModeBanner: false, home: MyHomePage());
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Color get randomColor => Color((Random().nextDouble() * 0xFFFFFF).toInt() << 0).withOpacity(1.0);

  Widget buildBody() {
    return ListView.builder(
      scrollDirection: Axis.vertical,
      itemCount: 20,
      itemBuilder: (context, index) {
        return Container(
          width: double.infinity,
          height: 400,
          color: randomColor,
          margin: const EdgeInsets.all(20.0),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: buildBody());
  }
}

Add Snap for Listview

We need a snap physics which is similar to PageScrollPhysics.

SnapScrollPhysics

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class SnapScrollPhysics extends ScrollPhysics {
  final double itemDimension;

  const SnapScrollPhysics({required this.itemDimension, ScrollPhysics? parent}) : super(parent: parent);

  @override
  SnapScrollPhysics applyTo(ScrollPhysics? ancestor) {
    return SnapScrollPhysics(itemDimension: itemDimension, parent: buildParent(ancestor));
  }

  double _getPage(ScrollMetrics position) {
    return position.pixels / itemDimension;
  }

  double _getPixels(double page) {
    return page * itemDimension;
  }

  double _getTargetPixels(ScrollMetrics position, Tolerance tolerance, double velocity) {
    double page = _getPage(position);
    if (velocity < -tolerance.velocity) {
      page -= 0.5;
    } else if (velocity > tolerance.velocity) {
      page += 0.5;
    }
    return _getPixels(page.roundToDouble());
  }

  @override
  Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
    if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
        (velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) {
      return super.createBallisticSimulation(position, velocity);
    }
    final Tolerance tolerance = this.tolerance;
    final double target = _getTargetPixels(position, tolerance, velocity);
    if (target != position.pixels) {
      return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
    }
    return null;
  }

  @override
  bool get allowImplicitScrolling => false;
}

Use the SnapScrollPhysics

Then in Listview, we use the SnapScrollPhysics. The itemDimension is the height of the container with the margin.

1
2
3
4
5
6
7
8
9
10
11
12
13
ListView.builder(
  scrollDirection: Axis.vertical,
  itemCount: 20,
  physics: const SnapScrollPhysics(itemDimension: 440),
  itemBuilder: (context, index) {
    return Container(
      width: double.infinity,
      height: 400,
      color: randomColor,
      margin: const EdgeInsets.all(20.0),
    );
  },
)

Go further

Add a special header

How about if we need a header on the first page? Let’s do this.

Step 1: Add an extra element in Listview for the header.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const double headerHeight = 200.0;

itemBuilder: (context, index) {
  if (index == 0) {
    return Container(
      width: double.infinity,
      height: headerHeight,
      color: Colors.amberAccent,
      margin: const EdgeInsets.all(20.0),
      child: const Center(child: Text('The header', style: TextStyle(fontSize: 30))),
    );
  }

  // ...
}
        

Step 2: modify the SnapScrollPhysics to support an extra header element.

We add headerDimension for the ScrollPhysics to know the height of the header and modify _getPage and _getPixels.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
final double headerDimension;

const SnapScrollPhysics({required this.itemDimension, this.headerDimension = 0, ScrollPhysics? parent})
    : super(parent: parent);

@override SnapScrollPhysics applyTo(ScrollPhysics? ancestor) {
  return SnapScrollPhysics(
    itemDimension: itemDimension, headerDimension: headerDimension, parent: buildParent(ancestor));
}
  
double _getPage(ScrollMetrics position) {
  if (position.pixels <= headerDimension) {
    return position.pixels / headerDimension;
  }

  return (position.pixels - headerDimension) / itemDimension + 1;
}

double _getPixels(double page) {
  if (page < 1) {
    return 0;
  }

  return (page - 1) * itemDimension + headerDimension;
}

Add pull refresh

Step 1: Install pull_to_refresh

Step 2: Add pull refresh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
final RefreshController refreshController = RefreshController(initialRefresh: false);

void onRefresh() async {
  await Future.delayed(const Duration(milliseconds: 1000));
  refreshController.refreshCompleted();
}

void onLoading() async {
  await Future.delayed(const Duration(milliseconds: 1000));
  refreshController.loadComplete();
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: SmartRefresher(
      enablePullDown: true,
      enablePullUp: true,
      header: const WaterDropHeader(),
      controller: refreshController,
      onRefresh: onRefresh,
      onLoading: onLoading,
      child: buildBody(),
    ),
  );
}

The full code of main.dart.

This post is licensed under CC BY 4.0 by the author.