MDC-104 Flutter: Komponen Lanjutan Material

1. Pengantar

logo_components_color_2x_web_96dp.png

Komponen Material (MDC) membantu developer menerapkan Desain Material. Dibuat oleh tim engineer dan desainer UX di Google, MDC memiliki banyak komponen UI yang indah dan fungsional serta tersedia untuk Android, iOS, web, dan Flutter.material.io/develop

Di codelab MDC-103, Anda menyesuaikan warna, ketinggian, tipografi, dan bentuk Komponen Material (MDC) untuk memberikan gaya ke aplikasi Anda.

Komponen di sistem Desain Material menjalankan tugas yang telah ditentukan sebelumnya dan memiliki karakteristik tertentu, seperti tombol. Namun, tombol bukan sekadar cara bagi pengguna untuk melakukan tindakan. Tombol juga merupakan ekspresi visual dari bentuk, ukuran, dan warna yang memberi tahu pengguna bahwa ini adalah hal yang interaktif dan akan terjadi sesuatu saat disentuh atau diklik.

Pedoman Desain Material menjelaskan komponen dari sudut pandang desainer. Pedoman ini menjelaskan berbagai fungsi dasar yang tersedia di seluruh platform, dan elemen anatomi yang menyusun setiap komponen. Misalnya, tampilan latar berisi lapisan belakang dan kontennya, lapisan depan dan kontennya, aturan gerakan, dan opsi tampilan. Setiap komponen ini dapat disesuaikan untuk kebutuhan, kasus penggunaan, dan konten setiap aplikasi.

Yang akan Anda bangun

Dalam codelab ini, Anda akan mengubah UI di aplikasi Shrine menjadi presentasi dua tingkat yang disebut "tampilan latar". Tampilan latar mencakup menu yang mencantumkan kategori yang dapat dipilih yang digunakan untuk memfilter produk yang ditampilkan dalam petak asimetris. Dalam codelab ini, Anda akan menggunakan komponen Flutter berikut:

  • Bentuk
  • Gerakan
  • Widget Flutter (yang sudah Anda gunakan di codelabs sebelumnya)

Android

iOS

aplikasi e-commerce bertema merah muda dan cokelat dengan panel aplikasi atas dan petak asimetris yang dapat di-scroll secara horizontal yang penuh dengan produk

aplikasi e-commerce bertema merah muda dan cokelat dengan panel aplikasi atas dan petak asimetris yang dapat di-scroll secara horizontal yang penuh dengan produk

menu yang menampilkan 4 kategori

menu yang menampilkan 4 kategori

Komponen dan subsistem MDC-Flutter di codelab ini

  • Bentuk

Bagaimana Anda menilai tingkat pengalaman Anda menggunakan pengembangan Flutter?

Pemula Menengah Mahir

2. Menyiapkan lingkungan pengembangan Flutter Anda

Anda memerlukan dua software untuk menyelesaikan lab ini—Flutter SDK dan editor.

Anda dapat menjalankan codelab menggunakan salah satu perangkat berikut:

  • Perangkat Android atau iOS fisik yang terhubung ke komputer dan disetel ke mode Developer.
  • Simulator iOS (perlu menginstal alat Xcode).
  • Android Emulator (memerlukan penyiapan di Android Studio).
  • Browser (Chrome diperlukan untuk proses debug).
  • Aplikasi desktop Windows, Linux, atau macOS. Anda harus melakukan pengembangan di platform tempat Anda berencana men-deploy aplikasi. Jadi, jika ingin mengembangkan aplikasi desktop Windows, Anda harus mengembangkannya di Windows untuk mengakses rantai build yang sesuai. Ada persyaratan spesifik per sistem operasi yang dibahas secara mendetail di docs.flutter.dev/desktop.

3. Mendownload aplikasi awal codelab

Melanjutkan dari MDC-103?

Jika Anda sudah menyelesaikan MDC-103, kode Anda seharusnya siap untuk codelab ini. Lewati ke langkah: Menambahkan menu tampilan latar.

Memulai dari awal?

Aplikasi awal terletak di direktori material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series.

...atau meng-clone codelab dari GitHub

Untuk meng-clone codelab ini dari GitHub, jalankan perintah berikut:

git clone https://github.com/material-components/material-components-flutter-codelabs.git
cd material-components-flutter-codelabs/mdc_100_series
git checkout 104-starter_and_103-complete

Membuka project dan menjalankan aplikasi

  1. Buka project di editor pilihan Anda.
  2. Ikuti petunjuk untuk "Run the app" (Jalankan aplikasi) di Get Started: Test drive pada editor pilihan Anda.

Berhasil! Anda akan melihat halaman login Shrine dari codelab sebelumnya di perangkat Anda.

Android

iOS

halaman login Shrine

halaman login Shrine

4. Menambahkan menu tampilan latar

Tampilan latar muncul di belakang semua konten dan komponen lainnya. Hal ini terdiri dari dua lapisan: lapisan belakang (yang menampilkan tindakan dan filter) dan lapisan depan (yang menampilkan konten). Anda dapat menggunakan latar belakang untuk menampilkan tindakan dan informasi yang interaktif, seperti navigasi atau filter konten.

Menghapus panel aplikasi beranda

Widget HomePage akan menjadi konten lapisan depan kita. Saat ini halaman beranda memiliki panel aplikasi. Kita akan memindahkan panel aplikasi ke lapisan belakang dan HomePage hanya akan menyertakan AsymmetricView.

Di home.dart, ubah fungsi build() menjadi hanya menampilkan AsymmetricView:

// TODO: Return an AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));

Menambahkan widget Tampilan latar

Buat widget yang disebut Tampilan latar yang menyertakan frontLayer dan backLayer.

backLayer menyertakan menu yang memungkinkan Anda memilih kategori untuk memfilter daftar (currentCategory). Karena kita ingin mempertahankan pemilihan menu, kita akan membuat widget stateful untuk Tampilan latar.

Tambahkan file baru bernama backdrop.dart ke /lib:

import 'package:flutter/material.dart';

import 'model/product.dart';

// TODO: Add velocity constant (104)

class Backdrop extends StatefulWidget {
  final Category currentCategory;
  final Widget frontLayer;
  final Widget backLayer;
  final Widget frontTitle;
  final Widget backTitle;

  const Backdrop({
    required this.currentCategory,
    required this.frontLayer,
    required this.backLayer,
    required this.frontTitle,
    required this.backTitle,
    Key? key,
  }) : super(key: key);

  @override
  _BackdropState createState() => _BackdropState();
}

// TODO: Add _FrontLayer class (104)
// TODO: Add _BackdropTitle class (104)
// TODO: Add _BackdropState class (104)

Perhatikan bahwa kami menandai properti tertentu dengan required. Ini adalah praktik terbaik untuk properti di konstruktor yang tidak memiliki nilai default dan tidak boleh null dan, oleh karena itu, tidak boleh dilupakan.

Pada definisi class Backdrop, tambahkan class _BackdropState:

// TODO: Add _BackdropState class (104)
class _BackdropState extends State<Backdrop>
    with SingleTickerProviderStateMixin {
  final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');

  // TODO: Add AnimationController widget (104)

  // TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
  Widget _buildStack() {
    return Stack(
    key: _backdropKey,
      children: <Widget>[
        // TODO: Wrap backLayer in an ExcludeSemantics widget (104)
        widget.backLayer,
        widget.frontLayer,
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    var appBar = AppBar(
      elevation: 0.0,
      titleSpacing: 0.0,
      // TODO: Replace leading menu icon with IconButton (104)
      // TODO: Remove leading property (104)
      // TODO: Create title with _BackdropTitle parameter (104)
      leading: Icon(Icons.menu),
      title: Text('SHRINE'),
      actions: <Widget>[
        // TODO: Add shortcut to login screen from trailing icons (104)
        IconButton(
          icon: Icon(
            Icons.search,
            semanticLabel: 'search',
          ),
          onPressed: () {
          // TODO: Add open login (104)
          },
        ),
        IconButton(
          icon: Icon(
            Icons.tune,
            semanticLabel: 'filter',
          ),
          onPressed: () {
          // TODO: Add open login (104)
          },
        ),
      ],
    );
    return Scaffold(
      appBar: appBar,
      // TODO: Return a LayoutBuilder widget (104)
      body: _buildStack(),
    );
  }
}

Fungsi build() menampilkan Scaffold dengan panel aplikasi seperti HomePage yang dulu. Tetapi, isi Scaffold adalah Stack. Turunan Stack dapat tumpang-tindih. Setiap ukuran dan lokasi turunan ditentukan relatif terhadap induk Stack.

Sekarang tambahkan instance Tampilan latar ke ShrineApp.

Di app.dart, impor backdrop.dart dan model/product.dart:

import 'backdrop.dart'; // New code
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart'; // New code
import 'supplemental/cut_corners_border.dart';

Di app.dart, ubah rute / dengan menampilkan Backdrop yang memiliki HomePage sebagai frontLayer-nya:

// TODO: Change to a Backdrop with a HomePage frontLayer (104)
'/': (BuildContext context) => Backdrop(
     // TODO: Make currentCategory field take _currentCategory (104)
     currentCategory: Category.all,
     // TODO: Pass _currentCategory for frontLayer (104)
     frontLayer: HomePage(),
     // TODO: Change backLayer field value to CategoryMenuPage (104)
     backLayer: Container(color: kShrinePink100),
     frontTitle: Text('SHRINE'),
     backTitle: Text('MENU'),
),

Simpan project, Anda akan melihat halaman beranda kita muncul, begitu juga panel aplikasi:

Android

iOS

halaman produk Shrine dengan latar belakang merah muda

halaman produk Shrine dengan latar belakang merah muda

backLayer menampilkan area berwarna merah muda di lapisan baru di belakang frontLayer halaman beranda.

Anda dapat menggunakan Flutter Inspector untuk memverifikasi bahwa Stack memiliki Container di belakang HomePage. Tampilannya akan terlihat serupa dengan ini:

4783ed30f1cc010.png

Kini Anda dapat menyesuaikan desain dan konten kedua lapisan tersebut.

5. Menambahkan bentuk

Di langkah ini, Anda akan memberi gaya lapisan depan dan menambahkan potongan di pojok kiri atas.

Desain Material mengacu pada jenis penyesuaian ini sebagai bentuk. Permukaan Material dapat memiliki sembarang bentuk. Bentuk menambahkan penekanan dan gaya pada platform dan dapat digunakan untuk mengekspresikan branding. Bentuk persegi panjang biasa dapat disesuaikan dengan sudut dan pinggiran yang melengkung atau miring, dan berapa pun jumlah sisinya. Bentuknya dapat bersifat simetris atau tidak beraturan.

Menambahkan bentuk ke lapisan depan

Logo Shrine yang miring memberikan inspirasi pada cerita bentuk aplikasi Shrine. Cerita bentuk adalah penggunaan yang umum dari bentuk yang diterapkan ke seluruh aplikasi. Misalnya, bentuk logo terpantulkan di elemen halaman login yang menerapkan bentuk. Di langkah ini, Anda akan memberi gaya lapisan depan dengan potongan miring di pojok kiri atas.

Di backdrop.dart, tambahkan class baru _FrontLayer:

// TODO: Add _FrontLayer class (104)
class _FrontLayer extends StatelessWidget {
  // TODO: Add on-tap callback (104)
  const _FrontLayer({
    Key? key,
    required this.child,
  }) : super(key: key);

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Material(
      elevation: 16.0,
      shape: const BeveledRectangleBorder(
        borderRadius: BorderRadius.only(topLeft: Radius.circular(46.0)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          // TODO: Add a GestureDetector (104)
          Expanded(
            child: child,
          ),
        ],
      ),
    );
  }
}

Kemudian, di fungsi _buildStack() _BackdropState, gabungkan lapisan depan dalam _FrontLayer:

  Widget _buildStack() {
    // TODO: Create a RelativeRectTween Animation (104)

    return Stack(
    key: _backdropKey,
      children: <Widget>[
        // TODO: Wrap backLayer in an ExcludeSemantics widget (104)
        widget.backLayer,
        // TODO: Add a PositionedTransition (104)
        // TODO: Wrap front layer in _FrontLayer (104)
          _FrontLayer(child: widget.frontLayer),
      ],
    );
  }

Muat ulang.

Android

iOS

halaman produk Shrine dengan bentuk kustom

halaman produk Shrine dengan bentuk kustom

Kita telah memberikan bentuk kustom pada platform utama Shrine. Karena ketinggian platform, pengguna dapat melihat bahwa terdapat sesuatu di belakang lapisan putih di depan. Mari tambahkan gerakan agar pengguna dapat melihat lapisan belakang tampilan latar.

6. Menambahkan gerakan

Gerakan adalah cara untuk membuat aplikasi Anda terlihat hidup. Ini dapat berupa gerakan yang besar dan dramatis, gerakan yang halus dan minimal, atau apa pun di antara keduanya. Namun, perlu diingat bahwa jenis gerakan yang Anda gunakan harus sesuai dengan situasinya. Gerakan yang diterapkan pada tindakan reguler yang diulang harus kecil dan halus, sehingga tindakan tidak mengganggu pengguna atau memerlukan waktu yang terlalu lama secara berkala. Namun, terdapat situasi yang sesuai, seperti ketika pengguna pertama kali membuka aplikasi, yang dapat lebih menarik, dan beberapa animasi dapat membantu memberi tahu pengguna tentang cara menggunakan aplikasi Anda.

Menambahkan gerakan membuka ke tombol menu

Di bagian atas backdrop.dart, di luar cakupan class atau fungsi apa pun, tambahkan konstanta untuk mewakili kecepatan yang diinginkan untuk animasi:

// TODO: Add velocity constant (104)
const double _kFlingVelocity = 2.0;

Tambahkan widget AnimationController ke _BackdropState, buat instance di fungsi initState(), dan buang di fungsi dispose() status:

  // TODO: Add AnimationController widget (104)
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 300),
      value: 1.0,
      vsync: this,
    );
  }

  // TODO: Add override for didUpdateWidget (104)

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  // TODO: Add functions to get and change front layer visibility (104)

AnimationController mengoordinasikan Animasi dan memberikan API kepada Anda untuk memutar, membalikkan, dan menghentikan animasi. Sekarang kita memerlukan fungsi yang membuatnya bergerak.

Tambahkan fungsi yang menentukan serta mengubah visibilitas lapisan depan:

  // TODO: Add functions to get and change front layer visibility (104)
  bool get _frontLayerVisible {
    final AnimationStatus status = _controller.status;
    return status == AnimationStatus.completed ||
        status == AnimationStatus.forward;
  }

  void _toggleBackdropLayerVisibility() {
    _controller.fling(
        velocity: _frontLayerVisible ? -_kFlingVelocity : _kFlingVelocity);
  }

Gabungkan backLayer di widget ExcludeSemantics. Widget ini akan mengecualikan utem menu backLayer dari hierarki semantik ketika lapisan belakang tidak terlihat.

    return Stack(
      key: _backdropKey,
      children: <Widget>[
        // TODO: Wrap backLayer in an ExcludeSemantics widget (104)
        ExcludeSemantics(
          child: widget.backLayer,
          excluding: _frontLayerVisible,
        ),
      ...

Ubah fungsi _buildStack() untuk mengambil BuildContext dan BoxConstraints. Selain itu, sertakan PositionedTransition yang mengambil Animasi RelativeRectTween:

  // TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
  Widget _buildStack(BuildContext context, BoxConstraints constraints) {
    const double layerTitleHeight = 48.0;
    final Size layerSize = constraints.biggest;
    final double layerTop = layerSize.height - layerTitleHeight;

    // TODO: Create a RelativeRectTween Animation (104)
    Animation<RelativeRect> layerAnimation = RelativeRectTween(
      begin: RelativeRect.fromLTRB(
          0.0, layerTop, 0.0, layerTop - layerSize.height),
      end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
    ).animate(_controller.view);

    return Stack(
      key: _backdropKey,
      children: <Widget>[
        // TODO: Wrap backLayer in an ExcludeSemantics widget (104)
        ExcludeSemantics(
          child: widget.backLayer,
          excluding: _frontLayerVisible,
        ),
        // TODO: Add a PositionedTransition (104)
        PositionedTransition(
          rect: layerAnimation,
          child: _FrontLayer(
            // TODO: Implement onTap property on _BackdropState (104)
            child: widget.frontLayer,
          ),
        ),
      ],
    );
  }

Terakhir, daripada memanggil fungsi _buildStack untuk isi Scaffold, kembalikan widget LayoutBuilder yang menggunakan _buildStack sebagai buildernya:

    return Scaffold(
      appBar: appBar,
      // TODO: Return a LayoutBuilder widget (104)
      body: LayoutBuilder(builder: _buildStack),
    );

Kita telah menunda build tumpukan lapisan depan/belakang sampai waktu lapisan menggunakan LayoutBuilder sehingga kita dapat menyertakan tinggi keseluruhan tampilan latar yang sebenarnya. LayoutBuilder adalah widget khusus yang pemanggilan buildernya memberikan batasan ukuran.

Di fungsi build(), ubah ikon menu utama di panel aplikasi menjadi IconButton dan gunakan itu untuk mengalihkan visibilitas lapisan depan saat tombol diketuk.

      // TODO: Replace leading menu icon with IconButton (104)
      leading: IconButton(
        icon: const Icon(Icons.menu),
        onPressed: _toggleBackdropLayerVisibility,
      ),

Muat ulang, lalu ketuk tombol menu di simulator.

Android

iOS

menu Shrine kosong dengan dua error

menu Shrine kosong dengan dua error

Lapisan depan bergerak (bergeser) ke bawah. Tetapi, jika Anda melihat ke bawah, ada error berwarna merah dan error tambahan. Hal ini dikarenakan AsymmetricView terjepit dan menjadi lebih kecil karena animasi ini, yang memberikan lebih sedikit ruang kepada Columns. Pada akhirnya, Columns tidak dapat mengatur tata letaknya dengan ruang yang diberikan dan menghasilkan error. Jika kita mengganti Columns dengan ListViews, ukuran kolom akan tetap saat kolom bergerak.

Menggabungkan kolom produk di ListView

Di supplemental/product_columns.dart, ganti Column dalam OneProductCardColumn dengan ListView:

class OneProductCardColumn extends StatelessWidget {
  const OneProductCardColumn({required this.product, Key? key}) : super(key: key);

  final Product product;

  @override
  Widget build(BuildContext context) {
    // TODO: Replace Column with a ListView (104)
    return ListView(
      physics: const ClampingScrollPhysics(),
      reverse: true,
      children: <Widget>[
        const SizedBox(
          height: 40.0,
        ),
        ProductCard(
          product: product,
        ),
      ],
    );
  }
}

Column mencakup MainAxisAlignment.end. Untuk memulai tata letak dari bawah, tandai reverse: true. Urutan turunan dibalik untuk mengimbangi perubahan.

Muat ulang, lalu ketuk tombol menu.

Android

iOS

menu Shrine kosong dengan satu error

menu Shrine kosong dengan satu error

Peringatan tambahan berwarna abu-abu di OneProductCardColumn hilang. Sekarang mari perbaiki peringatan lainnya.

Di supplemental/product_columns.dart, ubah cara imageAspectRatio dihitung, dan ganti Column di TwoProductCardColumn dengan ListView:

      // TODO: Change imageAspectRatio calculation (104)
      double imageAspectRatio = heightOfImages >= 0.0
          ? constraints.biggest.width / heightOfImages
          : 49.0 / 33.0;
      // TODO: Replace Column with a ListView (104)
      return ListView(
        physics: const ClampingScrollPhysics(),
        children: <Widget>[
          Padding(
            padding: const EdgeInsetsDirectional.only(start: 28.0),
            child: top != null
                ? ProductCard(
                    imageAspectRatio: imageAspectRatio,
                    product: top!,
                  )
                : SizedBox(
                    height: heightOfCards,
                  ),
          ),
          const SizedBox(height: spacerHeight),
          Padding(
            padding: const EdgeInsetsDirectional.only(end: 28.0),
            child: ProductCard(
              imageAspectRatio: imageAspectRatio,
              product: bottom,
            ),
          ),
        ],
      );

Kita juga menambahkan beberapa keselamatan ke imageAspectRatio.

Muat ulang. Lalu ketuk tombol menu.

Android

iOS

menu Shrine kosong

menu Shrine kosong

Tidak ada lagi tambahan.

7. Menambahkan menu di lapisan belakang

Menu adalah daftar item teks yang dapat diketuk yang memberi tahu pemroses kapan item teks disentuh. Di langkah ini, Anda akan menambahkan menu pemfilteran kategori.

Menambahkan menu

Tambahkan menu ke lapisan depan dan tombol interaktif ke lapisan belakang.

Buat file baru yang disebut lib/category_menu_page.dart:

import 'package:flutter/material.dart';

import 'colors.dart';
import 'model/product.dart';

class CategoryMenuPage extends StatelessWidget {
  final Category currentCategory;
  final ValueChanged<Category> onCategoryTap;
  final List<Category> _categories = Category.values;

  const CategoryMenuPage({
    Key? key,
    required this.currentCategory,
    required this.onCategoryTap,
  }) : super(key: key);

  Widget _buildCategory(Category category, BuildContext context) {
    final categoryString =
        category.toString().replaceAll('Category.', '').toUpperCase();
    final ThemeData theme = Theme.of(context);

    return GestureDetector(
      onTap: () => onCategoryTap(category),
      child: category == currentCategory
        ? Column(
            children: <Widget>[
              const SizedBox(height: 16.0),
              Text(
                categoryString,
                style: theme.textTheme.bodyText1,
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 14.0),
              Container(
                width: 70.0,
                height: 2.0,
                color: kShrinePink400,
              ),
            ],
          )
      : Padding(
        padding: const EdgeInsets.symmetric(vertical: 16.0),
        child: Text(
          categoryString,
          style: theme.textTheme.bodyText1!.copyWith(
              color: kShrineBrown900.withAlpha(153)
            ),
          textAlign: TextAlign.center,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        padding: const EdgeInsets.only(top: 40.0),
        color: kShrinePink100,
        child: ListView(
          children: _categories
            .map((Category c) => _buildCategory(c, context))
            .toList()),
      ),
    );
  }
}

GestureDetector menggabungkan kolom yang turunannya adalah nama kategori. Garis bawah digunakan untuk menunjukkan kategori yang dipilih.

Di app.dart, konversikan widget ShrineApp dari stateless menjadi stateful.

  1. Tandai ShrineApp.
  2. Berdasarkan IDE Anda, tampilkan tindakan kode:
  3. Android Studio: Press ⌥Enter (macOS) atau alt + enter
  4. VS Code: Tekan ⌘. (macOS) atau Ctrl+.
  5. Pilih "Convert to StatefulWidget".
  6. Ubah class ShrineAppState menjadi pribadi (_ShrineAppState). Klik kanan ShrineAppState, lalu
  7. Android Studio: pilih Refactor > Rename
  8. VS Code: pilih Rename Symbol
  9. Masukkan _ShrineAppState untuk membuat class menjadi pribadi.

Di app.dart, tambahkan variabel ke _ShrineAppState untuk Category yang dipilih dan callback saat diketuk:

class _ShrineAppState extends State<ShrineApp> {
  Category _currentCategory = Category.all;

  void _onCategoryTap(Category category) {
    setState(() {
      _currentCategory = category;
    });
  }

Kemudian ubah lapisan belakang menjadi CategoryMenuPage.

Di app.dart, impor CategoryMenuPage:

import 'backdrop.dart';
import 'category_menu_page.dart';
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart';
import 'supplemental/cut_corners_border.dart';

Di fungsi build(), ubah kolom backLayer menjadi CategoryMenuPage dan kolom currentCategory untuk mengambil variabel instance.

'/': (BuildContext context) => Backdrop(
              // TODO: Make currentCategory field take _currentCategory (104)
              currentCategory: _currentCategory,
              // TODO: Pass _currentCategory for frontLayer (104)
              frontLayer: HomePage(),
              // TODO: Change backLayer field value to CategoryMenuPage (104)
              backLayer: CategoryMenuPage(
                currentCategory: _currentCategory,
                onCategoryTap: _onCategoryTap,
              ),
              frontTitle: const Text('SHRINE'),
              backTitle: const Text('MENU'),
            ),

Muat ulang, lalu ketuk tombol Menu.

Android

iOS

menu Shrine dengan 4 kategori

menu Shrine dengan 4 kategori

Jika Anda mengetuk opsi menu dan tidak terjadi apa-apa. Ayo perbaiki.

Di home.dart, tambahkan variabel untuk Category dan teruskan ke AsymmetricView.

import 'package:flutter/material.dart';

import 'model/product.dart';
import 'model/products_repository.dart';
import 'supplemental/asymmetric_view.dart';

class HomePage extends StatelessWidget {
  // TODO: Add a variable for Category (104)
  final Category category;

  const HomePage({this.category = Category.all, Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // TODO: Pass Category variable to AsymmetricView (104)
    return AsymmetricView(
      products: ProductsRepository.loadProducts(category),
    );
  }
}

Di app.dart, teruskan _currentCategory untuk frontLayer:.

// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(category: _currentCategory),

Muat ulang. Ketuk tombol menu di simulator, lalu pilih Category.

Android

iOS

halaman produk Shrine yang difilter

halaman produk Shrine yang difilter

Ketuk ikon menu untuk melihat produk. Produk berhasil difilter!

Menutup lapisan depan setelah pemilihan menu

Di backdrop.dart, tambahkan pengganti untuk fungsi didUpdateWidget() (yang dipanggil kapan pun konfigurasi widget berubah) di _BackdropState:

  // TODO: Add override for didUpdateWidget() (104)
  @override
  void didUpdateWidget(Backdrop old) {
    super.didUpdateWidget(old);

    if (widget.currentCategory != old.currentCategory) {
      _toggleBackdropLayerVisibility();
    } else if (!_frontLayerVisible) {
      _controller.fling(velocity: _kFlingVelocity);
    }
  }

Simpan project Anda untuk memicu hot reload. Ketuk ikon menu dan pilih kategori. Menu akan otomatis menutup dan Anda akan melihat kategori item yang dipilih. Sekarang Anda juga akan menambahkan fungsi itu ke lapisan depan.

Mengalihkan lapisan depan

Di backdrop.dart, tambahkan callback saat diketuk ke lapisan tampilan latar:

class _FrontLayer extends StatelessWidget {
  // TODO: Add on-tap callback (104)
  const _FrontLayer({
    Key? key,
    this.onTap, // New code
    required this.child,
  }) : super(key: key);

  final VoidCallback? onTap; // New code
  final Widget child;

Kemudian tambahkan GestureDetector ke turunan _FrontLayer: turunan Column:.

      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          // TODO: Add a GestureDetector (104)
          GestureDetector(
            behavior: HitTestBehavior.opaque,
            onTap: onTap,
            child: Container(
              height: 40.0,
              alignment: AlignmentDirectional.centerStart,
            ),
          ),
          Expanded(
            child: child,
          ),
        ],
      ),

Kemudian terapkan properti onTap baru di _BackdropState di fungsi _buildStack():

          PositionedTransition(
            rect: layerAnimation,
            child: _FrontLayer(
              // TODO: Implement onTap property on _BackdropState (104)
              onTap: _toggleBackdropLayerVisibility,
              child: widget.frontLayer,
            ),
          ),

Muat ulang, lalu ketuk bagian atas lapisan depan. Lapisan akan terbuka dan tertutup setiap kali Anda mengetuk bagian atas lapisan depan.

8. Menambahkan ikon bermerek

Ikon bermerek juga mencakup ikon familier. Mari lakukan kustomisasi pada ikon reveal dan gabungkan dengan judul kita untuk mendapatkan tampilan bermerek yang unik.

Mengubah ikon tombol menu

Android

iOS

halaman produk Shrine dengan ikon bermerek

halaman produk Shrine dengan ikon bermerek

Di backdrop.dart, buat class _BackdropTitle baru.

// TODO: Add _BackdropTitle class (104)
class _BackdropTitle extends AnimatedWidget {
  final void Function() onPress;
  final Widget frontTitle;
  final Widget backTitle;

  const _BackdropTitle({
    Key? key,
    required Animation<double> listenable,
    required this.onPress,
    required this.frontTitle,
    required this.backTitle,
  }) : _listenable = listenable,
       super(key: key, listenable: listenable);

  final Animation<double> _listenable;

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = _listenable;

    return DefaultTextStyle(
      style: Theme.of(context).textTheme.headline6!,
      softWrap: false,
      overflow: TextOverflow.ellipsis,
      child: Row(children: <Widget>[
        // branded icon
        SizedBox(
          width: 72.0,
          child: IconButton(
            padding: const EdgeInsets.only(right: 8.0),
            onPressed: this.onPress,
            icon: Stack(children: <Widget>[
              Opacity(
                opacity: animation.value,
                child: const ImageIcon(AssetImage('assets/slanted_menu.png')),
              ),
              FractionalTranslation(
                translation: Tween<Offset>(
                  begin: Offset.zero,
                  end: const Offset(1.0, 0.0),
                ).evaluate(animation),
                child: const ImageIcon(AssetImage('assets/diamond.png')),
              )]),
          ),
        ),
        // Here, we do a custom cross fade between backTitle and frontTitle.
        // This makes a smooth animation between the two texts.
        Stack(
          children: <Widget>[
            Opacity(
              opacity: CurvedAnimation(
                parent: ReverseAnimation(animation),
                curve: const Interval(0.5, 1.0),
              ).value,
              child: FractionalTranslation(
                translation: Tween<Offset>(
                  begin: Offset.zero,
                  end: const Offset(0.5, 0.0),
                ).evaluate(animation),
                child: backTitle,
              ),
            ),
            Opacity(
              opacity: CurvedAnimation(
                parent: animation,
                curve: const Interval(0.5, 1.0),
              ).value,
              child: FractionalTranslation(
                translation: Tween<Offset>(
                  begin: const Offset(-0.25, 0.0),
                  end: Offset.zero,
                ).evaluate(animation),
                child: frontTitle,
              ),
            ),
          ],
        )
      ]),
    );
  }
}

_BackdropTitle adalah widget kustom yang akan mengganti widget Text biasa untuk parameter title widget AppBar. Widget ini memiliki ikon menu animasi dan transisi animasi antara judul depan dan judul belakang. Ikon menu animasi akan menggunakan aset baru. Referensi ke slanted_menu.png yang baru harus ditambahkan ke pubspec.yaml.

assets:
    - assets/diamond.png
    # TODO: Add slanted menu asset (104)
    - assets/slanted_menu.png
    - packages/shrine_images/0-0.jpg

Hapus properti leading di builder AppBar. Penghapusan diperlukan agar ikon bermerek kustom dirender di tempat widget leading asli. Animasi listenable dan handler onPress untuk ikon bermerek diteruskan ke _BackdropTitle. frontTitle dan backTitle juga diteruskan sehingga dapat dirender dalam judul tampilan latar. Parameter title dari AppBar akan terlihat seperti ini:

// TODO: Create title with _BackdropTitle parameter (104)
title: _BackdropTitle(
  listenable: _controller.view,
  onPress: _toggleBackdropLayerVisibility,
  frontTitle: widget.frontTitle,
  backTitle: widget.backTitle,
),

Ikon bermerek dibuat di _BackdropTitle. yang berisi Stack ikon animasi: menu miring dan berlian, yang digabungkan dalam IconButton sehingga dapat ditekan. IconButton kemudian digabung dalam SizedBox guna memberikan ruang untuk gerakan ikon horizontal.

Arsitektur "everything is a widget" Flutter memungkinkan tata letak AppBar default diubah tanpa harus membuat widget AppBar kustom yang sepenuhnya baru. Parameter title, yang awalnya widget Text, dapat diganti dengan _BackdropTitle yang lebih kompleks. Karena _BackdropTitle juga menyertakan ikon khusus, widget tersebut menggantikan properti leading, yang kini dapat dihilangkan. Penggantian widget sederhana ini dilakukan tanpa mengubah parameter lain, seperti ikon tindakan, yang tetap berfungsi sendiri.

Menambahkan pintasan kembali ke layar login

Di backdrop.dart, tambahkan pintasan kembali ke layar login dari dua ikon tambahan di panel aplikasi: Ubah label semantik ikon tersebut agar mencerminkan tujuan barunya.

        // TODO: Add shortcut to login screen from trailing icons (104)
        IconButton(
          icon: const Icon(
            Icons.search,
            semanticLabel: 'login', // New code
          ),
          onPressed: () {
            // TODO: Add open login (104)
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (BuildContext context) => LoginPage()),
            );
          },
        ),
        IconButton(
          icon: const Icon(
            Icons.tune,
            semanticLabel: 'login', // New code
          ),
          onPressed: () {
            // TODO: Add open login (104)
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (BuildContext context) => LoginPage()),
            );
          },
        ),

Pesan error akan muncul jika Anda mencoba memuat ulang. Impor login.dart untuk memperbaiki error:

import 'login.dart';

Muat ulang aplikasi dan ketuk tombol penelusuran atau penyempurnaan untuk kembali ke layar login.

9. Selamat!

Dalam empat codelab ini, Anda telah melihat cara menggunakan Komponen Material untuk membuat pengalaman pengguna yang unik dan elegan yang mengekspresikan gaya dan karakteristik merek.

Langkah berikutnya

Codelab ini, MDC-104, melengkapi urutan codelab ini. Anda dapat mempelajari lebih lanjut komponen di MDC-Flutter dengan mengunjungi katalog widget Komponen Material.

Untuk target yang lebih menantang, coba ganti ikon bermerek dengan AnimatedIcon yang bergerak di antara dua ikon saat tampilan latar dibuat terlihat.

Ada banyak codelab Flutter lain yang dapat dicoba sesuai minat Anda. Kami memiliki satu lagi codelab khusus Material yang mungkin Anda minati: Membuat Transisi Indah dengan Gerakan Material untuk Flutter.

Saya dapat menyelesaikan codelab ini dengan upaya dan dalam durasi waktu yang wajar

Sangat setuju Setuju Netral Tidak setuju Sangat tidak setuju

Saya ingin terus menggunakan Komponen Material pada masa mendatang

Sangat setuju Setuju Netral Tidak setuju Sangat tidak setuju