Dynamic UI Customization in Flutter: Real-Time User Preferences
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.
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
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 accessingshared_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
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! 🎨💻