flexify by Azerosoft

pub version pub points pub likes CI License: MIT Publisher: azerosoft.com

Responsive sizes from your design, and navigation without context.
Built and maintained by Azerosoft.


flexify takes the screen size your design was made for and scales every size you write to the screen the app runs on. It also lets you open and close screens from anywhere, without a BuildContext, with 14 ready-made page transitions.

Container(width: 200.rw, height: 72.rh, child: Text('Hi', style: TextStyle(fontSize: 16.rt)));

Flexify.go(const ProfileScreen(), animation: FlexifyRouteAnimations.slide);

Page transitions and a page that returns a result in the example app on iOS

Page transitions and a page that returns a result, in the example app on iOS.

Contents

Features

  • Responsive sizes. .rw, .rh, .rs and .rt scale numbers from your design to the current screen, and .sw and .sh give a percentage of it.
  • Phone, tablet or desktop. Flexify.value(phone: 2, tablet: 3, desktop: 4) picks a value for the kind of screen, and optional scale limits keep a phone design from growing too large on big screens.
  • Follows the screen. Rotation and window resizing rebuild the UI with the new sizes, on phones, tablets, the web and desktop. A portrait design can keep its proportions in landscape too.
  • Navigation without context. Flexify.go, goRemove, goRemoveAll and back work from anywhere, for example from a view model, and can return a result like Navigator.push.
  • 14 page transitions. Fade, slide, flip, blur and more, with your own duration.
  • Any app widget. Works with MaterialApp, CupertinoApp, WidgetsApp and MaterialApp.router with routers such as go_router and auto_route, and keeps your own navigatorKey.
  • Old and new Flutter. Supports Flutter 3.7 and every later version, including the latest stable release.
  • No dependencies. Only the Flutter SDK.

Requirements

Minimum
Flutter 3.7
Dart 2.19
Platforms Android, iOS, macOS, Web, Windows, Linux

Installation

flutter pub add flexify

Quick start

Wrap your app in Flexify and give it the size of the screen your design was made for, for example 375 × 812 from a Figma frame:

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

void main() {
  runApp(
    const Flexify(
      designWidth: 375,
      designHeight: 812,
      app: MaterialApp(home: HomeScreen()),
    ),
  );
}

Then write sizes from the design with an extension:

Padding(
  padding: EdgeInsets.all(16.rw),
  child: Text('Welcome', style: TextStyle(fontSize: 24.rt)),
);

Responsive sizes

Extension Scaled by Use it for
100.rw screen width ÷ design width widths, horizontal padding and spacing
100.rh screen height ÷ design height heights, vertical padding and spacing
100.rs the smaller of the two squares, icons, avatars, corner radii
16.rt the smaller of the two font sizes
20.verticalSpace same as .rh SizedBox(height: 20.rh)
20.horizontalSpace same as .rw SizedBox(width: 20.rw)
50.sw 50 % of the screen width sizes relative to the whole screen
25.sh 25 % of the screen height sizes relative to the whole screen

Negative numbers work the same way, for example (-12).rw for an offset.

The scale factors and the screen size are also available directly:

final double factor = Flexify.scaleWidth; // and Flexify.scaleHeight
final Size screen = Flexify.screenSize;

Landscape

A portrait design of 375 × 812 on a phone turned to landscape gives a height scale of about 0.5, so .rh, .rs and .rt shrink to about half their size. If your app supports landscape, set matchOrientation. The design is then compared with the screen turned on its side (812 × 375), and sizes stay close to the portrait ones:

const Flexify(
  designWidth: 375,
  designHeight: 812,
  matchOrientation: true,
  app: MaterialApp(home: HomeScreen()),
);

It is off by default, so existing layouts do not change.

Scale limits

A phone design keeps growing with the screen: in a browser window 1920 pixels wide, 100.rw is 512 pixels. maxScale and minScale keep the width and height scale within limits:

const Flexify(
  designWidth: 375,
  designHeight: 812,
  maxScale: 1.5,
  app: MaterialApp(home: HomeScreen()),
);

.sw and .sh are not limited, so 100.sw is always the full screen width.

Phone, tablet or desktop

Flexify.screenType tells what kind of screen the app runs on, and Flexify.value picks a value for it. tablet defaults to phone and desktop to tablet, so only the values that differ are needed:

GridView.count(
  crossAxisCount: Flexify.value(phone: 2, tablet: 3, desktop: 4),
  children: products,
);

if (Flexify.isPhone) {
  // Show a bottom navigation bar instead of a side menu.
}

On Android and iOS the shortest side of the screen decides, so a phone stays a phone in landscape: below 600 logical pixels it is a phone, otherwise a tablet. On the web and desktop the window width decides: below 600 a phone, below 1024 a tablet, and from 1024 on a desktop. Like the sizes, the screen type is updated when the window is resized.

The same design on iPhone 13 mini, iPhone 17 and iPad Air

The example app on three screens. The design is 375 × 812, so the iPhone 13 mini shows it at its original size.

All methods work from anywhere in your code, as long as the app is wrapped in Flexify.

Method Same as
Flexify.go(page) Navigator.push
Flexify.goRemove(page) Navigator.pushReplacement
Flexify.goRemoveAll(page) Navigator.pushAndRemoveUntil, removing every other screen
Flexify.back([result]) Navigator.pop, and does nothing on the first screen
Flexify.backToFirst() Navigator.popUntil, back to the first screen
Flexify.backUntil(predicate) Navigator.popUntil
Flexify.canGoBack Navigator.canPop
// Open a screen with the platform's default transition.
Flexify.go(const DetailsScreen());

// Open a screen with a transition of your choice.
Flexify.go(
  const DetailsScreen(),
  animation: FlexifyRouteAnimations.fade,
  duration: const Duration(milliseconds: 500),
);

// After logging in, replace the whole stack.
Flexify.goRemoveAll(const HomeScreen(), animation: FlexifyRouteAnimations.slideFromBottom);

Flexify.back();

// Close every screen above the first one, or go back to a named screen.
Flexify.backToFirst();
Flexify.backUntil(ModalRoute.withName('/home'));

Without an animation, a MaterialApp opens the page with a MaterialPageRoute, like Navigator.push usually does, so your theme's pageTransitionsTheme applies. By default that is the Cupertino slide with swipe back on iOS. In a CupertinoApp, iOS gets a CupertinoPageRoute. duration defaults to 300 ms and is used both for opening and for going back.

Results

go, goRemove and goRemoveAll return a Future that completes with the value passed to Flexify.back, like Navigator.push:

// On the first screen.
final String? city = await Flexify.go<String>(const CityPicker());

// In CityPicker, when the user picks a city.
Flexify.back('Baku');

Route names

Give a page a name with settings, so navigator observers, for example for analytics, see which screen was opened:

Flexify.go(const ProfileScreen(), settings: const RouteSettings(name: '/profile'));

On the web, the name also shows in the browser's address bar, for example /#/profile, and the browser's back button closes the page. If someone types such an address while the app is open, Flutter looks for a page with that name in your app's routes or onGenerateRoute. Add the names there if those addresses should open the pages, or use a router.

Apps with a router such as go_router or auto_route can use these methods too, see Using a router.

Page transitions

Value Effect
FlexifyRouteAnimations.fade Fades the page in.
FlexifyRouteAnimations.slide Slides the page in from the right.
FlexifyRouteAnimations.scale Grows the page from the center with an ease-in-out curve.
FlexifyRouteAnimations.rotate Spins the page in by one full turn.
FlexifyRouteAnimations.zoom Grows the page from the center at a constant speed.
FlexifyRouteAnimations.size Reveals the page from the middle outwards.
FlexifyRouteAnimations.elastic Grows the page with an elastic bounce.
FlexifyRouteAnimations.flip Flips the page around its vertical axis.
FlexifyRouteAnimations.slideFromBottom Slides the page up from the bottom.
FlexifyRouteAnimations.fadeAndScale Fades the page in while it grows from 80 % of its size.
FlexifyRouteAnimations.blur Fades the page in over a blurred background.
FlexifyRouteAnimations.slideAndFade Slides the page in from the right while fading it in.
FlexifyRouteAnimations.rotateAndScale Spins and grows the page at the same time.
FlexifyRouteAnimations.flipAndFade Flips the page around its vertical axis while fading it in.

Using a router

Flexify works with go_router, auto_route and other routers built on MaterialApp.router. Wrap the app in Flexify as usual, and .rw and the other extensions work on every screen:

final GoRouter router = GoRouter(routes: <RouteBase>[/* ... */]);

void main() {
  runApp(
    Flexify(
      designWidth: 375,
      designHeight: 812,
      app: MaterialApp.router(routerConfig: router),
    ),
  );
}

With auto_route, pass appRouter.config() as routerConfig.

Navigate with your router, for example context.go('/details') in go_router or context.router.push(const DetailsRoute()) in auto_route, so every page keeps its URL. Flexify.go and Flexify.back work on top of a router as well, for screens that do not need a URL. The router does not know about those screens, so the URL stays the same while they are open.

Page transitions with a router

Every transition is also available as a transitionsBuilder for the pages your router builds.

go_router:

GoRoute(
  path: '/details',
  pageBuilder: (BuildContext context, GoRouterState state) =>
      CustomTransitionPage<void>(
    key: state.pageKey,
    child: const DetailsScreen(),
    transitionsBuilder: FlexifyRouteAnimations.fade.transitionsBuilder,
  ),
),

auto_route:

CustomRoute<void>(
  page: DetailsRoute.page,
  transitionsBuilder: FlexifyRouteAnimations.fade.transitionsBuilder,
  duration: const Duration(milliseconds: 300),
),

How it works

  • Scaling. Flexify measures the space at the top of the widget tree, which is the screen size on every Flutter version. 100.rw is 100 × screen width ÷ design width, 100.rh uses the heights, and .rs and .rt use the smaller of the two factors so squares stay square and text never outgrows the screen.
  • Rebuilding. Widgets that use .rw and friends do not depend on anything Flutter can track, so when the size changes (rotation, window resize, split screen) Flexify rebuilds every widget below it once. The keyboard does not change the size and does not cause a rebuild.
  • Navigation. Flexify finds the app's root Navigator below it, so you do not have to hand it a key. Nested navigators, for example inside tabs, are not used.

Upgrading to 3.0

Most apps only need to change the version. Check these points:

  • Flutter 3.7 and Dart 2.19 or newer are required.
  • Replace animationDuration: with duration: in Flexify.go.
  • Replace FlexifyNavigator.go, goRemove, goRemoveAll and back with the same methods on Flexify. FlexifyNavigator.navigatorKey stays.
  • Replace FlexifyInit.instance.setWidth(x), setHeight(x) and setRt(x) with x.rw, x.rh and x.rt.
  • Rename FlexifyRouteAnimations.customFadeScale to fadeAndScale.
  • Remove any use of Flexify.globalKey. It was never attached to anything. Passing FlexifyNavigator.navigatorKey to your app still works but is no longer needed.
  • go, goRemove and goRemoveAll return a Future. With the unawaited_futures lint, wrap the calls you do not await in unawaited(...).
  • Negative values such as (-12).rw are scaled instead of throwing an ArgumentError.
  • Without an animation, a MaterialApp now uses a MaterialPageRoute, also on iOS. It looks the same unless your theme changes the page transitions.
  • Use a single Flexify at the top of the app. A second one is reported as an error in debug mode.

The full list is in the changelog.

Troubleshooting

"Flexify is not initialized"

.rw, .rh, .rs or .rt was used before Flexify measured the screen. Wrap your app in Flexify as shown in Quick start and use the extensions in the build methods of widgets below it. Code that runs before runApp, for example in main(), cannot use them because there is no screen size yet.

In widget tests, pump the widget under test inside Flexify too:

await tester.pumpWidget(
  const Flexify(
    designWidth: 375,
    designHeight: 812,
    app: MaterialApp(home: ProfileScreen()),
  ),
);
"Flexify could not find a Navigator"

Flexify.go, goRemove or goRemoveAll was called while no Flexify was on screen, or the app inside it has no Navigator. Wrap your app in Flexify and navigate only after runApp, for example from a button or a view model, not from main(). MaterialApp, CupertinoApp and MaterialApp.router all have a navigator.

Sizes do not change after rotating the device or resizing the window

Flexify rebuilds every widget below it when the screen size changes, so sizes worked out in build are always up to date. Sizes worked out once and stored, for example in a global or static variable, in a field of a State or in initState, keep the value of the first screen. Use the extensions inside build:

// Keeps the size of the first screen.
final double cardWidth = 300.rw;

// Follows the screen.
@override
Widget build(BuildContext context) => SizedBox(width: 300.rw);
Text and heights are too small in landscape

With a portrait design, the height scale of a phone in landscape is about half of the portrait one, and .rh, .rs and .rt follow it. Set matchOrientation: true on Flexify, see Landscape.

A page opens over the bottom navigation bar

Flexify.go uses the app's root Navigator, so the new page covers the whole app, including a bottom navigation bar or tabs. To open a page inside a tab that has its own Navigator, push it on that navigator. The Flexify transitions work there too:

Navigator.of(context).push(
  FlexifyRouteAnimationBuilder.build(
    context,
    const DetailsScreen(),
    FlexifyRouteAnimations.fade,
    const Duration(milliseconds: 300),
  ),
);
With go_router, the URL does not change

Pages opened with Flexify.go are pushed on the root navigator directly, so the router does not know about them. The browser URL stays the same and a link cannot open them. Keep using your router for pages that need their own URL, and Flexify navigation for screens that do not. Flexify's transitions work with router pages too, see Page transitions with a router.

Flexify.back() does nothing

The current screen is the first one, so there is nothing to go back to. Flexify leaves the app as it is and prints [flexify] There is no previous screen to go back to. to the console.

Example app

The example app shows the scale factors of the current screen, a few responsive boxes, a button for every page transition and a city picker that returns the picked city. The animation at the top of this page was recorded with it.

cd example
flutter run

Tested with

The minimum and maximum versions this package was tested with, as of October 2026. Versions in between were not all tested one by one, but they are expected to work as well. On every push, GitHub Actions runs the tests and builds the example for all platforms, once with Flutter 3.7.12 and once with the latest stable release.

flexify itself is plain Dart and needs nothing from the platforms, so the platform tables list the tools the example app was built with.

Flutter and Dart

Minimum Maximum
Flutter 3.7.12 3.47.6
Dart 2.19.6 3.13.5

Android

Minimum Maximum
Android Gradle Plugin 7.2.0 9.1.0
Gradle 7.5 9.3.1
Kotlin 1.7.10 2.4.0
JDK 11 17
compileSdk 33 36
minSdk 16 24

iOS

Minimum Maximum
Xcode 16.4 27.0
iOS SDK 18.5 27.0
Deployment target 11.0 15.0

macOS

Minimum Maximum
macOS 15.7 27.0
Xcode 16.4 27.0
macOS SDK 15.5 27.0
Deployment target 10.14 12.0

Windows

Minimum Maximum
Windows Server 2022 Server 2025
Visual Studio 2022 (17.14) 2026 (18.10)
CMake 3.31.6 4.4.3

Linux

Minimum Maximum
Ubuntu 24.04 24.04
GTK 3.24.41 3.24.41
Clang 18 18
CMake 3.31.6 3.31.6

Web

Minimum Maximum
Build JavaScript JavaScript and WebAssembly

Routers

A small app for each router was also tried on GitHub Actions: sizes after a size change, a Flexify transition in a router page, and Flexify.go and back on top of the router.

Minimum Maximum
go_router 12.1.1, with Flutter 3.7.12 18.0.2
auto_route 11.2.0 11.2.0

Tried by hand

The example app was also started on these systems, and the sizes and page transitions were checked by hand:

Platform System
Android Android API 37.2 emulator (Pixel 10), portrait and landscape
iOS iOS 27.0 simulators (iPhone 13 mini, iPhone 17, iPad Air 11-inch)
macOS macOS 27.0
Web Chromium 152 (WebAssembly build), at desktop and phone window sizes

Contributing

Bug reports, ideas and pull requests are welcome. Please read the contributing guide and our code of conduct first.

About Azerosoft

Azerosoft

Azerosoft is a design and development studio from Baku, Azerbaijan. We design interfaces and build websites, web and mobile apps, backends, CRM systems and 2D games. We use our packages in our own projects and maintain them for everyone.

Building something with Flutter? Tell us about your project.

License

Released under the MIT License.

Libraries

flexify