Tuesday, November 28, 2023

Flutter Interview Questions and Answers: Top 30 for Pros!

 

If you’re considering diving into this realm, it’s crucial to hire Flutter developers who are well-versed in the intricacies of the framework. Moreover, with the growing trend towards web applications, there’s an increasing demand to hire Flutter web developers to leverage the full potential of this versatile platform.

This article offers a set of sample questions tailored for interviewers, ensuring a comprehensive evaluation of potential candidates, regardless of your familiarity with the framework. Dive in to ensure you’re equipped with the right insights for your hiring process.

Technical Assessment: Short Overview

The technical interview is a pivotal phase in the hiring process for app developers. Typically attended by technical leads, senior developers, or hiring managers, its primary goal is to evaluate a candidate’s technical expertise and problem-solving abilities.

Depending on the depth and complexity of the interview, it can span anywhere from 30 minutes to several hours. The format and methods employed can vary, but they all aim to assess the candidate’s proficiency, problem-solving abilities, and depth of knowledge in Flutter.

Types of Technical Interviews:

  • Technical Quiz or Q&A: A straightforward method where candidates are quizzed on various technical topics. This provides a quick assessment of their theoretical grasp on essential subjects.
  • Whiteboard Interview: A classic approach where candidates demonstrate their problem-solving and algorithmic skills by tackling challenges on a whiteboard. It’s a test of both their technical knowledge and their ability to articulate their thought process.
  • Coding Challenge or Test Task: Candidates are tasked with a specific coding challenge, either during the interview or as a take-home assignment. This offers a hands-on evaluation of their coding skills and their proficiency with frameworks.
  • Live Coding Session: Conducted in real-time, this session allows interviewers to observe how candidates approach coding under pressure. It’s an immediate insight into their coding habits, problem-solving skills, and adaptability.

The Technical Quiz or Q&A stands out as a preferred method in many technical interviews due to its efficiency in gauging a candidate’s knowledge and understanding. This format offers a direct way to assess proficiency. However, the choice often depends on the specific requirements of the role and the company’s hiring practices.

As we explore the intricacies of technical interviews, we’ve compiled a selection of main topics and sample questions tailored for Flutter developers. Let’s take a closer look below.

Flutter Framework Overview

1. How does Flutter differ from other cross-platform frameworks?

Flutter, developed by Google, has rapidly gained traction in the realm of cross-platform development. When comparing it to other frameworks in the same domain, several distinctive features and advantages come to the fore:

  • Unique Rendering: Unlike many cross-platform frameworks that rely on native components for rendering, Flutter uses its own rendering engine, based on the Impeller on iOS and coming to Android, and Skia on other platforms, to draw widgets.
  • Dart Language: Flutter employs Dart, which combines optimal performance with features like hot-reloading, enhancing developer efficiency.
  • Comprehensive Widgets: Flutter’s widget catalog, adhering to Material Design and Cupertino, ensures native or custom app aesthetics.
  • Performance: Since Flutter apps are compiled to native machine code, they offer performance that’s often indistinguishable from native apps. This is especially notable in complex animations and transitions.

In essence, Flutter’s architecture, performance, and the robust Dart language set it apart in the cross-platform arena, making it a top choice for many developers and businesses.

2. What are the architectural layers of Flutter?

Flutter’s architecture is modular and layered. Here’s a breakdown:

  1. Embedder: The foundational layer, it provides platform-specific integrations, enabling Flutter to run on diverse systems.
  2. Engine: Written in C++, this layer manages core tasks like graphics rendering, text layout, and file/network operations.
  3. Framework: Sitting atop the Engine, it offers high-level classes for app development. This includes the Widget layer, which offers a vast array of visual, structural, platform, and interactive widgets, the Rendering layer that paints the widget onto the canvas, and several others that provide services and utilities.

3. What are Cupertino and Material in the context of Flutter?

Cupertino and Material are two comprehensive design systems provided by Flutter.

  • Material Design, inspired by Google’s design language, offers a rich set of components and guidelines for creating intuitive and consistent user interfaces across Android, iOS, and web apps.
  • On the other hand, Cupertino mimics Apple’s iOS design, providing a distinct look and feel that iOS users are familiar with.

In essence, while Material is a versatile design choice for any platform, Cupertino gives apps an authentic iOS appearance. Both design systems come with their own set of widgets in Flutter, allowing developers to create platform-specific or entirely custom UIs. They can be mixed and matched, offering flexibility in design while ensuring a native look and feel.

4. Which operating systems does Flutter support?

Flutter is a versatile framework that supports deployment across a variety of platforms:

  • Mobile Platforms (Android, iOS)
  • Desktop Platforms (Linux, MacOS, Windows)
  • Web Browsers (Chrome, Firefox, Safari, and Edge)

5. What is the purpose of pubspec.yaml in a Flutter project?

The pubspec.yaml file serves as a crucial configuration file in every Flutter project. It provides metadata about the app, such as its name, version, and description. Developers use this file to define the project’s dependencies, ensuring that the correct packages and versions are used during the build process. Additionally, the pubspec.yaml file is where assetslike images, fonts, and other resources are defined, allowing them to be bundled with the application. This centralized configuration ensures that the build process is consistent and that all required resources and dependencies are correctly incorporated into the final app.

Dart Language Fundamentals

6. How would you characterize the Dart language?

Dart, developed by Google, is a client-optimized language tailored for building mobile, desktop, and web applications. It stands out for its performance, thanks to ahead-of-time (AOT) compilation. Developers benefit from its hot reloadfeature, enhancing productivity by instantly reflecting UI changes. Dart’s strong typing system ensures code reliability, while its modern syntax promotes readability. Furthermore, its integration as the primary language for the Flutter framework underscores its significance in contemporary app development.

7. What are the differences between JIT and AOT?

JIT (Just-In-Time) and AOT (Ahead-Of-Time) are two compilation approaches in Dart.

  • JIT compilation happens at runtime, translating the code into machine language just before it’s executed. This allows for features like hot-reloading in Flutter, where changes can be injected into a running application.
  • AOT, on the other hand, compiles the code into machine language before the app is launched. This results in faster startup times and optimized performance, making it the preferred choice for production Flutter apps.

8. How do final and const differ in Dart?

In Dart, both final and const are used to declare variables that cannot be reassigned, but they serve different purposes:

  • The value of a final variable is determined at runtime. It can be set once, typically derived from runtime computations, making it suitable for values that remain constant during execution but might change between runs.
  • The value of a const variable is determined at compile-time, and it must also be immutable. This makes it ideal for values that are truly constant across all instances and runs, such as physical constants.

9. What access modifiers are available in Dart?

Dart offers a set of access modifiers to control the visibility of members: public (default, if no modifier is specified), private (indicated by a leading underscore _), and protected (not explicitly available but achieved through conventions). Dart's approach to privacy is library-based, meaning that private members are hidden within the same library file but can be accessed across classes within that file.

10. What are named parameters and optional parameters in Dart?

Named Parameters: In Dart, named parameters are specified by their name rather than their position during a function call. This enhances code readability, especially when a function has many parameters. They are defined within curly braces {} in the function declaration.

Optional Parameters: Dart supports two types of optional parameters: positional and named.

  • Positional: These are wrapped in square brackets [] in the function definition. When invoking the function, you can skip these parameters.
  • Named: These are combined with named parameters, allowing them to be omitted during a function call. They can either have a default value or be marked as nullable.

In summary, while named parameters enhance clarity in function calls, optional parameters provide flexibility in how functions are invoked.

11. What is the difference between a named constructor and a factory?

In Dart, a named constructor allows a class to define multiple ways to initialize, using different names for clarity. It directly creates a new instance of the class. On the other hand, a factory is a special kind of constructor that doesn’t always return a new instance. Instead, it can return an existing instance, an instance of a subtype, or even an instance of a completely different class. While named constructors offer varied initialization methods, factories provide greater control over the object creation process.

12. What are the four principles of Object-Oriented Programming (OOP)?

The four foundational principles of OOP are Encapsulation, Inheritance, Polymorphism, and Abstraction.

  • Encapsulation bundles data and methods operating on that data within a single unit, ensuring data integrity.
  • Inheritance allows a class to inherit properties and behaviors from another class.
  • Polymorphism permits one interface to be used for a general class of actions.
  • Abstraction hides complex implementations and exposes only the necessary functionalities.

13. Can you provide an overview of the SOLID principles?

SOLID is an acronym representing five design principles that ensure software is scalable, maintainable, and organized. They are:

  1. Single Responsibility Principle: A class should have only one reason to change.
  2. Open/Closed Principle: Software entities should be open for extension but closed for modification.
  3. Liskov Substitution Principle: Subtypes must be substitutable for their base types.
  4. Interface Segregation Principle: Clients should not be forced to depend on interfaces they don’t use.
  5. Dependency Inversion Principle: High-level modules should not depend on low-level ones; both should depend on abstractions

14. How do Object, dynamic, and var differ in Dart?

  • Object is the root class for all Dart classes, allowing a variable to hold any type of value but requires explicit casting for most operations.
  • dynamic is a type that bypasses static type checking, offering flexibility at the cost of forgoing some compile-time checks.
  • var is a keyword used to declare a variable without specifying its type. Dart determines and fixes the variable's type based on its initial value at compile-time.

15. Can you define the cascade and spread operators in Dart?

The cascade operator (..) allows for performing a series of operations on a single object without breaking the chain. For instance:var obj = Object()..method1()..method2();The spread operator (...) is used to insert multiple elements from one collection into another. It's especially useful when constructing lists or other collections:var list = [1, 2, ...otherList, 3];

16. How do mixins differ from interfaces in Dart?

Mixins and interfaces in Dart cater to different programming needs.

Mixins: These are tools for code reuse in Dart. They encapsulate functionalities that can be “mixed” into various class hierarchies without the complications of traditional inheritance. For instance, a Walker mixin can be added to both Humanand Robot classes, enabling both to walk without a shared parent class.

Interfaces: Dart uses classes to define interfaces, focusing on establishing contracts. Any class can act as an interface, and when another class implements it, it commits to providing implementations for all the interface’s members. Dart also introduces interface classes, enhancing safety by ensuring consistent method implementations within the same library.

In essence, mixins are about sharing functionalities, while interfaces are about adhering to specific contracts. Both are vital in Dart for creating structured and maintainable code.

17. What are the basics of null safety in Dart?

Null safety in Dart is a robust feature designed to avoid null reference errors, enhancing the stability of apps. The term “sound” in null safety implies that if an expression is determined by the static type system to be non-nullable, then, under no circumstances can the expression evaluate to null at runtime. This soundness is primarily ensured through static checks, but there can also be runtime checks, which are introduced by choice, to validate the non-nullability of an expression.

  • To denote a variable as nullable, you append a ?:int? nullableVariable;
  • Dart provides operators like ?? to assign a default value when a variable is null, and ?. to invoke a method if an object is non-null:int nonNullable = nullableVariable ?? 0;nullableVariable?.method();
  • To assert that a nullable variable is non-null, the ! operator is used:int nonNullable = nullableVariable!;

Employing null safety is vital for preventing null reference exceptions, ensuring the reliability and maintainability of the code, and allowing developers to identify and rectify potential errors during the development phase, contributing to the overall quality of the application.

18. Can you explain the concepts of Isolate, Event Loop, and Future in Dart?

In Dart, an Isolate is akin to a separate execution thread with its own memory, ensuring that Dart remains free of shared-state concurrency issues. Each isolate has its own memory heap, ensuring that no isolate’s state is accessible from any other.

The Event Loop is a mechanism that handles the execution of events or messages for a particular thread. It continually checks if there are tasks to execute and runs them in order.

Future represents a potential value or an error that will be available at some time in the future. It’s a core part of Dart’s asynchronous programming model, allowing developers to write non-blocking code for operations that might take time, like fetching data from a server.

Flutter Widgets and State Management

19. What are the differences between Stateless and Stateful widgets, and what is the purpose of setState()?

In Flutter, Stateless widgets are static and don’t change, ideal for elements like icons or labels that remain constant. They are less resource-intensive and efficient for static content. On the other hand, Stateful widgets can maintain state, allowing for dynamic and interactive UIs, essential for areas of the UI that user can interact with or that can change due to real-time data updates.

The setState() function is crucial in Stateful widgets; it signals the framework that the state of a widget has changed, prompting a UI rebuild. For instance, it can be used to change a button's appearance or trigger an action upon user interaction, ensuring the UI accurately reflects the most recent state.

20. What is the InheritedWidget in Flutter?

InheritedWidget is a foundational element in Flutter that facilitates efficient data propagation down the widget tree. It allows a widget to share data with its descendants without explicitly passing the data through a constructor. For instance, themes and locales are often provided using InheritedWidget. When a widget wants to access data from an InheritedWidget, it uses the context.dependOnInheritedWidgetOfExactType() method. This mechanism is particularly useful for providing configuration data or shared state to multiple widgets in a subtree.

21. Can you explain the role of keys in Flutter?

Keys in Flutter are essential for controlling the framework’s widget-rebuild optimization process. They uniquely identify widgets in the widget tree, ensuring that the framework syncs widgets with their underlying Element objects correctly.

For example, when working with a list of items that might change dynamically, using keys ensures that the state of an item remains consistent even if its position in the list changes. This is especially useful in scenarios like maintaining the scroll position or preserving the state of a widget during animations.

22. What is the Navigator in Flutter?

The Navigator in Flutter is essentially a widget that manages a stack of child widgets representing pages or screens in the application, known as routes. It is used for navigating between these routes and managing the app’s screen stack. A route is a single screen or page in Flutter, and it can be of different types, such as a full-screen route that covers the entire screen or a modal route that sits on top of existing screens.

The Navigator allows developers to define the app’s navigation structure, handle transitions between different routes, and manage the app’s history stack, ensuring a seamless and intuitive navigation experience for users.

23. What are the different approaches to state management in Flutter, and how do they operate?

In Flutter, state management refers to the way developers handle the data used by the app to influence its behavior and appearance. It’s about maintaining and manipulating the state, or data, of a widget, and determining how the changes in state reflect in the UI.

  • InheritedWidget is a foundational class in Flutter, which is particularly useful for small to medium-sized projects. It simplifies the transfer of data down the widget tree, eliminating the need for numerous constructor arguments, making the code cleaner and more manageable.
  • The Provider Package built atop InheritedWidget, is suitable for medium to large-sized projects, offering a range of features for handling state, including dependency injection, and it encapsulates common patterns of using InheritedWidget, making it more user-friendly.
  • Bloc Pattern is ideal for managing the state in large, complex projects. It promotes a clear separation between the user interface and business logic, making the components of the application easier to debug and test.
  • The Redux Pattern, originally developed for JavaScript applications, maintains all the application’s state information in a single entity called the store. It provides a single source of truth, making it easier to conceptualize the state of the application, but might be overkill for simpler state needs.
  • MobX is another approach, best suited for developers who prefer working with reactive programming paradigms. It provides a reactive state that automatically updates the UI when the state changes, making state management seamless and efficient.

The choice of approach should align with the project’s complexity and specific requirements, ensuring smooth development and optimal app performance.

24. What are the differences between Bloc and Cubit?

In Flutter, Bloc and Cubit are distinct state management solutions with unique mechanisms, catering to different levels of complexity.

  • Bloc, ideal for more complex scenarios, employs a reactive programming model, using streams and requiring the definition of events and states to manage state transitions meticulously. It’s particularly useful when multiple states and transitions are involved, necessitating a detailed and structured approach.
  • On the other hand, Cubit is simpler and more direct, eliminating the need for event definitions and allowing state changes through simple function calls. This makes Cubit suitable for situations where simplicity and rapid development are crucial.

In essence, while Bloc offers structured solutions for intricate scenarios, Cubit is optimal for simpler, more straightforward state management needs.

25. What are the differences between BlocBuilder, BlocListener, and BlocConsumer?

In the Bloc library, BlocBuilder, BlocListener, and BlocConsumer serve distinct purposes, each contributing to efficient state management in Flutter applications.

  1. BlocBuilder is a widget that rebuilds its UI at every state change in the Bloc. It’s primarily used for UI rendering and is ideal when the UI needs to be redrawn in response to state changes. For instance:BlocBuilder<MyBloc, MyState>( builder: (context, state) {return Text('$state');},)
  2. BlocListener is a widget that does not rebuild the UI but instead reacts to state changes, making it suitable for performing actions like navigation or showing a dialog. For example:BlocListener<MyBloc, MyState>(listener: (context, state) {}, child: Container(),)
  3. BlocConsumer combines the functionalities of both BlocBuilder and BlocListener. It rebuilds its UI and performs actions in response to state changes, allowing developers to handle both within the same widget. Here’s a simple usage:BlocConsumer<MyBloc, MyState>( listener: (context, state) {}, builder: (context, state) {},)

In summary, BlocBuilder is for UI rendering, BlocListener is for reacting to state changes without UI rebuilding, and BlocConsumer is a hybrid, catering to both UI and action-based reactions to state changes.

Integration and Testing

26. How does a package differ from a plugin in Flutter?

In Flutter, a package is a modular piece of code that can be easily imported into any application. It might contain general Dart classes or specific Flutter widgets.

A plugin, on the other hand, is a special kind of package that provides additional platform-specific functionality by making use of native code. It offers a bridge between Dart and native code, enabling features that aren’t available in the Flutter framework by default.

27. Can you provide an overview of SQLite in Flutter?

In Flutter, SQLite is utilized for local data persistence, serving as a self-contained, serverless SQL database engine, ideal for mobile devices. It is particularly beneficial for apps requiring offline functionality, enabling data access and modification without internet connectivity, with synchronization occurring once online.

Developers often use the sqflite package to integrate SQLite, leveraging its high-level APIs for various database operations like CRUD (Create, Read, Update, Delete), utilizing familiar SQL queries for interaction.

SQLite stores all its data in a single file on the device, ensuring data persistence across app launches. It is especially suitable for managing local, structured, and moderately-sized datasets, such as user preferences or game scores, providing a balanced solution for simple, efficient local data management in Flutter apps.

28. How do channels work for native platform communication in Flutter?

In Flutter, Platform Channels are essential mechanisms that enable interaction between the Dart code and the native code of the app, bridging the gap between Flutter and native functionalities. They are crucial when there is a need to integrate platform-specific functionalities that Flutter doesn’t provide out of the box.

Platform Channels operate through a shared method call interface, where each channel is distinctly named to prevent any conflicts, ensuring smooth communication. The messages are exchanged between the Dart side and the native side, allowing for bi-directional communication.

For example, if a Flutter app requires access to specific device features like battery status or Bluetooth capabilities, it leverages Platform Channels. The app sends requests through the channels and receives the necessary data from the native side, thereby ensuring seamless integration and a unified user experience. This approach guarantees that Flutter apps can fully harness the capabilities of the platform they run on, providing a comprehensive and cohesive user experience.

29. What are the test methods in Flutter?

Testing is paramount in Flutter to ensure app robustness and quality.

  • Unit tests focus on verifying individual functions, methods, or classes. They’re essential for checking the correctness of isolated logic.
  • Widget tests ensure that widgets can interact correctly with each other. They simulate user interactions and check the visual output and the widget’s response to these interactions. For instance, a widget test might tap a button and then check if a certain text appears.
  • Integration tests evaluate the app as a cohesive unit. They run a complete app and can interact with it as a user would, ensuring that all parts, including networking or database interactions, work harmoniously.

30. What are the primary capabilities of Dart DevTools, and how do they assist Flutter developers?

Dart DevTools is a suite of performance tools for Flutter developers, aiding in debugging and optimizing Flutter applications. It offers several key capabilities:

  • The Flutter Inspector allows developers to visually explore the widget tree to understand the layout and properties of widgets, aiding in identifying and debugging UI issues.
  • Timeline View helps in diagnosing applications frame by frame, allowing developers to identify and resolve performance issues related to rendering and computations, optimizing app responsiveness.
  • The Memory View enables monitoring of memory usage, helping in tracking memory leaks and optimizing memory allocation to avoid crashes due to memory overflow.
  • Source-level Debugger provides the ability to step through code, set breakpoints, and inspect variable values, aiding in efficient bug identification and resolution.
  • Network View allows inspection of HTTP requests and network traffic, ensuring optimized and error-free network communication within the app.
  • Logging View consolidates logs, aiding in effective monitoring, issue identification, and debugging through log message analysis.

Utilizing Dart DevTools, developers can enhance the quality and performance of their Flutter applications, ensuring they are bug-free and user-friendly.

Tips for Crafting Interview Questions

Conducting a technical interview for Flutter developers is a nuanced process that demands meticulous planning and execution. To ensure you get the most out of the interview and truly gauge the candidate’s capabilities, here are some pivotal tips to consider:

  1. Depth Over Breadth: Focus on the depth of understanding. A senior developer should be able to delve deeper into topics compared to a junior.
  2. Tailor to Your Project’s Domain: Align your questions with the domain area of your project. This ensures the candidate is a good fit for the specific role.
  3. Incorporate Real-world Scenarios: Use practical examples or challenges they might face in the role, giving insight into their problem-solving skills.
  4. Assess Communication Skills: Even in a technical interview, it’s vital to gauge if the candidate can articulate complex concepts clearly.
  5. Stay Updated: Keep your questions current, reflecting the latest advancements in Flutter. This tests both the candidate’s knowledge and their commitment to continuous learning.

While technical prowess is essential, the interview should also shed light on the candidate’s adaptability, communication skills, and alignment with your project’s specific needs. By following these tips, you’ll be better equipped to identify the right Flutter developer for your team.

Conclusion

Finding a proficient Flutter developer can be intricate, particularly without extensive knowledge in mobile technology. Opting to collaborate with a Flutter development agency can be a smart move. These agencies can assist in acquiring adept professionals or assembling a dedicated team tailored to your project’s needs, ensuring the optimal realization of your initiatives in the evolving digital environment. Leveraging such agencies can provide the essential expertise and reinforcement required for project advancement.

Source: 

https://medium.com/@flutterwtf/flutter-interview-questions-and-answers-top-30-for-pros-0cdbf1d40ebd

Friday, November 10, 2023

Clean architecture | Flutter

 

We work on many apps on a daily basis. Sometimes these apps are smaller projects e.g. for a small store, sometimes these apps are big, like DAZN. As always, we need to choose a proper tool. These kinds of tools are not only IDE, language, or frameworks. We should consider architecture like any other tool. No architecture is still architecture as well but well-planned architecture is much better because it allows us to create better, well-readable, and extensible software. It also makes our life easier when we have to change our software in the future. A good plan for the app but also for specific features is the key to success. The good news is that we can choose our architecture every day even if the project is big and developed for many years already.

MVC

Model View Controller (MVC) — MVC is a default architecture for iOS apps. Every screen is splitted into three components:

  • Model — where we put our business entities and application models
  • View — where we show formatted data to the user
  • Controller — to transform data and handle user interactions

MVP

Model View Presenter (MVP) — This architecture is very similar to MVC, but all the UI business logic is stored in the presenter. For example, we can ask the presenter to show the next view in case of handle button action. Also presenter interacts with a model to fetch and parse business entities. In other words, the presenter contains a method called by view and makes actions on a view. In this case presenter contains reference to the view.

MVVM

Model View ViewModel (MVVM) — Another similar architecture but in this case, we could treat viewmodel as another side of a view abstraction. Viewmodel provides wrapper to model data that can be linked to view. Viewmodel contains commands (methods) to communicate with the model and the view.

Example: View could ask for service data in case of tapping button. ViewModel is responsible for fetching the data from service, parsing, processing them, and returning to the view in a proper manner.

The main difference between ViewModel and the presenter is that viewmodel doesn’t know anything about the view.

Another difference is that viewmodel could be shared with other views. Presenters do not. Presenters are associated with a concrete view.

MVVM with Clean Architecture

MVVM and Clean Architecture are two architectural patterns commonly used in Android app development.

Combining these two patterns can lead to a well-structured, maintainable and testable Android application.

𝗠𝗩𝗩𝗠 separates your app into three main components — Model, View, and ViewModel.

- 𝗠𝗼𝗱𝗲𝗹: Represents your data and business logic.
- 𝗩𝗶𝗲𝘄: Represents the UI components.
- 𝗩𝗶𝗲𝘄𝗠𝗼𝗱𝗲𝗹: Acts as an intermediary between the Model and View, providing data to the View and handling user interactions.

Clean Architecture

𝗖𝗹𝗲𝗮𝗻 𝗔𝗿𝗰𝗵𝗶𝘁𝗲𝗰𝘁𝘂𝗿𝗲 emphasizes the separation of concerns and the independence of different layers within your app.

- 𝗗𝗼𝗺𝗮𝗶𝗻 𝗟𝗮𝘆𝗲𝗿: Contains your business logic and entities.
- 𝗗𝗮𝘁𝗮 𝗟𝗮𝘆𝗲𝗿: Manages data access and storage.
- 𝗣𝗿𝗲𝘀𝗲𝗻𝘁𝗮𝘁𝗶𝗼𝗻 𝗟𝗮𝘆𝗲𝗿: Handles UI and user interactions.

Auto-generate .g file terminal command

flutter packages pub run build_runner build
flutter pub run build_runner build --delete-conflicting-outputs

dart run build_runner watch

Every architecture has one common thing — separation of concerns.

Clean Architecture is an architectural pattern that was introduced by Robert C. Martin. It provides a way to structure applications that separate the different components of an application into modules, each with a well-defined purpose. The main idea behind Clean Architecture is to separate the application into three main layers: the presentation layer, the domain layer, and the data layer.

The Dependency Inversion Principle (DIP) asserts that high-level modules shouldn’t depend on low-level modules but rather that both should depend on abstractions. Flutter Clean Architecture abides by this rule. This typically entails utilizing abstract classes and interfaces in Flutter to specify the contracts across layers, facilitating easier testing and the flexibility of changing implementations.

We have 4 different layers here. Let’s go deeper.

Data Layer

Represents the data layer of the application. The Data module, which is a part of the outermost layer, is responsible for data retrieval. This can be in the form of API calls to a server and/or a local database. It also contains repository implementations.

  • Repositories (Domain): Actual implementations of the repositories in the Domain layer. Repositories are responsible for coordinating data from the different Data Sources.
  • DTO Models: Representation of JSON structure that allows us to interact with our data sources.
  • Data sources: Consist of remote and local Data Sources. Remote Data Source will perform HTTP requests on the API. While local Data Source will cache or persist data.
  • Mapper: Map Entity objects to Models and vice-versa.

Our data layer consists of converter, database, and repository packages. Since the data layer depends upon the domain layer, we do repository implementation here as said above. The repository communicates with the database, but for each database, we need a separate model, therefore the converter package.

Model class (mapper class) relationship Entities class

Mapper is a straightforward class, to map data that we get from the API — in our case the DTO — to our business object.

import 'package:freezed_annotation/freezed_annotation.dart';
part 'department_entity.freezed.dart';

@Freezed(copyWith: false, equal: false)
class DepartmentEntity with _$DepartmentEntity {
const factory DepartmentEntity(
{ final int? id,
final String? day}) = _DepartmentEntity;

const DepartmentEntity._();
}
import 'package:freezed_annotation/freezed_annotation.dart';

part 'department_mapper.freezed.dart';
part 'department_mapper.g.dart';

@Freezed(copyWith: false, equal: false)
class DepartmentMapper with _$DepartmentMapper {
const factory DepartmentMapper(
{@JsonKey(name: 'ID') final int? id,
@JsonKey(name: 'DAY')
final String? day}) = _DepartmentMapper;

const DepartmentMapper._();

factory DepartmentMapper.fromJson(Map<String, dynamic> json) =>
_$DepartmentMapperFromJson(json);
}

extension DepartmentExtension on DepartmentMapper {
DepartmentEntity toDomain() =>
DepartmentEntity(id: id, day: day);
}
response.data.map<DepartmentEntity>((data) => DepartmentMapper.fromJson(data).toDomain()).toList()

Equatable

Equatable is a popular Flutter package that makes it easy to compare objects for equality. It’s particularly useful when working with Dart’s built-in == operator, which can lead to unintended equality checks for objects with the same data but different references.

Equatable is a Dart package that helps you override the equality (==) and hash code (hashCode) methods in your classes without writing a lot of boilerplate code. It provides a concise and elegant way to define equality for objects based on their properties, rather than their memory addresses.

Serializable

part 'department_mapper.g.dart';

@JsonSerializable()
class DepartmentMapper extends DepartmentEntity {
@override
@JsonKey(name: JsonKeyConstant.idConsignmentJsonKey)
final int? id;

@override
@JsonKey(name: JsonKeyConstant.dayConsignmentJsonKey)
final String? day;

const DepartmentMapper({this.id, this.day})
: super(id: id, day: day);

factory DepartmentMapper.fromJson(Map<String, dynamic> json) =>
_$DepartmentMapperFromJson(json);

Map<String, dynamic> toJson() => _$DepartmentMapperToJson(this);
}
import 'package:equatable/equatable.dart';

class DepartmentEntity extends Equatable {
final int? _id;
final String? _day;

const DepartmentEntity({int? id, String? day})
: _id = id,
_day = day;

int? get id => _id;

String? get day => _day;

@override
List<Object> get props => [_id!, _day!];

@override
bool get stringify => true;
}
List<DepartmentMapper>.from(response.data
.map((i) => DepartmentMapper.fromJson(i)))

Use cases

The second layer is responsible for fetching entities from the backend. Just fetching — not parsing! This could be known in our app as Services. We could communicate with a server using sockets, REST, or any other known way. Use cases (or services) are not affected by controllers, view models, etc. but they could be affected if some of the entities change. This scenario is known as Dependency Rule and we use a pattern called Crossing Boundaries. We could cross architecture boundaries only in a specific way. Only from the top to the bottom.

  • Use Cases: Application-specific business rules. Ause case is a piece of business logic that represents a single task that the system needs to perform. The use case encapsulates the rules and logic required to perform the task, and defines the inputs and outputs required for the operation.
  • Entities: Business objects of the application
  • Repositories: Abstract classes that define the expected functionality of outer layers
class LoginRemote
extends UseCase<Either<RemoteFailure, LoginEntity>, LoginInput> {
final GetLoginRepository getLoginRepository;

LoginRemote(this.getLoginRepository);

// User login Use case
@override
Future<Either<RemoteFailure, LoginEntity>> call(LoginInput input) =>
getLoginRepository(input: input.toJson());
}
abstract class GetLoginRepository {
Future<Either<RemoteFailure, LoginEntity>> call({Map<String, dynamic>? input});
}
class GetLoginRepositoryImp extends GetLoginRepository {
final RemoteDataSource _remoteDataSource;

GetLoginRepositoryImp(this._remoteDataSource);

@override
Future<Either<RemoteFailure, LoginEntity>> call(
{Map<String, dynamic>? input}) {
return _remoteDataSource.login(input: input);
}
}
@Freezed(copyWith: false, equal: false)
class LoginInput with _$LoginInput {
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
factory LoginInput(
{@JsonKey(name:'MOBILE') required String mobile,
@JsonKey(name:'PASSWORD') required String password,
@JsonKey(name:'DEVICE_ID') String? deviceId,
@JsonKey(name:'FIREBASE_TOKEN')
String? firebaseToken}) = _LoginInput;

factory LoginInput.fromJson(Map<String, dynamic> json) =>
_$LoginInputFromJson(json);
}
//KOTLIN

data class Category(
val name: String,
val color: Int = Ivy.toArgb(),
val icon: String? = null,
val orderNum: Double = 0.0,

val isSynced: Boolean = false,
val isDeleted: Boolean = false,

val id: UUID = UUID.randomUUID()
) {
fun toEntity(): CategoryEntity = CategoryEntity(
name = name,
color = color,
icon = icon,
orderNum = orderNum,
isSynced = isSynced,
isDeleted = isDeleted,
id = id
)
}
//KOTLIN

@Serializable
internal data class MovieRemote(
val id: Int,
val title: String,
val overview: String,
@SerialName("poster_path")
val posterImage: String,
@SerialName("release_date")
val releaseDate: String
)

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class MovieRemote(
val id: Int,
val title: String,
val overview: String,
@SerialName("poster_path")
val posterImage: String,
@SerialName("release_date")
val releaseDate: String
)

internal fun MovieRemote.toMovie() : Movie {
return Movie(
id = id,
title = title,
description = overview,
imageUrl = getImageUrl(posterImage),
releaseDate = releaseDate
)
}

private fun getImageUrl(posterImage: String) = "https://www.themoviedb.org/t/p/w500/${posterImage}"


interface MovieRepository {
suspend fun getMovies(page: Int) : List<Movie>
suspend fun getMovie(movieId: Int) : Movie
}

internal class MovieRepositoryImpl(
private val remoteDataSource: RemoteDataSource
) : MovieRepository {
override suspend fun getMovies(page: Int): List<Movie> {
return remoteDataSource.getMovies(page = page).results.map {
it.toMovie()
}
}

override suspend fun getMovie(movieId: Int): Movie {
return remoteDataSource.getMovie(movieId = movieId).toMovie()
}
}

Interface adapters (controllers, viewmodels, interactors, presenters) — This layer is application-specific. We use them to change business models to application models which will be used by our views later. For example, we could store our entities (formatted, processed) in a database.

UI, Views — the last and the lowest level layer is responsible for presenting our data to the end user. Generally, we don’t write much code here. We only use this layer to communicate with other layers of our circle.

data/: This folder contains all the implementation details of the application's data layer. It is further divided into:

  • datasources/: This folder contains the implementation of data sources, which can be either remote or local. For example, an API client to communicate with a backend server can be a remote data source, while a local database can be a local data source.
  • repositories/: This folder contains the implementation of repositories, which act as a single source of truth for the data. Repositories provide an abstraction over the data sources and handle data retrieval and storage.
  • models/: This folder contains the implementation of data models, which are used to represent the data entities of the application.

domain/: This folder contains the implementation of the application's domain layer. The Domain layer will contain our core business rules (entity) and application-specific business rules (use cases). It is further divided into:

  1. Entities (main data holders)
  2. Repository interface definition (communication with datasources)
typedef AnalyticsResult = Either<RemoteFailure, String>;

abstract class GetAnalyticsRepository {
Future<AnalyticsResult> loginAnalytics({Map<String, dynamic>? input});
Future<AnalyticsResult> signUpAnalytics({Map<String, dynamic>? input});
Future<AnalyticsResult> productEntryAnalytics({Map<String, dynamic>? input});
Future<AnalyticsResult> unListingAnalytics({Map<String, dynamic>? input});
Future<AnalyticsResult> createNewPayeeAnalytics({Map<String, dynamic>? input});
Future<AnalyticsResult> logoutAnalytics({Map<String, dynamic>? input});
}
  1. Use cases (core business logic of app)
  • entities/: This folder contains the implementation of domain entities, which represent the real-world objects of the application. Entities should be independent of any specific implementation detail.
  • repositories/: we need interface definition for our repository and that is all we need, the rest will be done on the data layer.
  • usecases/: This folder contains the implementation of use cases, which define the business logic of the application. Use cases depend on the domain entities and the repository interfaces.

domain layer completely independent

It fetches and saves the data on various data sources, it must depend upon the data layer, right? Well, we want for data layer to depend upon the domain layer, the domain layer must not have ANY dependencies. That is why a repository interface is defined on the domain layer and its implementation is written on the data layer. That is called a Dependency Inversion Principle.

presentation/: This folder contains the implementation of the application's presentation layer. It is further divided into:

  • pages/: This folder contains the implementation of the pages or screens of the application.
  • widgets/: This folder contains the implementation of reusable widgets that are used across different pages or screens.
  • blocs/providers: This folder contains the implementation of BLoCs (Business Logic Components) or providers, which are responsible for managing the state of the application. BLoCs/providers depend on the use cases and provide the necessary data to the pages or screens.
  • utils/: This folder contains helper classes or functions that are used across the presentation layer.

injection_container.dart: This file contains the dependency injection setup for the application. It defines how the different components of the application should be created and wired together.

Either

And if we want to return a failure case we would wrap it up in a function called left:

return left(UsersListFailure(error: ex.toString()));

Now if we want to return our list / model of data from function, we would simply wrap it up in a function called right from the fpdart package, something like this:

return right(list.map((e) => UserJson.fromJson(e).toDomain()).toList());

Class DataSource no abstract class

class AuthenticationRepository {
final FirebaseAuth _firebaseAuth;
final GoogleSignIn _googleSignIn;

AuthenticationRepository(this._firebaseAuth, this._googleSignIn);

Future<UserCredential> createUserWithEmailAndPassword(
String email, String password) async {
return await _firebaseAuth.createUserWithEmailAndPassword(
email: email, password: password);
}
}

await injector
.get<AuthenticationRepository>()
.createUserWithEmailAndPassword(enter input, enter password);

Best coding structure style

Document, Comment, Detail add why change

import 'package:freezed_annotation/freezed_annotation.dart';

import '../../core/const/const.dart';

part 'login_input.freezed.dart';
part 'login_input.g.dart';

/// [mobile] - [JsonKeyConstant.mobileJsonParamKey] Enter the mobile number
/// [password] - [JsonKeyConstant.passwordJsonParamKey] Enter the password
/// [deviceId] - [JsonKeyConstant.deviceIdJsonParamKey] Get the device id from mobile
/// [firebaseToken] - [JsonKeyConstant.firebaseTokenJsonParamKey] Get the firebase token from Firebase

@Freezed(copyWith: false, equal: false)
class LoginInput with _$LoginInput {
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
factory LoginInput(
{@JsonKey(name: JsonKeyConstant.mobileJsonParamKey) required String mobile,
@JsonKey(name: JsonKeyConstant.passwordJsonParamKey) required String password,
@JsonKey(name: JsonKeyConstant.deviceIdJsonParamKey) String? deviceId,
@JsonKey(name: JsonKeyConstant.firebaseTokenJsonParamKey)
String? firebaseToken}) = _LoginInput;

factory LoginInput.fromJson(Map<String, dynamic> json) =>
_$LoginInputFromJson(json);
}

Cubit clean arch (Go router, get-it) download https://drive.google.com/file/d/1LObs9CRWSzIz1OpTBrQbfStNhJhamyM0/view?usp=sharing

Cubit clean arch (Go router, get-it, injectable) download https://drive.google.com/file/d/1X83OYAtpU5oovbjSrPpWd1yahRanivMJ/view?usp=sharing

Cubit clean arch (Go router, get-it, injectable, freezed) download https://drive.google.com/file/d/1VQS_2AWKWviEwfVQcnZfXYn9UYF4RMPq/view?usp=sharing

Cubit clean arch (Go router, get-it, injectable, freezed, localization, dio) download

get_it is a service locator used to get at run-time the concrete class of the abstract class we are requesting.
injectable and injectable_generator will be used to annotate and generate the get_it’s code (an official example of code generation).

Why injectable?

When the service locator pattern is used throughout the whole application and each abstract class can have several concrete classes it is very useful not to have to think about the implementation of get_it but to let the code generation take care of it.

Step new API integrate in Clean architecture

1) input parameter (Domain)
- equatable
- generated freezed file, serialization
2) Entity create (Data - Domain)
- equatable
- generated freezed file, serialization
3) Mapper create (Data)
4) Repositories create (Domain)
5) api name add (Core)
6) RemoteDataSource abstract method create and implement
7) (Domain) repositories implement (Data) repositories
8) Usecase create (Domain)
9) Cubit crate / GetX Controller (Controller, Binding) / Riverpod
- generated freezed class
10) UI Create

when vs whenOrNull

BlocConsumer<SplashCubit, SplashState>(
bloc: getIt<SplashCubit>()..isAuthenticatedUserCheck(),
listener: (_, state) {
///when MUST BE ALL CUBIT STATE DEFINE AND whenOrNull NEED TO CUBIT STATE DEFINE
state.whenOrNull(
authenticate: () => context.go(RouterPath.home),
unAuthenticatedState: () => context.go(RouterPath.login));
},
builder: (_, state) => Scaffold(
body:
Center(child: Text(appName, style: context.headerLine6Context))))

ListView.builder Set Data

 switch (state.runtimeType) { 
case UserDetailState.initial:
return Text('Clean archi');
}
  @override
Widget build(BuildContext context) => Scaffold(
body: BlocBuilder<ReturnListCubit, ReturnListState>(
bloc: getIt<ReturnListCubit>()..returnListGetData(),
builder: (context, state) {
final status = (state.loading, state.returnList.isEmpty);

return switch (status) {
(true, _) => const PlatformLoadingIndicatorWidget(),
(_, true) => const ListViewEmptyWidget(),
(_, false) => ListView.builder(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics()),
itemCount: state.returnList.length,
itemBuilder: (BuildContext context, int index) =>
_returnItemView(context: context))
};
}));
  • An Entity is a concept or thing that has a unique identity and represents something meaningful in the problem domain or system being modeled or built.
import 'package:get_it/get_it.dart';

class DependencyManager {

/// GetIt instance.
final getIt = GetIt.instance;

/// I use singleton to create this manager.
DependencyManager._internal();

static final DependencyManager _singleton = DependencyManager._internal();

factory DependencyManager() {
return _singleton;
}

T get<T extends Object>(){
return getIt.get<T>();
}

T getWithParam<T extends Object, P1>(dynamic p){
return getIt.get<T>(param1: p);
}

T getWith2Param<T extends Object, P1, P2>(dynamic p1, dynamic p2){
return getIt.get<T>(param1: p1, param2: p2);
}

bool isRegistered<T extends Object>(){
return getIt.isRegistered<T>();
}

void registerLazySingleton<T extends Object>(FactoryFunc<T> factoryFunc){
getIt.registerLazySingleton<T>(factoryFunc );
}

void registerFactory<T extends Object>(FactoryFunc<T> factoryFunc){
getIt.registerFactory<T>(factoryFunc );

}

void registerFactoryParam<T extends Object, P1, P2>(
FactoryFuncParam<T, P1, P2> factoryFunc, {
String? instanceName,
}){
getIt.registerFactoryParam<T,P1,P2>(factoryFunc);
}
}


void injectDependencies() {
try {

/// Get dependency manager
DependencyManager manager = DependencyManager();

/// Register UserSQLite as IUserDataSource
manager.registerFactory<IUserDataSource>(() => UserSQLite());

} catch (e2) {
debugPrint("Manage depency injections error");
}
}
 
Source:
https://medium.com/@kamal.lakhani56/clean-architecture-f23b7d9c6ee7