Facebook Pixel

Flutter cơ bản: Dark Mode và Dynamic Theme sử dụng Provider

31 Jul, 2021

Chau Le

Author

Làm thế nào để chúng ta có thể thêm nhiều theme vào ứng dụng Flutter của mình như dark mode bằng cách sử dụng provider?

Flutter cơ bản: Dark Mode và Dynamic Theme sử dụng Provider

Mục Lục

Tiếp tục seri tìm hiểu Flutter cơ bản, chúng ta đã sử dụng các provider để hoàn thành các task như quản lý state cho toàn bộ app và backend data. Nhưng chúng ta cũng có thể tận dụng các provider này cho nhiều việc khác. Một trong số đó là thêm các dynamic theme cho app Flutter.

Trong bài viết này, chúng ta sẽ xem làm thế nào để chúng ta có thể thêm nhiều theme vào ứng dụng Flutter của mình, tức là áp dụng dark mode bằng cách sử dụng provider. Chúng ta hãy bắt đầu nhé!

App demo

Đây là ứng dụng mà chúng ta đang demo:

Flutter Theme by Provider

Như bạn có thể thấy chúng ta có 1 nút toogle đổi toàn bộ theme của ứng dụng chuyển từ light sang dark và ngược lại. Điều này thực hiện bởi một provider ở phía sau.

Ngay sau khi theme data trong ứng dụng thay đổi, toàn bộ ứng dụng sẽ cập nhật với sự thay đổi đó!

Cách sử dụng dynamic theme

Bạn có thể tạo một dự án Flutter mới để thử dark mode hoặc bạn có thể tiếp tục với ứng dụng của riêng mình.

Cài đặt các packages cần thiết

Tại thời điểm viết blog này, Flutter 2.x đã ra mắt và phiên bản provider package ^5.0.0 đã publish. Nếu bạn bắt đầu với một dự án mới, mình khuyên bạn nên sử dụng các phiên bản mới nhất.

Trong trường hợp bạn đang triển khai điều này trong các dự án cũ của mình, mình không khuyên bạn nên nâng cấp Flutter SDK của mình và tiếp tục với provider package trước đó có thể thấp hơn phiên bản 5.0.0. Gợi ý nếu bạn đang tìm kiếm một phiên bản cụ thể cũ hơn, ^4.3.2+3 là lựa chọn ổn định và sẽ hoạt động tốt cho các dự án cũ của bạn.

Tạo Theme Provider

Bây giờ, trong thư mục lib, hãy tạo một thư mục providers nếu dự án của bạn không có. Tất cả các provider cho dự án của bạn đều ở đây. Trong providers directory, hãy tạo file theme_provider.dart. Tất cả logic liên quan đến theme sẽ ở đây và file sẽ trông như thế này:

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

class ThemeProvider with ChangeNotifier {
  ThemeMode _mode;
  ThemeMode get mode => _mode;
  ThemeProvider({
    ThemeMode mode = ThemeMode.light,
  }) : _mode = mode;

  void toggleMode() {
    _mode = _mode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
    notifyListeners();
  }
}
  • Trong file này, chúng ta import material.dart như thường lệ để sử dụng ChangeNotifier mixin. Nếu bạn cần một cái nhìn tổng quan chuyên sâu về ChangeNotifier và các provider nói chung, hãy tham khảo bài viết trước của mình.
  • Chúng ta cần một biến kiểu ThemeMode là một private variable bên trong ThemeProvider class này có tên _mode. Biến này giữ giá trị theme hiện tại của chúng ta!
  • Ngoài ra, để private variable _mode này có thể được truy cập từ bên ngoài, chúng ta cần một getter.
  • Ban đầu, default theme của ứng dụng là Light, do đó chúng ta khởi tạo nó bằng ThemeMode.light hoặc nếu có giá trị tồn tại từ trước thì chúng ta gán giá trị đó cho private variable trên.
  • Chúng ta có một hàm toggleMode có kiểu trả về là void được sử dụng để thay đổi app theme. Method này có thể được truy cập từ bất kỳ đâu trong ứng dụng của chúng ta miễn là widget đó được đính kèm với provider này. Đây là method được thực hiện bởi nút bật tắt của chúng ta để thay đổi theme.

Dùng Theme Provider trong file main.dart

Bạn nên dùng theme provider ở đầu tất cả các provider khác vì điều này kiểm soát theming state của toàn bộ ứng dụng của bạn. Do đó, file main.dart của chúng ta trông giống như thế này:

Dart
//Packages
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

//Providers
import './providers/theme_provider.dart';

//Screens
import './home_screen.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (ctx) => ThemeProvider(),
        ),
        //Your other providers goes here...
      ],
      child: Consumer<ThemeProvider>(
        builder: (ctx, themeObject, _) => MaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'Dynamic Theme Demo',
          themeMode: themeObject.mode,
          theme: ThemeData(
            primarySwatch: Colors.blue,
            primaryColor: Colors.blue[600],
            accentColor: Colors.amber[700],
            brightness: Brightness.light,
            backgroundColor: Colors.grey[100],
            fontFamily: 'Karla',
            visualDensity: VisualDensity.adaptivePlatformDensity,
          ),
          darkTheme: ThemeData(
            primarySwatch: Colors.blue,
            primaryColor: Colors.blue[300],
            accentColor: Colors.amber,
            brightness: Brightness.dark,
            backgroundColor: Colors.grey[900],
            fontFamily: 'Karla',
            visualDensity: VisualDensity.adaptivePlatformDensity,
          ),
          home: HomeScreen(),
        ),
      ),
    );
  }
}
  • Import các package, provider và widget như thường lệ, chúng ta thêm ThemeProvider ở đầu tất cả các provider khác trong MultiProvider. Chúng ta đang sử dụng ChangeNotifierProvider dùng cho provider vì provider data không cố định và sẽ thay đổi sau đó vào một thời điểm nào đó.
  • Tiếp theo, MaterialApp widget nằm ở đầu widget tree là active consumer của ThemeProvider do đó toàn bộ ứng dụng của chúng ta sẽ rebuild ngay khi data bên trong theme provider thay đổi và đây là những gì chúng ta muốn.
  • Trong constructor của MaterialApp widget, chúng ta dùng named arguments là themedarkTheme, nơi chúng ta có thể cung cấp các theme với nhiều thiết lập tương ứng. Chúng nhận các tham số object ThemeData.
  • Ngoài ra, chúng ta có tham số themeMode chấp nhận đối tượng ThemeMode. Thay vì cố định nó thành một giá trị duy nhất, chúng ta cung cấp cho nó giá trị dynamic từ themeObject mà chúng ta có được thông qua builder method của Consumer.
  • Sử dụng themeObject, chúng ta có thể truy cập getter method của ThemeProvider và lấy giá trị của private variable _mode: themeObject.mode.
  • Cuối cùng, chúng ta có đi đến HomeScreen với nút chuyển đổi theme. Vì thế hãy cùng xem tiếp file home_screen.dart.

HomeScreen widget

File home_screen.dart chỉ đơn giản là một Stateless Widget chứa các text widget và nút chuyển đổi theme. Nó trông như thế này:

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

import './mode_toggle_button.dart';

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: Text('Dynamic Theming Demo'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text('Toggle button to switch the app theme', textAlign: TextAlign.center,style: TextStyle(fontSize: 24),),
          ModeToggleButton(),
        ],
      ),
    );
  }
}

Ở đây, tất cả các widget được wrap bên trong một Column widget. Bạn sẽ thấy rằng nút chuyển theme của mình trông rất tuyệt vì nó chuyển icon Mặt Trời và Mặt Trăng tùy vào theme hiện tại. Mình đã build nó như thế nào? Đây là tất cả code và logic tạo nên nút này.

Bonus: Source Code tuỳ chỉnh nút chuyển đổi theme

Đây là file mode_toggle_button.dart:

Dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';

import './providers/theme_provider.dart';

class ModeToggleButton extends StatefulWidget {
  @override
  _ModeToggleButtonState createState() => _ModeToggleButtonState();
}

class _ModeToggleButtonState extends State<ModeToggleButton> {
  Size get s => MediaQuery.of(context).size;
  int isOn = 0;

  @override
  Widget build(BuildContext context) {
    bool _darkModeEnabled =
        Theme.of(context).brightness == Brightness.dark;
        _darkModeEnabled ?isOn=1 : isOn =0;
    return AnimatedContainer(
      duration: Duration(milliseconds: 360),
      width: s.width / 2,
      height: s.height / 4,
      color: Colors.transparent,
      child: Center(
        child: GestureDetector(
          onTap: () {
            HapticFeedback.mediumImpact();
            Provider.of<ThemeProvider>(context,listen:false).toggleMode();
            setState(() {
              isOn == 0 ? isOn = 1 : isOn = 0;
            });
          },
          child: Container(
            width: s.width / 4,
            height: s.width * 0.125,
            decoration: BoxDecoration(
              color: Color(0xff27173A),
              borderRadius: BorderRadius.circular(60),
            ),
            child: Stack(
              children: <Widget>[
                AnimatedPositioned(
                  duration: Duration(milliseconds: 360),
                  top: 0,
                  left: 0 + (s.width * 0.125) * isOn,
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Container(
                      width: s.width * 0.125 - 16,
                      height: s.width * 0.125 - 16,
                      decoration: BoxDecoration(
                        color: Color(0xffFFC209),
                        shape: BoxShape.circle,
                      ),
                    ),
                  ),
                ),
                AnimatedPositioned(
                  duration: Duration(milliseconds: 360),
                  top: isOn == 0 ? (s.width * 0.125 - 8) / 2 : 8,
                  left: 0 + (s.width * 0.125 - 8) * isOn,
                  child: AnimatedContainer(
                    duration: Duration(milliseconds: 360),
                    width: 8 + (s.width * 0.125 - 24) * isOn,
                    height: 8 + (s.width * 0.125 - 24) * isOn,
                    decoration: BoxDecoration(
                      color: Color(0xff27173A),
                      shape: BoxShape.circle,
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Đây là một StatefulWidget và cũng được đính kèm với provider. Nó cần internal state lưu giữ vị trí hiện tại của nút và một giá trị kiểu int tên là isOn.

  • Giá trị này được gán cho boolean variable _darkModeEnabled dựa trên thuộc tính brightness của theme hiện tại. Nếu brightnessdark, tức là Theme.of(context).brightness == Brightness.dark thì giá trị của isOn1, ngược lại nó là 0.
  • Khi nút được gạt qua off, tức là isOn = 0 thì current theme của ứng dụng là Light và ngược lại là Dark.
  • Khi user gạt nút qua lại, method toggleMode() trong ThemeProvider được thực thi, do đó thay theme đề từ Light sang Dark và thay đổi vị trí của nút này.

Tất cả các animation trong nút mà bạn nhìn thấy là do việc sử dụng AnimatedContainer widget và việc chuyển đổi từ hình tròn đầy đủ SUN sang MOON là do một vòng tròn nhỏ hơn chồng lên trên vòng tròn lớn hơn.

Kết

Đó là những gì cần cho bài viết này. Một sửa đổi trong tương lai mà bạn có thể cần đó là lưu lại theme hiện tại dưới thiết bị để thiết lập này của người dùng được lưu và họ sẽ không cần phải đổi đổi Light sang Dark theme nữa. Hãy sử dụng shared_prefrences package để lưu data vào thiết bị nhé.

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

Bài viết liên quan

Lập trình backend expressjs

xây dựng hệ thống microservices
  • Kiến trúc Hexagonal và ứng dụngal font-
  • TypeScript: OOP và nguyên lý SOLIDal font-
  • Event-Driven Architecture, Queue & PubSubal font-
  • Basic scalable System Designal font-

Đăng ký nhận thông báo

Đừng bỏ lỡ những bài viết thú vị từ 200Lab