How to Create a Flutter Architecture from Scratch: The First Part
This is the opening article in our series, where our colleague Eugene Efanov, a specialist in mobile development, will walk you through the process of building a Flutter architecture from the ground up. Let's dive in!
Introduction
Over the past five years, I've specialized in developing applications using Flutter. Initially, the landscape was populated with several popular architecture libraries like BloC, Redux, and Provider.
However, these libraries were relatively small and lacked many useful mechanics found in other libraries, which made them less appealing to me.
During one particular project, I started developing my own set of utility classes to enhance the architecture.
My ultimate goal is to create a component capable of responding to events, storing specific data, and providing access to it. This component should allow us to modify stored data and track changes, similar to BloC.
Additionally, I aim to implement a mechanism for storing these components and enabling them to connect with one another. This would allow us to replace real components with mocks during testing, effectively creating a simple DI container.
We also want to access components and their states globally throughout the app, akin to Redux.
Furthermore, deviating from existing methodologies, the retrieval and usage of components should be achieved without relying on BuildContext. This approach ensures comprehensive test coverage for all business logic components.
Finally, similar to widgets, I aim for a straightforward component lifecycle: initialization and deinitialization. This synthesis would combine the most user-friendly and efficient mechanisms from existing libraries.
An added benefit of this approach is its cross-platform compatibility, enabling the same mechanisms to be used in Flutter, SwiftUI, and Compose. This makes switching between platforms seamless as the same business logic components are utilized. In future articles, I will demonstrate how to implement these mechanisms on different platforms.
After consistently using this method, I felt compelled to share my experiences in an article. I believe it will be both useful and intriguing for other developers, helping them understand and implement these common mechanisms.
Lastly, I will include examples for testing these components and provide tips on organizing them according to architectural layers.
领英推荐
Create base class
Let's start by examining the base class, MvvmInstance, which will serve as the foundation for all our mechanisms. We'll establish two key methods: initialize and dispose, mirroring the lifecycle of standard widgets. Additionally, we'll incorporate an asynchronous initialization method, initializeAsync, allowing us to pass input to the instance.
By doing this, we abstract away from the constructor and gain control over the object initialization process. This keeps constructors standard and simplifies code generation for object creation while enabling asynchronous operations during initialization. Here is the class we currently have:
class MvvmInstanceConfiguration {
const MvvmInstanceConfiguration({
this.isAsync,
});
final bool? isAsync;
}
abstract class MvvmInstance<T> {
bool isInitialized = false;
bool isDisposed = false;
late final T input;
MvvmInstanceConfiguration get configuration => const MvvmInstanceConfiguration();
bool get isAsync {
return configuration.isAsync ?? false;
}
@mustCallSuper
void initialize(T input) {
this.input = input;
if (!isAsync) {
isInitialized = true;
}
}
@mustCallSuper
void dispose() {
isDisposed = true;
}
@mustCallSuper
Future<void> initializeAsync() async {
if (isAsync) {
isInitialized = true;
}
}
}
Here, we also configure the object with a single flag indicating whether it requires asynchronous initialization. In the future, we can expand this to include additional parameters, such as a dependency list for the instance.
Now, we can seamlessly link our object to any widget's state and bind it to its lifecycle.
class TestInstance extends MvvmInstance {}
class TestWidget extends StatefulWidget {
const TestWidget({super.key});
@override
State<TestWidget> createState() => _TestWidgetState();
}
class _TestWidgetState extends State<TestWidget> {
final testInstance = TestInstance();
@override
void initState() {
super.initState();
testInstance.initialize(1);
}
@override
void dispose() {
super.dispose();
testInstance.dispose();
}
@override
Widget build(BuildContext context) {
return Container();
}
}
With this structure in place, we can now integrate additional functions into our class, such as event handling and state management. This ensures that all class resources are initialized and disposed of in sync with the widget or any other business logic component that uses our class.
Subscribe to our newsletter to ensure you don't miss Part 2!
?? Get an estimate of your digital idea ?? [email protected]
Full Stack Java Developer | Passionate About Leveraging Technology for Positive Global Impact
4 个月Very informative ??