Berdebar. BlOC, Penyedia, async - Arsitektur Rak

pengantar


Ketika Anda mencoba menulis aplikasi, hal pertama yang Anda temui adalah bagaimana mengatur arsitektur aplikasi. Dan ketika datang ke Flutter, kepala dapat sepenuhnya berputar dari apa yang Google berikan - Vanilla, Scoped Model, BLoC, MVP, MVC, MVVM, MVI, dll. Misalkan Anda memutuskan untuk menggunakan cara yang paling modis (yang disarankan Google pada tahun 2018) dan menggunakan BLOC. Apa itu? Bagaimana cara menggunakannya? Atau mungkin Redux atau RxDart? - meskipun berhenti adalah tentang "yang lain" ... Tapi tetap saja, apa yang selanjutnya? Perpustakaan apa yang harus dihubungkan? Blok, Flutter_bloc, bloc_pattern, dll?

Sejumlah opsi dan alat arsitektur untuk implementasinya benar-benar dapat menunda tahap pemilihan untuk waktu yang lama.

Untuk siapa artikelnya


Artikel ini terutama bermanfaat bagi mereka yang baru mulai mempelajari Flutter dan tidak tahu harus mulai dari mana. Saya akan menunjukkan salah satu opsi untuk mengimplementasikan aplikasi pada Flutter. Ini akan memungkinkan Anda untuk "merasakan" Flutter, dan kemudian memutuskan sendiri bagaimana dan menggunakan mana Anda akan menulis aplikasi Anda.

Pola dan alat. Singkat dan sederhana


Jadi, mari kita mulai. Hal pertama yang perlu diperhatikan adalah bahwa ada arsitektur aplikasi (pola, templat, beberapa konsep konstruksi) - ini persis sama: BLoC, MVP, MVC, MVVM, MVI, dll. Banyak dari arsitektur ini digunakan tidak hanya dalam Flutter, tetapi juga dalam bahasa pemrograman lain. Pertanyaan - apa yang harus dipilih? Menurut pendapat saya, Anda perlu memilih apa yang Anda ketahui dengan baik, tetapi hanya jika ini menyiratkan reaktivitas dan pemisahan yang ketat dari logika bisnis dari antarmuka (ya, ya - "mobil dapat warna apa saja jika hitam"). Adapun pemisahan antarmuka dan logika bisnis, saya pikir tidak perlu dijelaskan, tetapi untuk reaktivitas - coba, jika Anda belum mencoba - pada akhirnya itu benar-benar sangat nyaman dan "indah". Jika Anda tidak dapat memilihnya sendiri, biarkan kami mengizinkannya dilakukan untuk kami oleh bukan orang paling bodoh dari Google - BLOC. Kami menemukan arsitekturnya.

Sekarang alat - ada perpustakaan yang sudah jadi - Bloc, Flutter_bloc, bloc_pattern - mana yang lebih baik? Saya tidak tahu - semua orang baik. Anda dapat memilih dan membandingkan untuk waktu yang lama, tetapi di sini lagi, seperti di pasukan - lebih baik untuk membuat keputusan yang salah untuk saat ini daripada tidak membuat. Dan untuk saat ini, saya sarankan kembali setelah mod dan menggunakan Penyedia (apa yang direkomendasikan orang yang sama menggunakan pada 2019).

Semua ini akan memungkinkan kami untuk membuat blok global dan lokal, sesuai kebutuhan. Banyak yang telah ditulis tentang arsitektur BLoC (yaitu, pola, bukan perpustakaan), saya pikir Anda tidak harus memikirkannya lagi secara detail. Saya perhatikan hanya satu poin dalam artikel ini, bukan BLoC klasik yang akan digunakan, tetapi sedikit dimodifikasi - dalam tindakan BLoC (peristiwa) tidak akan dikirim melalui Sinks, tetapi fungsi BLoC akan dipanggil. Hanya saja saat ini saya tidak melihat manfaat menggunakan Sinks - dan karena mereka tidak ada di sana, lalu mengapa menyulitkan hidup Anda?

Asynchrony dan Komputasi Paralel di Dart


Perlu juga sedikit klarifikasi tentang konsep asinkron di Dart, karena kita berbicara tentang reaktivitas. Sangat sering, pada tahap pertama berkenalan dengan Dart, arti fungsi asinkron (async) tidak dipahami dengan benar. Anda harus selalu ingat bahwa "secara default" program berjalan dalam satu utas, dan asinkron hanya memungkinkan Anda untuk mengubah urutan perintah, daripada menjalankannya secara paralel. Artinya, jika Anda hanya menjalankan fungsi dengan perhitungan besar hanya dengan menandainya async, maka antarmuka akan diblokir. Async TIDAK memulai utas baru. Bagaimana async dan menunggu bekerja ada banyak informasi di Internet, jadi saya tidak akan memikirkan hal ini juga.

Jika Anda perlu membuat perhitungan besar dan pada saat yang sama tidak memblokir antarmuka, Anda perlu menggunakan fungsi komputasi (untuk hardcore khusus Anda dapat menggunakan isolat). Ini akan benar-benar memulai utas eksekusi yang terpisah, yang juga akan memiliki area memori tersendiri (yang sangat sedih dan sedih). Anda dapat berkomunikasi dengan aliran seperti itu hanya melalui pesan yang dapat berisi tipe data sederhana, daftar mereka.

Mari kita mulai berlatih


Perumusan masalah


Mari kita coba menulis aplikasi yang paling sederhana - biarkan menjadi semacam direktori telepon. Kami akan menggunakan Firebase sebagai penyimpanan - ini akan memungkinkan kami untuk membuat aplikasi "cloud". Saya akan melewati cara menghubungkan Firebase ke proyek (lebih dari satu artikel telah ditulis tentang topik ini dan saya tidak melihat titik pengulangan. Catatan: Cloud Firestore digunakan dalam proyek ini.).

Seharusnya seperti ini:





Deskripsi aplikasi


Aplikasi kami akan berisi:

  1. Jendela otorisasi Firebase (logika dari jendela ini akan dimuat di MainBloc).
  2. Jendela informasi - akan menampilkan informasi tentang pengguna yang programnya diotorisasi (logika jendela ini juga akan dimuat dalam MainBloc).
  3. Jendela direktori dalam bentuk daftar telepon (logika dari jendela ini akan dimuat dalam Buku telepon terpisahBloc).
  4. Menu aplikasi yang akan berganti layar.

Aplikasi internal akan dibangun sebagai berikut: setiap layar akan berisi file dengan widget layar, file blok (dengan kelas blok yang sesuai), file tindakan (berisi kelas sederhana yang menjelaskan peristiwa yang mempengaruhi keadaan blok), file status (berisi kelas sederhana yang mencerminkan status blok) ), file data_model yang berisi kelas repositori (bertanggung jawab untuk menerima data) dan kelas data (menyimpan data logika bisnis blok).

Aplikasi akan berfungsi seperti ini - ketika layar dibuka, blok terkait diinisialisasi dengan nilai keadaan awal dan, jika perlu, beberapa tindakan awal disebut dalam konstruktor blok. Layar dibangun / dibangun kembali berdasarkan negara, yang mengembalikan blok. Pengguna melakukan beberapa tindakan dalam aplikasi yang memiliki tindakan yang sesuai. Tindakan diteruskan ke kelas blok, di mana mereka diproses dalam fungsi mapEventToState dan blok mengembalikan status baru kembali ke layar, berdasarkan di mana layar dibangun kembali.

Struktur file


Pertama-tama, kita membuat proyek Flutter kosong dan membuat struktur proyek semacam ini (saya perhatikan bahwa dalam proyek demo beberapa file pada akhirnya akan tetap kosong):



Jendela otorisasi. Mainbloc


Sekarang Anda perlu menerapkan otorisasi di Firebase.
Mari kita mulai dengan membuat kelas acara (lebih mudah untuk mentransfer data melalui acara dalam blok) dan menyatakan untuk blok Utama:

file MainBloc \ tindakan

abstract class MainBlocAction{
  String get password => null;
  String get email => null;
}

mengajukan MainBloc \ state

abstract class MainBlocState{
  bool busy;
  MainBlocState({this.busy = false});
  copy(bool busy) {
    return null;
  }
}

Bendera sibuk di kelas negara digunakan untuk menampilkan progress_hud di antarmuka dan mengecualikan data yang tidak perlu dibaca dari database saat menggulir daftar. Sebelum semua operasi di blok dimulai, keadaan baru dari tipe lama dengan set flag sibuk dikeluarkan ke aliran output - dengan cara ini antarmuka menerima pemberitahuan bahwa operasi telah dimulai. Di akhir operasi, negara baru dikirim ke aliran dengan bendera sibuk dihapus.

Ahli waris dari kelas MainBlocState menggambarkan keadaan blok aplikasi utama. Ahli waris MainBlocAction menggambarkan peristiwa yang terjadi di dalamnya.

Kelas MainBloc berisi 4 elemen utama - fungsi "mengubah" peristiwa menjadi status (Future mapEventToState), negara Bloc adalah _blocState, repositori negara blok adalah repo, dan aliran status "output" (yang melacak elemen antarmuka) blocStream. Pada dasarnya, ini semua adalah elemen yang menyediakan fungsionalitas blok-a. Terkadang disarankan untuk menggunakan 2 aliran output dalam satu blok - contoh seperti itu akan lebih rendah. Saya tidak akan mencantumkannya di sini - Anda dapat melihatnya dengan mengunduh proyek.

Kelas repositori blok berisi logika untuk bekerja dengan Firebase dan objek (data) yang menyimpan data yang diperlukan untuk logika bisnis yang diterapkan blok ini.

File MainBloc \ data_model

class MainRepo{

  final MainData data = MainData();

  FirebaseAuth get firebaseInst => MainData.firebaseInst;

  FirebaseUser _currentUser;

  Future<bool> createUserWithEmailAndPassword(
      String email, String password) async {
    var dataUser;
      try {
        dataUser =
            (await firebaseInst.createUserWithEmailAndPassword(
                email: email, password: password))
                .user;
      } catch (e) {
        print(Error.safeToString(e));
        print(e.code);
        print(e.message);
      }
      if (dataUser == null){
        data.setState(IsNotLogged());
        return false;
      }

      _currentUser = dataUser;
      data.setState(IsLogged(),
          uid: _currentUser.uid,
          email: _currentUser.email);
    return true;
  }

  ...}

class MainData {
  static final firebaseInst = FirebaseAuth.instance;
  static MainBlocState _authState = IsNotLogged();
  static MainBlocState get authState => _authState;
  static String _uid;
  static String get uid => _uid;
  static String _email;
  static String get email => _email;

  void setState(MainBlocState newState,
      {String uid = '', String email = ''}) {
    _authState = newState;
    _uid = uid;
    _email = email;
  }
}


Kelas MainData juga menyimpan status, tetapi status otorisasi di Firebase, dan bukan status Blok.

Kami menulis logika untuk blok utama, sekarang kami dapat mulai mengimplementasikan layar otorisasi / registrasi.

MainBloc diinisialisasi dalam file utama:

File utama

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return Provider(
        create: (context) => MainBloc(),
        dispose: (context, value) => value.dispose(),
        child: MaterialApp(
          routes: menuRoutes,
        ));
  }
}

Saatnya melakukan penyimpangan kecil tentang StreamBuilder, Penyedia, StreamProvider, Konsumen dan Pemilih.

Mundur tentang Penyedia


Penyedia - hanya mentransfer nilai yang disimpan ke pohon. Dan Anda dapat mengaksesnya hanya setelah membangun anak, yaitu Anda perlu membuat sub widget. Tidak bertanggung jawab untuk memperbarui widget.

StreamBuilder - widget yang memantau aliran dan sepenuhnya dibangun kembali ketika menerima objek baru dari aliran.

StreamProvider - widget yang memantau aliran dan setelah menerima objek baru, menandakan bahwa widget anak (yang dinyatakan sebagai kelas terpisah dengan metode build) harus dibangun kembali.

Konsumen dan Pemilih adalah "gula sintaksis", yaitu ini sebenarnya adalah "pembungkus" yang berisi build dan menyembunyikan widget di bawahnya. Di Selector-e, Anda dapat melakukan pemfilteran pembaruan tambahan.

Dengan demikian, ketika Anda perlu membangun kembali sebagian besar layar di setiap acara, Anda dapat menggunakan opsi dengan Penyedia dan StreamBuilder. Ketika perlu untuk membangun kembali bagian-bagian pohon widget dekat dengan daun, disarankan untuk menggunakan StreamProvider dalam kombinasi dengan Konsumen dan Pemilih untuk mengecualikan pembangunan kembali pohon yang tidak perlu.

Otorisasi Kelanjutan


Saat memasuki aplikasi, pengguna harus masuk ke jendela otorisasi / registrasi, dan pada saat itu menu aplikasi belum tersedia untuknya. Poin kedua - untuk menyegarkan sebagian layar ini tidak masuk akal, jadi kita bisa menggunakan StreamBuilder untuk membangun antarmuka. Dan poin ketiga dalam proyek ini adalah menggunakan Navigator untuk menavigasi antar layar. Setelah menerima acara otorisasi yang berhasil, perlu untuk memanggil transisi ke layar informasi. Tapi di dalam build StreamBuilder, ini tidak akan berfungsi - akan ada kesalahan. Untuk menyiasati ini, Anda dapat menggunakan kelas pembungkus tambahan StreamBuilderWithListener (Eugene Brusov - stackoverflow.com ).

Sekarang daftar layar ini adalah auth_screen sendiri (saya akan berikan di sini sebagian):

File auth_screen

Widget build(BuildContext context) {
  var bloc = Provider.of<MainBloc>(context, listen: false);
  return StreamBuilderWithListener<MainBlocState>(
      stream: bloc.blocStream.stream,
      listener: (value) {
        //not allowed call navigator push in build
        if (value is IsLogged) {
          Navigator.of(context).pushReplacementNamed(InfoScreen.nameMenuItem);
        }
      },
      initialData: bloc.state,
      builder: (context, snappShot) {
        if (snappShot.data is IsLoggedOnStart) {
          return LoggedWidget();
        } else if (snappShot.data is IsLogged) {
          //not allowed call navigator push in build
          return ModalProgressHUD(
              inAsyncCall: true,
          child: Text(''),);
        } else if (snappShot.data is IsNotLogged) {
          return SignInAndSignUpWidget();
        }
        return Scaffold(body: Text("                Unknown event"));
      });
}

Pertama, StreamBuilderWithListener dibuat untuk mendengarkan aliran dari blok. Dan berdasarkan kondisi saat ini, baik widget LoggedWidget (jika pengguna sudah diotorisasi) atau SignInAndSignUpWidget (jika pengguna belum diotorisasi) dipanggil. Jika blok mengembalikan status IsLogged, beralih ke layar baru menggunakan Navigator tidak terjadi di pembangun (yang akan menyebabkan kesalahan), tetapi di pendengar. Di widget yang mendasarinya, antarmuka dibangun berdasarkan data yang dikembalikan di sini. Di sini, bundel Provider + StreamBuilder sebenarnya digunakan, karena ketika keadaan blok berubah, hampir seluruh antarmuka berubah.

Untuk mentransfer data ke blok, TextEditingController dan parameter tindakan digunakan:

file auth_screen

class _SignUpWidgetWidgetState extends State {
  String _email, _password;

  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  @override
  void initState() {
    super.initState();
    _emailController.addListener(_onEmailChanged);
    _passwordController.addListener(_onPasswordChanged);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        TextFormField(
          controller: _emailController,
          decoration: InputDecoration(
              labelText: 'email'),
        ),
        TextFormField(
          controller: _passwordController,
          obscureText: true,
          decoration: InputDecoration(
              labelText: 'password'),
        ),
        RaisedButton(
            child: Text('sign up'),
            onPressed: () {
              Provider.of<MainBloc>(context, listen: false).mapEventToState(
                  Registration(_email, _password));
            })
      ],
    );
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  void _onEmailChanged() {
    _email = _emailController.text;
  }

  void _onPasswordChanged() {
    _password = _passwordController.text;
  }
}
 

Jendela PhoneBookScreen


Dan sekarang mari kita bicara sedikit tentang jendela PhoneBookScreen kita. Ini adalah jendela yang paling menarik - di sini antarmuka dibangun berdasarkan 2 aliran dari blok, dan ada juga daftar dengan gulir dan pagination (pagination).

File layar Buku telepon \ Layar

class PhonebookTopPart extends StatelessWidget {

  StatefulWidget caseWidget(PhonebookState state) {
    if (state is PhonebookListOpened) {
      return PhonebookList();
    //} else if (data is PhonebookCardToViewOpened) {
    }else ModalProgressHUD(
      inAsyncCall: true,
      child: Text(''),);
    return null;
  }

  @override
  Widget build(BuildContext context) {
    var bloc = Provider.of<PhonebookBloc>(context, listen: false);
    return StreamProvider<PhonebookState>(
        create: (context) => bloc.blocStream.stream,
        initialData: bloc.state,
        child: Selector<PhonebookState,PhonebookState>(
            selector: (_,state)=>state,
            shouldRebuild: (previous, next){return (previous.runtimeType!=next.runtimeType);},
            builder: (_, state, __) { return ModalProgressHUD(
                inAsyncCall: state.busy,
                child: Scaffold(
                  appBar: AppBar(
                    title: Text("Phones list"),
                  ),
                  drawer: MenuWidget(),
                  body: caseWidget(state),
                ));}
        ));
  }
}

StreamProvider pertama diperlukan untuk beralih di antara berbagai layar direktori - daftar, kartu kontak, kartu kontak untuk diedit, dll. Widget untuk layar dipilih dalam fungsi caseWidget (tetapi dalam contoh ini hanya tampilan untuk daftar yang diterapkan - Anda dapat mencoba menerapkan tampilan untuk kartu kontak - ini sangat sederhana dan tidak akan menjadi awal yang buruk.).

Di layar ini, sekelompok StreamProvider + Selector / Consumer sudah digunakan, karena ada gulungan daftar dan tidak disarankan untuk membangun kembali seluruh layar dengannya (mis. membangun kembali widget berasal dari Pemilih / Konsumen yang sesuai dan lebih rendah di pohon).

Dan di sini adalah implementasi dari daftar itu sendiri:

File layar Buku telepon \ layar

class _PhonebookListState extends State<PhonebookList> {
  ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_scrollListener);
  }

  @override
  Widget build(BuildContext context) {
    var bloc = Provider.of<PhonebookBloc>(context, listen: false);
    var list = bloc.repo.data.list;
    return Container(
        child: StreamProvider<PhonebookState>(
            create: (context) => bloc.scrollStream.stream,
            initialData: bloc.scrollState,
            child: Consumer<PhonebookState>(
              builder: (_, state, __) {
                return ListView.builder(
                    controller: _scrollController,
                    itemCount: list.length,
                    itemBuilder: (BuildContext context, int index) {
                      return ListTile(
                        title: Text(list[index].data['name']),
                        subtitle: Text(list[index].data['phone']),
                      );
                    });
              },
            )));
  }

  void _scrollListener() {
    double delta = MediaQuery
        .of(context)
        .size
        .height * 3;
    double maxScroll = _scrollController.position.maxScrollExtent;
    double currentScroll = _scrollController.position.pixels;
    if (maxScroll - currentScroll <= delta) {
      Provider.of<PhonebookBloc>(context, listen: false)
          .mapEventToState(ScrollPhonebook());
    }
  }

  @override
  void dispose() {
    _scrollController.removeListener(_scrollListener);
    super.dispose();
  }
}

Di sini kita melihat StreamProvider kedua, yang memantau aliran blok kedua, yang bertanggung jawab atas gulir. Pagination diatur sebagai standar melalui _scrollListener (controller: _scrollController). Meskipun jendelanya menarik, tetapi mengingat deskripsi terperinci dari jendelanya, tidak ada lagi yang bisa dikatakan di sini. Karena itu, itu saja hari ini.

Tujuan artikel ini bukan untuk menunjukkan kode ideal, yaitu, di sini Anda dapat menemukan banyak poin untuk optimasi - dengan benar “dipecah” oleh file, gunakan instance, mixin dan sejenisnya di suatu tempat. Juga, apa yang "meminta" langkah selanjutnya - Anda dapat membuat kartu kontak. Tugas utama adalah untuk membangun pengetahuan, menetapkan vektor tertentu untuk membangun aplikasi, memberikan penjelasan tentang beberapa saat merancang aplikasi di Flutter yang tidak terlalu jelas pada tahap pertama dari kenalan.

Proyek dapat diunduh di (untuk pendaftaran Anda dapat menggunakan surat apa pun dengan kata sandi minimal 6 karakter. Saat melakukan otorisasi ulang, kata sandi harus sama dengan yang digunakan saat pendaftaran).

All Articles