State Management. But WHY and WHEN ?
before you watch another architecture tutorial...
When it comes to discussing the architecture of any software project state management is mostly at the centre of the design process. From a broad perspective, an app’s state is concerned with all the memory it requires while it’s running. As an app grows and becomes increasingly complex, different segments of the app need to access the state which is not local to them. This is where state management comes in.
Since Flutter’s release in 2017, state management libraries have been coming in from all sides, all promising to be a little better than the others and offering a more efficient solution to deal with the complexities that arise from building applications that are meant to scale in the future. Provider, bloc, Getx, Mobx, GetIt and the list never ends.
The hype around these libraries is so evidently out there that using setState() even in a one-page application feels like a crime. What was supposed to make our code efficient, extensible, testable and maintainable is now just confusing beginners about what goes where.
I just finished working on this project for a client which had multiple pages stemming from a single home page and then the pages related to these pages following them.
Most of the pages stemming from the Dashboard displayed a list of items and the state of the pages had nothing to do with the state of the dashboard. I wrote a single file for each page that looked like this:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class PageName extends StatefulWidget {
const PageName({Key? key}) : super(key: key);
@override
State<PageName> createState() => _PageNameState();
}
class _PageNameState extends State<PageName> {
bool isLoading = false;
List dataList = [];
String userId = "";
APIService service = APIService();
Future getData() async {
setState(() => isLoading = true);
userId = Provider.of<AuthenticationProvider>(context, listen: false).userid;
try {
dataList = await service.fetchDataList(userId, "category");
} catch (e) {
dataList = [];
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(e.toString()),
),
);
}
setState(() => isLoading = false);
}
@override
Widget build(BuildContext context) {
return isLoading
? loadingScreen()
: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar( ),
body: SafeArea(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TitleText(
text: "Category"
),
...dataList.map(
(e) {
return ListTile(title: e,);
},
),
],
),
),
),
);
}
}
As you can see there isn’t any use of any state management libraries(except for sharing user credentials) because they aren’t needed. This is a fairly readable code and the business logic is minimal so there’s no need to make a separate class within a separate file to take care of it, and that is the case with most apps that developers make. This is why I am writing this article to explain when to use service classes for state management for every stateful widget.
Now coming to the part where state management was used in this app. Most APIs will need some form of user credentials and other data related to the user’s app usage to perform CRUD operations for that particular user and in this app that was a userID. It is sent through the login API and any page making any API call (essentially all of them) will need to send it in the request body. Here’s how it’s done:
Set up your main. dart file to accommodate the credential provider.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
.
.
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: AuthenticationProvider()),
],
child: const MaterialApp(
home: Dashboard(),
),
);
}
}
The provider file being used for keeping the distributing user credentials:
import 'package:flutter/cupertino.dart';
class AuthenticationProvider with ChangeNotifier {
late String _userid;
late String _product;
void setCredentials(
String userid,
String product,
) {
_userid = userid;
_product = product;
notifyListeners();
}
String get userid => _userid;
String get product => _product;
}
The user credentials can be easily set by calling the setCredentials() function after login when the API returns them.
Provider.of<AuthenticationProvider>(context, listen: false).setCredentials(value["userid"], value["product"]);
//Here value is the API response
With all of this said, I am not trying to say that one should refrain from using state management solutions, they are useful in improving the readability of the code and making it ready for scaling in the future. But forcing such things as a beginner can slow you down since it can get a little complex to understand at first.
Here are some additional resources I would recommend you go through if you want to learn more about state management and when to use it.
medium.com/flutterworld/flutter-mvvm-archit..
medium.com/flutter-community/stop-using-sta..
sei.cmu.edu/our-work/software-architecture
docs.flutter.dev/development/data-and-backe..
subscription.packtpub.com/book/programming/..
Follow me on Twitter: https://twitter.com/saurabhdhingraa