---
title: "Flutter cơ bản: Dark Mode và Dynamic Theme sử dụng Provider"
description: "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?"
canonical: "https://200lab.io/blog/flutter-co-ban-dark-mode-va-dynamic-theme-su-dung-provider"
published: "2021-07-31T07:00:35Z"
updated: "2026-08-21T07:17:53Z"
authors: ["Chau Le"]
tags: ["Flutter"]
reading_minutes: 9
access: "public"
---

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](https://assets.200lab.io/images/2024/12/05/0e54c2ec-3cda-4c55-b06a-de1d285f9d47-caf80ed0e04e97a3ea7a.webp)

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:

```
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:

```
//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à `theme` và `darkTheme`, 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:

```
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`:

```
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 `brightness` là `dark`, tức là `Theme.of(context).brightness == Brightness.dark` thì giá trị của `isOn` là `1`, 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](<https://shashankbiplav.me/flutter-advanced-adding-dark-mode-or-dynamic-theme-using-provider>).
