Buổi meeting Angular 13 to Flutter 3.0 tháng 8 năm 2022

27th Aug 2022
Buổi meeting Angular 13 to Flutter 3.0 tháng 8 năm 2022
Table of contents

Xin chào các bạn, sau buổi meeting có một số bạn hỏi và muốn trải nghiệm thử hai web build với Angular và Flutter nhưng lúc present mình không show được nhiều, các bạn có thể comment bên dưới, mình sẽ gửi một account test mình khởi tạo cho các bạn.

Angular webapp: https://kyonsvn.web.app/student/

Flutter webapp: https://kyonsvn.web.app/m/

Buổi meeting của group rất bổ ích. Chúc các bạn cuối tuần vui vẻ

Similarities in Development

Splash screen and App Bootstrap

With Flutter web it’s a must

class SplashScreen extends HookWidget {
  const SplashScreen({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    AnimationController animationController1 =
        useAnimationController(duration: const Duration(seconds: 3), initialValue: 1);
    AnimationController animationController2 =
        useAnimationController(duration: const Duration(seconds: 3), initialValue: 0.75)..repeat();
    animationController1.repeat(reverse: false);
    final animTurn = Tween<double>(begin: 0.0, end: -1.0).animate(animationController2);
    return Directionality(
      textDirection: TextDirection.ltr,
      child: Container(
        color: AppColors.primaryBlue,
        child: Stack(children: [
          Align(
            alignment: Alignment.center,
            child: RotationTransition(
              turns: animationController1,
              child: SizedBox(
                height: 100,
                width: 100,
                child: CustomPaint(painter: ThreeFourthsCirclePainter()),
              ),
            ),
          ),
          Align(
            alignment: Alignment.center,
            child: RotationTransition(
              turns: animTurn,
              child: SizedBox(
                height: 75,
                width: 75,
                child: CustomPaint(painter: ThreeFourthsCirclePainter()),
              ),
            ),
          ),
          Align(
            alignment: Alignment.center,
            child: RotationTransition(
              turns: animationController1,
              child: SizedBox(
                height: 50,
                width: 50,
                child: CustomPaint(painter: ThreeFourthsCirclePainter()),
              ),
            ),
          ),
        ]),
      ),
    );
  }
}

with Angular it’s nice to have

<div id="loading">
  <div id="circle-loading">
    <span></span>
    <span></span>
    <span></span>
  </div>
  <style>
    app-root,
    #loading {
      background-color: #2c3d5b;
      align-items: center;
      display: flex;
      height: 100%;
      justify-content: center;
      width: 100%;
    }
    #loading #circle-loading {
      width: 100px;
      height: 100px;
      position: relative;
    }
    #circle-loading span:first-child {
      width: 100%;
      height: 100%;
      border-color: #fff;
      border-left-color: transparent;
      top: 0;
      left: 0;
      -webkit-animation: effect-1-1 3s infinite linear;
      -moz-animation: effect-1-1 3s infinite linear;
      -ms-animation: effect-1-1 3s infinite linear;
      -o-animation: effect-1-1 3s infinite linear;
      animation: effect-1-1 3s infinite linear;
    }
    #circle-loading span:nth-child(2) {
      width: 75%;
      height: 75%;
      border-color: #fff;
      border-left-color: transparent;
      top: 12.5%;
      left: 12.5%;
      -webkit-animation: effect-1-2 3s infinite linear;
      -moz-animation: effect-1-2 3s infinite linear;
      -ms-animation: effect-1-2 3s infinite linear;
      -o-animation: effect-1-2 3s infinite linear;
      animation: effect-1-2 3s infinite linear;
    }
    #circle-loading span:last-child {
      width: 50%;
      height: 50%;
      border-color: #fff;
      border-left-color: transparent;
      top: 25%;
      left: 25%;
      -webkit-animation: effect-1-1 3s infinite linear;
      -moz-animation: effect-1-1 3s infinite linear;
      -ms-animation: effect-1-1 3s infinite linear;
      -o-animation: effect-1-1 3s infinite linear;
      animation: effect-1-1 3s infinite linear;
    }
    #circle-loading span {
      display: block;
      -webit-border-radius: 50%;
      -moz-border-radius: 50%;
      -ms-border-radius: 50%;
      -o-border-radius: 50%;
      border-radius: 50%;
      border: 2px solid #fff;
      position: absolute;
      top: 50%;
      left: 50%;
      -webkit-transform: translate(-50%, -50%);
      -moz-transform: translate(-50%, -50%);
      -ms-transform: translate(-50%, -50%);
      -o-transform: translate(-50%, -50%);
      transform: translate(-50%, -50%);
    }
    @keyframes effect-1-1 {
      0% {
        -webkit-transform: rotate(0deg);
        -moz-transform: rotate(0deg);
        -ms-transform: rotate(0deg);
        -o-transform: rotate(0deg);
        transform: rotate(0deg);
      }
      100% {
        -webkit-transform: rotate(360deg);
        -moz-transform: rotate(360deg);
        -ms-transform: rotate(360deg);
        -o-transform: rotate(360deg);
        transform: rotate(360deg);
      }
    }
    @keyframes effect-1-2 {
      0% {
        -webkit-transform: rotate(0deg);
        -moz-transform: rotate(0deg);
        -ms-transform: rotate(0deg);
        -o-transform: rotate(0deg);
        transform: rotate(0deg);
      }
      100% {
        -webkit-transform: rotate(-360deg);
        -moz-transform: rotate(-360deg);
        -ms-transform: rotate(-360deg);
        -o-transform: rotate(-360deg);
        transform: rotate(-360deg);
      }
    }
  </style>
</div>

Design and Styling

Can use any tool like **Figma** to design, in my case Flutter will have the mobile design, Angular has a desktop one

Design system example

Design system example

Desktop

Desktop

Mobile

# Mobile

- Angular I choose TailwindsCSS and Angular Material so the configuration is on the file `[tailwind.config.js]

tailwind.config.js

colors = require('tailwindcss/colors');

module.exports = {
  mode: 'jit',
  purge: {
    content: ['./src/**/*.{html,ts}'],
  },
  darkMode: 'class', // or 'media' or 'class'
  theme: {
    extend: {
      colors: {
        primaryBlue: '#2C3D5B',
        secondaryBlue: '#315D93',
        orange: '#FB7200',
        lightOrange: {
          1: '#FB923C',
          2: '#FDBA74',
          3: '#FED7AA',
          4: '#FFEDD5',
        },
        darkOrange: {
          1: '#DE6501',
          2: '#C15802',
          3: '#A54B03',
          4: '#883E04',
        },
        emerald: {
          0: '#34D399',
          1: '#6EE7B7',
          2: '#A7F3D0',
          3: '#D1FAE5',
        },
        darkEmerald: '#109867',
        red: {
          0: '#EF4444',
          1: '#FEE2E2',
        },
        darkRed: '#B90F0F',
        lightBlue: {
          1: '#06A5FF',
          2: '#27B1FF',
          3: '#5BC3FE',
          4: '#98D6FA',
          5: '#B6E5FF',
          6: '#D0EEFF',
        },
        blueGrey: {
          50: '#F8FAFC',
          100: '#F1F5F9',
          200: '#E2E8F0',
          300: '#CBD5E1',
          400: '#94A3B8',
          500: '#64748B',
          600: '#475569',
          700: '#334155',
          800: '#1E293B',
          900: '#0F172A',
        },
        black: colors.black,
        white: colors.white,
      },
      width: {
        '200px': '200px',
        '260px': '260px',
        '120px': '120px',
      },
      height: {
        '120px': '120px',
        '200px': '200px',
      },
      boxShadow: {
        1: '0px 3px 5px rgba(0, 0, 0, 0.1)',
        2: '0px 5px 10px rgba(0, 0, 0, 0.15)',
        3: '0px 15px 15px rgba(0, 0, 0, 0.3)',
      },
      borderWidth: {
        10: '10px',
      },
      lineHeight: {
        full: '100%',
      },
      stroke: {
        primaryBlue: '#2C3D5B',
        orange: '#FB7200',
      },
    },
  },
  variants: {
    extend: {},
  },
  plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],
};

and can add the global style, ex `src\app\presentation\assets\scss`

- Flutter is OOP framework, I use Material Design so I create customized theme in 

[themes.dart]

import 'package:another_flushbar/flushbar.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

part 'app_button_theme.dart';
part 'app_colors.dart';
part 'app_font_sizes.dart';
part 'app_icons.dart';
part 'app_message.dart';
part 'app_screen_sizes.dart';
part 'app_spacings.dart';
part 'app_text_theme.dart';

ThemeData lightTheme() {
  final result = ThemeData.light().copyWith(
    textTheme: GoogleFonts.montserratTextTheme(
      Typography().black.copyWith(
            headlineMedium: const TextStyle(
              fontWeight: FontWeight.w700,
              color: AppColors.primaryBlue,
            ),
            bodyMedium: const TextStyle(
              fontSize: AppFontSizes.paragraph,
              fontWeight: FontWeight.w400,
              color: AppColors.primaryBlue,
            ),
            titleMedium: const TextStyle(
              color: AppColors.primaryBlue,
            ),
            labelMedium: const TextStyle(
              color: AppColors.primaryBlue,
            ),
          ),
    ),
  );
  return result.copyWith(
    useMaterial3: true,
    visualDensity: VisualDensity.standard,
    scaffoldBackgroundColor: AppColors.blueGray100,
    colorScheme: ColorScheme.fromSeed(seedColor: AppColors.primaryBlue),
    inputDecorationTheme: InputDecorationTheme(
      floatingLabelBehavior: FloatingLabelBehavior.never,
      border: MaterialStateOutlineInputBorder.resolveWith((states) {
        final bool isFocused = states.contains(MaterialState.focused);
        final bool isDisabled = states.contains(MaterialState.disabled);
        final bool hasError = states.contains(MaterialState.error);

        final Color color = isDisabled
            ? AppColors.blueGray400
            : hasError
                ? AppColors.red
                : isFocused
                    ? AppColors.primaryBlue
                    : AppColors.blueGray400;
        const double width = 1.0;
        const double radius = 5.0;

        return OutlineInputBorder(
            borderSide: BorderSide(color: color, width: width),
            borderRadius: const BorderRadius.all(Radius.circular(radius)));
      }),
      labelStyle: MaterialStateTextStyle.resolveWith((states) {
        final bool isDisabled = states.contains(MaterialState.disabled);
        final bool hasError = states.contains(MaterialState.error);

        final Color color = isDisabled
            ? AppColors.blueGray400
            : hasError
                ? AppColors.primaryBlue
                : AppColors.blueGray400;

        return result.textTheme.bodyMedium!.copyWith(color: color);
      }),
      fillColor: MaterialStateColor.resolveWith((states) {
        final bool isDisabled = states.contains(MaterialState.disabled);
        final Color color = isDisabled ? AppColors.blueGray200 : AppColors.white;

        return color;
      }),
      filled: true,
      contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 9.5),
    ),
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ButtonStyle(
        backgroundColor: MaterialStateProperty.resolveWith((states) {
          if (states.contains(MaterialState.disabled)) {
            return AppColors.blueGray200;
          }
          return AppColors.orange;
        }),
        foregroundColor: MaterialStateProperty.resolveWith((states) {
          if (states.contains(MaterialState.pressed)) return AppColors.lightOrange3;
          if (states.contains(MaterialState.disabled)) return AppColors.blueGray400;
          return AppColors.white;
        }),
        overlayColor: MaterialStateProperty.resolveWith((states) {
          if (states.contains(MaterialState.disabled)) {
            return AppColors.blueGray200;
          }
          return AppColors.orange;
        }),
        textStyle: MaterialStateProperty.all(result.textTheme.button!.copyWith(
          fontWeight: FontWeight.w600,
          fontSize: AppFontSizes.button,
        )),
        shape:
            MaterialStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppSizesUnit.small5))),
        elevation: MaterialStateProperty.resolveWith((states) {
          if (states.contains(MaterialState.hovered)) return 1;
          return null;
        }),
        padding: MaterialStateProperty.all(const EdgeInsets.symmetric(vertical: 17, horizontal: 20)),
        mouseCursor: MaterialStateProperty.resolveWith((states) {
          if (states.contains(MaterialState.disabled)) return SystemMouseCursors.forbidden;
          return SystemMouseCursors.click;
        }),
      ),
    ),
    textButtonTheme: TextButtonThemeData(
      style: ButtonStyle(
        backgroundColor: MaterialStateProperty.all(Colors.transparent),
        foregroundColor: MaterialStateProperty.all(AppColors.orange),
        overlayColor: MaterialStateProperty.all(Colors.transparent),
        textStyle: MaterialStateProperty.all(
          result.textTheme.button!.copyWith(
              fontWeight: FontWeight.w600, fontSize: AppFontSizes.button, decoration: (TextDecoration.underline)),
        ),
        mouseCursor: MaterialStateProperty.resolveWith((states) {
          if (states.contains(MaterialState.disabled)) return SystemMouseCursors.forbidden;
          return MaterialStateMouseCursor.clickable;
        }),
      ),
    ),
    outlinedButtonTheme: OutlinedButtonThemeData(
      style: ButtonStyle(
        backgroundColor: MaterialStateProperty.all(AppColors.white),
        foregroundColor: MaterialStateProperty.resolveWith((states) {
          if (states.contains(MaterialState.disabled)) return AppColors.blueGray400;
          return AppColors.orange;
        }),
        overlayColor: MaterialStateProperty.resolveWith((states) {
          if (states.contains(MaterialState.disabled)) return AppColors.blueGray300;
          return AppColors.white;
        }),
        textStyle: MaterialStateProperty.resolveWith((states) {
          return result.textTheme.button!.copyWith(
            fontWeight: FontWeight.w600,
            fontSize: AppFontSizes.button,
            color: states.contains(MaterialState.disabled) ? AppColors.blueGray300 : AppColors.orange,
          );
        }),
        shape: MaterialStateProperty.resolveWith((states) {
          return RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(AppSizesUnit.small5),
          );
        }),
        side: MaterialStateProperty.resolveWith((states) {
          Color borderColor = AppColors.blueGray300;
          if (states.contains(MaterialState.hovered)) {
            borderColor = AppColors.orange;
          }
          return BorderSide(
            color: borderColor,
          );
        }),
        elevation: MaterialStateProperty.resolveWith((states) {
          if (states.contains(MaterialState.hovered)) return 1;
          return null;
        }),
        padding: MaterialStateProperty.all(const EdgeInsets.symmetric(vertical: 17, horizontal: 20)),
        mouseCursor: MaterialStateProperty.resolveWith((states) {
          if (states.contains(MaterialState.disabled)) return SystemMouseCursors.forbidden;
          return SystemMouseCursors.click;
        }),
      ),
    ),
    canvasColor: AppColors.orange,
    drawerTheme: const DrawerThemeData(
      backgroundColor: AppColors.primaryBlue,
      elevation: 1.0,
      width: double.infinity,
    ),
    dividerTheme: const DividerThemeData(
      endIndent: AppSizesUnit.medium16,
      indent: AppSizesUnit.medium16,
      space: 0,
      color: AppColors.blueGray400,
    ),
    iconTheme: const IconThemeData(color: AppColors.blueGray400),
    primaryIconTheme: const IconThemeData(color: AppColors.orange),
    appBarTheme: const AppBarTheme(
      color: AppColors.white,
      iconTheme: IconThemeData(
        color: AppColors.blueGray400,
      ),
      scrolledUnderElevation: 0,
    ),
    errorColor: AppColors.red,
    bottomSheetTheme: const BottomSheetThemeData(
      modalBackgroundColor: AppColors.primaryBlue,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(AppSizesUnit.medium24)),
      ),
    ),
    progressIndicatorTheme: const ProgressIndicatorThemeData(
      color: AppColors.lightBlue1,
      linearTrackColor: AppColors.blueGray300,
    ),
  );
}

[app_colors.dart]

part of 'themes.dart';

class AppColors {
  AppColors._();

  static const Color primaryBlue = Color(0xFF2C3D5B);
  static const Color secondaryBlue = Color(0xFF315D93);
  static const Color black = Color(0xFF000000);
  static const Color white = Color(0xFFFFFFFF);
  static const Color orange = Color(0xFFFB7200);
  static const Color lightOrange1 = Color(0xFFFB923C);
  static const Color lightOrange2 = Color(0xFFFDBA74);
  static const Color lightOrange3 = Color(0xFFFED7AA);
  static const Color lightOrange4 = Color(0xFFFFEDD5);
  static const Color darkOrange1 = Color(0xFFDE6501);
  static const Color darkOrange2 = Color(0xFFC15802);
  static const Color darkOrange3 = Color(0xFFA54B03);
  static const Color darkOrange4 = Color(0xFF883E04);
  static const Color darkEmerald = Color(0xFF109867);
  static const Color emerald = Color(0xFF34D399);
  static const Color emerald1 = Color(0xFF6EE7B7);
  static const Color emerald2 = Color(0xFFA7F3D0);
  static const Color emerald3 = Color(0xFFD1FAE5);
  static const Color darkRed = Color(0xFFB90F0F);
  static const Color red = Color(0xFFEF4444);
  static const Color red1 = Color(0xFFFEE2E2);
  static const Color lightBlue1 = Color(0xFF06A5FF);
  static const Color lightBlue2 = Color(0xFF27B1FF);
  static const Color lightBlue3 = Color(0xFF5BC3FE);
  static const Color lightBlue4 = Color(0xFF98D6FA);
  static const Color lightBlue5 = Color(0xFFB6E5FF);
  static const Color lightBlue6 = Color(0xFFD0EEFF);
  static const Color blueGray50 = Color(0xFFF8FAFC);
  static const Color blueGray100 = Color(0xFFF1F5F9);
  static const Color blueGray200 = Color(0xFFE2E8F0);
  static const Color blueGray300 = Color(0xFFCBD5E1);
  static const Color blueGray400 = Color(0xFF94A3B8);
  static const Color blueGray500 = Color(0xFF64748B);
  static const Color blueGray600 = Color(0xFF475569);
  static const Color blueGray700 = Color(0xFF334155);
  static const Color blueGray800 = Color(0xFF1E293B);
  static const Color blueGray900 = Color(0xFF0F172A);
  static const Color trasparent = Color(0x00000000);

  static const Color shadow = Color.fromRGBO(0, 0, 0, 0.3);
  static const Color buttonShadow = Color.fromRGBO(0, 0, 0, 0.1);
}

Demo color selection in the code

Navigation / Route

    - Angular we can use `Routes from "@angular/router"`, then in html component we use directive `[routerLink]` or in ts file we use `Router.navigate` from `@angular/router`

    - Flutter we can use **GoRouter** with some config in `router.dart`, navigate using `context.go` or `context.push` with a String path and parameters

UI Element

    - Angular use *Angular Material Component* [https://material.angular.io/components/categories](https://material.angular.io/components/categories). Style for component can set in global css

    - Flutter we can use *Material Components widgets* out of the box [https://docs.flutter.dev/development/ui/widgets/material](https://docs.flutter.dev/development/ui/widgets/material).

We can create new one that adapted app style

Working with API

    - Angular, for interceptor we have buildin `HttpInterceptor` (`interceptor.ts`). Then call `HttpClient.get()` or `HttpClient.post()`

    - Flutter we can use buildin *Http* feature or package **Dio** for easy interceptor, then simply use `Dio.get()` or `Dio.post()` etc (`api.dart`)

Flavor and build web

Angular: npx nx run kyons-angular:build --configuration=firebase

"firebase": {
  "baseHref": "/student/",
  "outputPath": "dist/student/",
  "fileReplacements": [
    {
      "replace": "src/app/app-routing.module.ts",
      "with": "src/app/app-routing.module.firebase.ts"
    }
  ],
  "buildOptimizer": false,
  "optimization": false,
  "vendorChunk": true,
  "extractLicenses": false,
  "sourceMap": true,
  "namedChunks": true,
  "outputHashing": "all"
}

Flutter: flutter build web --base-href \"/m/\" --web-renderer canvaskit --target=\"lib/main_dev.dart\”

When and why did I use Angular and Flutter?

Recently I use Angular to made all the front-end stuffs, include our very first idea, demo concept, then the MVP with student portal, tutor portal… They are build very fast, easy to change when we need. After that when users need mobile app, I started porting code into Flutter.
    
Since the Flutter app was done, we have the benefit of supporting multiple platforms, and have some special abilities such as call native api by using [Dart_ffi](https://docs.flutter.dev/development/platform-integration/android/c-interop). Ex for connecting with NFT blockchain with NEAR protocal we just import one RPC api written in Rust and then we can use everywhere. Or developing new mobile game with Flutter, which is my favorite part of work, will be easier with Flutter game engine like [Flame](https://flame-engine.org/).

Bạn thấy bài viết này như thế nào?
1 reaction

Add new comment

Image CAPTCHA
Enter the characters shown in the image.

Related Articles

Bạn sẽ thấy những tên tuổi vô cùng quen thuộc như FILIP HRÁČEK, CRAIG LABENZ, REMI ROUSSELET, FELIX ANGELOV, SIMON LIGHTFOOT

Mình thấy các bạn trẻ đang loay hoay tìm kiếm cách học và làm việc với Flutter tuy nhiên chưa có đủ

Như các bạn đã biết, Flutter là một SDK được Google xây dựng, để phát triển các ứng dụng di động nhanh hơn, tiết kiệm chi phí hơn. Giờ đây, các developer chỉ cần viết code một lần mà ứng dụng chạy được tận 2 nền tảng.