Blog Infos
Author
Published
Topics
, , , ,
Published

This document contains the approach for clean code and best practices in flutter.

Image Source “Google Gemini”

 

Overview of Flutter
  • Definition: Flutter is an open-source UI software development kit created by Google. It allows developers to create natively compiled applications for mobile, web, and desktop from a single code base.
Importance of Clean Code and Best Practices
  • Maintainability: Clean code is easier to maintain, understand, and extend by any developer, reducing the cost and effort over the lifecycle of the application.
  • Readability: Well-organized code with clear naming conventions and structure is easier to read and understand.
  • Scalability: Following best practices ensures that the application can scale efficiently as it grows in size and complexity.
Principles of Clean Code

Naming Conventions : Consistent naming conventions improve readability and make the code base easier to navigate.

  • Variables and Methods : Use camelCase for variables, functions, and parameters name. Use descriptive names that convey the purpose. Method name should describe the action or behavior.
// Bad 
var x = 10; 
void foo() {}  

// Good 
var itemCount = 10; 
void fetchData() {}
  • Classes: Use PascalCase for class names. Names should reflect the entity or concept.
// Bad
class Foo {}  

// Good
class HomeViewModel {}
  • File Name: Name files using snake_case.

Consistent Formatting:

  • Indentation: Use consistent indentation (typically 2 spaces for Dart).
  • Style Guide: Use tools like flutter_lints to enforce coding standards.
  • Braces: Always use braces for conditional statements and loops.
// Bad 
if (condition)   
  doSomething();  

// Good 
if (condition) {   
  doSomething(); 
}

Simplicity: Simplify Logic by Breaking down complex logic into smaller, manageable functions.

// Bad 
bool isEven(int number) {   
  if (number % 2 == 0) {     
    return true;   
  } else {     
    return false;   
  } 
}  

// Good 
bool isEven(int number) => number % 2 == 0;

Code Reviews: Regular code reviews by peer ensure adherence to standards and improve code quality.

Best Practices in Flutter
Organizing Your Project
  • Data Layer: Handles data retrieval and storage.
lib/
├── data/
│   ├── models/
│   └── data sources/
  • Domain Layer: Contains business logic and use cases.
├── domain/
│   ├── provider/
│   └── repositories/
  • Presentation Layer: Manages UI and user interaction.
└── presentation/
    ├── screens/
    └── widgets/

Use Dart’s Strong Typing

  • Why: Helps catch errors at compile time rather than at runtime.
  • How: Always define types for variables, function parameters, and return types.

Leverage Flutter’s Built-In Widgets

  • Why: Flutter provides a rich set of pre-built widgets that are optimized for performance and consistency.
  • How: Use StatelessWidget for static content and StatefulWidget when the UI needs to change. Use ‘SearchDelegate’ to implement local search.

Optimize for Performance

  • Why: Ensures a smooth and responsive user experience.
  • How:
  • Avoid rebuilding widgets unnecessarily.
  • Use const constructors for widgets that don’t change.
  • Minimize the use of setState and use more efficient state management techniques.
  • Use RepaintBoundary to limit the area of the screen that needs to be repainted.
  • Minimize the use of below widgets as they are expensive to build and manage
  • Clip: Clipping operations can be computationally expensive, especially if applied frequently or to complex widgets.
  • Opacity: The Opacity widget can cause performance issues because it forces the entire subtree to be composited, even if only a small part of the tree is visible. Consider using the Visibility widget.
  • Nested Scroll: Using multiple nested ListViews or ScrollViews can cause performance degradation and lead to complicated scrolling behavior. Consider using a single ScrollView or employing a CustomScrollView with Sliver widgets to create complex scrolling layouts more efficiently.
  • AnimatedBuilder with Expensive Build Methods: AnimatedBuilder is powerful for creating animations, but if the builder method contains complex UI logic, it can cause frame drops during animation. Move complex or expensive operations (like: Hitting n/w and parsing response) outside of the builder method, and only update what is necessary during the animation.
  • Using Global Keys Excessively: GlobalKeys are powerful but can be memory-heavy and slow down the app because they keep track of widgets across the entire widget tree. Use GlobalKeys only when absolutely necessary, such as when maintaining the state of a widget across different parts of the widget tree.
  • Nested Builders: Nesting multiple builders like FutureBuilder inside StreamBuilder can complicate the widget tree and cause unnecessary rebuilds. Combine the logic outside the build method or refactor the code to minimize the nesting of builders.
  • Raw Pointer and Gesture Detectors: While GestureDetector and Listener are essential for handling gestures, using them excessively or inappropriately can cause performance issues and unexpected behavior. Prefer using higher-level gesture widgets like InkWell or InkResponse that are optimized for handling common gestures within material design guidelines.

Use Linting and Code Analysis Tools

  • Why: Ensures that your code adheres to best practices and coding standards.
  • How: Use the flutter_lints package or customize your linter rules. Regularly run flutter analyze to catch potential issues.

Write Unit and Integration Tests

  • Why: Ensures that your code is working as expected and helps in catching bugs early.
  • How: Use Flutter’s testing framework for writing unit, widget, and integration tests. Aim for good test coverage, especially for critical business logic.

Manage App State Efficiently

  • Why: Proper state management ensures that your app remains responsive and maintains a clear separation of concerns.
  • How: Choose a state management solution that fits your project, such as Provider, Riverpod, or GetX, and use it consistently across the app.

Keeping Widgets Small and Reusable

  • Decompose Widgets: Break down large widgets into smaller, reusable components.
// Bad
class ComplexWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: [
          Text('Title'),
          TextField(),
          RaisedButton(onPressed: () {}),
        ],
      ),
    );
  }
}

// Good
class TitleWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('Title');
  }
}

class InputWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TextField();
  }
}

class ButtonWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(onPressed: () {});
  }
}

class ComplexWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: [
          TitleWidget(),
          InputWidget(),
          ButtonWidget(),
        ],
      ),
    );
  }
}

Use Dependency Injection: Use DI tools/plugins, like GetIt, for injecting dependencies. Avoid to create objects directly.

final getIt = GetIt.instance;

void setup() {
  getIt.registerSingleton<SomeService>(SomeService());
}

// Accessing the service
class SomeWidget extends StatelessWidget {
  final SomeService someService = getIt<SomeService>();

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

Graceful Error Handling: Use try-catch blocks to handle exceptions gracefully.

try {
  // Code that might throw an exception
} catch (e) {
  // Handle the exception
}

Custom Error Classes: Create custom error classes for specific error handling.

class NetworkException implements Exception {
  final String message;

  NetworkException(this.message);
}

void fetchData() {
  try {
    // Simulate network call
    throw NetworkException('Failed to fetch data');
  } catch (e) {
    if (e is NetworkException) {
      print(e.message);
    }
  }
}

Code Documentation: Write comments for classes, methods, and complex logic.

/// This is a CounterModel class that manages the counter state.
class CounterModel extends ChangeNotifier {
  int _count = 0;

  /// Returns the current count value.
  int get count => _count;

  /// Increments the counter by 1.
  void increment() {
    _count++;
    notifyListeners();
  }
}
Testing

Importance of Testing

  • Code Quality: Ensures code works as expected and reduces bugs.
  • Reliability: Confidence in code changes and refactoring.
  • Documentation: Tests serve as documentation for expected behavior.

Example of Unit Test for CounterModel

// Counter model test
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/models/counter_model.dart';

void main() {
  test('Counter value should be incremented', () {
    final counter = CounterModel();

    counter.increment();

    expect(counter.count, 1);
  });
}

Example of widget test of MyApp widget

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/main.dart';

void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());

    // Verify that our counter starts at 0.
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Tap the '+' icon and trigger a frame.
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    // Verify that our counter has incremented.
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });
}

Example of Integration Test:

import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('Counter App', () {
    final counterTextFinder = find.byValueKey('counter');
    final buttonFinder = find.byValueKey('increment');

    FlutterDriver driver;

    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    tearDownAll(() async {
      if (driver != null) {
        driver.close();
      }
    });

    test('starts at 0', () async {
      expect(await driver.getText(counterTextFinder), "0");
    });

    test('increments the counter', () async {
      await driver.tap(buttonFinder);
      expect(await driver.getText(counterTextFinder), "1");
    });
  });
}
Performance Optimization

Avoid Unnecessary Widget Builds

  • Use const constructors: Whenever possible, use const constructors for immutable widgets.
// Bad
return Text('Hello World');

// Good
return const Text('Hello World');
  • Use keys: Keys help Flutter determine whether widgets should be reused or recreated.
// Bad
return ListView(
  children: items.map((item) => Text(item)).toList(),
);

// Good
return ListView(
  children: items.map((item) => Text(item, key: Key(item))).toList(),
);
  • Use ListView.builder: For long lists, use ListView.builder to lazily build list items.
// Bad
return ListView(
  children: [
    Text('Item 1'),
    Text('Item 2'),
    // More items...
  ],
);

// Good
return ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return Text(items[index]);
  },
);
  • Cache Images: Use CachedNetworkImage to cache images for better performance.
CachedNetworkImage(
  imageUrl: "https://example.com/image.jpg",
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
);
  • Lazy Loading: Implement lazy loading for long lists or paginated data. The purpose of using LazyLoadScrollView in this context is to implement lazy loading or infinite scrolling in the ListView. When the user scrolls to the end of the list, the onEndOfPage callback is triggered, allowing the application to load more data and append it to the list. This approach helps in efficiently managing large datasets by loading data incrementally as the user scrolls, rather than loading all data at once.
LazyLoadScrollView(
  onEndOfPage: () => fetchMoreData(),
  child: ListView.builder(
    itemCount: items.length,
    itemBuilder: (context, index) {
      return ListTile(title: Text(items[index]));
    },
  ),
);
Network Optimization

Choose the Right HTTP Client:

  • Use http package for basic requests.
  • Consider dio for more advanced features like interceptors, retries, and timeouts.
// Example with Dio
Dio dio = Dio();
Future fetchData() async {
  try {
    var response = await dio.get('https://example.com/data');
    print(response.data);
  } catch (e) {
    print(e);
  }
}

Implement Caching:

  • Use shared_preferences or hive for device level caching.
  • Use Api level caching to reduce the number of call to server.

Data Compression:

  • Compress data before sending to reduce network usage.
  • Consider using gzip compression.

Error Handling:

  • Implement robust error handling to gracefully handle network failures.
  • Provide informative error messages to the user.

Progress Indicators:

  • Display progress indicators during network requests to improve user experience.

API Optimization:

  • Minimize data transfer by sending or receiving only necessary data.
Image Optimization

Use CachedNetworkImage:

  • Efficiently loads and caches images.

Compress Images:

  • Reduce image size without compromising quality.

Use Placeholders:

  • Display placeholders while images are loading.

Lazy Loading:

  • Load images only when needed to improve performance.
Data Fetching Optimization

Pagination:

  • Load data in chunks to improve performance and reduce initial load time.

Infinite Scrolling:

  • Load more data as the user scrolls.

Selective Field Selection:

  • Fetch only required fields from the server or database.
Memory Management
  • Dispose Controllers: Always dispose of TextEditingController, AnimationController, etc., to free up resources.
class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
  final TextEditingController _controller = TextEditingController();
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return TextField(controller: _controller);
  }
}

Memory Leaks:

  • Use tools like Flutter DevTools to identify memory leaks.
  • Pay attention to global variables and static fields.

Profiling:

  • Regularly profile your app to identify performance bottlenecks.

I hope this explanation has provided some insights into clean code principles and best practices in Flutter. I look forward to writing more about other topics. 👏 Please clap 👏 if you have learned at least one thing, and share if you believe the content deserves it. Feel free to provide feedback, comment, or start a discussion.

Find me!

LinkedIn: www.linkedin.com/in/radheshyam-singh-9a2747a9

Happy coding!

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Collections are a set of interfaces and classes that implement highly optimised data structures.…
READ MORE
blog
This article will continue the story about the multi-platform implementation of the in-app review.…
READ MORE
blog
This is final part of a three part series on using Flutter with AWS…
READ MORE
blog
In today’s interconnected world, mobile applications have become integral to our daily lives, handling…
READ MORE
Menu