MDC-104 Flutter: Komponen Lanjutan Material (Flutter)

logo_components_color_2x_web_96dp.png

Komponen Material (MDC) membantu developer menerapkan Desain Material. Dibuat oleh tim engineer dan desainer UX di Google, MMD 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, serta 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. Sebagian besar dari bagian ini adalah tampilan, kontrol, dan fungsi tradisional dari SDK platform Anda.

Meskipun pedoman Desain Material menyebutkan banyak komponen, tidak semua komponen tersebut adalah kandidat yang baik untuk kode yang dapat digunakan kembali, dan karena itu tidak ditemukan di MDC. Anda dapat membuat pengalaman ini sendiri untuk mencapai gaya yang disesuaikan untuk aplikasi Anda, semua menggunakan kode tradisional.

Yang akan Anda buat

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

Komponen MDC-Flutter di codelab ini

  • Bentuk

Bagaimana Anda menilai tingkat pengalaman Anda menggunakan pengembangan Flutter?

Pemula Menengah Mahir

Sebelum memulai

Untuk mulai mengembangkan aplikasi seluler dengan Flutter, Anda harus:

  1. Mendownload dan menginstal Flutter SDK.
  2. Mengupdate PATH Anda dengan Flutter SDK.
  3. Menginstal Android Studio dengan plugin Flutter dan Dart, atau editor favorit Anda.
  4. Menginstal emulator Android, simulator iOS (perlu Mac dengan Xcode), atau menggunakan perangkat fisik.

Untuk informasi selengkapnya terkait penginstalan Flutter, lihat Get Started: Install. Untuk menyiapkan editor, lihat Get Started: Set up an editor. Saat menginstal emulator Android, Anda dapat menggunakan opsi default seperti ponsel Pixel 3 dengan Image Sistem terbaru. Tindakan ini disarankan, tetapi tidak diperlukan untuk mengaktifkan akselerasi VM. Setelah menyelesaikan 4 langkah di atas, Anda dapat kembali ke codelab. Untuk menyelesaikan codelab ini, cukup instal Flutter untuk satu platform (Android atau iOS).

Pastikan Flutter SDK Anda berada dalam status yang tepat

Sebelum melanjutkan codelab ini, pastikan SDK Anda berada dalam status yang tepat. Jika Flutter SDK telah diinstal sebelumnya, gunakan flutter upgrade untuk memastikan bahwa SDK berada dalam status terbaru.

 flutter upgrade

Dengan menjalankan flutter upgrade, flutter doctor. akan otomatis dijalankan. Jika penginstalan Flutter ini baru saja dilakukan dan tidak memerlukan upgrade, jalankan flutter doctor secara manual. Anda akan mendapat laporan jika ada dependensi yang harus diinstal untuk menyelesaikan penyiapan. Anda dapat mengabaikan tanda centang yang tidak relevan bagi Anda (misalnya, Xcode jika Anda tidak bermaksud melakukan pengembangan untuk iOS).

 flutter doctor

Pertanyaan Umum (FAQ)

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?

Download aplikasi awal

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

...atau meng-clone 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

Menyiapkan project

Petunjuk berikut menganggap Anda menggunakan Android Studio (IntelliJ).

Membuka project

1. Buka Android Studio.

2. Jika Anda melihat layar selamat datang, klik Open an existing Android Studio project.

3 Buka direktori material-components-flutter-codelabs/mdc_100_series dan klik Open. Project akan terbuka. Anda dapat mengabaikan error yang Anda lihat di Analisis Dart sampai Anda telah berhasil satu kali mem-build project.

4. Jika diminta:

  • Instal update platform dan plugin atau FlutterRunConfigurationType.
  • Jika Dart atau Flutter SDK tidak dikonfigurasi, tetapkan jalur Flutter SDK untuk plugin Flutter.
  • Konfigurasikan framework Android.
  • Klik "Get dependencies" atau "Run ‘flutter packages get'".

lalu mulai ulang Android Studio.

Menjalankan aplikasi awal

Petunjuk berikut menganggap Anda melakukan pengujian di emulator atau perangkat Android, tetapi Anda juga dapat melakukan pengujian di Simulator atau perangkat iOS jika Anda telah menginstal Xcode.

1. Pilih perangkat atau emulator. Jika emulator Android belum berjalan, pilih Tools -> Android -> AVD Manager untuk membuat perangkat virtual dan memulai emulator. Jika AVD sudah ada, Anda dapat memulai emulator langsung dari pemilih perangkat di Android Studio, seperti yang ditunjukkan di langkah berikutnya. (Untuk Simulator iOS, jika belum berjalan, luncurkan simulator di mesin pengembangan Anda dengan memilih Flutter Device Selection -> Open iOS Simulator.)

2. Mulai aplikasi Flutter Anda:

  • Cari menu dropdown Flutter Device Selection di bagian atas layar editor Anda, lalu pilih perangkat (misalnya, iPhone SE atau Android SDK yang dibuat untuk <version>).
  • Tekan ikon Play ().

Berhasil! Anda seharusnya melihat halaman login Shrine dari codelab sebelumnya di simulator atau emulator.

Android

iOS

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 'package:meta/meta.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,
  })  : assert(currentCategory != null),
        assert(frontLayer != null),
        assert(backLayer != null),
        assert(frontTitle != null),
        assert(backTitle != null);

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

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

Paket meta sangat penting untuk menandai properti @required. Ini adalah praktik terbaik jika Anda memiliki properti di konstruktor yang tidak memiliki nilai default dan tidak dapat null dan, oleh karena itu, tidak boleh dilupakan. Perhatikan bahwa kita juga memiliki pernyataan setelah konstruktor yang memeriksa nilai yang diteruskan ke kolom tersebut, benar-benar bukan null.

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(
      brightness: Brightness.light,
      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 fungsi build() ShrineApp. Ubah home: menjadi Tampilan latar yang memiliki HomePage sebagai frontLayer:

      // TODO: Change home: to a Backdrop with a HomePage frontLayer (104)
      home: 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'),
      ),

Jika Anda menekan tombol Play, Anda akan melihat halaman beranda kita muncul, begitu juga panel aplikasi:

Android

iOS

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:

ad988a22875b5e82.png

Kini Anda dapat menyesuaikan desain dan konten kedua lapisan tersebut.

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,
    this.child,
  }) : super(key: key);

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Material(
      elevation: 16.0,
      shape: 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

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.

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)
  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: 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: 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: Icon(Icons.menu),
        onPressed: _toggleBackdropLayerVisibility,
      ),

Muat ulang, lalu ketuk tombol menu di simulator.

Android

iOS

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 {
  OneProductCardColumn({this.product});

  final Product product;

  @override
  Widget build(BuildContext context) {
    // TODO: Replace Column with a ListView (104)
    return ListView(
      physics: const ClampingScrollPhysics(),
      reverse: true,
      children: <Widget>[
        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

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: EdgeInsetsDirectional.only(start: 28.0),
            child: top != null
                ? ProductCard(
                    imageAspectRatio: imageAspectRatio,
                    product: top,
                  )
                : SizedBox(
                    height: heightOfCards,
                  ),
          ),
          SizedBox(height: spacerHeight),
          Padding(
            padding: 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

Tidak ada lagi tambahan.

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 'package:meta/meta.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,
  })  : assert(currentCategory != null),
        assert(onCategoryTap != null);

  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>[
            SizedBox(height: 16.0),
            Text(
              categoryString,
              style: theme.textTheme.bodyText1,
              textAlign: TextAlign.center,
            ),
            SizedBox(height: 14.0),
            Container(
              width: 70.0,
              height: 2.0,
              color: kShrinePink400,
            ),
          ],
        )
      : Padding(
        padding: 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: 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. Tekan alt (option) + enter.
  3. Pilih "Convert to StatefulWidget".
  4. Ubah class ShrineAppState menjadi pribadi (_ShrineAppState). Untuk melakukan ini dari menu utama IDE, pilih Refactor > Rename. Atau, dari dalam kode, Anda dapat menandai nama class, ShrineAppState, lalu klik kanan dan pilih Refactor > Rename. Masukkan _ShrineAppState untuk membuat class menjadi pribadi.

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

// TODO: Convert ShrineApp to stateful widget (104)
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 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'category_menu_page.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.

      home: 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: Text('SHRINE'),
        backTitle: Text('MENU'),
      ),

Muat ulang, lalu ketuk tombol Menu.

Android

iOS

Jika Anda mengetuk opsi menu, belum ada yang akan terjadi. Ayo perbaiki.

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

import 'package:flutter/material.dart';

import 'model/products_repository.dart';
import 'model/product.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});

  @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

Ketuk ikon menu untuk melihat produk. Produk berhasil difilter!

Menutup lapisan depan setelah pemilihan menu

Di backdrop.dart, tambahkan pengganti untuk fungsi didUpdateWidget() 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);
    }
  }

Hot reload, lalu 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
    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.

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

Di backdrop.dart, buat class _BackdropTitle baru.

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

  const _BackdropTitle({
    Key key,
    Listenable listenable,
    this.onPress,
    @required this.frontTitle,
    @required this.backTitle,
  })  : assert(frontTitle != null),
        assert(backTitle != null),
        super(key: key, listenable: listenable);

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

    return DefaultTextStyle(
      style: Theme.of(context).primaryTextTheme.headline6,
      softWrap: false,
      overflow: TextOverflow.ellipsis,
      child: Row(children: <Widget>[
        // branded icon
        SizedBox(
          width: 72.0,
          child: IconButton(
            padding: EdgeInsets.only(right: 8.0),
            onPressed: this.onPress,
            icon: Stack(children: <Widget>[
              Opacity(
                opacity: animation.value,
                child: ImageIcon(AssetImage('assets/slanted_menu.png')),
              ),
              FractionalTranslation(
                translation: Tween<Offset>(
                  begin: Offset.zero,
                  end: Offset(1.0, 0.0),
                ).evaluate(animation),
                child: 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: Interval(0.5, 1.0),
              ).value,
              child: FractionalTranslation(
                translation: Tween<Offset>(
                  begin: Offset.zero,
                  end: Offset(0.5, 0.0),
                ).evaluate(animation),
                child: backTitle,
              ),
            ),
            Opacity(
              opacity: CurvedAnimation(
                parent: animation,
                curve: Interval(0.5, 1.0),
              ).value,
              child: FractionalTranslation(
                translation: Tween<Offset>(
                  begin: 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 kustom, 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: Icon(
            Icons.search,
            semanticLabel: 'login', // New code
          ),
          onPressed: () {
            // TODO: Add open login (104)
            Navigator.push(
              context,
              MaterialPageRoute(builder: (BuildContext context) => LoginPage()),
            );
          },
        ),
        IconButton(
          icon: 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.

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 Flutter Widgets Catalog.

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

Untuk mempelajari cara menghubungkan aplikasi ke Firebase untuk backend yang berfungsi, lihat codelab Firebase for 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