
(awalnya diterbitkan di Medium )
Flutter menyediakan kerangka kerja gaya reaksi modern, koleksi dan tool widget kaya, tetapi tidak ada yang serupa dengan panduan Android untuk arsitektur aplikasi .
Memang, tidak ada arsitektur utama yang akan memenuhi semua persyaratan yang mungkin, namun mari kita hadapi kenyataan bahwa sebagian besar aplikasi seluler yang sedang kita kerjakan memiliki setidaknya beberapa fungsi berikut:
- Meminta / mengunggah data dari / ke jaringan.
- Memetakan, mengubah, menyiapkan data, dan menyajikannya kepada pengguna.
- Masukkan / dapatkan data ke / dari database.
Dengan mempertimbangkan hal ini, saya telah membuat contoh aplikasi yang memecahkan masalah yang sama persis menggunakan tiga pendekatan berbeda untuk arsitektur.
Pengguna disajikan dengan tombol "Muat data pengguna" di tengah layar. Ketika pengguna mengklik tombol pemuatan data tidak sinkron dipicu dan tombol diganti dengan indikator pemuatan. Setelah data dimuat, indikator memuat diganti dengan data.
Mari kita mulai.

Data
Untuk tujuan kesederhanaan, saya telah membuat kelas Repository
yang berisi metode getUser()
yang mengemulasi panggilan jaringan asinkron dan mengembalikan objek Future<User>
dengan nilai yang di-hardcoded.
Jika Anda tidak terbiasa dengan Futures dan pemrograman asinkron di Dart, Anda dapat mempelajari lebih lanjut dengan mengikuti tutorial ini dan membaca dokumen .
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
Mari kita membangun aplikasi dengan cara yang akan dilakukan kebanyakan pengembang setelah membaca dokumentasi Flutter resmi.
Menavigasi ke layar VanillaScreen
menggunakan Navigator
Navigator.push( context, MaterialPageRoute( builder: (context) => VanillaScreen(_repository), ), );
Karena keadaan widget dapat berubah beberapa kali selama masa pakai widget, kita harus memperpanjang StatefulWidget
. Menerapkan widget stateful juga mengharuskan memiliki kelas State
. Fields bool _isLoading
dan User _user
di kelas _VanillaScreenState
mewakili status widget. Kedua bidang diinisialisasi sebelum metode build(BuildContext context)
dipanggil.
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(), ); } }
Ketika objek keadaan widget dibuat metode build(BuildContext context)
dipanggil untuk membangun UI. Semua keputusan tentang widget yang harus dibangun untuk mewakili keadaan saat ini dibuat dalam kode deklarasi UI.
body: SafeArea( child: _isLoading ? _buildLoading() : _buildBody(), )
Untuk menampilkan indikator kemajuan ketika pengguna mengklik tombol "Muat rincian pengguna" kami lakukan mengikuti.
setState(() { _isLoading = true; });
Memanggil setState () memberi tahu kerangka kerja bahwa keadaan internal objek ini telah berubah dengan cara yang mungkin berdampak pada antarmuka pengguna di subtree ini, yang menyebabkan kerangka kerja menjadwalkan pembangunan untuk objek Negara ini.
Itu berarti bahwa setelah memanggil metode setState()
metode build(BuildContext context)
dipanggil oleh framework lagi dan seluruh pohon widget dibangun kembali . Karena _isLoading
sekarang disetel ke metode true
_buildLoading()
disebut sebagai ganti _buildBody()
dan indikator pemuatan ditampilkan di layar. Hal yang persis sama terjadi ketika kami menangani panggilan balik dari getUser()
dan memanggil setState()
untuk menetapkan kembali bidang _user
dan _user
.
widget._repository.getUser().then((user) { setState(() { _user = user; _isLoading = false; }); });
Pro
- Mudah dipelajari dan dipahami.
- Tidak ada perpustakaan pihak ketiga yang diperlukan.
Cons
- Keseluruhan pohon widget dibangun kembali setiap kali kondisi widget berubah.
- Itu melanggar prinsip tanggung jawab tunggal. Widget tidak hanya bertanggung jawab untuk membangun UI, tetapi juga bertanggung jawab untuk memuat data, logika bisnis, dan manajemen negara.
- Keputusan tentang bagaimana keadaan saat ini harus diwakili dibuat dalam kode deklarasi UI. Jika kita akan memiliki kode negara sedikit lebih mudah dibaca akan berkurang.
Model tertutup
Scoped Model adalah paket pihak ketiga yang tidak termasuk ke dalam kerangka Flutter. Beginilah para pengembang Scoped Model menggambarkannya:
Satu set utilitas yang memungkinkan Anda untuk dengan mudah meneruskan Model data dari Widget induk ke turunannya. Selain itu, ini juga membangun kembali semua anak yang menggunakan model ketika model diperbarui. Perpustakaan ini awalnya diekstraksi dari basis kode Fuchsia.
Mari kita membangun layar yang sama menggunakan Scoped Model. Pertama, kita perlu menginstal paket Scoped Model dengan menambahkan dependensi pubspec.yaml
ke pubspec.yaml
bawah bagian dependencies
.
scoped_model: ^1.0.1
Mari kita lihat widget UserModelScreen
dan bandingkan dengan contoh sebelumnya yang dibuat tanpa menggunakan Scoped Model. Mari kita lihat widget UserModelScreen dan bandingkan dengan contoh sebelumnya yang dibuat tanpa menggunakan Scoped Model. Karena kami ingin membuat model kami tersedia untuk semua keturunan widget, kami harus membungkusnya dengan ScopedModel generik dan menyediakan widget dan model.
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, seluruh pohon widget dibangun kembali ketika status widget berubah. Tetapi apakah kita benar-benar harus membangun kembali seluruh layar? Misalnya AppBar tidak boleh berubah sama sekali sehingga tidak ada gunanya membangunnya kembali. Idealnya, kita harus membangun kembali hanya widget yang diperbarui. Scoped Model dapat membantu kita menyelesaikannya.
ScopedModelDescendant<UserModel>
digunakan untuk menemukan UserModel
di pohon Widget. Ini akan secara otomatis dibangun kembali setiap kali UserModel
memberitahukan bahwa perubahan telah terjadi.
Peningkatan lainnya adalah bahwa UserModelScreen
tidak lagi bertanggung jawab atas manajemen negara dan logika bisnis.
Mari kita lihat kode 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); }
Sekarang UserModel
memegang dan mengelola negara. Untuk memberi tahu pendengar (dan membangun kembali keturunan) bahwa perubahan tersebut terjadi, notifyListeners()
harus dipanggil.
Pro
- Logika bisnis, manajemen negara dan pemisahan kode UI.
- Mudah dipelajari.
Cons
- Membutuhkan perpustakaan pihak ketiga.
- Karena model semakin kompleks, sulit untuk melacak ketika Anda harus memanggil
notifyListeners()
.
BLoC
BLoC (Komponen Keberhasilan Logika) adalah pola yang direkomendasikan oleh pengembang Google. Ini memanfaatkan fungsionalitas stream untuk mengelola dan menyebarkan perubahan status.
Untuk pengembang Android: Anda dapat menganggap objek Bloc
sebagai ViewModel
dan StreamController
sebagai LiveData
. Ini akan membuat kode berikut ini sangat mudah karena Anda sudah terbiasa dengan konsep-konsepnya.
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; }
Tidak ada metode panggilan tambahan yang diperlukan untuk memberi tahu pelanggan ketika keadaan berubah.
Saya telah membuat 3 kelas untuk mewakili kemungkinan kondisi layar:
UserInitState
untuk keadaan, saat pengguna membuka layar dengan tombol di tengah.
UserLoadingState
untuk keadaan, saat memuat indikator ditampilkan saat data sedang dimuat.
UserDataState
untuk keadaan, saat data dimuat dan ditampilkan di layar.
Menyebarkan perubahan status dengan cara ini memungkinkan kita untuk menyingkirkan semua logika dalam kode deklarasi UI. Sebagai contoh dengan Scoped Model kami masih memeriksa apakah _isLoading
true
dalam kode deklarasi UI untuk memutuskan widget mana yang harus kami render. Dalam hal BLoC, kami menyebarkan status layar dan satu-satunya tanggung jawab widget UserBlocScreen
adalah merender UI untuk keadaan 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 UserBlocScreen
menjadi lebih sederhana dibandingkan dengan contoh sebelumnya. Untuk mendengarkan perubahan perubahan negara, kami menggunakan StreamBuilder . StreamBuilder
adalah StatefulWidget
yang membangun dirinya berdasarkan snapshot terbaru dari interaksi dengan Stream .
Pro
Tidak diperlukan perpustakaan pihak ketiga.
Logika bisnis, manajemen negara dan pemisahan logika UI.
Itu reaktif. Tidak ada panggilan tambahan yang diperlukan seperti dalam kasus NotifyListeners Model Scoped notifyListeners()
.
Cons
Diperlukan pengalaman bekerja dengan stream atau rxdart.
Tautan
Anda dapat memeriksa kode sumber dari contoh di atas dari repo github ini.
Awalnya artikel dipublikasikan di Medium