Cloud Computing: The Digital Shift 2.0 – Understanding the Current Market Trends

Flutter – Best practices to start with – Josh Software

Flutter SDK is Google’s UI toolkit for crafting beautiful, natively compiled applications for mobile, web, and desktop from a single codebase. Flutter cross platform uses dart language to build apps. So I assume you have already gone through the basics of Dart language.

So let’s start on this one by one.

1. Follow clean architecture while creating your directory structure

Directory structure with clean architecture

Keep your Models, Views & Data/Repository layers separate. Views(Widgets in flutter cross platform) can be further divided into modules. You can create Utility for commonly used functions/constants.

For example,

  • Create SharedPrefHelper class for storing shared preferences, where you can write all the preference keys with storing and accessing functions in that class.
  • Create AppUtil class with common functions for frequently used tasks like showing spacebar or common dialogs or loader.
  • Define all your app-specific colors in Colors class and use it by just importing your Colors class.

The repository layer should be used separately for network, database, etc., so that in the future if you change your network lib then you will only have to make a change in your network helper class.

Create network helper class

class NetworkHelper {
  static const String baseUrl = "https://yourdomain.com/";

  Future<dynamic> post(String url, String reqBody) async {
    var responseJson;
    try {
      String cookie = await getTokenPref();
      Map<String, String> myHeaders = <String, String>{
        'Content-Type': 'application/json',
        'Authentication': cookie,
      };
      final response = await http.post(
        baseUrl + url,
        headers: myHeaders,
        body: reqBody,
      );
      responseJson = _returnResponse(response);
    } on SocketException {
      throw FetchDataException('No Internet connection'); //custom excepton
    }
    return responseJson;
  }
  //similar methods for GET, PUT, DELETE
}

Create network repository class to use Netowork helper class

class NetworkRepository {
  NetworkHelper _helper = NetworkHelper();

  Future<LoginModel> fetchLoginData(String body) async {
    final response = await _helper.post('api/login');
    return LoginModel.fromJson(response);
  }

  //methods for other network requests
}

And use this repository in your widget.

NetworkRepository networkRepository = NetworkRepository();

var input = LoginInput(_email.trim(), _password.trim());
var jsonBody = jsonEncode(input.toJson());
//showLoader
networkRepository.fetchLoginData(jsonBody).then((user) {
  //showLoader
  // do stuff with user model
}).catchError((error) {
  print('login error  = $error');
  //hideLoader
});

2. Create generic custom widgets for repeated designs

For example, if your app has too many containers in your project with rounded corners and borders, then you can create the following custom widget.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class RoundedContainer extends StatelessWidget {
  final double width;
  final double height;
  final double radius;
  final double shadow;
  final int bgColor;
  final int borderColor;
  final double borderWidth;
  final Widget child;
  final double padding;

  RoundedContainer({
    this.width,
    this.height,
    this.radius,
    this.shadow = 0,
    this.bgColor,
    this.borderColor,
    this.borderWidth,
    this.child,
    this.padding = 0,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(padding),
      alignment: Alignment.center,
      width: this.width,
      height: this.height,
      child: child,
      decoration: BoxDecoration(
        color: Color(bgColor),
        border: Border.all(
          color: Color(borderColor),
          width: borderWidth
        ),
        borderRadius: BorderRadius.all(
          Radius.circular(radius),
        ),
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withOpacity(0.5),
            blurRadius: shadow,
          ),
        ],
      ),
    );
  }
}

and use it as

child: RoundedContainer(
  width: MediaQuery.of(context).size.width * 0.45,
  height: MediaQuery.of(context).size.height * 0.1,
  padding: 16.0,
  radius: 5,
  shadow: 4,
  borderColor: Colors.red,
  bgColor: Colors.white,
  borderWidth: 1,
  child: Text('I am in rounded container')
)

3. Always wrap your root widgets in SafeArea.

SafeArea allows you to organize your UI widgets excluding the top status bar and bottom navigation bar.

 @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: YourWidget
    };
 }

4. Go through the lifecycle of Stateless and Stateful widgets

When you start with flutter SDK you should be able to choose whether you should create stateless widget or stateful widget. While creating a stateful widget you should know how it works from its creation to destruction.

For e.g. initState() method gets called only once in the creation of the widget lifecycle, but it’s not guaranteed that view has been created at this point in time. If there is a requirement to assign data to view once after the creation of the widget, then you may use the following snippet of code in initState() to ensure the widget has been already created.

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
    loadPreviousData(data);
  });
}

5. Use state wisely

class _AutoReadOtpState extends State<CountdownText> {
 Timer countdownTimer;
 static const int WAITING_DURATION = 60;
 int updatedWaitingDuration = WAITING_DURATION;

 @override
 void initState() {
   super.initState();

   if (countdownTimer == null) {
     countdownTimer = Timer.periodic(
       Duration(seconds: 1),
           (Timer t) => setState(() {
         updatedWaitingDuration -= 1;
         if (updatedWaitingDuration <= 0) {
           countdownTimer.cancel();
           widget.onCompleted(0);
         }
       }),
     );
   }
 }

 @override
 void dispose() {
   // TODO: implement dispose
   super.dispose();
   countdownTimer.cancel();
 }

 @override
 Widget build(BuildContext context) {
   return Column(
     children: [
       //...other widgets
       Text(
         "Resend otp in $updatedWaitingDuration seconds",
       ),
       //..other widgets
     ],
   );
 }
}

In the above example, while we are updating text with updated countdown time, the complete widget is getting re-rendered which is not optimized and bad for UI performance. Here we should separate Countdown text with the new stateful widget as follows.

Is Flutter the Future of Cross-Platform Mobile App Development

class _CountdownTextState extends State<CountdownText> {
  Timer countdownTimer;
  static const int WAITING_DURATION = 60;
  int updatedWaitingDuration = WAITING_DURATION;

  @override
  void initState() {
    super.initState();

    if (countdownTimer == null) {
      countdownTimer = Timer.periodic(
        Duration(seconds: 1),
            (Timer t) => setState(() {
          updatedWaitingDuration -= 1;
          if (updatedWaitingDuration <= 0) {
            countdownTimer.cancel();
            widget.onCompleted(0);
          }
        }),
      );
    }
  }

  @override
  void dispose() {
// TODO: implement dispose
    super.dispose();
    countdownTimer.cancel();
  }

  @override
  Widget build(BuildContext context) {
    return  Text(
      "Resend otp in $updatedWaitingDuration seconds",
    );
  }
}

And use this widget in the above example in place of a Text widget, so that on calling of setState inside that will only update Text widget, not the parent widget.

6. Use InheritedWidget for globally required data in the same widget tree.

If you have a long widget tree and need to access data from parent to any children widgets, then, instead of passing data from widget to widget, create an Inherited widget in the parent widget, and then you can access that widget anywhere in any of the children widgets by just passing context.

Create a new class that extends InheritedWidget

class UserDataProvider extends InheritedWidget {
  final User user;
  final Widget child;

  UserDataProvider(
      {this.user, this.child});

  @override
  bool updateShouldNotify(UserDataProvider oldWidget) {
    return true;
  }

  static UserDataProvider of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<UserDataProvider>();
}

and use this inside your parent widget as follows

class _DashboardPageState extends State<DashboardPage> {
  User user;

  @override
  void initState() {
    super.initState();
    // assign user here
  }

  @override
  Widget build(BuildContext context) {
    return DashboardDataProvider(
      user: user,
      child: _getBody(context),
    );
  }

7. Use SharedPrefHelper utility file for Shared Preferences as follows

class SharedPrefHelper {

  static const String USER = "user";

  static Future<User> setUserPref(User user) async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    String userJson = jsonEncode(user);
    await prefs.setString(USER, userJson);
    return user;
  }

  static Future<User> getUserPref() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    String user = prefs.getString(USER) ?? null;
    if (user == null || user == "null") {
      return null;
    } else {
      return User.fromJson(jsonDecode(user));
    }
  }
}

and use this file in your widgets

@override
void initState() {
  super.initState();
  SharedPrefHelper.getUserPref().then((user) {
    setState(() {
      this.user = user;
    });
  });
}

8 . Wrap your root widgets in SafeArea

SafeArea widget insets child by removing padding required to OS controls like status bar, bottom navigations buttons like back button etc.

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(),
    body: SafeArea(
      child: YourWidget(),
    },
  };
}

9. Use FutureBuilder for asynchronous tasks like getting data from a network or database.

If your UI is completely network/database data dependant then you can wrap your root widget in FutureBuilder and display the UI according to the state of the widget. Check the following code snippet.

Future<User> _user;
Future<Promos> _promos;

@override
void initState() {
  super.initState();
  _promos = NetworkHelper.getPromos();
  _user = SharedPrefHelper.getUserPref();
}

@override
Widget build(BuildContext context) {
  return FutureBuilder(
    future: Future.wait([
      _user,
      _promos,
    ]),
    builder: (context, AsyncSnapshot<List<dynamic>> snapshot) {
      if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
        user = snapshot.data[0];
        promos = snapshot.data[1];
        return YourDataDisplayingWidget(
          promos: promos,
          userId: user.id,
          child: getBody(context),
        );
      } else if(snapshot.hasError){
        return ErrorDisplayingWidget();
      } else {
        return ProgressBar();
      }
    }
  }
}

10. Use callback to transfer data/actions from widgets

Declare the following callback function in your widget.

typedef void NavigateToPageCallback(int pageNo)

and pass it to the child widget as follows

child: ChildWidget(
  (pageNo) {
    setState(() {
      this.pageNo = pageNo;
    });
  }
)

and call that callback in ChildWidget as follows

class ChildWidget extends StatefulWidget {
  final NavigateToPageCallback callback;

  ChildWidget(this._scaffoldKey, this._pageNo, this.callback);

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

class _ChildWidgetState extends State<ChildWidget> {

  @override
  Widget build(BuildContext context) {

    return InkWell(
      onTap: () {
        widget.callback(2);
      },
      child: Text("click")
    );
  }
}    

11. Create a new file for the colors you want to use in your app and use them by importing in your widget.

Create a file named MyColors.dart

import 'dart:ui';

class MyColors {
  static const int colorPrimary = 0xFF21333D;
  static const int colorPrimaryDark = 0xFF1a2931;
  static const int colorPrimaryLight = 0xFF29404c;
}

And use them in your widget as follows

body: Container(
  color: Color(MyColors.colorPrimary),
}

12. Some UI related points to be noted

  • Use expanded only in Row, Column, or Flex widget when it’s not wrapped in Scrollable widget.
  • Always try to use dynamic dimensions to the widget using screen width & height so that UI will look similar on different screen sizes
  • Create a custom text widget for fonts so that if you change the font in the future then you will have to make minimal updates.
  • Avoid using nested scrolling widgets.
  • Try to break down large screens into smaller widgets according to functionality which helps in code understanding and simplicity.
  • Clear all the resources in dispose() callback of the stateful widget
  • For iOS, specific widgets use Cupertino lib.

Hope this article is helpful for beginners to start with flutter SDK. For more advanced features you can explore libs like provider, redux, bloc, etc. Happy coding !!!

Author
Publish Info
Follow Us On