Skip to content

Commit

Permalink
feat: add responsive layout builder and gap widget
Browse files Browse the repository at this point in the history
  • Loading branch information
thisissandipp committed Jun 29, 2024
1 parent 6acb577 commit f6842e0
Show file tree
Hide file tree
Showing 10 changed files with 393 additions and 0 deletions.
11 changes: 11 additions & 0 deletions lib/layout/breakpoints.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// Defines the breakpoints for the Sudoku app UI.
abstract class SudokuBreakpoint {
/// Max width for a small layout.
static const double small = 742;

/// Max width for a medium layout.
static const double medium = 1280;

/// Max width for a large layout.
static const double large = 1440;
}
3 changes: 3 additions & 0 deletions lib/layout/layout.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export 'breakpoints.dart';
export 'responsive_gap.dart';
export 'responsive_layout_builder.dart';
35 changes: 35 additions & 0 deletions lib/layout/responsive_gap.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:flutter/widgets.dart';
import 'package:gap/gap.dart';
import 'package:sudoku/layout/layout.dart';

/// {@template responsive_gap}
/// A wrapper around [Gap] that renders a [small], [medium]
/// or a [large] gap depending on the screen size.
/// {@endtemplate}
class ResponsiveGap extends StatelessWidget {
/// {@macro responsive_gap}
const ResponsiveGap({
this.small = 0,
this.medium = 0,
this.large = 0,
super.key,
});

/// A gap rendered on a small layout.
final double small;

/// A gap rendered on a medium layout.
final double medium;

/// A gap rendered on a large layout.
final double large;

@override
Widget build(BuildContext context) {
return ResponsiveLayoutBuilder(
small: (_, __) => Gap(small),
medium: (_, __) => Gap(medium),
large: (_, __) => Gap(large),
);
}
}
67 changes: 67 additions & 0 deletions lib/layout/responsive_layout_builder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import 'package:flutter/widgets.dart';
import 'package:sudoku/layout/layout.dart';

/// Represents the layout size passed to [ResponsiveLayoutBuilder.child].
enum ResponsiveLayoutSize {
/// Small layout
small,

/// Medium layout
medium,

/// Large layout
large,
}

/// Signature for the individual builders (`small`, `medium`, `large`).
typedef ResponsiveLayoutWidgetBuilder = Widget Function(BuildContext, Widget?);

/// {@template responsive_layout_builder}
/// A wrapper around [LayoutBuilder] which exposes builders
/// for various responsive breakpoints.
/// {@endtemplate}
class ResponsiveLayoutBuilder extends StatelessWidget {
/// {@macro responsive_layout_builder}
const ResponsiveLayoutBuilder({
required this.small,
required this.medium,
required this.large,
this.child,
super.key,
});

/// [ResponsiveLayoutWidgetBuilder] for small layout.
final ResponsiveLayoutWidgetBuilder small;

/// [ResponsiveLayoutWidgetBuilder] for medium layout.
final ResponsiveLayoutWidgetBuilder medium;

/// [ResponsiveLayoutWidgetBuilder] for large layout.
final ResponsiveLayoutWidgetBuilder large;

/// Optional child widget builder based on the current layout size
/// which will be passed to the `small`, `medium` and `large` builders
/// as a way to share/optimize shared layout.
final Widget Function(ResponsiveLayoutSize currentSize)? child;

@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final screenWidth = MediaQuery.of(context).size.width;

if (screenWidth <= SudokuBreakpoint.small) {
return small(context, child?.call(ResponsiveLayoutSize.small));
}
if (screenWidth <= SudokuBreakpoint.medium) {
return medium(context, child?.call(ResponsiveLayoutSize.medium));
}
if (screenWidth <= SudokuBreakpoint.large) {
return large(context, child?.call(ResponsiveLayoutSize.large));
}

return large(context, child?.call(ResponsiveLayoutSize.large));
},
);
}
}
8 changes: 8 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
gap:
dependency: "direct main"
description:
name: gap
sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d
url: "https://pub.dev"
source: hosted
version: "3.0.1"
glob:
dependency: transitive
description:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies:
flutter_bloc: ^8.1.4
flutter_localizations:
sdk: flutter
gap: ^3.0.1
google_fonts: ^6.2.1
intl: ^0.19.0

Expand Down
1 change: 1 addition & 0 deletions test/helpers/helpers.dart
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export 'pump_app.dart';
export 'set_display_size.dart';
28 changes: 28 additions & 0 deletions test/helpers/set_display_size.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sudoku/layout/layout.dart';

extension ResponsiveWidgetTester on WidgetTester {
void setDisplaySize(Size size) {
view
..physicalSize = size
..devicePixelRatio = 1.0;
addTearDown(() {
view
..resetPhysicalSize()
..resetDevicePixelRatio();
});
}

void setLargeDisplaySize() {
setDisplaySize(const Size(SudokuBreakpoint.large, 1000));
}

void setMediumDisplaySize() {
setDisplaySize(const Size(SudokuBreakpoint.medium, 1000));
}

void setSmallDisplaySize() {
setDisplaySize(const Size(SudokuBreakpoint.small, 1000));
}
}
68 changes: 68 additions & 0 deletions test/layout/responsive_gap_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// ignore_for_file: prefer_const_constructors

import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gap/gap.dart';
import 'package:sudoku/layout/layout.dart';

import '../helpers/helpers.dart';

void main() {
group('ResponsiveGap', () {
const smallGap = 10.0;
const mediumGap = 15.0;
const largeGap = 20.0;

late ResponsiveGap widget;

setUp(() {
widget = ResponsiveGap(
small: smallGap,
medium: mediumGap,
large: largeGap,
);
});

testWidgets('renders a large gap on a large display', (tester) async {
tester.setLargeDisplaySize();
await tester.pumpApp(
SingleChildScrollView(child: widget),
);

expect(
find.byWidgetPredicate(
(widget) => widget is Gap && widget.mainAxisExtent == largeGap,
),
findsOneWidget,
);
});

testWidgets('renders a medium gap on a medium display', (tester) async {
tester.setMediumDisplaySize();
await tester.pumpApp(
SingleChildScrollView(child: widget),
);

expect(
find.byWidgetPredicate(
(widget) => widget is Gap && widget.mainAxisExtent == mediumGap,
),
findsOneWidget,
);
});

testWidgets('renders a small gap on a small display', (tester) async {
tester.setSmallDisplaySize();
await tester.pumpApp(
SingleChildScrollView(child: widget),
);

expect(
find.byWidgetPredicate(
(widget) => widget is Gap && widget.mainAxisExtent == smallGap,
),
findsOneWidget,
);
});
});
}
Loading

0 comments on commit f6842e0

Please sign in to comment.