
Prinsip umum
Flutter adalah kerangka kerja reaktif, dan bagi pengembang yang berspesialisasi dalam pengembangan asli, filosofinya mungkin tidak biasa. Karena itu, kami mulai dengan ulasan singkat.
Antarmuka pengguna pada Flutter, seperti dalam kebanyakan kerangka kerja modern, terdiri dari pohon komponen (widget). Ketika suatu komponen berubah, ini dan semua komponen turunannya dirender kembali (dengan optimisasi internal, yang dijelaskan di bawah). Ketika tampilan berubah secara global (misalnya, memutar layar), seluruh pohon widget digambar ulang.
Pendekatan ini mungkin tampak tidak efektif, tetapi pada kenyataannya memberikan kontrol programmer atas kecepatan kerja. Jika Anda memperbarui antarmuka pada tingkat tertinggi tanpa perlu, semuanya akan bekerja lambat, tetapi dengan tata letak widget yang benar, aplikasi pada Flutter bisa sangat cepat.
Flutter memiliki dua jenis widget - Stateless dan Stateful. Yang pertama (analog dengan Komponen Murni dalam Bereaksi) tidak memiliki keadaan dan sepenuhnya dijelaskan oleh parameter mereka. Jika kondisi tampilan tidak berubah (misalnya, ukuran area di mana widget harus ditampilkan) dan parameternya, sistem menggunakan kembali representasi visual widget yang dibuat sebelumnya, sehingga menggunakan widget Stateless memiliki efek yang baik pada kinerja. Pada saat yang sama, bagaimanapun, setiap kali widget digambar ulang, sebuah objek baru secara resmi dibuat dan konstruktor diluncurkan.
Widget stateful mempertahankan beberapa kondisi antara rendering. Untuk melakukan ini, mereka dijelaskan oleh dua kelas. Kelas pertama, widget itu sendiri, menjelaskan objek yang dibuat selama setiap rendering. Kelas kedua menjelaskan keadaan widget dan objeknya ditransfer ke objek widget yang dibuat. Widget stateful state adalah sumber utama redrawing antarmuka. Untuk melakukan ini, Anda perlu mengubah propertinya di dalam panggilan ke metode SetState. Dengan demikian, tidak seperti banyak kerangka kerja lainnya, Flutter tidak memiliki pelacakan status implisit - perubahan apa pun pada properti widget di luar metode SetState tidak mengarah pada menggambar ulang antarmuka.
Sekarang, setelah menjelaskan dasar-dasarnya, Anda bisa mulai dengan aplikasi sederhana yang menggunakan widget Stateless dan Stateful:
Aplikasi dasarimport 'dart:math'; import 'package:flutter/material.dart'; void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', theme: new ThemeData( primarySwatch: Colors.blue, ), home: Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new MyHomePage(), ), ); } } class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { Random rand = Random(); @override Widget build(BuildContext context) { return new ListView.builder(itemBuilder: (BuildContext context, int index) { return Text('Random number ${rand.nextInt(100)}',); }); } }
Contoh lengkap
Jika Anda membutuhkan kondisi yang lebih ulet
Mari kita lanjutkan. Keadaan widget tetap dipertahankan di antara antarmuka menggambar ulang, tetapi hanya selama widget diperlukan, mis. benar-benar terletak di layar. Mari kita lakukan eksperimen sederhana - letakkan daftar kami di tab:
Aplikasi tab class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { Random rand = Random(); TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [ new ListView.builder(itemBuilder: (BuildContext context, int index) { return Text('Random number ${rand.nextInt(100)}',); }), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } }
Contoh lengkap
Saat memulai, Anda dapat melihat bahwa saat berpindah antar tab, status dihapus (metode buang () dipanggil), saat dikembalikan, dibuat lagi (metode initState ()). Ini masuk akal, karena menyimpan status widget yang tidak dapat ditampilkan akan menghabiskan sumber daya sistem. Dalam hal keadaan widget harus selamat dari penyembunyian lengkapnya, beberapa pendekatan mungkin:
Pertama, Anda dapat menggunakan objek terpisah (ViewModel) untuk menyimpan status. Dart di tingkat bahasa mendukung konstruktor pabrik yang dapat digunakan untuk membuat pabrik dan lajang yang menyimpan data yang diperlukan.
Saya lebih suka pendekatan ini, karena Ini memungkinkan Anda untuk mengisolasi logika bisnis dari antarmuka pengguna. Ini terutama benar karena Flutter Release Preview 2 menambahkan kemampuan untuk membuat antarmuka pixel-sempurna untuk iOS, tetapi Anda perlu melakukan ini, tentu saja, pada widget yang sesuai.
Kedua, adalah mungkin untuk menggunakan pendekatan peningkatan-negara, yang akrab dengan programmer Bereaksi, ketika data disimpan dalam komponen yang terletak di hulu. Karena Flutter menggambar ulang antarmuka hanya ketika metode setState () dipanggil, data ini dapat diubah dan digunakan tanpa rendering. Pendekatan ini agak lebih kompleks dan meningkatkan konektivitas widget dalam struktur, tetapi memungkinkan Anda untuk menentukan tingkat penyimpanan data secara langsung.
Akhirnya, ada perpustakaan penyimpanan negara seperti flutter_redux .
Untuk mempermudah, kami menggunakan pendekatan pertama. Mari kita buat kelas ListData yang terpisah, singleton, yang menyimpan nilai untuk daftar kita. Saat menampilkan, kami akan menggunakan kelas ini.
Aplikasi Pemulihan Data Tab class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [ new ListView.builder(itemBuilder: ListData().build), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } class ListData { static ListData _instance = ListData._internal(); ListData._internal(); factory ListData() { return _instance; } Random _rand = Random(); Map<int, int> _values = new Map(); Widget build (BuildContext context, int index) { if (!_values.containsKey(index)) { _values[index] = _rand.nextInt(100); } return Text('Random number ${_values[index]}',); } }
Contoh lengkap
Menyimpan posisi gulir
Jika Anda menggulir ke bawah daftar dari contoh sebelumnya, lalu beralih di antara tab, mudah untuk melihat bahwa posisi gulir tidak disimpan. Ini logis, karena ini tidak disimpan di kelas ListData kami, dan status widget itu sendiri tidak bertahan beralih antar tab. Kami menerapkan penyimpanan keadaan gulir secara manual, tetapi untuk bersenang-senang kami akan menambahkannya bukan ke kelas yang terpisah dan tidak ke ListData, tetapi ke status tingkat yang lebih tinggi untuk menunjukkan cara bekerja dengannya.
Perhatikan widget ScrollController dan NotificationListener (serta DefaultTabController yang sebelumnya digunakan). Konsep widget yang tidak memiliki tampilan sendiri harus familier bagi pengembang yang bekerja dengan React / Redux - komponen kontainer secara aktif digunakan dalam bundel ini. Di Flutter, widget non-display biasanya digunakan untuk menambah fungsionalitas ke widget child. Ini memungkinkan Anda untuk membiarkan widget visual itu sendiri tetap ringan dan tidak memroses acara sistem yang tidak diperlukan.
Kode ini didasarkan pada solusi yang diusulkan oleh Marcin SzaΕek di Stakoverflow ( https://stackoverflow.com/questions/45341721/flutter-listview-inside-on--tabbarview-loses-its-scroll-position ). Rencananya adalah sebagai berikut:
- Tambahkan ScrollController ke daftar untuk bekerja dengan posisi gulir.
- Tambahkan NotificationListener ke daftar untuk melewati status gulir.
- Kami menyimpan posisi gulir di _MyHomePageState (yang satu tingkat di atas tab) dan mengaitkannya dengan daftar gulir.
Aplikasi dengan menyimpan posisi gulir class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { double listViewOffset=0.0; TabController _tabController; final List<Tab> myTabs = <Tab>[ new Tab(text: 'FIRST'), new Tab(text: 'SECOND'), ]; @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Sample app'), ), body: new TabBarView( controller: _tabController, children: [new ListTab( getOffsetMethod: () => listViewOffset, setOffsetMethod: (offset) => this.listViewOffset = offset, ), Text('Second tab'), ],), bottomNavigationBar: new TabBar( controller: _tabController, tabs: myTabs, labelColor: Colors.blue, ), ); } } class ListTab extends StatefulWidget { ListTab({Key key, this.getOffsetMethod, this.setOffsetMethod}) : super(key: key); final GetOffsetMethod getOffsetMethod; final SetOffsetMethod setOffsetMethod; @override _ListTabState createState() => _ListTabState(); } class _ListTabState extends State<ListTab> { ScrollController scrollController; @override void initState() { super.initState(); //Init scrolling to preserve it scrollController = new ScrollController( initialScrollOffset: widget.getOffsetMethod() ); } @override Widget build(BuildContext context) { return NotificationListener( child: new ListView.builder( controller: scrollController, itemBuilder: ListData().build, ), onNotification: (notification) { if (notification is ScrollNotification) { widget.setOffsetMethod(notification.metrics.pixels); } }, ); } }
Contoh lengkap
Mengalami shutdown aplikasi
Menyimpan informasi selama durasi aplikasi itu baik, tetapi seringkali Anda ingin menyimpannya di antara sesi, terutama mengingat kebiasaan sistem operasi untuk menutup aplikasi latar belakang ketika tidak ada cukup memori. Opsi utama untuk penyimpanan data persisten di Flutter adalah:
- Preferensi bersama ( https://pub.dartlang.org/packages/shared_preferences ) adalah pembungkus di sekitar NSUserDefaults (di iOS) dan SharedPreferences (di Android) dan memungkinkan Anda untuk menyimpan sejumlah kecil pasangan nilai kunci. Bagus untuk menyimpan pengaturan.
- sqflite ( https://pub.dartlang.org/packages/sqflite ) adalah plugin untuk bekerja dengan SQLite (dengan beberapa batasan). Mendukung kueri dan pembantu tingkat rendah. Selain itu, dengan analogi dengan Kamar, ini memungkinkan Anda untuk bekerja dengan versi skema database dan mengatur kode untuk memperbarui skema ketika memperbarui aplikasi.
- Cloud Firestore ( https://pub.dartlang.org/packages/cloud_firestore ) adalah bagian dari keluarga plugin FireBase resmi.
Untuk mendemonstrasikan, kami akan menyimpan status gulir di Preferensi bersama. Untuk melakukan ini, tambahkan pemulihan posisi gulir saat menginisialisasi keadaan _MyHomePageState dan simpan saat menggulir.
Di sini kita perlu memikirkan model Flutter / Dart asinkron, karena semua layanan eksternal bekerja pada panggilan asinkron. Prinsip operasi model ini mirip dengan node.js - ada satu utas utama eksekusi (utas), yang terganggu oleh panggilan asinkron. Pada setiap interupsi berikutnya (dan UI membuatnya terus-menerus), hasil dari operasi asinkron yang selesai diproses. Pada saat yang sama, dimungkinkan untuk menjalankan perhitungan berat di utas latar belakang (melalui fungsi komputasi).
Jadi, menulis dan membaca di SharedPreferences dilakukan secara asinkron (walaupun perpustakaan memungkinkan pembacaan secara sinkron dari cache). Untuk mulai dengan, kita akan berurusan dengan membaca. Pendekatan standar untuk pengambilan data asinkron terlihat seperti ini - mulai proses asinkron, dan setelah selesai, jalankan SetState, tulis nilai yang diterima. Akibatnya, antarmuka pengguna akan diperbarui menggunakan data yang diterima. Namun, dalam hal ini, kami tidak bekerja dengan data, tetapi dengan posisi gulir. Kita tidak perlu memperbarui antarmuka, kita hanya perlu memanggil metode jumpTo di ScrollController. Masalahnya adalah bahwa hasil pemrosesan permintaan asinkron dapat kembali kapan saja dan tidak harus apa dan di mana gulir. Agar dijamin untuk melakukan operasi pada antarmuka sepenuhnya diinisialisasi, kita perlu ... masih gulir di dalam setState.
Kami mendapatkan sesuatu seperti kode ini:
Pengaturan negara @override void initState() { super.initState(); //Init scrolling to preserve it scrollController = new ScrollController( initialScrollOffset: widget.getOffsetMethod() ); _restoreState().then((double value) => scrollController.jumpTo(value)); } Future<double> _restoreState() async { SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getDouble('listViewOffset'); } void setScroll(double value) { setState(() { scrollController.jumpTo(value); }); }
Dengan catatan itu, semuanya jadi lebih menarik. Faktanya adalah bahwa dalam proses pengguliran, peristiwa yang melaporkan hal ini selalu terjadi. Memulai perekaman asinkron setiap kali nilai diubah dapat menyebabkan kesalahan aplikasi. Kami hanya perlu memproses acara terakhir dari rantai. Dalam hal pemrograman reaktif, ini disebut debounce dan kami akan menggunakannya. Dart mendukung fitur-fitur utama pemrograman reaktif melalui aliran data, jadi kita perlu membuat aliran dari pembaruan posisi gulir dan berlangganan, mengubahnya dengan menggunakan Debounce. Untuk mengonversi, kita memerlukan pustaka stream_transformasi . Sebagai pendekatan alternatif, Anda dapat menggunakan RxDart dan bekerja dengan ReactiveX.
Ternyata kode berikut:
Catatan Status StreamSubscription _stream; StreamController<double> _controller = new StreamController<double>.broadcast(); @override void initState() { super.initState(); _tabController = new TabController(vsync: this, length: myTabs.length); _stream = _controller.stream.transform(debounce(new Duration(milliseconds: 500))).listen(_saveState); } void _saveState(double value) async { SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setDouble('listViewOffset', value); }
Contoh lengkap