Blog

  • [Flutter]Tạo Widget với Android Native bằng Platform Views

    [Flutter]Tạo Widget với Android Native bằng Platform Views

    Một ví dụ về tạo 2 widget được triển khai dưới Android Native

    Dưới Native

    Đầu tiên, chúng ta sẽ tạo 2 class FirstWidget.kt và SecondWidget.kt:

    • FirstWidget.kt : Sử dụng file .xml
    
    import android.content.Context
    import android.view.LayoutInflater
    import android.view.View
    import io.flutter.plugin.platform.PlatformView
    
    internal class FirstWidget(context: Context, id: Int, creationParams: Map<String?, Any?>?) : PlatformView {
        private val view: View
    
        override fun getView(): View {
            return view
        }
    
        init {
            view = LayoutInflater.from(context).inflate(R.layout.first_widget, null)
        }
    
        override fun dispose() {
        }
    }
    • first_widget.xml
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:context=".MainActivity"
        tools:showIn="@layout/first_widget">
    
        <TextView
            android:id="@+id/first_widget_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="FirstWidget from Android!"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    • SecondWidget.kt: 1 cách khác để tạo view
    import android.content.Context
    import android.graphics.Color
    import android.view.View
    import android.widget.TextView
    import io.flutter.plugin.platform.PlatformView
    
    internal class SecondWidget(context: Context, id: Int, creationParams: Map<String?, Any?>?) : PlatformView {
        private val textView = TextView(context)
    
        override fun getView(): View {
            return textView
        }
    
        override fun dispose() {}
    
        init {
            textView.textSize = 20f
            textView.setBackgroundColor(Color.rgb(255, 255, 255))
            textView.text = "Rendered on a native Android view (id: $id)"
        }
    }

    Tạo 2 class FirstWidgetFactory.kt và SecondWidgetFactory.kt:

    • FirstWidgetFactory.kt
    import android.content.Context
    import io.flutter.plugin.common.StandardMessageCodec
    import io.flutter.plugin.platform.PlatformView
    import io.flutter.plugin.platform.PlatformViewFactory
    
    class FirstWidgetFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
        override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
            val creationParams = args as Map<String?, Any?>?
            return FirstWidget(context, viewId, creationParams)
        }
    }
    • SecondWidgetFactory.kt
    import android.content.Context
    import io.flutter.plugin.common.StandardMessageCodec
    import io.flutter.plugin.platform.PlatformView
    import io.flutter.plugin.platform.PlatformViewFactory
    
    class SecondWidgetFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
        override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
            val creationParams = args as Map<String?, Any?>?
            return SecondWidget(context, viewId, creationParams)
        }
    }

    Tại MainActivity.kt:

    import io.flutter.embedding.android.FlutterActivity
    import io.flutter.embedding.engine.FlutterEngine
    import io.flutter.plugins.GeneratedPluginRegistrant
    
    class MainActivity : FlutterActivity() {
    
        override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
            GeneratedPluginRegistrant.registerWith(flutterEngine)
            flutterEngine
                    .platformViewsController
                    .registry
                    .registerViewFactory("com.example.flutter_app_demo1.FirstWidgetPlugin", FirstWidgetFactory())
            flutterEngine
                    .platformViewsController
                    .registry
                    .registerViewFactory("com.example.flutter_app_demo1.SecondWidgetPlugin", SecondWidgetFactory())
        }
    
    }

    Về phần Dart

    Tạo 2 file first_widget.dart và second_widget.dart:

    • first_widget.dart
    import 'package:flutter/foundation.dart';
    import 'package:flutter/gestures.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter/rendering.dart';
    import 'package:flutter/services.dart';
    
    
    class FirstWidget extends StatefulWidget {
      const FirstWidget({
        Key? key,
      }) : super(key: key);
    
    
      @override
      State<StatefulWidget> createState() => _FirstWidgetState();
    }
    
    class _FirstWidgetState extends State<FirstWidget> {
       String viewType = 'com.example.flutter_app_demo1.FirstWidgetPlugin';
       Map<String, dynamic> creationParams = <String, dynamic>{};
      @override
      Widget build(BuildContext context) {
        if (defaultTargetPlatform == TargetPlatform.android) {
          return SizedBox(
            width: 200,
            child: PlatformViewLink(
              viewType: viewType,
              surfaceFactory:
                  (BuildContext context, PlatformViewController controller) {
                return AndroidViewSurface(
                  controller: controller as AndroidViewController,
                  gestureRecognizers:  const <Factory<OneSequenceGestureRecognizer>>{},
                  hitTestBehavior: PlatformViewHitTestBehavior.opaque,
                );
              },
              onCreatePlatformView: (PlatformViewCreationParams params) {
                return PlatformViewsService.initSurfaceAndroidView(
                  id: params.id,
                  viewType: viewType,
                  layoutDirection: TextDirection.ltr,
                  creationParams: creationParams,
                  creationParamsCodec: const StandardMessageCodec(),
                  onFocus: () {
                    params.onFocusChanged(true);
                  },
                )
                  ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
                  ..create();
              },
            ),
          );
        }
        return const Text('iOS platform version is not implemented yet.');
      }
    
    }
    • second_widget.dart
    import 'package:flutter/foundation.dart';
    import 'package:flutter/gestures.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter/rendering.dart';
    import 'package:flutter/services.dart';
    
    class SecondWidget extends StatefulWidget {
      const SecondWidget({
        Key? key,
      }) : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _SecondWidgetState();
    }
    
    class _SecondWidgetState extends State<SecondWidget> {
      String viewType = "com.example.flutter_app_demo1.SecondWidgetPlugin";
      Map<String, dynamic> creationParams = <String, dynamic>{};
      @override
      Widget build(BuildContext context) {
        if (defaultTargetPlatform == TargetPlatform.android) {
          return PlatformViewLink(
            viewType: viewType,
            surfaceFactory:
                (BuildContext context, PlatformViewController controller) {
              return AndroidViewSurface(
                controller: controller as AndroidViewController,
                gestureRecognizers:  const <Factory<OneSequenceGestureRecognizer>>{},
                hitTestBehavior: PlatformViewHitTestBehavior.opaque,
              );
            },
            onCreatePlatformView: (PlatformViewCreationParams params) {
              return PlatformViewsService.initSurfaceAndroidView(
                id: params.id,
                viewType: viewType,
                layoutDirection: TextDirection.ltr,
                creationParams: creationParams,
                creationParamsCodec: const StandardMessageCodec(),
                onFocus: () {
                  params.onFocusChanged(true);
                },
              )
                ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
                ..create();
            },
          );
        }
        return const Text('iOS platform version is not implemented yet.');
      }
    
    }
    

    Sau đó gọi lại 2 Widget vừa rồi tại main.dart

    @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Flutter PlatformView Example'),
          ),
          body: Column(
            mainAxisSize: MainAxisSize.min,
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: const <Widget>[
              Card(
                child: SizedBox(
                  height: 200,
                  child: FirstWidget(),
                ),
              ),
              Card(
                child: SizedBox(
                  height: 200,
                  child: SecondWidget(),
                ),
              ),
            ],
          ),
        );
      }

    PlatformView

    PlatformView là một tính năng Flutter cần thực hiện để hiển thị Native-UIs toàn diện trên Android View / UIKitView.

    Nếu bạn muốn biết thêm thông tin về PlatformView, hãy xem tài liệu chính thức:

  • [FLUTTER] MỘT SỐ WIDGET HỮU ÍCH TRONG FLUTTER (PHẦN CUỐI)

    [FLUTTER] MỘT SỐ WIDGET HỮU ÍCH TRONG FLUTTER (PHẦN CUỐI)

    Ở phần cuối này mình sẽ giới thiệu cho anh em nốt những widget hay ho mà mình tìm hiểu được.
    Anh em có thể đọc lại 2 phần trước ở đây nhé:

    Phần 1
    Phần 2

    Bắt đầu luôn nào!!!

    1. RichText

    Có lúc nào anh em muốn viết một dòng chữ nhưng các phần trong đó lại có định dạng khác nhau? Ví dụ có thể kể ra là một phần của đoạn văn bản có thể click vào như ảnh dưới đây:

    Anh em có thể nghĩ đến việc sử dụng Row với 2 Text con bên trong, nhưng có vẻ hơi cồng kềnh nhỉ? Và đó chính là lúc RichText phát huy tác dụng của mình. Thay vì viết như này:

            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text("Not a user?"),
                TextButton(
                  onPressed: () {},
                  child: const Text(
                    "Sign up",
                    style: TextStyle(decoration: TextDecoration.underline),
                  ),
                )
              ],
            ),

    Chúng ta có thể viết như này để dễ hiểu hơn (về mặt logic, code thì cũng same nhau à :D):

            RichText(
              text: TextSpan(
                text: "Not a user? ",
                style: const TextStyle(color: Colors.black),
                children: <TextSpan>[
                  TextSpan(
                      text: "Signup",
                      style: const TextStyle(
                          color: Colors.blue, decoration: TextDecoration.underline),
                      recognizer: TapGestureRecognizer()..onTap = () {})
                ],
              ),
            ),

    RichText sẽ nhận vào một text là InlineSpan, ở đây ta sẽ sử dụng TextSpan. Ở trong TextSpan lại có các children là InlineSpan, cho nên ta có thể nối nhiều đoạn text lại với nhau, mỗi đoạn có thể có các style riêng biệt.

    2. SafeArea

    Các thiết bị di động ngày nay đã khác xưa rất nhiều, không chỉ đơn giản là một màn hình hình chữ nhật nữa. Chúng ta có màn hình “tai thỏ”, “nốt ruồi” rồi thậm chí là “giọt nước”. Các thiết kế phức tạp trên khiến người lập trình cũng phải để ý khi tạo ra các ứng dụng mới.

    Và để tránh nội dung của ứng dụng bị che khuất bởi các vị trí “nhạy cảm” kia, ở Flutter chúng ta có SafeArea.

    So sánh khi không sử dụng / sử dụng SafeArea

    Ở ảnh trên ta thấy, khi sử dụng SafeArea, AppBar sẽ không đè lên phần status bar của máy nữa (AppBar màu xanh dương), rất phù hợp khi thiết bị có những “nốt ruồi” hay “tai thỏ”.

    thông thường ta sẽ wrap cả Scaffold ở trong widget SafeArea để đảm bảo không bị mất nội dung.

    3. SelectableText

    Chắc mình sẽ không cần phải nói nhiều về widget này vì cái tên đã thể hiện quá rõ tác dụng của nó rồi. 😀 😀

    Khi anh em muốn cho phép người dùng chọn được đoạn văn bản thì có thể sử dụng widget này.

    SelectableText

    À mà anh em có thể kết hợp widget này với RichText ở bên trên bằng cách sử dụng SelectableText.rich() nhé, thật tiện lợi phải không ^^

    Tổng kết

    Vậy là qua 3 phần mình đã giới thiệu đến anh em một số widget mà mình cảm thấy hữu ích trong quá trình làm việc với Flutter. Do kiến thức còn hạn chế, khả năng viết bài cũng non nớt nên nếu có lỗi sai thì mong anh em hãy comment góp ý để mọi người có thể nâng cao kiến thức của mình nhé. Cảm ơn anh em đã theo dõi 😀 😀

  • [Flutter] Kiến trúc MVVM

    [Flutter] Kiến trúc MVVM

    Kiến trúc MVVM là gì?

    MVVM (Model View ViewModel) là một kiến ​​trúc thiết kế tạo điều kiện cho việc chia nhỏ các nguyên tắc thiết kế, thường được gọi là sự tách biệt phát triển của những người nghiệp dư về công nghệ, thành các phần riêng biệt của GUI. Tư tưởng cơ bản đằng sau việc triển khai các phương pháp hay nhất về kiến ​​trúc ứng dụng dành cho thiết bị di động là xây dựng “View Model” có thể đại diện cho dữ liệu thông qua một chế độ xem.

    Lý do tại sao phần lớn các nhà phát triển, nói đến thiết kế ứng dụng Android hoặc iOS, ưa thích MVVM là vì nó tách biệt Activity và Fragment  khỏi logic. Để viết một codebase linh hoạt, các nhà phát triển ứng dụng bắt buộc phải xây dựng một lớp View-Model có thể được sử dụng bởi một số ‘Views’ nhất định. Hơn nữa, kiến ​​trúc MVVM cũng giúp các nhà phát triển tự động hóa việc truyền các sửa đổi bên trong View-Model tới Views.

    Ba thành phần quan trọng của kiến ​​trúc MVVM trong Flutter

    Mẫu thiết kế MVVM được quản lý bởi ba thành phần chính là Model, View và ViewModel . Các yếu tố chính này giúp thiết lập một khối cho toàn bộ cơ sở mã của mẫu thiết kế MVVM. Mặc dù mỗi thành phần có vai trò và chức năng khác nhau, nhưng sự tương tác logic giữa các thành phần này trong quá trình phát triển ứng dụng đóng một vai trò quan trọng. Trong kiến ​​trúc MVVM, khung nhìn tương tác với mô hình khung nhìn để liên kết dữ liệu và mô hình khung nhìn giao tiếp với mô hình. Để hiểu tại sao MVVM lại quan trọng đối với dự án ứng dụng của bạn, điều cần thiết là phải chăm chỉ nhận thức từng yếu tố. Vì vậy, chúng ta hãy bắt đầu.

    1. Model

    Vai trò chính của Model trong MVVM là thực hiện logic nghiệp vụ trong mẫu thiết kế mà chỉ hoạt động dựa trên nguồn dữ liệu cần thiết trong một hoạt động. Hiểu theo cách khác, phần tử model của mẫu kiến ​​trúc MVVM được sử dụng để đại diện cho dữ liệu thời gian thực cuối cùng sẽ được sử dụng trong phát triển ứng dụng.

    Điều tốt nhất về model là hiển thị sự tương tác giữa tất cả các thành phần hoàn toàn bằng cách tìm nạp dữ liệu từ cơ sở dữ liệu phòng. Nếu chúng ta phải xác định model trong một câu duy nhất, chúng ta có thể nói – nó là một phần tử lưu trữ dữ liệu và logic liên quan của kiến ​​trúc.

    2. View

    View là viết tắt của các thành phần UI như HTML, CSS. Trong MVVM, View chịu trách nhiệm về lớp trình bày trong mẫu thiết kế và là một điểm vào ứng dụng. Khi chúng ta nói về lợi ích của kiến ​​trúc MVVM, các thuộc tính của view đã được ca ngợi. Có một tập hợp phân cấp được duy trì giữa mỗi mô hình MVVM trong khi thể hiện sự tương tác. Ví dụ: View không bao giờ tương tác trực tiếp với Model mà thông qua ViewModel. Tất cả dữ liệu được thu thập bởi Model giúp tạo dữ liệu bản trình bày cho View. View thực thi logic thiết kế UI-UX và yêu cầu mô hình gửi lại đầu ra cho người dùng.

    3. ViewModel

    Ngoài việc làm trung gian giữa các thành phần Model và View, ViewModel triển khai dữ liệu và các lệnh được kết nối với View trong kiến ​​trúc MVVM để thông báo cho phần tử sau về các thay đổi trạng thái. Có một ví dụ ViewModel Android để thực hiện các công việc khác nhau như tạo lớp con hoặc thêm phần phụ thuộc, v.v. ViewModels có thể được liên kết với một hoặc nhiều mô hình.
    Về cơ bản, vai trò của ViewModel trong kiến trúc MVVM là hỗ trợ trạng thái của View và hoạt động như một lớp logic của toàn bộ cộng đồng kiến ​​trúc MVVM. Sẽ không sai khi trích dẫn ViewModel là phần tích hợp của tất cả những gì làm cho Mô hình duy trì dữ liệu thực tế và phần View đóng gói dữ liệu cập nhật giữ bộ điều khiển như một cổng giữa chúng.

    Lợi ích của kiến ​​trúc MVVM dành cho ứng dụng Android và iOS

    Mục tiêu cơ bản của mô hình kiến ​​trúc ứng dụng dành cho thiết bị di động trong phát triển ứng dụng là củng cố các chiến lược phát triển ứng dụng dành cho thiết bị di động với nhiều kỹ thuật dựa trên các tiêu chuẩn của ngành và nhà cung cấp cụ thể để cuối cùng thúc đẩy việc xây dựng ứng dụng, Android hoặc iOS. Một câu hỏi khiến hầu hết các chủ dự án không hài lòng là – Tại sao MVVM lại quan trọng đối với ứng dụng của bạn? Nói cách khác, câu hỏi đề xuất – “điều kỳ diệu mà mẫu thiết kế này có thể làm với ứng dụng của bạn”.

    MVVM, một biến thể viết tắt của Model View ViewModel, nhằm mục đích tách ứng dụng thành ba thành phần hợp lý và sau đó xử lý các khía cạnh phát triển cụ thể của ứng dụng. Mặc dù thị trường có rất nhiều mẫu kiến ​​trúc cải thiện hiệu suất ứng dụng và khả năng của thiết bị, MVVM đặc biệt tập trung vào tầm quan trọng của kiến ​​trúc giải pháp mang lại trải nghiệm UI tốt hơn cho người dùng. Bên cạnh việc quản lý và trình bày các đối tượng dữ liệu, kiến ​​trúc MVVM có rất nhiều lợi ích để cung cấp, một số lợi ích được liệt kê dưới đây.

    1. Khả năng tái sử dụng

    Khi hiểu được toàn bộ khái niệm về việc giới thiệu kiến ​​trúc MVVM trong quá trình thiết kế ứng dụng, có thể xác định đây là mô hình giúp các nhà phát triển phân biệt rõ ràng giữa các ứng dụng được kết hợp lỏng lẻo và kết hợp chặt chẽ. Khớp nối là yếu tố phụ thuộc của thiết kế trong ứng dụng và có những thiết kế kiến ​​trúc có khớp nối chặt chẽ, điều này cuối cùng làm tăng chi phí bảo trì phát triển trang web và giảm khả năng tái sử dụng của thành phần. Kiến trúc MVVM đi kèm với khớp nối lỏng lẻo để đạt được sự phân tách trách nhiệm và nâng cao mức độ khớp nối lỏng lẻo. Mặt khác, yếu tố khả năng tái sử dụng cho phép các mã được sử dụng để xây dựng các mẫu thiết kế trong các ứng dụng khác.

    2. Thúc đẩy phát triển mã độc lập

    MVVM là một mô hình kiến ​​trúc độc lập cung cấp dữ liệu dưới dạng đầu ra thông qua View. Toàn bộ cơ sở mã của mẫu MVVM được tạo ra theo cách khắc phục các trục trặc phổ biến mà hầu hết các mẫu kiến ​​trúc trở thành nạn nhân của nó. Với ba thành phần chính của nó, kiến ​​trúc MVVM truy xuất và lưu giữ các đối tượng thông qua một dịch vụ. Dù nói đến thiết kế mô hình kiến ​​trúc MVVM Android Architecture hoặc iOS , mô hình MVVM thúc đẩy sự phát triển độc lập của ứng dụng. Vì kiến ​​trúc mã và đơn giản hóa mẫu thiết kế là mục tiêu cốt lõi của các nhà phát triển, phương pháp tiếp cận mẫu thiết kế MVVM sẽ giải quyết một số vấn đề tồn tại trong khuôn khổ thiết kế của một ứng dụng và xem xét tất cả các khía cạnh quan trọng mang lại kết quả tuyệt vời ở phần cuối của dự án.

    3. Nâng cao khả năng kiểm tra

    Mẫu thiết kế MVVM Android hoặc iOS dường như đang thu hút sự chú ý trên thị trường do các nhà phát triển có kỹ năng điều hành. Phần lớn các nhà phát triển tin rằng MVVM có thể đọc được, có thể mở rộng và có thể kiểm tra được so với các mô hình thiết kế kiến ​​trúc khác. Trong số tất cả các thành phần, ViewModel có trách nhiệm rất lớn là đại diện cho dữ liệu và khởi tạo trường hợp thử nghiệm trong mô hình kiến ​​trúc MVVM. Chính ViewModel làm cho thành phần View có thể tái sử dụng và có thể kiểm tra được, giúp cho việc khen ngợi logic nghiệp vụ kiểm thử đơn vị trở nên dễ dàng hơn. Do sự phụ thuộc của phần tử này, việc bắt đầu các trường hợp thử nghiệm trở nên ít phức tạp hơn một chút.

    4. Khả năng bảo trì

    Mặc dù một số nhà phát triển có thể thấy các chỉ số hiệu suất ứng dụng dành cho thiết bị di động và tính năng chia sẻ của kiến ​​trúc MVVM hơi khó chịu và phức tạp, nhưng khả năng bảo trì của kiến ​​trúc MVVM mang lại lợi ích cho mô hình là mạch lạc và đơn giản hóa, lưu ý các yêu cầu bổ sung. Khả năng bảo trì lồng vào nhau cung cấp khả năng mở rộng tối đa với sự phân tách ranh giới rõ ràng.
    Khả năng thay thế hoặc thêm các khối mã mới trong quá trình ứng dụng là một điều lý tưởng cần xem xét để đặt các mã trong cơ sở hạ tầng ứng dụng một cách phù hợp. Ngoài ra, tính năng ánh xạ trong kiến ​​trúc MVVM là chưa từng có. Mô hình MVVM sử dụng ánh xạ một đến nhiều giữa View và ViewModel.

  • Final và Constant trong Dart – Cách xác định Hằng số trong Dart

    Final và Constant trong Dart – Cách xác định Hằng số trong Dart

    Xin chào!. Trong bài viết này, chúng ta sẽ tim hiểu về final và constant trong dart là gì. Cách xác định constant trong Dart. Câu hỏi quan trọng nhất là Tại sao chúng ta cần final và const và mục đích của những từ khóa này là gì?

    Final Constant của dart

    Nếu bạn không bao giờ muốn thay đổi một giá trị thì hãy sử dụng các từ khóa final và const.

    Có rất nhiều trường hợp mà chúng ta không muốn thay đổi giá trị của một biến. Ví dụ, giá trị của Pi.

    const PI = 3,14;

    Hoặc có thể bạn không muốn thay đổi tên trong toàn bộ chương trình.

    final name = ”Waheed”;

    Hằng số PI sẽ chứa giá trị 3,14 và final name sẽ có giá trị ‘Waheed’ trong toàn bộ mã. Bây giờ trong cả hai trường hợp, bạn sẽ không thể thay đổi giá trị của biến PI hoặc name. Câu hỏi phổ biến đó là Sự khác biệt giữa final và const là gì.

    Sự khác biệt giữa Final và Const

    • Một biến final chỉ có thể được đặt một lần và nó chỉ được khởi tạo khi được truy cập.
      • Ở đây mấu chốt ở đây là “nó chỉ được khởi tạo chỉ khi nó được truy cập” nghĩa là bạn chỉ sử dụng được biến final trong chương trình của mình khi giá trị được khởi tạo và vị trí bộ nhớ được cấp phát. Và nếu bạn không bao giờ sử dụng biến final, giá trị sẽ không bao giờ được khởi tạo trong chương trình của bạn.
    • Một biến const hoàn toàn là final nhưng nó là một hằng số compile time
      • Nó có nghĩa là tất cả các biến const cũng là final.
      • Nó là một hằng số compile time đã biên dịch có nghĩa là nó được khởi tạo trong quá trình biên dịch. Bất cứ khi nào bạn biên dịch chương trình, giá trị của PI sẽ được khởi tạo và bộ nhớ sẽ được cấp phát. Không quan trọng bạn có đang sử dụng biến này hay không.
    • Một biến instance có thể là final nhưng không thể là const
      • Nếu bạn muốn tạo một Hằng số ở class, hãy khai báo nó là static const chứ không chỉ mỗi const.

    Bây giờ đã đến lúc làm rõ các khái niệm bằng một ví dụ thực tế.

    Chúng tôi đã xác định một biến final name và sau đó chúng tôi đang cố gắng thay đổi giá trị của biến final name nhưng chúng tôi sẽ nhận được thông báo lỗi như thế này.

    ‘error‘name’, a final variable, can only be set once.‘ Or something like ‘ cannot assign to final variable name‘

    Có nghĩa là: ‘‘Error‘name’, một biến final, chỉ có thể được đặt một lần. ‘Hoặc một cái gì đó như‘ không thể gán cho biến final name ‘’

    Bạn có thể nhận thấy khi chưa xác định kiểu dữ liệu cho tên biến. Chà, Dart hiểu nó là một Chuỗi theo giá trị nghĩa đen của nó. Bạn có thể xác định rõ ràng nó là một String nhưng nó là tùy chọn.

    Chúng ta cũng hãy xem một ví dụ về const.

    Đây là cách chúng ta định nghĩa const trong Dart.

    Có một khái niệm rất quan trọng khác cần hiểu. Hãy nhớ rằng chúng tôi đã nghiên cứu rằng ‘Một biến instance có thể là biến final nhưng không thể là const.’ Hãy làm rõ khái niệm này với sự trợ giúp của một ví dụ. Để làm được điều đó, chúng ta phải tạo một Class. Nếu bạn không quen thuộc với class là gì, xin đừng lo lắng, chúng tôi sẽ sớm đề cập đến khái niệm class. Còn bây giờ chỉ cần tiếp tục theo dõi với chúng tôi.

    Chà, trình biên dịch sẽ cung cấp cho chúng ta một lỗi tại const weight. ‘Chỉ có thể khai báo các trường static là const‘.

    Vì vậy, nếu bạn xác định một const ở mức Class thì bạn cũng phải sử dụng từ khóa static. Chúng ta sẽ đề cập đến từ khóa static là gì nhưng hiện tại, hãy nhớ rằng chúng ta không thể khai báo một biến const ở cấp độ Class một cách đơn giản. Nó phải là static const.

    Đó là một hướng dẫn rất ngắn nhưng chi tiết về final và const của dart. Chúng tôi chắc chắn rằng bây giờ bạn sẽ không bao giờ quên các khái niệm final và const. Cảm ơn vì đã theo dõi.

  • Khác biệt giữa Future và Stream

    [FLUTTER] Sự khác biệt giữa Future và Stream trong Flutter

    Lập trình không đồng bộ trong Flutter được đặc trưng bởi hai lớp Future và Stream

    1. Future

    Khi một hàm bất đồng bộ được thực hiện xong nó sẽ trả về một Future.

    • Một hàm Future có thể trả về một giá trị.
    • Một hàm Future cũng có thể trả về một lỗi nếu có bất kì ngoại lệ nào xảy ra.
    Future<void> fetchUserOrder() {
      // Imagine that this function is fetching user info from another service or database.
      return Future.delayed(const Duration(seconds: 2), () => print('Large Latte'));
    }
    
    void main() {
      fetchUserOrder();
      print('Fetching user order...');
    }

    2. Stream

    Định nghĩa: Stream là một chuỗi các sự kiện không đồng bộ. Nó giống như một Lặp lại không đồng bộ – trong đó, thay vì nhận được sự kiện tiếp theo khi bạn yêu cầu, luồng cho bạn biết rằng có một sự kiện khi nó sẵn sàng.

    Future<int> sumStream(Stream<int> stream) async {
      var sum = 0;
      await for (final value in stream) {
        sum += value;
      }
      return sum;
    }
    
    Stream<int> countStream(int to) async* {
      for (int i = 1; i <= to; i++) {
        yield i;
      }
    }
    
    void main() async {
      var stream = countStream(10);
      var sum = await sumStream(stream);
      print(sum); // 55
    }

    3. Khác biệt giữa Future và Stream

    Như chúng ta có thể thấy ở trên thì điểm khác biệt rõ rệt nhất của Future và Stream là:

    • Trong quá trình xử lý Future sẽ chờ đợi đến khi hoàn thành và chỉ trả lại kết quả tại thời điểm đó.
    • Stream thì sẽ trả về dữ liệu liên tục nếu nó vẫn tiếp tục được chuyển về, tạo thành một luồng.

    Từ những sự khác biệt này chúng ta có thể suy luận ra các trường hợp nào nên sử dụng Future, trường hợp nào nên sử dụng Stream.

    Future:

    • Chụp một bức ảnh từ camera, hoặc lấy từ trong bộ nhớ.
    • Lẩy các thông tin về file.
    • Tạo các https request.

    Stream:

    • Lắng nghe sự thay đổi của vị trí.
    • Chơi nhạc.
    • Đồng hồ bấm giờ.
    • Làm việc với web-socket.

    Hãy chọn cho mình những cách phù hợp nhất với các vấn đề chúng ta cần giải quyết.

    Tài liệu tham khảo:

    Author: LamNT59

  • Stack with Heap Memory Allocation

    1. Stack memory allocation

    Stack memory allocation: Việc cấp phát này xảy ra trên các khối bộ nhớ liền kề. Chúng ta gọi nó là Stack memory allocation vì việc cấp phát xảy ra trong ngăn xếp lệnh gọi hàm. Kích thước của bộ nhớ được cấp phát đã được trình biên dịch biết và bất cứ khi nào một hàm được gọi, các biến của nó sẽ nhận được bộ nhớ được cấp phát trên ngăn xếp. Và bất cứ khi nào gọi hàm kết thúc, bộ nhớ cho các biến sẽ được hủy cấp phát. Tất cả điều này xảy ra bằng cách sử dụng một số quy trình được xác định trước trong trình biên dịch. Lập trình viên không phải lo lắng về việc cấp phát bộ nhớ và hủy cấp phát các biến ngăn xếp. Loại cấp phát bộ nhớ này còn được gọi là Cấp phát bộ nhớ tạm thời bởi vì ngay sau khi phương thức kết thúc việc thực thi, tất cả dữ liệu thuộc về phương thức đó sẽ tự động thoát ra khỏi ngăn xếp. Có nghĩa là, bất kỳ giá trị nào được lưu trữ trong lược đồ bộ nhớ ngăn xếp đều có thể truy cập được miễn là phương thức chưa hoàn thành việc thực thi và hiện ở trạng thái đang chạy.

    Những điểm chính của bộ nhớ stack:

    1. Nó cấp phát bộ nhớ tạm thời trong đó các thành viên dữ liệu chỉ có thể truy cập được nếu phương thức chứa chúng hiện đang chạy.
    2. Nó tự động cấp phát hoặc hủy cấp phát bộ nhớ ngay sau khi phương thức tương ứng hoàn thành việc thực thi.
    3. Chúng ta nhận được nếu bộ nhớ ngăn xếp được lấp đầy hoàn toàn.
    4. Cấp phát bộ nhớ stack được coi là an toàn hơn so với cấp phát bộ nhớ heap vì dữ liệu được lưu trữ chỉ có thể được truy cập bởi luồng chủ của nó.
    5. Cấp phát và thu hồi cấp phát bộ nhớ nhanh hơn so với cấp phát bộ nhớ Heap.
    6. Bộ nhớ stack được cấp phát bộ nhớ lưu trữ nhỏ hơn so với bộ nhớ Heap.

    Tất cả các biện trên sẽ được cấp pháp bộ nhớ trong bộ nhớ stack.

    2. Heap memory allocation

    Bộ nhớ được cấp phát trong quá trình thực thi các lệnh do người lập trình viết. Lưu ý rằng tên heap không liên quan gì đến cấu trúc dữ liệu heap. Nó được gọi là heap vì nó là một không gian bộ nhớ có sẵn cho các lập trình viên để cấp phát và thu hồi cấp phát. Mỗi khi chúng ta tạo một đối tượng, nó luôn tạo ra trong Heap-space và thông tin tham chiếu đến các đối tượng này luôn được lưu trữ trong bộ nhớ stack. Phân bổ bộ nhớ Heap không an toàn như phân bổ bộ nhớ Stack vì dữ liệu được lưu trữ trong vùng này có thể truy cập hoặc hiển thị cho tất cả các chuỗi. Nếu một lập trình viên không xử lý tốt bộ nhớ này, thì chương trình có thể bị thiếu bộ nhớ.

    Việc cấp phát bộ nhớ Heap được chia thành ba loại. Ba loại này giúp chúng ta sắp xếp thứ tự ưu tiên dữ liệu (đối tượng) sẽ được lưu trữ trong bộ nhớ Heap hoặc trong quá trình thu gom rác (quá trình xác định và loại bỏ các Object không được sử dụng (unreferenced) khỏi bộ nhớ Heap) bao gồm:

    1. Young Generation: Phần bộ nhớ nơi tất cả dữ liệu (đối tượng) mới được tạo ra để phân bổ vùng và bất cứ khi nào bộ nhớ này được lấp đầy hoàn toàn thì phần còn lại của dữ liệu sẽ được lưu trữ trong bộ sưu tập Rác.
    2. Old or Tenured Generation: Phần của bộ nhớ Heap chứa các đối tượng dữ liệu cũ hơn không được sử dụng thường xuyên hoặc không được sử dụng nữa sẽ được đặt.
    3. Permanent Generation: Đây là phần của bộ nhớ Heap chứa siêu dữ liệu của JVM cho các lớp thời gian chạy và các phương thức ứng dụng.

    Những điểm chính của hepa memory allocation:

    1. Chúng ta nhận được thông báo lỗi tương ứng nếu Heap-space đã đầy hoàn toàn.
    2. Việc cấp phát bộ nhớ này khác với bộ nhớ stack, ở đây không cung cấp tính năng tự động thu hồi bộ nhớ. Chúng ta cần sử dụng quá trình xác định và loại bỏ rác để loại bỏ các đối tượng cũ không sử dụng để sử dụng bộ nhớ một cách hiệu quả.
    3. Thời gian xử lý (Thời gian truy cập) của bộ nhớ này khá chậm so với bộ nhớ stack.
    4. Bộ nhớ Heap cũng không phải là một luồng an toàn như bộ nhớ stack vì dữ liệu được lưu trữ trong bộ nhớ Heap được hiển thị cho tất cả các luồng.
    5. Bộ nhớ đống có thể truy cập hoặc tồn tại miễn là toàn bộ ứng dụng chạy.
    6. bộ nhớ Heap được cấp phát bộ nhớ lưu trữ lớn hơn so với bộ nhớ stack.

    Bộ nhớ này cho 10 kiểu dữ liệu interger được cấp phát trong Heap.

    Ví dụ:

    Trong ví dụ trên:

    • Khi chúng ta bắt đầu thực thi chương trình có, tất cả các lớp thời gian chạy được lưu trữ trong không gian bộ nhớ Heap.
    • Sau đó, chúng ta tìm thấy phương thức main() trong dòng tiếp theo được lưu trữ trong bộ nhớ stack cùng với tất cả các phương thức nguyên thủy (hoặc cục bộ) và biến tham chiếu Emp kiểu Emp_detail cũng sẽ được lưu trữ trong bộ nhớ stack và sẽ trỏ đến đối tượng tương ứng được lưu trữ trong bộ nhớ Heap.
    • Sau đó, dòng tiếp theo sẽ gọi đến phương thức khởi tạo tham số Emp(int, String) từ main() và nó cũng sẽ cấp phát cho phần trên cùng của cùng một khối bộ nhớ Stack. Điều này sẽ lưu trữ:
      • Đối tượng tham chiếu của đối tượng được gọi của bộ nhớ Stack.
      • Giá trị nguyên thủy (kiểu dữ liệu nguyên thủy) int id trong bộ nhớ Stack.
      • Biến tham chiếu của đối số String emp_name sẽ trỏ đến chuỗi thực từ nhóm chuỗi vào bộ nhớ heap.
    • Sau đó, phương thức main sẽ lại gọi đến phương thức static Emp_detail(), phương thức này sẽ được thực hiện trong khối bộ nhớ Stack trên đầu khối bộ nhớ trước đó.
    • Vì vậy, đối với đối tượng mới tạo Emp kiểu Emp_detail và tất cả các biến thể hiện sẽ được lưu trữ trong bộ nhớ heap.

    Cụ thể như hình dưới:

  • Widget Tree & Element Tree & Render Tree trong Flutter

    Widget Tree & Element Tree & Render Tree trong Flutter

    Flutter không vẽ lại hay tạo lại toàn bộ giao diện người dùng mỗi khi ta phương thức build(){...} được gọi.

    • Flutter cố gắng đáp ứng để ứng dụng chạy ở 60 FPS. Vì vậy, nó cập nhật màn hình 60 lần mỗi giây. Có nghĩa là màn hình được Flutter repaint lại 60 lần mỗi giây. Điều này dễ hiểu vì tất cả các ứng dụng và trò chơi chạy ở tốc độ 60 FPS trở lên theo mặc định.‌‌‌‌
    • Điều này sẽ chỉ trở nên kém hiệu quả nếu Flutter cứ phải tính toán lại toàn bộ bố cục 60 lần mỗi giây.
    • Nếu Flutter vẽ thứ gì đó lên màn hình lần đầu tiên, nó cần tính toán ra vị trí, màu sắc, văn bản, v.v. của mọi phần tử trên màn hình.
    • Đối với các lần sửa/vẽ tiếp theo, để làm mới giao diện người dùng nếu không có gì thay đổi thì Flutter sẽ lấy thông tin cũ mà nó đã có từ trước đó và vẽ thông tin đó lên màn hình rất nhanh và rất hiệu quả.‌‌ Từ đó, tốc độ vẽ lại không phải là vấn đề, sẽ chỉ là vấn đề nếu Flutter phải tính toán lại mọi thứ trên màn hình với mỗi lần làm mới mà thôi.

    Đây là những gì chúng ta sẽ thảo luận chi tiết ở bài viết này: liệu Flutter có tính toán lại mọi thứ mỗi khi phương thức build(){...} được gọi hay không?

    Widget Tree

    • Widget Tree chỉ đơn giản là tất cả các Widget mà ta đang dùng để xây dựng ứng dụng, tức là tất cả code mà ta viết sẽ tạo nên widget tree.‌‌
    • Nó hoàn toàn do ta kiểm soát. Bạn khai báo các widget lồng ghép chúng lại với nhau để tạo nên giao diện mong muốn.‌‌
    • Widget tree được xây dựng bởi Flutter khi call phương thức build(){...} từ code của chúng ta, chúng chỉ là một loạt các cài đặt cấu hình mà Flutter sẽ xử lý.‌‌
    • Nó không chỉ đơn giản xuất hiện ra trên màn hình rồi thôi. Thay vào đó, nó sẽ cho Flutter biết những gì sẽ vẽ lên màn hình ở lần tiếp theo. Widget tree được rebuild rất thường xuyên.‌‌

    Element Tree

    • Element Tree liên kết vào Widget Tree, là thông tin được thiết lập với các đối tượng/phần tử thực sự được hiển thị. Nó rất hiếm khi rebuild.‌‌
    • Element Tree được quản lý theo một cách khác và sẽ không rebuild khi phương thức build(){...} được gọi. ‌‌
    • Ở mỗi Widget trong Widget Tree, Flutter sẽ tự động tạo một element cho nó. Nó được thực hiện ngay khi Flutter xử lý Widget ở lần đầu tiên.‌‌
    • Ở đây chúng ta có thể nói rằng một element là một đối tượng được quản lý trong bộ nhớ bởi Flutter, nó có liên quan đến Widget trong Widget Tree.‌‌
    • Element chỉ giữ một tham chiếu tới Widget (trong Widget Tree) đang giữ các thông số giao diện đầu cuối.‌‌‌‌

    Tóm lại

    Khi Flutter nhìn thấy stateful widget, nó sẽ tạo element và sau đó cũng gọi phương thức createState() để tạo một state object mới dựa trên state class.‌‌ Do đó, state là một đối tượng độc lập trong một Stateful Widget được kết nối với cả hai là element trong Element Tree và Widget trong Widget Tree.‌‌

    Render Tree

    • Render Tree đại diện của các element/đối tượng thực sự được hiển thị trên màn hình.‌‌
    • Render Tree cũng không rebuild thường xuyên.
    • Element Tree cũng được liên kết với Render Tree. Element trong Element Tree trỏ đến render object mà chúng ta thực sự thấy trên màn hình.‌‌
    • Bất cứ khi nào Flutter thấy một element chưa được render trước đó thì nó sẽ tham chiếu đến Widget trong Widget Tree để thiết lập, sau đó tạo một element trong element tree.
    • Flutter cũng có một layout phase, giai đoạn mà nó tính toán và lấy không gian diện tích có sẵn trên màn hình, chiều, kích thước, hướng, ….‌‌
    • Nó cũng có một phase khác để thiết lập các listeners với các Widget để chúng ta có thể thao tác các sự kiện, ….

    Tóm lại

    Một cách đơn giản, chúng ta có thể thấy rằng phần tử chưa được render thì sẽ được render ra màn hình. Element (trong element tree) sau đó có có một con trỏ đến Render Object (trong Render Tree) trên màn hình. Nó cũng có một con trỏ tới Widget (trong Widget Tree) mang theo các thông tin cấu hình cho phần tử này.

    Cách Flutter thực thi phương thức build(){...}‌‌

    Phương thức build(){...} được Flutter gọi bất cứ khi nào state thay đổi. Về cơ bản, có hai kích hoạt quan trọng có thể dẫn đến việc rebuild.‌‌

    • Một là khi phương thức setState(){...} được gọi trong một Stateful Widget. Việc call setState(){...} khi được gọi tự động sẽ dẫn đến phương thức build(){...} được gọi ngay sau đó.‌‌
    • Thứ hai, bất cứ khi nào MediaQuery hoặc lệnh Theme.of(...) được gọi, bàn phím ảo xuất hiện hoặc biến mất, v.v. Bất cứ khi nào dữ liệu của những thứ này thay đổi, nó sẽ tự động kích hoạt phương thức build(){...}.‌‌

    Chính xác là việc gọi setState(){...} sẽ đánh dấu phần tử tương ứng là dirty. Đối với lần render tiếp theo, diễn ra 60 lần mỗi giây, Flutter sau đó sẽ xem xét đến các thông tin thiết lập mới được tạo bởi phương thức build(){…} và sau đó cập nhật màn hình.‌‌

    Tất cả các Widget lồng vào nhau bên trong Widget được đánh dấu là dirty sẽ được tạo các đối tượng mới widget/dart cho chúng. Do đó, một Widget Tree mới sẽ được tạo ra tương ứng các phiên bản mới của tất cả các Widget này.‌‌

    Chú thích

    Một số Widget sẽ không bao giờ thay đổi ngay cả khi rebuild Widget Tree nên bạn có thể tối ưu hóa quá trình build hơn. Bạn có thể sử dụng từ khóa const phía trước chúng để cho Flutter biết rằng Widget này sẽ không bao giờ thay đổi, do đó làm cho Flutter bỏ qua việc rebuild hoàn toàn widget đó.

    Ví dụ trên ta chỉ định const bỏ qua việc rebuild Padding Widget.

    Tác giá: DangDH9

  • Làm thế nào để chuyển màn hình mà không cần context trong Flutter?

    Làm thế nào để chuyển màn hình mà không cần context trong Flutter?

    Làm thế nào để chuyển màn hình mà không cần context trong Flutter?

    Chúng ta sẽ học cách loại bỏ context khi navigate trong Flutter nhé.

    Navigate là một phần không thể thiếu trong bất kì ứng dụng nào. Flutter sẽ hỗ trợ bạn navigate đến bất cứ màn hình nào một cách dễ dàng hơn chỉ với việc sử dụng các chức năng navigate đơn giản như Push và Pop.

    Để Push:

    Để Pop:

    Điều này hoạt động khá tốt cho đến khi ứng dụng của bạn mở rộng quy mô và bạn tách logic nghiệp vụ của mình với logic UI. Và bây giờ bạn phải chuyển BuildContext từ một function này sang một function khác. Đôi khi việc này sẽ trở nên rắc rối khiến bạn muốn tránh việc chuyển context.

    Đừng lo lắng nhé vì NavigatorKey sẽ giải cứu bạn.

    1. Đầu tiên, hãy tạo Navigator Key

    2. Tiếp đến, hãy chuyển Navigator Key trong MaterialApp

    3. Cuối cùng, hãy push bằng Navigator Key

    Vậy là xong, chỉ với 3 bước đơn giản, bạn đã có thể loại bỏ context ra khỏi Navigation của mình. Bạn có thể kiểm tra code đầy đủ trên Github.

    Bài viết được tham khảo từ Divyanshu Bhargava.

  • Try catch , error và exception trong ngôn ngữ dart

    Lỗi (Error)
    Lỗi là vấn đề khá là nghiêm trọng và khó có thể “deal with” với nó, và không thể phục hồi. ví dụ: out of memory (đầy bộ nhớ).

    Ngoại lệ (Exceptions)] nhằm truyền đạt thông tin cho người dùng về lỗi,  để lỗi có thể được giải quyết theo chương trình. Nó được dự định là  đã bắt được và nó phải chứa các trường dữ liệu hữu ích.


    Khi đang chạy chương trình, đột nhiên ngừng lại và xuất hiện thông báo lỗi – đó chính là ngoại lệ ( Exceptions).

    Trong quá trình xây dựng phần mềm sẽ có thể sảy ra nhiều lỗi và những ngoại lệ , điều này là không tránh khỏi. Vậy cách nào để kiểm soát và phát hiện chúng ? Ở bài viết này mình sẽ sử dụng try catch để xử lý lỗi giúp cho app không bị chết đột ngột .

    Cú pháp

    Try{

    // Khối lệnh có nguy cơ xảy ra exception

    } catch(e){

    }finally{

    }

    Sau đây chúng ta hãy đi vào ví dụ cụ thể :

    Ở ví dụ trên ta có thể thấy đã xảy ra lỗi nhưng bản thân mỗi lập trình viên không thể vì lỗi mà làm chết app ảnh hưởng đến trải nhiệm người dùng . Trong thực tế app có thể chết do mạng internet hay filenotfound hay nhiều  vô vàn lý do khác . Các lập trình viên phải handle các lỗi nhiều nhất có thể để ứng dụng luôn luôn “sống” .

    finally : try-catch-finally hay try – finally , khối lệnh trong finally sẽ được thực hiện bất chấp có sảy ra lỗi trong khối try hay không.

    Try-on-catch : để bắt được loại ngoại lệ cụ thể. Ví dụ:

    Khi bắt được chính xác loại ngoại lệ ở trước thì khối lệnh trong catch sẽ không được thực thi.

    Tóm lại bài viết này nhằm mục đích gới thiệu về try catch finaly một cách cơ bản , hi vọng giúp ích được cho bạn

  • Cách viết các ứng dụng mạnh mẽ mọi lúc, bằng cách sử dụng “Clean Architecture”

    Cách viết các ứng dụng mạnh mẽ mọi lúc, bằng cách sử dụng “Clean Architecture”

    Là nhà phát triển, chúng ta không thể tiếp tục sử dụng các thư viện và framework bên ngoài trong hệ thống của mình. Việc cộng đồng tạo ra những công cụ hữu ích và việc sử dụng chúng là điều đương nhiên. Tuy nhiên, mọi thứ đều có mặt trái của nó.

    Các nhóm và các cá nhân bất cẩn có thể rơi vào tình huống nguy hiểm bằng cách cấu trúc hệ thống của họ xung quanh các công cụ mà họ sử dụng. Các business logic có thể bị lẫn lộn với các chi tiết thực hiện. Điều này có thể dẫn đến một hệ thống khó mở rộng và bảo trì. Những gì nên thay đổi nhanh chóng trong GUI cuối cùng lại biến thành một cuộc truy tìm lỗi kéo dài hàng giờ. Nhưng câu chuyện không cần thiết phải như thế này.

    Kiến trúc phần mềm đề xuất các mô hình và quy tắc để xác định cấu trúc (classes, interfaces, và structs) trong một hệ thống và cách chúng liên quan với nhau. Các quy tắc này thúc đẩy khả năng tái sử dụng và sự tách biệt các mối quan tâm đối với các yếu tố này. Điều này giúp dễ dàng thay đổi các chi tiết triển khai như DBMS hoặc thư viện front-end. Trình tái cấu trúc và sửa lỗi ảnh hưởng đến càng ít bộ phận của hệ thống càng tốt. Và thêm các tính năng mới trở nên dễ dàng.

    Trong bài viết này, tôi sẽ giải thích một mô hình kiến trúc được đề xuất vào năm 2012 bởi Robert C. Martin, Uncle Bob. Ông là tác giả của những tác phẩm kinh điển như Clean Code và The Clean Coder…vv.

    Mô hình có được xây dựng dựa trên các khái niệm đơn giản:

    Chia thành phần của hệ thống thành các lớp với các vai trò riêng biệt và được xác định rõ ràng. Và hạn chế các mối quan hệ giữa các entities ở các tầng khác nhau. Không có gì mới trong việc tách ứng dụng của bạn thành các lớp. Nhưng tôi đã chọn cách tiếp cận này vì nó là cách đơn giản nhất để nắm bắt và thực hiện. Và nó làm cho các usecase thử nghiệm trở nên đơn giản.

    Chúng tôi chỉ cần đảm bảo Tương tác hoạt động bình thường. Đừng lo lắng nếu từ “Tương tác” có vẻ xa lạ với bạn, chúng ta sẽ tìm hiểu về chúng ngay sau đây.

    Từ trong ra ngoài, chúng ta sẽ khám phá từng lớp sâu hơn một chút. Chúng tôi sẽ sử dụng một ứng dụng mẫu khá quen thuộc với chúng ta: counter app. Không mất thời gian để hiểu, vì vậy chúng ta có thể tập trung vào chủ đề của bài viết này.

    Entities

    Entities trong sơ đồ là Business Rules. Các Entities bao gồm các Business Rules phổ biến cho một công ty. Chúng đại diện cho các entities cơ bản đối với lĩnh vực hoạt động của nó. Chúng là những thành phần có mức độ trừu tượng cao nhất.

    Trong ví dụ counter app của chúng tôi, có một Thực thể rất rõ ràng: chính là Counter.

    Use Cases

    Các trường hợp sử dụng được chỉ ra dưới dạng Application Business Rules. Chúng đại diện cho từng trường hợp sử dụng của một ứng dụng. Mỗi phần tử của lớp này cung cấp một giao diện cho lớp bên ngoài và hoạt động như một trung tâm giao tiếp với các phần khác của hệ thống. Chúng chịu trách nhiệm thực hiện hoàn chỉnh các usecase và thường được gọi là Tương tác.

    Trong ví dụ của chúng tôi, chúng tôi có một Trường hợp sử dụng để tăng hoặc giảm bộ đếm của chúng

    Lưu ý rằng factory function cho ChangeCounterInteractor nhận một tham số kiểu CounterGateway. Chúng ta sẽ thảo luận về sự tồn tại của loại hình này ở phần sau của bài viết. Nhưng chúng ta có thể nói rằng Gateways là thứ đứng giữa các usecase và layer tiếp theo.

    Interface Adapters

    Lớp này bao gồm ranh giới giữa các business rules của hệ thống và các công cụ cho phép hệ thống tương tác với các phần bên ngoài, như database và UI. Các phần tử trong lớp này hoạt động như các phần tử trung gian, nhận dữ liệu từ một lớp và chuyển nó sang lớp kia, điều chỉnh dữ liệu khi cần thiết.

    Trong ví dụ của chúng tôi, chúng tôi có một vài Interface Adapters. Một trong số đó là React component trình bày Bộ đếm và các điều khiển của nó để tăng và giảm:

    Lưu ý rằng component không sử dụng một thể hiện Counter để trình bày giá trị của nó mà thay vào đó là một instance của CounterViewData. Chúng tôi đã thực hiện thay đổi này để tách present logic khỏi business data. Một ví dụ về điều này là logic hiển thị của counter dựa trên view mode. Cách triển khai CounterViewData ở bên dưới:

    Một ví dụ khác về Interface Adapter sẽ là triển khai Redux của ứng dụng của chúng tôi. Các mô-đun chịu trách nhiệm về các yêu cầu tới máy chủ và việc sử dụng bộ nhớ cục bộ cũng sẽ nằm trong layer này.

    Frameworks and Drivers

    Các công cụ mà hệ thống của bạn sử dụng để giao tiếp với các phần bên ngoài tạo nên lớp ngoài cùng. Chúng tôi thường không viết mã trong lớp này, bao gồm các thư viện như React / Redux, browser API, v.v.

    The Dependency Rule

    Sự phân chia thành các lớp này có hai mục tiêu chính. Một trong số đó là làm rõ trách nhiệm của từng bộ phận trong hệ thống. Hai là đảm bảo vai trò của mỗi lớp độc lập với nhau nhất có thể. Để điều này xảy ra, có một quy tắc nêu rõ các yếu tố phải phụ thuộc vào nhau như thế nào:

    Một phần tử không được phụ thuộc vào bất kỳ phần tử nào thuộc một lớp bên ngoài lớp của nó.

    Ví dụ: một phần tử trong Use Cases layer không được có bất kỳ thông tin nào về bất kỳ lớp hoặc mô-đun nào liên quan đến GUI hoặc tính ổn định của dữ liệu. Tương tự như vậy, một Entity không thể biết các Use Cases nào sử dụng nó.

    Quy tắc này có thể đã đặt ra câu hỏi trong đầu bạn. Lấy ví dụ về một Use Case. Nó được kích hoạt do tương tác của người dùng với UI. Việc thực thi nó liên quan đến việc cập nhật trong một số bộ lưu trữ dữ liệu liên tục như cơ sở dữ liệu. Làm cách nào Interactor có thể thực hiện các lệnh gọi liên quan đến quy trình cập nhật mà không phụ thuộc vào Interface Adapter chịu trách nhiệm về tính ổn định của dữ liệu?

    Câu trả lời nằm trong một yếu tố mà chúng tôi đã đề cập trước đây: Gateways. Họ chịu trách nhiệm thiết lập giao diện mà các usecase cần để thực hiện công việc của họ. Sau khi họ đã thiết lập giao diện này, Interface Adapters có thể thực hiện các khía cạnh của chúng, như thể hiện trong sơ đồ ở trên. Chúng tôi có CounterGateway interface và triển khai cụ thể bằng Redux bên dưới:

    Có thể bạn không cần nó

    Tất nhiên, ứng dụng mẫu này hơi phức tạp đối với counter app tăng / giảm. Và tôi muốn nói rõ rằng bạn không cần tất cả những điều này cho một dự án nhỏ hoặc nguyên mẫu. Nhưng hãy tin tôi, khi ứng dụng của bạn lớn hơn, bạn sẽ muốn tối đa hóa khả năng tái sử dụng và khả năng bảo trì. Kiến trúc phần mềm tốt giúp cho các dự án có khả năng chống chọi với thời gian.