Use of SOLID Principles in Flutter for Top-Notch Mobile Applications

Use of SOLID Principles in Flutter for Top-Notch Mobile Applications

A set of design principles called SOLID principles used to create scalable and maintainable software.

These principles make software adaptable, simple to change, and error- and bug-resistant. They are often used in software development and have been found to speed up development and increase code quality.

Flutter and SOLID may give developers an effective toolkit for developing excellent mobile apps. Programmers may write modular, maintainable, and scalable code that is simple to test and debug using SOLID principles during Flutter app development.

The rest of the article will examine how Flutter and SOLID are connected and how you can use SOLID principles to improve the quality and maintainability of your Flutter applications.

How are SOLID principles and Flutter connected?

SOLID and Flutter are connected in several different ways. As a starting point, Flutter makes developing modular and reusable components simple, which is a major SOLID objective. To write code that is simpler to comprehend, alter, and test, Flutter engineers must first divide apps into smaller, more specialized components.

1. The Single Responsibility Principle (S.R.P.):

The S.R.P. encourages programmers to design classes or modules with a single responsibility. This may entail designing widgets for Flutter that do a single, narrowly defined task, such as handling user input or displaying data.

Consider a Flutter application that lists weather forecasts. The app must retrieve the weather information from an external API and show it to the user.

By dividing the duties of obtaining and showing the meteorological data into two distinct classes, we seek to follow S.R.P.

We may first make a class called WeatherService that is in charge of obtaining the weather information from the API:

import 'dart:convert';
import 'package:http/http.dart' as http;

class GetWeatherService {
  final String apiKey;
  final http.Client? httpClient;

  GetWeatherService({required this.apiKey, this.httpClient});

  Future<WeatherData> getWeatherData(String city) async {
    final url =
        'https://api.openweathermap.org/data/2.5/weather?q=$city&appid=$apiKey';
    final response = await httpClient?.get(Uri.parse(url));
    if (response?.statusCode == 200) {
      return WeatherData.fromJson(jsonDecode(response!.body));
    } else {
      throw Exception('Failed to fetch corresponding city weather data');
    }
  }

  Widget build(BuildContext context, WeatherData weatherData) {
    return const SizedBox();//Your widget here
  }
}

2. OCP (Open-Closed Principle):

In object-oriented design, the Open-Closed Principle (OCP) says that software entities should be open for expansion but closed for evolution. We should create our code in a way that makes adding new functionality simple without changing the current code.

Here, we wish to adhere to OCP by letting the app show the weather information in various ways without changing the original code.

We may define a build method in an abstract ShowWeather class:

abstract class ShowWeather {
  Widget build(BuildContext context, WeatherData weatherData);
}

The build function may be implemented in many ways by specific ShowWeather subclasses we create later.

For instance:

class ShowTemperature extends ShowWeather {
  @override
  Widget build(BuildContext context, WeatherData weatherData) {
    final temperature = weatherData.temperature;
    return Text(
      '${temperature.toStringAsFixed(1)}\u00B0C',
      style: const TextStyle(fontSize: 20),
    );
  }
}

class ShowHumidity extends ShowWeather {
  @override
  Widget build(BuildContext context, WeatherData weatherData) {
    final humidity = weatherData.humidity;
    return Text(
      'Humidity: ${humidity.toStringAsFixed(1)}%',
      style: const TextStyle(fontSize: 25),
    );
  }
}

To accept an instance of ShowWeather, we can finally display the weather data. To show the weather data in various ways, we may pass in several ShowWeather objects in the following methods:

class WeatherScreen extends StatefulWidget {
  final String city;
  final GetWeatherService service;
  final GetWeatherService display;


  const WeatherScreen({
    super.key,
    required this.city,
    required this.service,
    required this.display,
  });


  @override
  _WeatherScreenState createState() => _WeatherScreenState();
}


class _WeatherScreenState extends State<WeatherScreen> {
 late Future<WeatherData> weatherData;

  @override
  void initState() {
    super.initState();
    weatherData = widget.service.getWeatherData(widget.city);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Weather in ${widget.city}'),
      ),
      body: Center(
        child: FutureBuilder<WeatherData>(
          future: weatherData,
          builder: (BuildContext context, AsyncSnapshot<WeatherData> snapshot) {
            if (snapshot.hasData) {
              return widget.display.build(context, snapshot.data!);
            } else if (snapshot.hasError) {
              return Text('${snapshot.error}');
            } else {
              return const CircularProgressIndicator();
            }
          },
        ),
      ),
    );
  }
}

3. L.S.P. (Liskov Substitution Principle):

According to the L.S.P., any instance of a subtype should be able to be substituted in place of an instance of its parent type without impairing the program’s ability to run correctly.

We want to ensure that any ShowWeather subtype may be used interchangeably with the ShowWeather class to adhere to the Liskov Substitution Principle.

This may be accomplished by ensuring that every ShowWeather subclass implements the build method exactly as specified in the ShowWeather class.

class ShowTemperature extends ShowWeather {
  @override
  Widget build(BuildContext context, WeatherData weatherData) {
    final temperature = weatherData.temperature;
    return Text(
      '${temperature.toStringAsFixed(1)}\u00B0C',
      style: const TextStyle(fontSize: 25),
    );
  }
}

class ShowHumidity extends ShowWeather {
  @override
  Widget build(BuildContext context, WeatherData weatherData) {
    final humidity = weatherData.humidity;
    return Text(
      'Humidity: ${humidity.toStringAsFixed(1)}%',
      style: const TextStyle(fontSize: 25),
    );
  }
}

Flutter Developers from Flutter Agency

4. I.S.P. (Interface Segregation Principle used in SOLID Principles):

According to the I.S.P., a class shouldn’t be made to depend on methods it doesn’t utilize.

We want to ensure that each class that implements an interface only defines the methods relevant to its tasks to follow the interface segregation principle.

As an illustration, we might create an interface called WeatherDataSource that defines the getWeatherData function and have WeatherService implement it:

class GetWeatherService implements WeatherSourceData{
  final String apiKey;
  final http.Client? httpClient;

  GetWeatherService({required this.apiKey, this.httpClient});

  Future<WeatherData> getWeatherData(String city) async {
    final url =
        'https://api.openweathermap.org/data/2.5/weather?q=$city&appid=$apiKey';
    final response = await httpClient?.get(Uri.parse(url));
    if (response?.statusCode == 200) {
      return WeatherData.fromJson(jsonDecode(response!.body));
    } else {
      throw Exception('Failed to fetch corresponding city weather data');
    }
  }

  Widget build(BuildContext context, WeatherData weatherData) {
    return const SizedBox(); //Your widget here
  }
}

5. D.I.P. (Dependency Inversion Principle):

High-level modules shouldn’t be dependent on low-level modules, according to the Dependency Inversion Principle. Both of them should rely on abstractions.

We want to ensure that high-level modules depend on abstractions rather than certain implementations to adhere to the Dependency Inversion Principle.

In our illustration, we may build a WeatherRepository class that is dependent on WeatherSourceData, enabling us to quickly swap between several WeatherDataSource implementations without having to modify the WeatherRepository code:

class WeatherRepository {
  final WeatherSourceData dataSource;

  WeatherRepository(this.dataSource);

  Future<WeatherData> getWeatherData(String city) =>
      dataSource.getWeatherData(city);
}

Finally, we can change the WeatherScreen class to utilize WeatherRepository rather than GetWeatherService, enabling us to swap between several WeatherSourceData implementations quickly:

class WeatherScreen extends StatefulWidget {
  final String city;
  final WeatherRepository weatherRepository;
  final GetWeatherService display;

  const WeatherScreen({
    super.key,
    required this.city,
    required this.weatherRepository,
    required this.display,
  });

  @override
  _WeatherScreenState createState() => _WeatherScreenState();
}

class _WeatherScreenState extends State<WeatherScreen> {
  late Future<WeatherData> weatherData;

  @override
  void initState() {
    super.initState();
    weatherData = widget.weatherRepository.getWeatherData(widget.city);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Weather in ${widget.city}'),
      ),
      body: Center(
        child: FutureBuilder<WeatherData>(
          future: weatherData,
          builder: (BuildContext context, AsyncSnapshot<WeatherData> snapshot) {
            if (snapshot.hasData) {
              return widget.display.build(context, snapshot.data!);
            } else if (snapshot.hasError) {
              return Text('${snapshot.error}');
            } else {
              return const CircularProgressIndicator();
            }
          },
        ),
      ),
    );
  }
}

Important Things To Remember for SOLID Principles

When using SOLID principles in Flutter development, keep the following in mind:

1. S.R.P. (Single Responsibility Principle): A class should have just one change-related factor.
2. OCP (Open-Closed Principle): Software entities should be open for expansion but closed for modification, according to the OCP (Open-Closed Principle).
3. Liskov Substitution Principle (L.S.P.): Subtypes need to be able to replace their base types.
4. I.S.P. (Interface Segregation Principle): Clients shouldn’t depend on interfaces as they don’t utilize them.
5. D.I.P. (Dependency Inversion Principle): It is not advisable for high-level modules to rely on low-level ones. Both of them must depend on abstractions.

Conclusion

The SOLID principles offer developers of mobile apps important instructions for writing clear, extendable, and maintainable code. Coders may design apps adaptable to change, simple to comprehend, and simple to maintain by following these concepts. SOLID principles are used in creating mobile apps to facilitate conversation among team members, make testing and debugging easier, and enable scalability. Thus, by incorporating SOLID principles into Flutter mobile app development, we can produce scalable, adaptable, and easy-to-maintain apps that may change with time to meet the demands of the user.

Frequently Asked Questions (FAQs)

1. Is Flutter appropriate for developing mobile applications?

With Flutter’s highly adaptable platform, you can build a wide variety of apps, from little ones for startups to huge ones for multinationals. Small applications benefit greatly since it enables fast application development and low-cost final product delivery.

2. Which architecture fits Flutter the best for SOLID Principles?

The best practice for your mobile projects with the Flutter architecture is that you can utilize well-known frameworks like MVC and MVVM. However, because of Flutter’s distinctiveness and emphasis on widgets, BLoC is typically seen as the perfect Flutter architecture.

3. Why is Flutter the future for developing mobile apps?

Faster Development Time: The “hot reload” feature of Flutter’s framework enables programmers to make changes and observe the effects in real-time swiftly. As a result, developers can test and iterate their code much more quickly, cutting down on development time and improving processes.

Book Your Flutter Developer Now

Abhishek Dhanani

Written by Abhishek Dhanani

Abhishek Dhanani, a skilled software developer with 3+ years of experience, masters Dart, JavaScript, TypeScript, and frameworks like Flutter and NodeJS. Proficient in MySQL, Firebase, and cloud platforms AWS and GCP, he delivers innovative digital solutions.

Leave a comment

Your email address will not be published. Required fields are marked *


ready to get started?

Fill out the form below and we will be in touch soon!

"*" indicates required fields

✓ Valid number ✕ Invalid number