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:
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ộtgetter
. - 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ố objectThemeData
. - Ngoài ra, chúng ta có tham số
themeMode
chấp nhận đối tượngThemeMode
. 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ínhbrightness
của theme hiện tại. Nếubrightness
làdark
, tức làTheme.of(context).brightness == Brightness.dark
thì giá trị củaisOn
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.
Chau Le
Cực thích đồ ngọt như sữa, kem. Thích phiêu lưu mạo hiểm nhưng vì Covid mà vô tình trở thành người chia sẻ nội dung lập trình :))
follow me :
Bài viết liên quan
Tự học Dart: Các Dart Operators (toán tử) bạn cần biết
Sep 02, 2023 • 18 min read
Flutter cơ bản: Điều cần biết khi lập trình ứng dụng đầu tiên
Aug 23, 2023 • 13 min read
Dart là gì? Giới thiệu cơ bản về ngôn ngữ lập trình Dart
Aug 21, 2023 • 11 min read
Flutter là gì? Vì sao nên học công cụ lập trình Flutter?
Aug 19, 2023 • 11 min read
Flutter cơ bản: Widget Tree, Element Tree & Render Tree
Aug 19, 2023 • 10 min read
So sánh StatelessWidget và StatefulWidget
Jul 17, 2022 • 12 min read