
(artikel asli diterbitkan dalam Bahasa Inggris di Media )
Flutter menyediakan kerangka kerja responsif modern, sejumlah besar widget dan alat. Namun, sayangnya, dokumentasi tersebut tidak seperti panduan arsitektur aplikasi Android yang direkomendasikan .
Tidak ada arsitektur universal yang ideal yang dapat memenuhi persyaratan teknis apa pun, tetapi mari kita akui bahwa sebagian besar aplikasi seluler tempat kita bekerja memiliki fungsi sebagai berikut:
- Meminta dan mengunduh data.
- Transformasi dan persiapan data untuk pengguna.
- Menulis dan membaca data dari database atau sistem file.
Mengingat semua ini, saya membuat aplikasi demo yang memecahkan masalah yang sama menggunakan pendekatan arsitektur yang berbeda.
Awalnya, pengguna diperlihatkan layar dengan tombol "Muat data pengguna" yang terletak di tengah. Ketika pengguna mengklik tombol, pemuatan data tidak sinkron terjadi, dan tombol itu diganti dengan indikator pemuatan. Ketika pengunduhan data selesai, indikator pengunduhan digantikan oleh data.
Jadi mari kita mulai.

Data
Untuk menyederhanakan tugas, saya membuat kelas Repository
, yang berisi metode getUser()
. Metode ini mensimulasikan pemuatan data yang tidak sinkron dari jaringan dan mengembalikan Future<User>
.
Jika Anda tidak terbiasa dengan Futures dan pemrograman asinkron di Dart, kita dapat membaca lebih lanjut di sini dan membaca dokumentasi kelas Future .
class Repository { Future<User> getUser() async { await Future.delayed(Duration(seconds: 2)); return User(name: 'John', surname: 'Smith'); } }
class User { User({ @required this.name, @required this.surname, }); final String name; final String surname; }
Vanilla
Ayo kembangkan aplikasi, seperti yang akan dilakukan pengembang jika dia membaca dokumentasi Flutter di situs web resmi.
Buka layar VanillaScreen
menggunakan Navigator
Navigator.push( context, MaterialPageRoute( builder: (context) => VanillaScreen(_repository), ), );
Karena keadaan widget dapat berubah beberapa kali selama siklus hidupnya, kami perlu mewarisi dari StatefulWidget
. Untuk mengimplementasikan widget stateful Anda, Anda juga memerlukan kelas State
. Bidang bool _isLoading
dan User _user
di kelas _VanillaScreenState
mewakili status widget. Kedua bidang diinisialisasi sebelum metode build(BuildContext context)
dipanggil untuk pertama kalinya.
class VanillaScreen extends StatefulWidget { VanillaScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _VanillaScreenState(); } class _VanillaScreenState extends State<VanillaScreen> { bool _isLoading = false; User _user; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Vanilla'), ), body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), ), ); } Widget _buildBody() { if (_user != null) { return _buildContent(); } else { return _buildInit(); } } Widget _buildInit() { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { setState(() { _isLoading = true; }); widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); }); }, ), ); } Widget _buildContent() { return Center( child: Text('Hello ${_user.name} ${_user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } }
Setelah objek status widget dibuat, metode build(BuildContext context)
dipanggil untuk membangun UI. Semua keputusan tentang widget mana yang harus ditampilkan di layar dibuat benar dalam kode deklarasi UI.
body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), )
Untuk menampilkan indikator progres, ketika pengguna mengklik tombol "Muat detail pengguna", kami melakukan hal berikut.
setState(() { _isLoading = true; });
Dari dokumentasi (terjemahan):
Panggilan ke metode setState () memberi tahu kerangka kerja bahwa keadaan internal objek ini telah berubah dan dapat memengaruhi antarmuka pengguna di subtree. Ini adalah alasan framework memanggil metode build pada objek state ini.
Ini berarti bahwa setelah memanggil metode setState()
, framework akan memanggil metode build(BuildContext context)
lagi, yang akan membuat ulang seluruh pohon widget . Karena nilai bidang _isLoading
berubah menjadi true
, alih-alih metode _buildBody()
, metode _buildLoading()
akan dipanggil dan indikator progres akan ditampilkan di layar.
Hal yang persis sama akan terjadi ketika kita mendapatkan panggilan balik dari getUser()
dan memanggil metode
setState()
untuk menetapkan nilai baru ke bidang _isLoading
dan _user
.
widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); });
Pro
- Ambang entri rendah.
- Tidak diperlukan perpustakaan pihak ketiga.
Cons
- Ketika keadaan widget berubah, pohon widget sepenuhnya dibuat ulang setiap kali.
- Melanggar prinsip tanggung jawab tunggal. Widget ini bertanggung jawab tidak hanya untuk membuat UI, tetapi juga untuk memuat data, logika bisnis, dan manajemen negara.
- Keputusan tentang cara menampilkan keadaan saat ini dibuat langsung dalam kode UI. Jika keadaan menjadi lebih kompleks, maka keterbacaan kode akan sangat berkurang.
Model tertutup
Scoped Model adalah perpustakaan pihak ketiga . Begini cara pengembang menggambarkannya:
Seperangkat utilitas yang memungkinkan Anda mentransfer Model Data widget leluhur ke semua turunannya. Selain itu, ketika data model berubah, semua keturunan yang menggunakan model akan dibuat kembali. Perpustakaan ini awalnya diambil dari kode proyek Fuchsia .
Mari kita membuat layar yang sama seperti pada contoh sebelumnya, tetapi menggunakan Model Scoped. Pertama, kita perlu menambahkan perpustakaan Scoped Model ke proyek. Tambahkan dependensi scoped_model
ke file scoped_model
di bagian dependencies
.
scoped_model: ^1.0.1
Mari kita lihat kode UserModelScreen
dan bandingkan dengan contoh sebelumnya, di mana kita tidak menggunakan Scoped Model. Untuk membuat model kami dapat diakses oleh keturunan widget, kita perlu membungkus widget dan model di ScopedModel
.
class UserModelScreen extends StatefulWidget { UserModelScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _UserModelScreenState(); } class _UserModelScreenState extends State<UserModelScreen> { UserModel _userModel; @override void initState() { _userModel = UserModel(widget._repository); super.initState(); } @override Widget build(BuildContext context) { return ScopedModel( model: _userModel, child: Scaffold( appBar: AppBar( title: const Text('Scoped model'), ), body: SafeArea( child: ScopedModelDescendant<UserModel>( builder: (context, child, model) { if (model.isLoading) { return _buildLoading(); } else { if (model.user != null) { return _buildContent(model); } else { return _buildInit(model); } } }, ), ), ), ); } Widget _buildInit(UserModel userModel) { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { userModel.loadUserData(); }, ), ); } Widget _buildContent(UserModel userModel) { return Center( child: Text('Hello ${userModel.user.name} ${userModel.user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } }
Pada contoh sebelumnya, setiap kali widget berubah status, pohon widget sepenuhnya dibuat ulang. Tetapi apakah kita benar-benar perlu membuat ulang seluruh pohon widget (layar penuh)? Misalnya, AppBar tidak berubah sama sekali, dan tidak ada gunanya untuk membuatnya kembali. Idealnya, Anda harus membuat ulang hanya widget yang harus berubah sesuai dengan perubahan status. Dan Model Scoped dapat membantu kami memecahkan masalah ini.
ScopedModelDescendant<UserModel>
digunakan untuk menemukan UserModel
di pohon widget. Ini akan dibuat ulang secara otomatis setiap kali UserModel
memberitahukan bahwa telah ada perubahan.
Peningkatan lainnya adalah bahwa UserModelScreen
tidak lagi bertanggung jawab atas manajemen negara, logika bisnis, dan pemuatan data.
Mari kita lihat kode untuk kelas UserModel
.
class UserModel extends Model { UserModel(this._repository); final Repository _repository; bool _isLoading = false; User _user; User get user => _user; bool get isLoading => _isLoading; void loadUserData() { _isLoading = true; notifyListeners(); _repository.getUser().then((user) { _user = user; _isLoading = false; notifyListeners(); }); } static UserModel of(BuildContext context) => ScopedModel.of<UserModel>(context); }
UserModel
sekarang berisi dan mengelola status. Untuk memberi tahu pendengar (dan membuat ulang keturunan) bahwa perubahan telah terjadi, notifyListeners()
harus dipanggil.
Pro
- Manajemen negara, logika bisnis, dan pemuatan data terpisah dari kode UI.
- Ambang entri rendah.
Cons
- Ketergantungan perpustakaan pihak ketiga.
- Jika model menjadi cukup kompleks, akan sulit untuk melacak kapan benar-benar perlu memanggil
notifyListeners()
untuk menghindari rekreasi yang tidak perlu.
BLoC
BLoC (Komponen Logika Cnis) adalah pola yang direkomendasikan oleh pengembang dari Google. Streaming digunakan untuk mengelola keadaan dan untuk memberi tahu tentang perubahan keadaan.
Untuk pengembang Android: Anda dapat membayangkan bahwa Bloc
adalah ViewModel
, dan StreamController
adalah LiveData
. Ini akan membuat kode berikut mudah dimengerti, karena Anda sudah terbiasa dengan prinsip-prinsip dasar.
class UserBloc { UserBloc(this._repository); final Repository _repository; final _userStreamController = StreamController<UserState>(); Stream<UserState> get user => _userStreamController.stream; void loadUserData() { _userStreamController.sink.add(UserState._userLoading()); _repository.getUser().then((user) { _userStreamController.sink.add(UserState._userData(user)); }); } void dispose() { _userStreamController.close(); } } class UserState { UserState(); factory UserState._userData(User user) = UserDataState; factory UserState._userLoading() = UserLoadingState; } class UserInitState extends UserState {} class UserLoadingState extends UserState {} class UserDataState extends UserState { UserDataState(this.user); final User user; }
Kode menunjukkan bahwa tidak perlu lagi memanggil metode tambahan untuk memberi tahu tentang perubahan status.
Saya membuat 3 kelas untuk mewakili kemungkinan status:
UserInitState
untuk keadaan saat pengguna membuka layar dengan tombol di tengah.
UserLoadingState
untuk keadaan saat indikator memuat ditampilkan saat data dimuat.
UserDataState
untuk keadaan saat data sudah dimuat dan ditampilkan di layar.
Status lewat dengan cara ini memungkinkan kita untuk sepenuhnya menghilangkan logika dalam kode UI. Dalam contoh Model yang Dicakup, kami masih memeriksa apakah nilai bidang _isLoading
true
atau false
untuk menentukan widget mana yang akan dibuat. Dalam kasus BLoC, kami meneruskan status baru ke streaming, dan satu-satunya tugas widget UserBlocScreen
membuat UI untuk kondisi saat ini.
class UserBlocScreen extends StatefulWidget { UserBlocScreen(this._repository); final Repository _repository; @override State<StatefulWidget> createState() => _UserBlocScreenState(); } class _UserBlocScreenState extends State<UserBlocScreen> { UserBloc _userBloc; @override void initState() { _userBloc = UserBloc(widget._repository); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Bloc'), ), body: SafeArea( child: StreamBuilder<UserState>( stream: _userBloc.user, initialData: UserInitState(), builder: (context, snapshot) { if (snapshot.data is UserInitState) { return _buildInit(); } if (snapshot.data is UserDataState) { UserDataState state = snapshot.data; return _buildContent(state.user); } if (snapshot.data is UserLoadingState) { return _buildLoading(); } }, ), ), ); } Widget _buildInit() { return Center( child: RaisedButton( child: const Text('Load user data'), onPressed: () { _userBloc.loadUserData(); }, ), ); } Widget _buildContent(User user) { return Center( child: Text('Hello ${user.name} ${user.surname}'), ); } Widget _buildLoading() { return const Center( child: CircularProgressIndicator(), ); } @override void dispose() { _userBloc.dispose(); super.dispose(); } }
Kode untuk widget UserBlocScreen
bahkan lebih sederhana dari pada contoh sebelumnya. Untuk mendengarkan perubahan keadaan, StreamBuilder digunakan. StreamBuilder
adalah StatefulWidget
yang membuat dirinya sesuai dengan nilai terakhir (Snapshot) dari aliran ( Stream ).
Pro
- Tidak diperlukan perpustakaan pihak ketiga.
- Logika bisnis, manajemen negara, dan pemuatan data terpisah dari kode UI.
- Reaktivitas Tidak perlu memanggil metode tambahan, seperti pada contoh dengan Scoped Model notifyListeners
notifyListeners()
.
Cons
- Ambang entri sedikit lebih tinggi. Butuh pengalaman dengan stream atau rxdart.
Tautan
Anda dapat membaca kode lengkap dengan mengunduhnya dari repositori github saya .
Artikel asli diterbitkan di Medium