Tag: Flutter

  • [Mobile – Flutter] Slider trong Flutter

    [Mobile – Flutter] Slider trong Flutter

    Bản thân Flutter là một framework khá hoàn thiện đến mức việc tạo giao diện với Flutter trở nên dễ dàng và nhanh chóng đến mức bất ngờ! Điều này khiến lập trình viên Flutter cảm thấy việc lập trình mobile quá dễ (trừ khi họ là người đã từng nếm trái đắng từ khi code native). Mình cũng thấy dễ cho đến khi nhìn thấy giao diện ứa nước mắt mà designer gửi (đôi khi là kèm theo video animation siêu xịn…).

    Thế bạn đã trang bị cho mình gì để ứng phó trước những design đẹp (hoặc k) nào? Bạn đã biết custom Slider trong Flutter chưa? Hay chỉ là những bé slider cũ mèn như này này này:

    mặc định
    range slider
    cupertino *hình ảnh chụp từ video youtube nên hơi mờ

    Nhưng khi nhận được design thì trông như này

    như này nè
    rồi như này
    trông cũng dễ?

    Chắc ai nhận design cũng sẽ nghĩ: trông dễ phết, khó thì dùng thư viện (nhiều thư viện Slider quá mà). Có thể kể ra một số thư viện cho ai muốn dùng nè:

    Đọc đến đây thì ai thích dùng thư viện có thể dừng lại và tham khảo 3 thư viện này. Đôi khi một số lại thấy thư viện thì quá là nặng thêm nhiều thứ lắt nhắt, rồi cho customize đủ thứ mà cái mình cần thì không có? Hoặc bạn muốn tự mình thử sức làm những slider này, thì tiếp tục đọc cách custom slider nào :3

    Đầu tiên, chúng ta sẽ tìm hiểu về các thành phần của 1 slider và tên gọi của nó trong Flutter với thiết kế theo Material: (đoạn này cop trên document lười dịch nên nhờ bạn đọc tạm nhé)

    • The “thumb”, which is a shape that slides horizontally when the user drags it.
    • The “track”, which is the line that the slider thumb slides along.
    • The “value indicator”, which is a shape that pops up when the user is dragging the thumb to indicate the value being selected.
    • The “active” side of the slider is the side between the thumb and the minimum value.
    • The “inactive” side of the slider is the side between the thumb and the maximum value.
    • The “tick marks”, which are regularly spaced marks that are drawn when using discrete divisions.
    • The “value indicator”, which appears when the user is dragging the thumb to indicate the value being selected.
    • The “overlay”, which appears around the thumb, and is shown when the thumb is pressed, focused, or hovered. It is painted underneath the thumb, so it must extend beyond the bounds of the thumb itself to actually be visible.

    Để thiết kế ra một Slider thì đầu tiên chúng ta sẽ xem đến các widget trực tiếp để tạo ra slider: Slider, RangeSlider, and CupertinoSlider(Flutter Widget of the Week) – YouTube. Tuy nhiên khi xem document (ờ lại là document, document của Flutter là tài liệu hữu ích nhất mà dev Flutter có thể tìm được bên cạnh source code open của nó) thì bạn phát hiện ra nó không có nhiều thứ liên quan đến giao diện cho lắm, ngoài việc cho phép đổi tí màu. Bạn bắt đầu tuyệt vọng.

    một constructor của slider
    có gì đó

    và rồi cuối document, họ bảo, vẻ bề ngoài của nó thì dùng SliderThemeData từ widget SliderTheme hoặc một cái theme nào đó cha ông của nó mà có định nghĩa sliderTheme. Yeah XD

    Bạn tiếp tục đi đến wiki của SliderThemeData xem tùy chỉnh được gì, ở đây nhé SliderThemeData class – material library – Dart API (flutter.dev). Có thể chỉnh được kha khá, hình tròn hình chữ nhật, bo góc (hoặc k), … bạn có thể dừng lại, đọc, và thử xem bạn có thể custom như nào (ảnh phía trên là 1 slider đã được 1 tác giả khác tạo ra nhờ tùy chỉnh các thuộc tính này).

    Để rồi nhận ra rằng: nó không giống mấy cái design nhận được xíu nào 🙁

    hình ảnh slider trong blog cuối bài

    Thế làm sao để custom chúng nó như này? Hãy xem Flutter Team đã tạo ra Slider như nào đã nhé :3

    ồ là một file hơn 3000 dòng code và rất nhiều tác giả

    Họ đã tạo ra RoundedRectSliderTrackShape, RoundSliderThumbShape, PaddleSliderValueIndicatorShape, PaddleSliderValueIndicatorShape, RoundSliderTickMarkShape,… và document cũng nói là:

    The thumb, track, tick marks, value indicator, and overlay can be customized by creating subclasses of SliderTrackShapeSliderComponentShape, and/or SliderTickMarkShape. See RoundSliderThumbShapeRectangularSliderTrackShapeRoundSliderTickMarkShapeRectangularSliderValueIndicatorShape, and RoundSliderOverlayShape for examples.

    Document

    Nên cứ nghe theo thôi ha, họ bảo code mẫu rồi đấy, muốn custom thì xem mà học :3 họ bảo kế thừa mấy cái có sẵn này đi mà custom, nhưng custom sao thì họ không nói 🙁 nên t ở đây để giúp bạn đây hehe. Không dài dòng nữa, bắt đầu thôi :3

    Khi bạn extend các lớp kể ở trên, bạn sẽ được trình gợi ý yêu cầu override hàm paint, đây là hàm để vẽ nên chúng nó. Trong hàm paint có một số thứ bạn cần chú ý:
    – Đầu tiên là PaintingContext context, ở đây bạn sẽ lấy được canvas ra và vẽ những gì bạn cần (context.canvas)
    – Tiếp theo là center với Thumb và parentBox với Track, bạn sẽ tìm được những điểm hữu dụng để vẽ
    – Tiếp theo là sliderTheme, từ đó bạn có thể lấy ra các thuộc tính khác mà bạn có thể truyền vào theme của slider (màu active, inactive…)
    – Cuối cùng, mn luôn luôn có thể tham khảo code open của Flutter xem họ làm như nào với các shape mặc định, hoặc tham khảo code của mình sau đây :3

    Đây là code để tạo ra chiếc slider này

    Chụp từ màn hình điện thoại
    import 'package:flutter/material.dart';
    
    class CustomSlider extends StatelessWidget {
      final double max, min, value;
      final void Function(double) onValueChange;
    
      const CustomSlider({
        required this.min,
        required this.max,
        required this.value,
        required this.onValueChange,
        Key? key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Container(
          decoration: BoxDecoration(
            border: Border.all(color: Colors.grey),
            borderRadius: BorderRadius.circular(10),
          ),
          padding: const EdgeInsets.fromLTRB(4, 16, 4, 16),
          child: Row(
            children: [
              Text(min.toInt().toString()),
              const SizedBox(width: 6),
              Expanded(
                child: SliderTheme(
                  data: SliderTheme.of(context).copyWith(
                    trackHeight: 14.0,
                    trackShape: CustomTrackShape(),
                    activeTrackColor: const Color(0xFF219653),
                    inactiveTrackColor: const Color(0xFFF1E1E1),
                    thumbShape: CustomSliderMarkShape(sliderValue: value, tickMarkRadius: 16),
                    thumbColor: const Color(0xFF219653),
                    // overlayColor: Color(0xFF219653).withOpacity(0.5),
                    // overlayShape: RoundSliderOverlayShape(overlayRadius: 24),
                    overlayShape: SliderComponentShape.noOverlay,
                    showValueIndicator: ShowValueIndicator.always,
                    valueIndicatorShape: const RectangularSliderValueIndicatorShape(),
                    valueIndicatorColor: Colors.black,
                    valueIndicatorTextStyle: const TextStyle(
                      color: Colors.white,
                      fontSize: 16.0,
                    ),
                  ),
                  child: Slider(
                    min: min,
                    max: max,
                    value: value,
                    label: '${value.round()}',
                    onChanged: onValueChange,
                  ),
                ),
              ),
              const SizedBox(width: 6),
              Text(max.toInt().toString()),
            ],
          ),
        );
      }
    }
    
    class CustomSliderMarkShape extends SliderComponentShape {
      final double tickMarkRadius;
      final double sliderValue;
    
      CustomSliderMarkShape({
        required this.tickMarkRadius,
        required this.sliderValue,
      });
    
      @override
      Size getPreferredSize(bool isEnabled, bool isDiscrete) {
        return Size(tickMarkRadius, tickMarkRadius);
      }
    
      @override
      void paint(
        PaintingContext context,
        Offset center, {
        required Animation<double> activationAnimation,
        required Animation<double> enableAnimation,
        required bool isDiscrete,
        required TextPainter labelPainter,
        required RenderBox parentBox,
        required SliderThemeData sliderTheme,
        required TextDirection textDirection,
        required double value,
        required double textScaleFactor,
        required Size sizeWithOverflow,
      }) {
        final Canvas canvas = context.canvas;
    
        canvas.drawRRect(
          RRect.fromRectAndRadius(
            Rect.fromCenter(center: center, width: 36, height: 24), const Radius.circular(16)),
          Paint()..color = Colors.white,
        );
    
        canvas.drawRRect(
          RRect.fromRectAndRadius(
            Rect.fromCenter(center: center, width: 36, height: 24), const Radius.circular(16)),
          Paint()..color = Colors.black
            ..style = PaintingStyle.stroke
            ..strokeWidth = 2,
        );
    
        TextSpan span = TextSpan(
          style: TextStyle(
            fontSize: tickMarkRadius * 0.9,
            fontWeight: FontWeight.w700,
            color: Colors.black
          ),
          text: sliderValue.round().toString(),
        );
    
        TextPainter tp = TextPainter(
          text: span,
          textAlign: TextAlign.center,
          textDirection: TextDirection.ltr,
        );
    
        tp.layout();
    
        Offset textCenter = Offset(
          center.dx - (tp.width / 2),
          center.dy - (tp.height / 2),
        );
        tp.paint(canvas, textCenter);
      }
    }
    
    class CustomTrackShape extends RoundedRectSliderTrackShape {
      @override
      Rect getPreferredRect({
        required RenderBox parentBox,
        Offset offset = Offset.zero,
        required SliderThemeData sliderTheme,
        bool isEnabled = false,
        bool isDiscrete = false,
      }) {
        final double trackHeight = sliderTheme.trackHeight ?? 8;
        final double trackLeft = offset.dx;
        final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
        final double trackWidth = parentBox.size.width;
        return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
      }
    }
    // sử dụng
    CustomSlider(
      max: 100,
      min: 10,
      onValueChange: (_) {  },
      value: 30,
    ),


    Mình đã custom cái slider này từ rất lâu rồi (nên code của nó mình không chắc là ổn lắm).

    Và lí do mình viết bài này chủ yếu để nhớ kĩ hơn và muốn chia sẻ với mọi người khi mà chiều nay mình đã custom ra mấy cái extend SliderComponentShape, rồi ghép vào SliderTheme mà reload restart hoài UI không nhận :'( Mình rất bối rối muốn ném máy ra cửa sổ thì nhận ra nếu là RangeSlider thì cần extend RangeSliderTrackShape, RangeSliderThumbShape… (thêm Range cơ). Hóa ra do mình không đọc kĩ (tiện muốn kể chuyện InteractiveViewer ghê mà ai muốn nghe ib nhé :v)

    range slider sau khi custom
    import 'dart:math';
    
    import 'package:flutter/material.dart';
    import 'dart:ui' as ui;
    
    class CustomRangeSliderTrack extends RangeSliderTrackShape {
      const CustomRangeSliderTrack();
    
      @override
      Rect getPreferredRect({
        required RenderBox parentBox,
        Offset offset = Offset.zero,
        required SliderThemeData sliderTheme,
        bool isEnabled = false,
        bool isDiscrete = false,
      }) {
        final double overlayWidth = sliderTheme.overlayShape!.getPreferredSize(isEnabled, isDiscrete).width;
        final double trackHeight = sliderTheme.trackHeight!;
    
        final double trackLeft = offset.dx + overlayWidth / 2;
        final double trackTop = offset.dy + (parentBox.size.height - trackHeight) / 2;
        final double trackRight = trackLeft + parentBox.size.width - overlayWidth;
        final double trackBottom = trackTop + trackHeight;
        // If the parentBox'size less than slider's size the trackRight will be less than trackLeft, so switch them.
        return Rect.fromLTRB(min(trackLeft, trackRight), trackTop, max(trackLeft, trackRight), trackBottom);
      }
    
      @override
      void paint(
        PaintingContext context,
        Offset offset, {
        required RenderBox parentBox,
        required SliderThemeData sliderTheme,
        required Animation<double> enableAnimation,
        required Offset startThumbCenter,
        required Offset endThumbCenter,
        bool isEnabled = false,
        bool isDiscrete = false,
        required TextDirection textDirection,
        double additionalActiveTrackHeight = 2,
      }) {
        final ColorTween inactiveTrackColorTween = ColorTween(
          begin: sliderTheme.disabledInactiveTrackColor,
          end: sliderTheme.inactiveTrackColor,
        );
    
        final Rect trackRect = getPreferredRect(
          parentBox: parentBox,
          offset: offset,
          sliderTheme: sliderTheme,
          isEnabled: isEnabled,
          isDiscrete: isDiscrete,
        );
    
        final Paint activePaint = Paint()
          ..shader = ui.Gradient.linear(
            Offset(0, trackRect.top),
            Offset(0, trackRect.bottom),
            [
              Color(0xFF4BC65E),
              Color(0xFF379A46),
            ],
          );
        final Paint inactivePaint = Paint()
          ..color = inactiveTrackColorTween.evaluate(enableAnimation)!;
        final Size thumbSize = sliderTheme.rangeThumbShape!.getPreferredSize(isEnabled, isDiscrete);
        final double thumbRadius = thumbSize.width / 2;
        assert(thumbRadius > 0);
    
        final Radius trackRadius = Radius.circular(trackRect.height / 2);
    
        context.canvas.drawRRect(
          RRect.fromLTRBAndCorners(
            trackRect.left,
            trackRect.top,
            startThumbCenter.dx,
            trackRect.bottom,
            topLeft: trackRadius,
            bottomLeft: trackRadius,
          ),
          inactivePaint,
        );
        context.canvas.drawRect(
          Rect.fromLTRB(
            startThumbCenter.dx,
            trackRect.top - (additionalActiveTrackHeight / 2),
            endThumbCenter.dx,
            trackRect.bottom + (additionalActiveTrackHeight / 2),
          ),
          activePaint,
        );
        context.canvas.drawRRect(
          RRect.fromLTRBAndCorners(
            endThumbCenter.dx,
            trackRect.top,
            trackRect.right,
            trackRect.bottom,
            topRight: trackRadius,
            bottomRight: trackRadius,
          ),
          inactivePaint,
        );
      }
    }
    
    class CustomRangeSliderThumb extends RangeSliderThumbShape {
      const CustomRangeSliderThumb();
      
      @override
      Size getPreferredSize(bool isEnabled, bool isDiscrete) {
        return Size.fromRadius(20);
      }
      
      @override
      void paint(PaintingContext context,
        Offset center, {
        required Animation<double> activationAnimation, 
        required Animation<double> enableAnimation, 
        bool isDiscrete = false,
        bool isEnabled = false,
        bool? isOnTop,
        required SliderThemeData sliderTheme,
        TextDirection? textDirection,
        Thumb? thumb,
        bool? isPressed,
      }) {
        final Canvas canvas = context.canvas;
        final outerPaint = Paint()
          ..style = PaintingStyle.fill
          ..shader = ui.Gradient.linear(
            Offset(0, 0),
            Offset(0, 30),
            [
              Color(0xFF4BC65E),
              Color(0xFF379A46),
            ],
          );
        canvas.drawCircle(center, 10, outerPaint);
        canvas.drawCircle(center, 5, Paint()..color = Colors.white);
      }
    }
    // dùng như này để có 1 range slider như ý
    // có thể custom thêm gì đó tùy ý nha
    SliderTheme(
      data: SliderThemeData(
        trackHeight: 4,
        rangeTrackShape: CustomRangeSliderTrack(),
        rangeThumbShape: CustomRangeSliderThumb(),
      ),
      child: RangeSlider(
        values: priceRange,
        min: minPrice,
        max: maxPrice,
        onChanged: (value) {
          setState(() {
            priceRange = value;
          });
        },
      ),
    ),

    Tham khảo: Flutter Slider widgets: A deep dive with examples – LogRocket Blog

  • Flutter Custom Radio Button với Custom Shapes

    Sự phát triển của Flutter đang trở nên phổ biến và ngày càng phổ biến hơn do có nhiều tùy biến, widget tùy chỉnh và cách tiếp cận rất dễ thực hiện. Hôm nay chúng ta sẽ tìm hiểu cách tạo nút radio tùy chỉnh Flutter với các hình dạng tùy chỉnh như hộp đựng hình vuông, hộp đựng hình tròn hoặc biểu tượng. Tôi đã cố gắng tạo ra các phương pháp khác nhau để chúng có thể được sử dụng theo nhu cầu và nhà phát triển sẽ học từ hướng dẫn này cũng có thể có ý tưởng tạo nút radio tùy chỉnh rung của riêng mình, với hình dạng hoặc tiện ích theo yêu cầu của mình.

    Ví dụ về Nút radio tùy chỉnh Flutter này sẽ tăng tốc độ phát triển của bạn và tiết kiệm thời gian của bạn bằng cách gọi nó trực tiếp như một phần tử con của bất kỳ tiện ích nào hoặc bằng cách thực hiện các phương thức tĩnh khi nó cần thiết. Bởi vì không có ứng dụng nào có thể được thực hiện mà không thực hiện bất kỳ widegt tùy chỉnh nào, tất cả các widget được tạo sẵn không phải lúc nào cũng giúp ích được. Vì vậy, học cách chế tạo widget tùy chỉnh là phần quan trọng nhất của quá trình phát triển Flagship.

    Hãy bắt đầu thực hiện từng bước để chúng ta có thể hiểu những gì cần phải làm để đạt được kết quả đầu ra mong muốn. Đầu tiên, chúng ta sẽ tạo widget home, nơi chúng ta sẽ hiển thị các nút radio tùy chỉnh.

    Nó là một widget toàn diện có khả năng xử lý tất cả các chức năng của nút radio, như hiển thị thiết kế tùy chỉnh, sự kiện chạm và lựa chọn, v.v.

    home_screen.dart

    class HomeScreen extends StatefulWidget {
      const HomeScreen({Key? key}) : super(key: key);
      @override
      _HomeScreenState createState() => _HomeScreenState();
    }
    class _HomeScreenState extends State {
      List radioOptions = [];
      @override
      void initState() {
        super.initState();
        radioOptions.add(RadioButtonModel(false, 'A', 'Option A'));
        radioOptions.add(RadioButtonModel(false, 'B', 'Option B'));
        radioOptions.add(RadioButtonModel(false, 'C', 'Option C'));
        radioOptions.add(RadioButtonModel(false, 'D', 'Option D'));
      }
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Custom Check Box Example'),
          ),
          body: MyRadioButtonWidget(
            options: radioOptions,
            onItemSelected: (index) {
              print('I am index: $index');
            },
          ),
        );
      }
    }

    Giờ hãy tạo nút radio tùy chỉnh

    Lớp này chịu trách nhiệm xử lý tất cả các hoạt động của nút radio tùy chỉnh, như thiết kế, sự kiện và gọi lại. Nếu bạn nhận thấy, tôi đã thêm một chức năng gọi lại để trả về chỉ mục mục được nhấn hiện tại. Trong trường hợp bạn muốn làm cho lớp nút radio tùy chỉnh của mình độc lập và chung chung, bạn có thể tạo cho các sự kiện theo yêu cầu của mình.

    final Function(int)? onItemSelected;

    my_radio_button_widget.dart

    class MyRadioButtonWidget extends StatefulWidget {
      final List<RadioButtonModel>? options;
      final Function(int)? onItemSelected;
      const MyRadioButtonWidget({Key? key, this.options, this.onItemSelected})
          : super(key: key);
      @override
      createState() {
        return MyRadioButtonWidgetState();
      }
    }
    class MyRadioButtonWidgetState extends State<MyRadioButtonWidget> {
      @override
      Widget build(BuildContext context) {
        return ListView.builder(
          itemCount: widget.options!.length,
          itemBuilder: (BuildContext context, int index) {
            return InkWell(
              onTap: () {
                setState(() {
                  for (var element in widget.options!) {
                    element.isSelected = false;
                  }
                  widget.options![index].isSelected = true;
                  widget.onItemSelected!(index);
                });
              },
              child: CircleRadioButtonItem(widget.options![index]),
            );
          },
        );
      }
    }

    Radio Button Model

    class RadioButtonModel {
      bool isSelected;
      final String buttonText;
      final String text;
      RadioButtonModel(this.isSelected, this.buttonText, this.text);
    }

    Bây giờ chúng tôi sẽ tạo các hình dạng tùy chỉnh cho nút radio của chúng ta, ta có nhiều tùy chọn, bạn có thể tìm thấy ba lớp khác nhau để thể hiện phong cách khác nhau. bằng cách sử dụng ý tưởng này, bạn có thể tự tạo và sửa đổi các ví dụ đã cho theo ý muốn.

    Nút radio vuông

    Ở đây chúng tôi đang tạo hộp chứa hình vuông với văn bản bên trong nó.

    class SquareRadioButtonItem extends StatelessWidget {
      final RadioButtonModel _item;
      const SquareRadioButtonItem(this._item, {Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context) {
        return Container(
          margin: const EdgeInsets.all(15.0),
          child: Row(
            mainAxisSize: MainAxisSize.max,
            children: [
              Container(
                height: 50.0,
                width: 50.0,
                child: Center(
                  child: Text(_item.buttonText,
                      style: TextStyle(
                          color: _item.isSelected ? Colors.white : Colors.black,
                          //fontWeight: FontWeight.bold,
                          fontSize: 18.0)),
                ),
                decoration: BoxDecoration(
                  color: _item.isSelected ? Colors.blueAccent : Colors.transparent,
                  border: Border.all(
                      width: 1.0,
                      color: _item.isSelected ? Colors.blueAccent : Colors.grey),
                  borderRadius: const BorderRadius.all(Radius.circular(2.0)),
                ),
              ),
              Container(
                margin: const EdgeInsets.only(left: 10.0),
                child: Text(_item.text),
              )
            ],
          ),
        );
      }
    }

    Nút radio tròn

    class CircleRadioButtonItem extends StatelessWidget {
      final RadioButtonModel _item;
      const CircleRadioButtonItem(this._item, {Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context) {
        return Container(
          margin: const EdgeInsets.all(15.0),
          child: Row(
            mainAxisSize: MainAxisSize.max,
            children: [
              Container(
                height: 50.0,
                width: 50.0,
                child: Center(
                  child: Text(_item.buttonText,
                      style: TextStyle(
                          color: _item.isSelected ? Colors.white : Colors.black,
                          //fontWeight: FontWeight.bold,
                          fontSize: 18.0)),
                ),
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: _item.isSelected ? Colors.blueAccent : Colors.transparent,
                  border: Border.all(
                      width: 1.0,
                      color: _item.isSelected ? Colors.blueAccent : Colors.grey),
                ),
              ),
              Container(
                margin: const EdgeInsets.only(left: 10.0),
                child: Text(_item.text),
              )
            ],
          ),
        );
      }
    }

    Icon Radio

    class IconRadioButtonItem extends StatelessWidget {
      final RadioButtonModel _item;
      const IconRadioButtonItem(this._item, {Key? key}) : super(key: key);
      @override
      Widget build(BuildContext context) {
        return Container(
          margin: const EdgeInsets.all(15.0),
          child: Row(
            mainAxisSize: MainAxisSize.max,
            children: [
              _item.isSelected
                  ? const Icon(Icons.circle)
                  : const Icon(Icons.circle_outlined),
              Container(
                margin: const EdgeInsets.only(left: 10.0),
                child: Text(_item.text),
              )
            ],
          ),
        );
      }
    }

  • [Flutter]Push Notification

    [Flutter]Push Notification

    0. Thêm dependency

    Thêm flutter_local_notifications: ^9.1.2 vào pubspec.yaml.

    Đừng quên sử dụng Pub get để tải về.

    1. Thiết lập hình ảnh biểu tượng của bạn

    Thêm hình ảnh vào đường dẫn này:

    icon path: PROJECT_NAME\android\app\src\main\res\drawable\icon.png

    2. Tạo một class NotificationService

    Bạn sẽ nhập lớp này vào main.dart.(Chúng tôi sẽ thực hiện việc này ở bước sau 3.) Mã của lớp này ở cuối bài viết này.

    3. Thực hiện một số thao tác khởi tạo

    trong main()

    4. Chức năng hiển thị cuộc gọi trực tiếp ở bất cứ đâu khi bạn muốn

    showNotification()là một hàm của lớp NotificationService mà chúng ta vừa tạo trong tệp notification_service.dart.

    Ví dụ: tôi muốn thông báo đẩy khi tôi nhấp vào một nút, sau đó tôi có thể gọi chức năng này trong _onPressed()nút. Mã ở dưới đây, hãy kiểm tra nó.

    main.dart

    import 'package:flutter/material.dart';
    import 'service/notification_service.dart';
    
    Future<void> main() async{
      // Step 3. Initialization
      WidgetsFlutterBinding.ensureInitialized();
      NotificationService notificationService = NotificationService();
      await notificationService.init();
    
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(
          home: MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({Key? key}) : super(key: key);
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      /// Step 4. For pusing notification.
      NotificationService notificationService = NotificationService();
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text("My Main Page"),
          ),
          body: const Text('hello world'),
          floatingActionButton: FloatingActionButton(
            onPressed: () async{
              /// When I click the button, the notification will be pushed.
              /// Step 4. just call this function anywhere you want.
              await notificationService.showNotification(0, 'This is title...', "Tis is body...",);
            },
            child: const Icon(Icons.radio_button_on),
          ),
        );
      }
    }

    notification_service.dart

    import 'package:flutter_local_notifications/flutter_local_notifications.dart';
    /// Step 2. Create a NotificationService class
    class NotificationService {
      // Singleton pattern, https://en.wikipedia.org/wiki/Singleton_pattern
      // 1.The _internal() construction is just a name often given to constructors
      // that are private to the class
      // 2.Use the factory keyword when implementing a constructor
      // that does not create a new instance of its class.
      NotificationService._internal();
      static final NotificationService _notificationService = NotificationService._internal();
      factory NotificationService() {
        return _notificationService;
      }
    
      // Configuration for platform-specific initialization settings.
      Future<void> init() async {
        // Specifies the default icon for notifications.
        // icon path: PROJECT_NAME\android\app\src\main\res\drawable\icon.png
        const AndroidInitializationSettings androidInitializationSettings =
          AndroidInitializationSettings("icon");
        const InitializationSettings initializationSettings =
          InitializationSettings(android: androidInitializationSettings);
        await flutterLocalNotificationsPlugin.initialize(initializationSettings);
      }
    
      // Some configurations for platform-specific notification's details.
      static const AndroidNotificationDetails _androidNotificationDetails =
        AndroidNotificationDetails(
          'ChannelId',
          'ChannelName',
          channelDescription: "Responsible for all local notifications",
          playSound: true,
          priority: Priority.high,
          importance: Importance.high,
        );
      final NotificationDetails notificationDetails =
        const NotificationDetails(android: _androidNotificationDetails);
    
      // Now we need to call the show() method of FlutterLocalNotificationsPlugin.
      // Show() is responsible for showing the local notification.
      final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();
    
      Future<void> showNotification(int id, String title, String body) async {
        await flutterLocalNotificationsPlugin.show(id, title, body, notificationDetails);
      }
    }
  • Flutter – Các phím tắt IDE để giúp bạn code nhanh hơn

    Flutter – Các phím tắt IDE để giúp bạn code nhanh hơn

    Flutter – Các phím tắt IDE để giúp bạn code nhanh hơn

    Nếu bạn chưa quen với việc phát triển Flutter thì bạn phải tìm hiểu kỹ về các cấu trúc lồng vào nhau (nested structures) để biết mức độ khó khăn để thêm hoặc xóa các widget ở giữa code hay để tìm một nơi mà một widget kết thúc và một widget khác bắt đầu. Sau đó, bạn phải dành cả ngày để khớp các dấu ngoặc mở với dấu đóng của chúng. Chúng mình đã mất thời gian để tìm ra các phím tắt. Vì vậy, bạn sẽ không phải mất thời gian để thực hiện lại điều đó vì mình sẽ cung cấp cho bạn; và mình đã sắp xếp tất cả các phím tắt cho phép phát triển nhanh hơn và mượt mà hơn trong Flutter.

    Note: Tất cả các phím tắt này đều hoạt động cho Android Studio và IntelliJ trong Windows.

    Tạo Stateless widget hoặc Stateful widget

    Đoán xem nào? Bạn không phải viết thủ công các widget class của mình và ghi đè các build function. IDE có thể làm điều đó thay bạn.

    Bạn chỉ cần gõ stless để tạo một Stateless Widget như sau:

    Hoặc stful để tạo Stateful widget:

    Điều gì sẽ xảy ra nếu bạn đã tạo một StatelessWidget và thêm nhiều children, nhưng sau đó nhận ra rằng sau cùng thì bạn sẽ cần một State? Bạn có nên tạo một StatefulWidget mới và sau đó chuyển tất cả code của bạn sang nó theo cách thủ công? Bạn không cần phải làm như vậy.

    Bạn chỉ cần đặt con trỏ vào StatelessWidget, nhấn Alt + Enter và nhấp vào Convert to StatefulWidget. Tất cả code soạn sẵn sẽ được tạo tự động cho bạn.

    Những điều kỳ diệu hơn bạn có thể làm với Alt + Enter

    Alt + Enter là cây đũa thần giúp bạn sử dụng để phát triển nhanh hơn trong Flutter. Bạn có thể nhấp vào bất kỳ tiện ích con nào, nhấn Alt + Enter để xem bạn có những tùy chọn nào cho tiện ích cụ thể đó.

    Thêm Padding xung quanh Widget

    Giả sử bạn có một widget không phải là Container, vì vậy nó không có padding property. Bạn muốn cung cấp một số padding nhưng lại sợ làm rối widget structure của mình. Với cây đũa thần của chúng mình, bạn có thể thêm Padding của mình mà không làm rối tung bất cứ thứ gì:

    Chỉ cần nhấn Alt + Enter trên widget cần một padding xung quanh nó và nhấp vào Add Padding. Bây giờ bạn có thể sửa đổi padding mặc định thành bất kỳ thứ gì bạn muốn.

    Center Widget

    Đây không phải là điều gì quá phi thường. Nó chỉ căn giữa widget của bạn trong không gian có sẵn. Điều này không hoạt động bên trong Column hoặc Row.

    Wrap bằng Container, Column, Row hoặc bất kỳ Widget nào khác

    Bạn có thể sử dụng cách tiếp cận tương tự để bọc widget của mình bằng Container. Vì vậy, Container mới sẽ trở thành parent của Widget của bạn.

    Hoặc, bạn thậm chí có thể kết hợp nhiều widget với một Colunm hoặc Row chỉ trong một cú nhấp chuột!

    Hoặc bọc chúng bằng bất kỳ widget nào khác:

    Bạn thậm chí có thể bọc chúng bằng StreamBuilder nếu bạn có phiên bản plugin Flutter mới nhất. Cảm ơn Bhavik Makwana đã cho mình biết về điều đó.

    Bạn không thích một widget nào đó? Hãy loại bỏ nó bằng Magic Wand.

    Đúng vậy, việc xóa một widget cũng dễ dàng như thêm một widget mới.

    Xem code bên trong Widget

    Đó là điều tốt nhất về một open source framework. Nếu bạn muốn biết điều gì đang diễn ra đằng sau một widget tuyệt vời hoặc một class, thì bạn có thể đặt con trỏ vào nó và nhấn Ctrl + B. Nó sẽ hoạt động như một liên kết, đưa bạn đến thẳng source code của Widget, nơi bạn có thể đọc mọi thứ về nó. Flutter cũng sử dụng các bình luận để giải thích rất nhiều code, một tài liệu tuyệt vời.

    Kiểm tra các thuộc tính của Widget  mà không cần rời khỏi tệp hoặc tab

    Nếu bạn muốn kiểm tra xem Widget của bạn có thể làm được những điều tuyệt vời nào mà không cần rời khỏi tệp của bạn để tìm hiểu tài liệu, chỉ cần nhấn Ctrl + Shift + I để xem nhanh constructor của Widget.

    Chọn nhanh toàn bộ Widget

    Rất nhiều lần chúng ta cần giải nén/xóa toàn bộ widget và chúng ta cố gắng chọn chúng theo cách thủ công:

    Nếu đó là một widget thực sự lớn, thì việc tìm ra dấu đóng ngoặc thuộc về Widget nào có thể rất mất thời gian và chúng ta không muốn làm rối toàn bộ cấu trúc của mình.

    Những lúc như thế này, mình thích sử dụng phím tắt siêu hữu ích này.

    Chỉ cần nhấp vào widget bạn muốn giải nén và nhấn Ctrl + W. Toàn bộ Widget sẽ được chọn cho bạn mà không cần di chuyển con trỏ của bạn một inch.

    Format code

    Đôi khi code của bạn sẽ chỉ là một mớ hỗn độn. Kiểu như thế này:

    Bây giờ, hầu hết các IDE đều có tính năng này, (mặc dù có thể không phải là tổ hợp phím giống nhau). Chỉ cần nhấn Ctrl + Alt + L để sửa lỗi thụt lề và định dạng lại code của bạn.‌

    Xem outline UI ứng dụng của bạn

    Hầu hết các Widget của chúng ta không chỉ có một child trong tree của chúng. Chúng có những children tree có children riêng của chúng và nhiều thứ khác nữa. Nếu Widget của bạn có các children được lồng vào nhau phức tạp, thì bạn có thể gặp chút rắc rối với việc hiểu cấu trúc của code. Nhưng rất may là chúng ta có Flutter Outline để giải cứu!

    Bạn có thể tìm thấy Flutter Outline ở  bên phải IDE của mình. Nó là một trong những tab dọc và nằm ngay phía trên Flutter Inspector. Khi bạn mở nó lên, nó trông giống như sau:

    Giờ đây, bạn có thể thấy rõ Widget nào đang ở đâu, chúng được sắp xếp như thế nào trong giao diện người dùng và widget nào có các children widget khác. Dễ như ăn bánh!

    Extract code thành một method

    Flutter Outline là một công cụ khá hữu ích. Bạn có thể thực hiện hầu hết những việc bạn đã làm với Alt + Enter, chẳng hạn như bọc một Column và Center một Widget, nhưng có những thứ tuyệt vời hơn nữa có sẵn trong tab Flutter Outline! Một trong số đó là nút Extract Method.

    Nếu bạn cảm thấy như đang viết một Widget quá dài và có lẽ phải là một Widget tùy chỉnh, thì thay vì chuyển code theo cách thủ công thành một method, bạn có thể sử dụng công cụ này để làm điều kỳ diệu cho mình!

    Move Widget lên và xuống

    Một điều điên rồ khác mà bạn có thể làm với Flutter Outline là nếu bạn có nhiều children trong một widget, bạn có thể dễ dàng sắp xếp lại thứ tự của chúng:

    Bạn cũng có thể di chuyển chỉ một dòng lên hoặc xuống bằng cách nhấn Shift + Alt + Up / Down

    Refactor Renaming

    Đây là một công cụ khá cơ bản mà hầu hết các IDE đều có. Điều này cho phép bạn đổi tên một method, Widget, class hoặc tên tệp và nó đảm bảo rằng các tham chiếu đến nó cũng được đổi tên. Chỉ cần sử dụng Shift + F6 và nhập tên mới:

    Remove Unused Imports

    Vì vậy, bạn đang làm việc trên một dự án và bạn đã import rất nhiều file, nhưng theo thời gian, code của bạn ngày càng được tối ưu hóa. Cuối cùng, bạn có thể không cần những file đó nữa. Có thể bạn thường xóa chúng theo cách thủ công, nhưng mình đã ở đây để giúp bạn thực hiện nó dễ dàng hơn, đây là một tổ hợp bàn phím khá đẹp: Ctrl + Alt + O

    Đó là tất cả các phím tắt mà mình biết bây giờ. Hãy nhớ kiểm tra lại thường xuyên để biết thêm các mẹo, thủ thuật và những thứ tuyệt vời khác!

    Bài viết được lược dịch từ Pooja Bhaumik.

  • [Flutter] Bố cục Layout (Phần 2)

    [Flutter] Bố cục Layout (Phần 2)

    Tiếp tục phần trước, ta sẽ tiếp tục tìm hiểu thêm các widget giúp điều chỉnh bố cục hiển thị.

    IntrinsicWidth and IntrinsicHeight

    Tất cả tiện ích con bên trong Hàng hoặc Cột có chiều cao/rộng bằng tiện ích con cao nhất/rộng nhất

    Trong trường hợp bạn có kiểu bố trí này:

    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(title: Text('IntrinsicWidth')),
        body: Center(
          child: Column(
            children: <Widget>[
              RaisedButton(
                onPressed: () {},
                child: Text('Short'),
              ),
              RaisedButton(
                onPressed: () {},
                child: Text('A bit Longer'),
              ),
              RaisedButton(
                onPressed: () {},
                child: Text('The Longest text button'),
              ),
            ],
          ),
        ),
      );
    }

    Nhưng bạn muốn có tất cả các nút theo nút rộng nhất , chỉ cần sử dụng :IntrinsicWidth

    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(title: Text('IntrinsicWidth')),
        body: Center(
          child: IntrinsicWidth(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                RaisedButton(
                  onPressed: () {},
                  child: Text('Short'),
                ),
                RaisedButton(
                  onPressed: () {},
                  child: Text('A bit Longer'),
                ),
                RaisedButton(
                  onPressed: () {},
                  child: Text('The Longest text button'),
                ),
              ],
            ),
          ),
        ),
      );
    }

    Trong trường hợp bạn gặp vấn đề tương tự nhưng bạn muốn có tất cả các widget có chiều cao theo widget cao nhất chỉ cần sử dụng kết hợp của IntrinsicHeight và Row .

    IntrinsicWidthIntrinsicHeight còn được sử dụng để định kích thước con của nó theo chiều rộng/cao nội tại tối đa của nó. Nó có thể hữu ích nếu chiều rộng/cao khả dụng là không giới hạn, nhưng bạn muốn đặt kích thước của tiện ích con thành chiều rộng nội tại của nó.

    . . .

    Stack

    Chồng các Widget lên nhau

    @override
    Widget build(BuildContext context) {
      Widget main = Scaffold(
        appBar: AppBar(title: Text('Stack')),
      );
    
      return Stack(
        fit: StackFit.expand,
        children: <Widget>[
          main,
          Banner(
            message: "Top Start",
            location: BannerLocation.topStart,
          ),
          Banner(
            message: "Top End",
            location: BannerLocation.topEnd,
          ),
          Banner(
            message: "Bottom Start",
            location: BannerLocation.bottomStart,
          ),
          Banner(
            message: "Bottom End",
            location: BannerLocation.bottomEnd,
          ),
        ],
      );
    }

    . . .

    Bạn có thể các widget của mình vào Positioned Widget để xác định vị trí hiện thị

    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(title: Text('Stack')),
        body: Stack(
          fit: StackFit.expand,
          children: <Widget>[
            Material(color: Colors.yellowAccent),
            Positioned(
              top: 0,
              left: 0,
              child: Icon(Icons.star, size: 50),
            ),
            Positioned(
              top: 340,
              left: 250,
              child: Icon(Icons.call, size: 50),
            ),
          ],
        ),
      );
    }

    . . .

    Nếu bạn không muốn đoán các giá trị trên cùng/dưới cùng, bạn có thể sử dụng LayoutBuilder để lấy chúng

    Widget build(BuildContext context) {
      const iconSize = 50;
      return Scaffold(
        appBar: AppBar(title: Text('Stack with LayoutBuilder')),
        body: LayoutBuilder(
          builder: (context, constraints) =>
            Stack(
              fit: StackFit.expand,
              children: <Widget>[
                Material(color: Colors.yellowAccent),
                Positioned(
                  top: 0,
                  child: Icon(Icons.star, size: iconSize),
                ),
                Positioned(
                  top: constraints.maxHeight - iconSize,
                  left: constraints.maxWidth - iconSize,
                  child: Icon(Icons.call, size: iconSize),
                ),
              ],
            ),
        ),
      );
    }

    . . .

    Expanded

    Expanded hoạt động với bố cục Flex\Flexbox và rất tốt để phân phối không gian giữa nhiều mục.

    Row(
      children: <Widget>[
        Expanded(
          child: Container(
            decoration: const BoxDecoration(color: Colors.red),
          ),
          flex: 3,
        ),
        Expanded(
          child: Container(
            decoration: const BoxDecoration(color: Colors.green),
          ),
          flex: 2,
        ),
        Expanded(
          child: Container(
            decoration: const BoxDecoration(color: Colors.blue),
          ),
          flex: 1,
        ),
      ],
    ),

    . . .

    ConstrainedBox

    Theo mặc định, hầu hết các tiện ích con sẽ sử dụng ít dung lượng nhất có thể:

    Card(child: const Text('Hello World!'), color: Colors.yellow)

    . . .

    ConstrainedBox cho phép tiện ích con sử dụng không gian còn lại như mong muốn.

    ConstrainedBox( 
      constraints: BoxConstraints.expand(),
      child: const Card(
        child: const Text('Hello World!'), 
        color: Colors.yellow,
      ), 
    ),

    . . .

    Bằng cách sử dụng BoxConstraints, bạn chỉ định lượng không gian mà một widget có thể có – bạn chỉ định minmax của heightwidth.

    BoxConstraints.expand sử dụng lượng không gian vô hạn (tất cả khả dụng) trừ khi được chỉ định:

    ConstrainedBox(
      constraints: BoxConstraints.expand(height: 300),
      child: const Card(
        child: const Text('Hello World!'), 
        color: Colors.yellow,
      ),
    ),

    Và nó giống như:

    ConstrainedBox(
      constraints: BoxConstraints(
        minWidth: double.infinity,
        maxWidth: double.infinity,
        minHeight: 300,
        maxHeight: 300,
      ),
      child: const Card(
        child: const Text('Hello World!'), 
        color: Colors.yellow,
      ),
    ),

    . . .

    Align

    Đôi khi bạn gặp khó khăn trong việc đặt tiện ích con của chúng tôi ở kích thước phù hợp – ví dụ: nó liên tục bị kéo căng khi bạn không muốn:

    Ví dụ ở trên xảy ra khi bạn có Column với CrossAxisAlignment.stretch và bạn chỉ muốn nút không bị kéo căng:

    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(title: Text('Align: without Align')),
        body: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            Align(
              child: RaisedButton(
                onPressed: () {},
                child: const Text('Button'),
              ),
            ),
          ],
        ),
      );
    }

    Khi tiện ích con của bạn không tuân theo các ràng buộc mà bạn cố gắng thiết lập, trước tiên hãy thử kết hợp với nó Align

    Kết thúc phần 2!

    . . .

  • [Flutter] Bố cục Layout (Phần 1)

    [Flutter] Bố cục Layout (Phần 1)

    Bạn cần các mẫu bố cục đơn giản cho Flutter?
    Tôi giới thiệu cho bạn tập hợp các đoạn mã bố cục Flutter của tôi. Tôi sẽ giữ cho nó ngắn gọn, dễ hiểu và đơn giản với vô số ví dụ trực quan.

    Row và Column

    MainAxisAlignment

    Row /*or Column*/( 
      mainAxisAlignment: MainAxisAlignment.start,
      children: <Widget>[
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 50),
      ],
    ),

    . . .

    Row /*or Column*/( 
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 50),
      ],
    ),

    . . .

    Row /*or Column*/( 
      mainAxisAlignment: MainAxisAlignment.end,
      children: <Widget>[
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 50),
      ],
    ),

    . . .

    Row /*or Column*/( 
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: <Widget>[
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 50),
      ],
    ),

    . . .

    Row /*or Column*/( 
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: <Widget>[
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 50),
      ],
    ),

    . . .

    Row /*or Column*/( 
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: <Widget>[
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 50),
      ],
    ),

    . . .

    CrossAxisAlignment

    Bạn nên sử dụng CrossAxisAlignment.baseline nếu bạn yêu cầu căn chỉnh đường cơ sở của các văn bản khác nhau

    Row(
      crossAxisAlignment: CrossAxisAlignment.baseline,
      textBaseline: TextBaseline.alphabetic,
      children: <Widget>[
        Text(
          'Baseline',
          style: Theme.of(context).textTheme.display3,
        ),
        Text(
          'Baseline',
          style: Theme.of(context).textTheme.body1,
        ),
      ],
    ),

    . . .

    Row /*or Column*/( 
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 200),
        Icon(Icons.star, size: 50),
      ],
    ),

    . . .

    Row /*or Column*/( 
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 200),
        Icon(Icons.star, size: 50),
      ],
    ),

    . . .

    Row /*or Column*/( 
      crossAxisAlignment: CrossAxisAlignment.end,
      children: <Widget>[
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 200),
        Icon(Icons.star, size: 50),
      ],
    ),

    . . .

    Row /*or Column*/( 
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: <Widget>[
        Icon(Icons.star, size: 50),
        Icon(Icons.star, size: 200),
        Icon(Icons.star, size: 50),
      ],
    ),

    Kết thúc phần 1!

  • Shared Preferences trong Flutter

    [FLUTTER] Shared Preferences trong Flutter

    Để lưu trữ các dữ liệu ở local trong ứng dụng Flutter, ngoài cách lưu bằng sqlite, chúng ta còn thể lưu dữ liệu vào Shared Preferences

    1. Sơ lược về Shared Preferences trong Flutter

    • Dùng để lưu những tập dữ liệu nhỏ dưới dạng key-value
    • Các loại dữ liệu có thể lưu như là int, double, bool, String and List<String>
    • Các dữ liệu được lưu lại trong một file .xml và được lưu vào trong bộ nhớ đệm của máy
    • Các dữ liệu chúng ta có thể dùng để lưu như là các thông số về Settings, token,, …

    2. Cách sử dụng

    • Thêm thư viện vào trong file pubspect.yaml:
    shared_preferences: ^2.0.13

    Vì các hàm xử lý lưu dữ liệu trong shared_preferences đều là các hàm Future, nên chúng ta cần dùng await để gọi:

    • Hàm lưu dữ liệu
    // Obtain shared preferences.
    final prefs = await SharedPreferences.getInstance();
    
    // Save an integer value to 'counter' key. 
    await prefs.setInt('counter', 10);
    // Save an boolean value to 'repeat' key. 
    await prefs.setBool('repeat', true);
    // Save an double value to 'decimal' key. 
    await prefs.setDouble('decimal', 1.5);
    // Save an String value to 'action' key. 
    await prefs.setString('action', 'Start');
    // Save an list of strings to 'items' key. 
    await prefs.setStringList('items', <String>['Earth', 'Moon', 'Sun']);
    • Hàm đọc dữ liệu
    // Try reading data from the 'counter' key. If it doesn't exist, returns null.
    final int? counter = prefs.getInt('counter');
    // Try reading data from the 'repeat' key. If it doesn't exist, returns null.
    final bool? repeat = prefs.getBool('repeat');
    // Try reading data from the 'decimal' key. If it doesn't exist, returns null.
    final double? decimal = prefs.getDouble('decimal');
    // Try reading data from the 'action' key. If it doesn't exist, returns null.
    final String? action = prefs.getString('action');
    // Try reading data from the 'items' key. If it doesn't exist, returns null.
    final List<String>? items = prefs.getStringList('items');
    • Nếu chúng ta muốn xóa bỏ dữ liệu đã được lưu
    // Remove data for the 'counter' key. 
    final success = await prefs.remove('counter');

    Tài liệu tham khảo: shared_preferences | Flutter Package (pub.dev)

    Author: LamNT59

  • [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.