Working on non-functional requirements - Flutter
When it comes to building apps with Flutter, developers must take into account both functional and non-functional requirements. While functional requirements refer to what the app should do, non-functional requirements (NFRs) define how the app should perform, operate, and behave. In this article, we will focus on the non-functional requirements of Flutter apps and explore how to work on them with code examples.
Performance
Performance is a crucial non-functional requirement that determines how fast and responsive the app is. To ensure that your Flutter app performs well, you can follow these tips:
Use asynchronous programming:
Use asynchronous programming to avoid blocking the UI thread and provide a smooth user experience. Use the async and await keywords to make async calls to APIs and databases.
Future<List<Post>> fetchPosts() async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
if (response.statusCode == 200) {
List<dynamic> jsonResponse = json.decode(response.body);
return jsonResponse.map((post) => Post.fromJson(post)).toList();
} else {
throw Exception('Failed to load posts');
}
}
Optimize images:
Optimize images to reduce their file size and improve app performance. Use tools like Image.asset, Image.network, and FadeInImage to load images efficiently.
Image.asset(
'assets/images/logo.png',
width: 100,
height: 100,
)
Use lazy loading:
Use lazy loading to load content as the user scrolls, rather than loading everything at once. This approach can help reduce loading times and improve app performance.
ListView.builder(
itemCount: posts.length,
itemBuilder: (BuildContext context, int index) {
return index == posts.length - 1
? CircularProgressIndicator()
: PostItem(post: posts[index]);
},
controller: _scrollController,
)
Security
Security is another important non-functional requirement that must be considered when building Flutter apps. To ensure that your app is secure, you can follow these tips:
Use secure storage:
Use secure storage to store sensitive information like user credentials and API keys. The flutter_secure_storage package provides an easy way to store data securely.
final storage = new FlutterSecureStorage();
await storage.write(key: 'token', value: 'my_secret_token');
Use SSL/TLS:
Use SSL/TLS to encrypt data sent between the app and the server. The http package provides an easy way to make secure HTTP calls.
final response = await http.get(Uri.parse('https://example.com'));
Avoid hardcoding secrets:
Avoid hardcoding sensitive information like passwords and API keys in your app’s code. Use environment variables or configuration files instead.
final apiKey = Platform.environment['API_KEY'];
final password = await storage.read(key: 'password');
Usability
Usability is a non-functional requirement that refers to how easy it is to use the app. To ensure that your Flutter app is usable, you can follow these tips:
Keep it simple:
Use a simple and intuitive UI design that is easy for users to understand and navigate. Avoid cluttering the app with too many features or information.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('My App'),
),
body: Center(
child: Text('Hello, World!'),
),
),
);
}
}
Use consistent navigation:
Use consistent navigation patterns throughout the app to make it easy for users to move around. For example, use a bottom navigation bar or a drawer menu to provide easy access to different sections of the app.
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
int _selectedIndex = 0;
static const List<Widget> _widgetOptions = <Widget>[
HomeScreen(),
ProfileScreen(),
SettingsScreen(),
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My App'),
),
body: Center(
child: _widgetOptions.elementAt(_selectedIndex),
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Profile',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Settings',
),
],
currentIndex: _selectedIndex,
onTap: _onItemTapped,
),
);
}
}
Use readable fonts and colors:
Use readable fonts and colors that are easy on the eyes. Avoid using small fonts or bright colors that make it difficult for users to read.
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
theme: ThemeData(
primarySwatch: Colors.blue,
textTheme: TextTheme(
headline1: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
bodyText1: TextStyle(
fontSize: 16,
color: Colors.black87,
),
),
),
home: Scaffold(
appBar: AppBar(
title: Text('My App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Welcome to My App',
style: Theme.of(context).textTheme.headline1,
),
SizedBox(height: 16),
Text(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ac odio hendrerit, dictum turpis sit amet, vestibulum tellus.',
style: Theme.of(context).textTheme.bodyText1,
textAlign: TextAlign.center,
),
],
),
),
),
);
}
}
By focusing on usability, you can create Flutter apps that are easy to use and provide a great user experience. The code examples above demonstrate how to keep your UI simple and intuitive, use consistent navigation patterns, and use readable fonts and colors.
Scalability
Scalability is a non-functional requirement that refers to how well the app can handle increasing amounts of data and traffic. To ensure that your Flutter app is scalable, you can follow these tips:
Use caching:
Use caching to store frequently accessed data in memory or on disk, reducing the need to make repeated API calls.
class MyApp extends StatelessWidget {
final _httpClient = HttpClient();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: FutureBuilder(
future: _getPosts(),
builder: (BuildContext context, AsyncSnapshot<List<Post>> snapshot) {
if (snapshot.hasData) {
return PostList(posts: snapshot.data);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
} else {
return CircularProgressIndicator();
}
},
),
);
}
Future<List<Post>> _getPosts() async {
final cache = await CacheClient().getCache();
if (cache != null) {
return Post.fromJsonList(json.decode(cache));
}
final response = await _httpClient.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
final body = utf8.decode(await consolidateHttpClientResponseBytes(response));
await CacheClient().setCache(body);
return Post.fromJsonList(json.decode(body));
}
}
class CacheClient {
static const String CACHE_KEY = 'my_app_cache_key';
final _storage = FlutterSecureStorage();
Future<void> setCache(String data) async {
await _storage.write(key: CACHE_KEY, value: data);
}
Future<String> getCache() async {
return await _storage.read(key: CACHE_KEY);
}
}
Use scalable architecture:
Use scalable architecture patterns like BLoC (Business Logic Component) or Provider to build your app’s architecture. These patterns separate the UI, business logic, and data layers, making it easier to manage large and complex apps.
class PostList extends StatelessWidget {
final List<Post> posts;
const PostList({Key? key, required this.posts}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: posts.length,
itemBuilder: (BuildContext context, int index) {
return PostItem(post: posts[index]);
},
);
}
}
class PostItem extends StatelessWidget {
final Post post;
const PostItem({Key? key, required this.post}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(post.title),
subtitle: Text(post.body),
);
}
}
class PostBloc {
final _httpClient = HttpClient();
final _postController = StreamController<List<Post>>.broadcast();
Stream<List<Post>> get posts => _postController.stream;
Future<void> fetchPosts() async {
final response = await _httpClient.get(Uri.parse(‘https://jsonplaceholder.typicode.com/posts’));
final body = utf8.decode(await consolidateHttpClientResponseBytes(response));
_postController.add(Post.fromJsonList(json.decode(body)));
}
void dispose() {
_postController.close();
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final postBloc = PostBloc();
postBloc.fetchPosts();
return MaterialApp(
title: ‘My App’,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: StreamBuilder<List<Post>>(
stream: postBloc.posts
builder: (BuildContext context, AsyncSnapshot<List<Post>> snapshot) {
if (snapshot.hasData) {
return PostList(posts: snapshot.data);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
} else {
return CircularProgressIndicator();
}
},
),
);
Conclusion
Working on non-functional requirements is an essential part of building high-quality Flutter apps. By focusing on aspects like performance, security, usability, and scalability, you can create apps that not only work well but also provide a great user experience.
In this article, I covered some tips and code examples for working on non-functional requirements in Flutter apps. By following these tips, you can build apps that are performant, secure, usable, and scalable.
Remember to always consider the non-functional requirements of your app during the development process and test your app thoroughly to ensure that it meets these requirements. With these best practices in mind, you can build Flutter apps that are fast, secure, easy to use, and scalable.