Dynamic UI Customization in Flutter: Real-Time User Preferences

Maxim Gorin
4 min readNov 19, 2024

--

Personalized settings are a powerful way to enhance user engagement and satisfaction in mobile apps. Allowing users to tailor the interface to their preferences — whether it’s switching between light and dark themes or adjusting the scale of the UI — can make your app more enjoyable and accessible.

Themes | Mobile | Android Developers

In the previous article, Dynamic Theming in Flutter: Time-Based UI Adaptation, we explored how to create a responsive UI that automatically adjusts its theme based on the time of day. From morning to night, the app adapted its colors and visuals, enhancing the user experience with smooth, time-sensitive transitions. This approach demonstrated how dynamic theming can make your app feel more intuitive and in sync with the user’s environment.

In this article, we’ll dive into building a dynamic Flutter app that offers real-time customization options. We’ll explore how to let users seamlessly change themes and interface scaling, store these preferences with shared_preferences, and ensure the UI updates instantly to reflect these choices. By the end, you’ll have the tools to create a more flexible, user-friendly experience that keeps users coming back for more.

Adding Dependencies

To save user preferences on the device, we use the shared_preferences package. If you’re working in an IDE (Android Studio, VS Code), add the following to your pubspec.yaml:

dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.1.0

After adding the dependency, run:

flutter pub get

Theory

  • shared_preferences allows us to save key-value pairs on the device. This is perfect for storing user settings like theme selection and font size.
  • In DartPad, you don’t need to update pubspec.yaml since the necessary packages are already included. However, the data won’t persist between sessions due to DartPad’s isolated environment.

Defining Themes and Scaling Options

TextTheme class — material library — Dart API

We’ll create two themes (light and dark) and add scaling options for adjusting the entire UI, not just the text size.

import 'package:flutter/material.dart';
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';

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

final ThemeData lightTheme = ThemeData(
brightness: Brightness.light,
primaryColor: Colors.blue,
scaffoldBackgroundColor: Colors.white,
textTheme: TextTheme(
bodyLarge: TextStyle(fontSize: 16, color: Colors.black),
),
);

final ThemeData darkTheme = ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.indigo,
scaffoldBackgroundColor: Colors.black,
textTheme: TextTheme(
bodyLarge: TextStyle(fontSize: 16, color: Colors.white),
),
);

enum ScaleFactor { small, medium, large }

double getScaleFactor(ScaleFactor scale) {
switch (scale) {
case ScaleFactor.small:
return 0.8;
case ScaleFactor.medium:
return 1.0;
case ScaleFactor.large:
return 1.2;
}
}

Theory

  • Themes define the visual style of the app. We use light and dark themes to provide different color schemes.
  • Scale Factor: Instead of only changing text size, we use a scaling factor that adjusts the entire UI, enhancing the user experience for those who prefer larger or smaller elements.

Storing User Preferences

We use shared_preferences to store and retrieve user settings, making them persistent across app sessions (in IDE or on a real device).

class UserPreferences {
static const _keyTheme = 'theme';
static const _keyScale = 'scale';

static Future<void> saveTheme(bool isDarkMode) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keyTheme, isDarkMode);
}

static Future<bool> loadTheme() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_keyTheme) ?? false;
}

static Future<void> saveScaleFactor(ScaleFactor scale) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_keyScale, scale.index);
}

static Future<ScaleFactor> loadScaleFactor() async {
final prefs = await SharedPreferences.getInstance();
final index = prefs.getInt(_keyScale) ?? ScaleFactor.medium.index;
return ScaleFactor.values[index];
}
}

Theory

  • Async Storage: We use asynchronous functions (await) to handle data retrieval since accessing shared_preferences takes time.
  • DartPad Limitation: Settings are saved only during the current session. When the page is refreshed or restarted, all saved data is lost.

Building the Real-Time UI Customization

Example: Building the Real-Time UI Customization

We create a StatefulWidget to manage the theme and scale factor changes in real-time.

class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
bool _isDarkMode = false;
ScaleFactor _scaleFactor = ScaleFactor.medium;

@override
void initState() {
super.initState();
_loadPreferences();
}

Future<void> _loadPreferences() async {
_isDarkMode = await UserPreferences.loadTheme();
_scaleFactor = await UserPreferences.loadScaleFactor();
setState(() {});
}

void _updateTheme(bool isDarkMode) {
setState(() {
_isDarkMode = isDarkMode;
});
UserPreferences.saveTheme(isDarkMode);
}

void _updateScaleFactor(ScaleFactor scale) {
setState(() {
_scaleFactor = scale;
});
UserPreferences.saveScaleFactor(scale);
}

@override
Widget build(BuildContext context) {
final theme = _isDarkMode ? darkTheme : lightTheme;
final scaleFactor = getScaleFactor(_scaleFactor);

return MaterialApp(
theme: theme,
builder: (context, child) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(scaleFactor),
),
child: child!,
);
},
home: Scaffold(
appBar: AppBar(title: Text("Dynamic UI Customization")),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("Hello, Flutter!"),
SizedBox(height: 20),
_buildThemeSwitch(),
SizedBox(height: 20),
_buildScaleFactorSelector(),
],
),
),
);
}

Widget _buildThemeSwitch() {
return SwitchListTile(
title: Text("Dark Mode"),
value: _isDarkMode,
onChanged: _updateTheme,
);
}

Widget _buildScaleFactorSelector() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: ScaleFactor.values.map((scale) {
return ElevatedButton(
onPressed: () => _updateScaleFactor(scale),
child: Text(scale.name.toUpperCase()),
);
}).toList(),
);
}
}

Conclusion

In this article, we built a Flutter app that allows users to customize the UI in real-time by changing the theme and adjusting the scale of the interface. We used shared_preferences to save user preferences, making them persistent across app sessions (in an IDE or on a real device). Although DartPad resets state with each run, you can still test all real-time changes within a single session.

Feel free to extend this app by adding more customization options, like choosing a custom color palette or adjusting line spacing. Keep experimenting, and have fun building dynamic, user-friendly apps in Flutter!

Happy coding! 🎨💻

--

--

Maxim Gorin
Maxim Gorin

Written by Maxim Gorin

Team lead in mobile development with a passion for Fintech and Flutter. Sharing insights and stories from the tech and dev world on this blog.

No responses yet