Mem-build aplikasi Cupertino dengan Flutter

Selamat datang di codelab Cupertino Flutter.

Di codelab ini, Anda akan membuat aplikasi Cupertino (gaya iOS) menggunakan Flutter. Flutter SDK dilengkapi dengan dua library widget bergaya (selain library widget dasar):

  • Widget Material menerapkan bahasa desain Material untuk iOS, Android, web, dan desktop.
  • Widget Cupertino menerapkan bahasa desain iOS saat ini berdasarkan Human Interface Guidelines Apple.

Mengapa menulis aplikasi Cupertino? Bahasa desain Material dibuat untuk platform apa pun, bukan hanya Android. Ketika Anda menulis aplikasi Material di Flutter, aplikasi tersebut memiliki tampilan dan nuansa Material di semua perangkat, termasuk iOS. Jika Anda ingin aplikasi terlihat seperti aplikasi bergaya iOS standar, gunakan library Cupertino.

Secara teknis, Anda dapat menjalankan aplikasi Cupertino di Android atau iOS, tetapi (karena masalah pemberian lisensi) Cupertino tidak akan memiliki font yang benar di Android. Untuk alasan ini, gunakan perangkat khusus iOS ketika menulis aplikasi Cupertino.

Anda akan menerapkan aplikasi belanja bergaya Cupertino yang berisi tiga tab: satu untuk daftar produk, satu untuk penelusuran produk, dan satu untuk keranjang belanja.

f104a94356854c24.png 6f345bfa17663f9a.png

daf61aa9d823646a.png

Yang akan Anda pelajari di codelab ini

  • Cara membuat aplikasi Flutter dengan tampilan dan nuansa bergaya iOS.
  • Cara membuat beberapa tab dan menavigasi di antara tab tersebut.
  • Cara menggunakan paket provider untuk mengelola status di antara layar.

Apa yang ingin Anda pelajari dari codelab ini?

Saya baru mengenal topik ini, jadi saya ingin melihat ringkasan yang bagus. Saya sedikit paham soal topik ini, tetapi saya perlu mengingat kembali. Saya sedang mencari kode contoh untuk digunakan dalam project saya. Saya sedang mencari penjelasan tentang sesuatu yang spesifik.

Anda memerlukan dua software untuk menyelesaikan lab ini: Flutter SDK dan editor. Anda dapat menggunakan editor pilihan Anda, seperti Android Studio atau IntelliJ dengan plugin Flutter dan Dart yang sudah diinstal, atau Visual Studio Code dengan ekstensi Flutter dan Dart Code.

Anda dapat menjalankan codelab ini menggunakan salah satu perangkat berikut:

Anda juga memerlukan:

  • Mac yang dikonfigurasikan dengan Xcode.

Membuat aplikasi awal menggunakan CupertinoPageScaffold.

b2f84ff91b0e1396.pngBuat project Flutter yang disebut cupertino_store dan migrasikan ke keamanan null seperti berikut.

$ flutter create cupertino_store
$ cd cupertino_store
$ dart migrate --apply-changes

b2f84ff91b0e1396.png Ganti konten lib/main.dart. Hapus semua kode dari lib/main.dart, yang membuat aplikasi penghitung tombol bertema Material. Ganti dengan kode berikut, yang melakukan inisialisasi aplikasi Cupertino.

lib/main.dart

import 'package:flutter/cupertino.dart';

import 'app.dart';

void main() {
  return runApp(CupertinoStoreApp());
}

cf1e10b838bf60ee.png Pengamatan

  • Impor paket Cupertino. Tindakan ini menyediakan semua widget dan konstanta Cupertino untuk aplikasi Anda.

b2f84ff91b0e1396.png Buat lib/styles.dart. Tambahkan file ke direktori lib yang disebut styles.dart. Class Styles mendefinisikan pemberian gaya teks dan warna untuk menyesuaikan aplikasi. Berikut contoh filenya, tetapi Anda dapat memperoleh konten lengkapnya di GitHub: lib/styles.dart.

lib/styles.dart

// THIS IS A SAMPLE FILE. Get the full content at the link above.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';

abstract class Styles {
  static const TextStyle productRowItemName = TextStyle(
    color: Color.fromRGBO(0, 0, 0, 0.8),
    fontSize: 18,
    fontStyle: FontStyle.normal,
    fontWeight: FontWeight.normal,
  );

  static const TextStyle productRowTotal = TextStyle(
    color: Color.fromRGBO(0, 0, 0, 0.8),
    fontSize: 18,
    fontStyle: FontStyle.normal,
    fontWeight: FontWeight.bold,
  );

 // ...
// THIS IS A SAMPLE FILE. Get the full content at the link above.

cf1e10b838bf60ee.png Pengamatan

  • Kita dapat memusatkan definisi gaya dengan cara yang serupa dengan cara developer web memusatkan markup gaya mereka di file CSS dengan mengelompokkan semua definisi kita dalam satu file. Tindakan ini memberi kita cara termudah untuk menggunakan kembali dan menentukan kembali gaya di seluruh aplikasi.

b2f84ff91b0e1396.png Buat lib/app.dart dan tambahkan class CupertinoStoreApp. Tambahkan class CupertinoStoreApp berikut ke lib/app.dart.

lib/app.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';

class CupertinoStoreApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // This app is designed only to work vertically, so we limit
    // orientations to portrait up and down.
    SystemChrome.setPreferredOrientations(
        [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);

    return CupertinoApp(
      theme: const CupertinoThemeData(brightness: Brightness.light),
      home: CupertinoStoreHomePage(),
    );
  }
}

cf1e10b838bf60ee.png Pengamatan

  • Impor services library. Tindakan ini menyediakan layanan platform, seperti papan klip dan menyetel orientasi perangkat, untuk aplikasi Anda.
  • Buat instance CupertinoApp, yang memberikan tema, navigasi, arah teks, dan default lainnya yang diperlukan untuk membuat aplikasi yang diharapkan pengguna iOS.
  • Buat instance CupertinoStoreHomePage sebagai halaman beranda.
  • Aplikasi ini dirancang hanya untuk berfungsi secara vertikal, sehingga orientasi perangkat dibatasi ke potret.

b2f84ff91b0e1396.png Tambahkan class CupertinoStoreHomePage. Tambahkan class CupertinoStoreHomePage berikut ke lib/app.dart untuk membuat tata letak untuk halaman beranda.

lib/app.dart

class CupertinoStoreHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text('Cupertino Store'),
      ),
      child: SizedBox(),
    );
  }
}

cf1e10b838bf60ee.png Pengamatan

  • Paket Cupertino memberikan dua jenis scaffold halaman. CupertinoPageScaffold mendukung satu halaman dan menerima menu navigasi bergaya Cupertino, warna latar belakang, dan menampung hierarki widget untuk halaman. Anda akan mempelajari jenis scaffold kedua di langkah berikutnya.
  • Halaman ini memiliki judul, dan hierarki widget-nya berisi satu container kosong.

b2f84ff91b0e1396.png Perbarui file pubspec.yaml. Di bagian atas project, edit file pubspec.yaml. Tambahkan library yang akan Anda perlukan dan daftar aset gambar. Berikut adalah contoh file, temukan konten lengkapnya di GitHub: pubspec.yaml.

pubspec.yaml

# THIS IS A SAMPLE OF THE FILE. Get the full file at the link above.
name: cupertino_store
description: Creating a Store in Cupertino widgets
publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1

environment:
  sdk: ">=2.12.0 <3.0.0"

dependencies:
  cupertino_icons: ^1.0.2
  flutter:
    sdk: flutter
  intl: ^0.17.0
  provider: ^5.0.0
  shrine_images: ^2.0.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  pedantic: ^1.11.0

flutter:
  assets:
    - packages/shrine_images/0-0.jpg
# THIS IS A SAMPLE OF THE FILE. Get the full file at the link above.

cf1e10b838bf60ee.png Pengamatan

  • Hal ini menarik beberapa paket, termasuk shrine_images, yang berisi produk untuk mengisi toko.
  • Paket provider memberikan cara sederhana untuk mengelola status di seluruh layar.
  • Paket intl memberikan fasilitas pelokalan dan internasionalisasi.
  • Paket cupertino_icons berisi aset ikon untuk widget Cupertino.

b2f84ff91b0e1396.png Jalankan aplikasi. Anda akan melihat layar putih berikut yang berisi menu navigasi dan judul Cupertino:

5705e4da178665a5.png

Terjadi masalah?

Jika aplikasi tidak berjalan dengan baik, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

Aplikasi akhir menampilkan 3 tab:

  • Daftar produk
  • Penelusuran produk
  • Keranjang belanja

Di langkah ini, Anda akan memperbarui halaman beranda dengan tiga tab menggunakan CupertinoTabScaffold. Anda juga akan menambahkan sumber data yang memberikan daftar item yang dijual, dengan foto dan harga.

Di langkah sebelumnya, Anda membuat class CupertinoStoreHomePage menggunakan CupertinoPageScaffold. Gunakan scaffold ini untuk halaman yang tidak memiliki tab. Aplikasi final memiliki tiga tab, ganti CupertinoPageScaffold dengan CupertinoTabScaffold.

Tab Cupertino memiliki scaffold terpisah karena di iOS, tab bawah biasanya ada di atas rute bertingkat, bukan di dalam halaman.

b2f84ff91b0e1396.png Perbarui lib/app.dart. Ganti class CupertinoStoreHomePage dengan kode berikut, yang menyiapkan scaffold 3 tab:

lib/app.dart

class CupertinoStoreHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CupertinoTabScaffold(
      tabBar: CupertinoTabBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.home),
            label: 'Products',
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.search),
            label: 'Search',
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.shopping_cart),
            label: 'Cart',
          ),
        ],
      ),
      tabBuilder: (context, index) {
        late final CupertinoTabView returnValue;
        switch (index) {
          case 0:
            returnValue = CupertinoTabView(builder: (context) {
              return CupertinoPageScaffold(
                child: ProductListTab(),
              );
            });
            break;
          case 1:
            returnValue = CupertinoTabView(builder: (context) {
              return CupertinoPageScaffold(
                child: SearchTab(),
              );
            });
            break;
          case 2:
            returnValue = CupertinoTabView(builder: (context) {
              return CupertinoPageScaffold(
                child: ShoppingCartTab(),
              );
            });
            break;
        }
        return returnValue;
      },
    );
  }
}

cf1e10b838bf60ee.png Pengamatan

  • CupertinoTabBar memerlukan setidaknya dua item, atau Anda akan melihat error saat runtime.
  • tabBuilder: bertanggungjawab memastikan tab yang ditentukan dibuat. Dalam hal ini, memanggil konstruktor class untuk menyiapkan setiap tab, menggabungkan ketiganya di CupertinoTabView dan CupertinoPageScaffold.

b2f84ff91b0e1396.png Tambahkan class stub untuk konten tab baru. Buat file lib/product_list_tab.dart untuk tab pertama yang mengompilasi dengan rapi, tetapi hanya menampilkan layar putih. Gunakan konten berikut:

lib/product_list_tab.dart

import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';

import 'model/app_state_model.dart';

class ProductListTab extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<AppStateModel>(
      builder: (context, model, child) {
        return const CustomScrollView(
          slivers: <Widget>[
            CupertinoSliverNavigationBar(
              largeTitle: Text('Cupertino Store'),
            ),
          ],
        );
      },
    );
  }
}

cf1e10b838bf60ee.png Pengamatan

  • Tab daftar produk adalah widget stateless.
  • Consumer, dari paket provider, membantu pengelolaan status. Selengkapnya mengenai model nanti.
  • Ada 2 varian menu navigasi di iOS. Jenis statis pendek umum yang terlihat sejak iOS 1, dan jenis judul besar dan tinggi yang dapat di-scroll yang diperkenalkan di iOS 11. Halaman ini menerapkan yang terakhir disebut di dalam CustomScrollView dengan widget CupertinoSliverNavigationBar.

b2f84ff91b0e1396.png Tambahkan stub halaman penelusuran. Buat file lib/search_tab.dart yang mengompilasi dengan rapi, tetapi hanya menampilkan layar putih. Gunakan konten berikut:

lib/search_tab.dart

import 'package:flutter/cupertino.dart';

class SearchTab extends StatefulWidget {
  @override
  _SearchTabState createState() {
    return _SearchTabState();
  }
}

class _SearchTabState extends State<SearchTab> {
  @override
  Widget build(BuildContext context) {
    return const CustomScrollView(
      slivers: <Widget>[
        CupertinoSliverNavigationBar(
          largeTitle: Text('Search'),
        ),
      ],
    );
  }
}

cf1e10b838bf60ee.png Pengamatan

  • Tab penelusuran adalah widget stateful karena daftar hasil berubah seiring dengan pengguna melakukan penelusuran.

b2f84ff91b0e1396.png Tambahkan stub halaman keranjang belanja. Buat file lib/shopping_cart_tab.dart yang mengompilasi dengan rapi, tetapi hanya menampilkan layar putih. Gunakan konten berikut:

lib/shopping_cart_tab.dart

import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';

import 'model/app_state_model.dart';

class ShoppingCartTab extends StatefulWidget {
  @override
  _ShoppingCartTabState createState() {
    return _ShoppingCartTabState();
  }
}

class _ShoppingCartTabState extends State<ShoppingCartTab> {
  @override
  Widget build(BuildContext context) {
    return Consumer<AppStateModel>(
      builder: (context, model, child) {
        return const CustomScrollView(
          slivers: <Widget>[
            CupertinoSliverNavigationBar(
              largeTitle: Text('Shopping Cart'),
            ),
          ],
        );
      },
    );
  }
}

cf1e10b838bf60ee.png Pengamatan

  • Tab keranjang belanja adalah widget stateful karena mempertahankan daftar pembelian dan info pelanggan.
  • Halaman ini juga menggunakan CustomScrollView.

b2f84ff91b0e1396.png Perbarui lib/app.dart. Perbarui pernyataan impor di lib/app.dart untuk menarik widget tab baru:

lib/app.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'product_list_tab.dart';   // NEW
import 'search_tab.dart';         // NEW
import 'shopping_cart_tab.dart';  // NEW

Di bagian kedua langkah ini, lanjutkan ke halaman berikutnya, Anda akan menambahkan kode untuk mengelola dan membagikan status di seluruh tab.

Aplikasi memiliki data umum yang perlu dibagikan dengan beberapa layar, sehingga Anda perlu cara sederhana untuk mengalirkan data ke setiap objek yang memerlukannya. Paket provider memberikan cara mudah untuk melakukannya. Di provider, Anda menentukan model data, kemudian menggunakan ChangeNotifierProvider untuk menyediakan model data ke bawah hierarki.

b2f84ff91b0e1396.png Buat class model data. Buat direktori model di bawah lib. Tambahkan file lib/model/product.dart yang menentukan data produk yang berasal dari sumber data:

lib/model/product.dart

enum Category {
  all,
  accessories,
  clothing,
  home,
}

class Product {
  const Product({
    required this.category,
    required this.id,
    required this.isFeatured,
    required this.name,
    required this.price,
  });

  final Category category;
  final int id;
  final bool isFeatured;
  final String name;
  final int price;

  String get assetName => '$id-0.jpg';
  String get assetPackage => 'shrine_images';

  @override
  String toString() => '$name (id=$id)';
}

cf1e10b838bf60ee.png Pengamatan

  • Setiap instance class Produk mendeskripsikan produk yang dijual.

Class ProductsRepository berisi daftar lengkap produk yang dijual, bersama dengan harga, teks judul, dan kategori produk tersebut. Aplikasi kita tidak akan melakukan apa pun dengan properti isFeatured. Class juga menyertakan metode loadProducts() yang menampilkan semua produk, atau semua produk dalam kategori tertentu.

b2f84ff91b0e1396.png Buat repositori produk. Buat file lib/model/products_repository.dart. File ini berisi semua produk yang dijual. Setiap produk termasuk dalam suatu kategori. Berikut contoh filenya, tetapi Anda dapat memperoleh seluruh konten di GitHub: products_repository.dart.

lib/model/products_repository.dart

// THIS IS A SAMPLE FILE. Get the full content at the link above.

import 'product.dart';

class ProductsRepository {
 static const _allProducts = <Product>[
   Product(
     category: Category.accessories,
     id: 0,
     isFeatured: true,
     name: 'Vagabond sack',
     price: 120,
   ),
   Product(
     category: Category.home,
     id: 9,
     isFeatured: true,
     name: 'Gilt desk trio',
     price: 58,
   ),
   Product(
     category: Category.clothing,
     id: 33,
     isFeatured: true,
     name: 'Cerise scallop tee',
     price: 42,
   ),
   // THIS IS A SAMPLE FILE. Get the full content at the link above.
 ];

 static List<Product> loadProducts(Category category) {
   if (category == Category.all) {
     return _allProducts;
   } else {
     return _allProducts.where((p) => p.category == category).toList();
   }
 }
}

cf1e10b838bf60ee.png Observations

  • Dalam hal ini, kita membuat database produk palsu untuk memudahkan pengembangan, tetapi ini harus disalurkan ke aplikasi sebagai API. Cara mudah untuk melakukan hal ini yang menangani realitas ponsel yang sebagian terputus adalah Cloud Firestore.

Kini Anda siap untuk menentukan model. Buat file lib/model/app_state_model.dart. Di class AppStateModel, berikan metode untuk mengakses data dari model tersebut. Misalnya, tambahkan metode untuk mengakses total keranjang belanja, metode lain untuk daftar produk terpilih yang akan dibeli, metode lain untuk biaya pengiriman, dan sebagainya.

b2f84ff91b0e1396.png Buat class model. Berikut adalah daftar tanda tangan metode yang diberikan class ini. Dapatkan konten lengkapnya di GitHub: lib/model/app_state_model.dart.

lib/model/app_state_model.dart

// THIS IS A SAMPLE FILE ONLY. Get the full content at the link above.

import 'package:flutter/foundation.dart' as foundation;

import 'product.dart';
import 'products_repository.dart';

double _salesTaxRate = 0.06;
double _shippingCostPerItem = 7;

class AppStateModel extends foundation.ChangeNotifier {
 List<Product> _availableProducts = [];
 Category _selectedCategory = Category.all;
 final _productsInCart = <int, int>{};

 Map<int, int> get productsInCart
 int get totalCartQuantity
 Category get selectedCategory
 double get subtotalCost
 double get shippingCost

 double get tax
 double get totalCost
 List<Product> getProducts()
 List<Product> search(String searchTerms)
 void addProductToCart(int productId)
 void removeItemFromCart(int productId)
 Product getProductById(int id)
 void clearCart()
 void loadProducts()
 void setCategory(Category newCategory)
// THIS IS A SAMPLE FILE ONLY. Get the full content at the link above.

cf1e10b838bf60ee.png Observations

  • AppStateModel menunjukkan cara memusatkan status aplikasi, dan membuat status tersebut tersedia di seluruh aplikasi. Di langkah berikutnya, kita akan menggunakan status ini untuk mendorong fungsi Penelusuran dan Keranjang Belanja.

b2f84ff91b0e1396.png Perbarui lib/main.dart. Di metode main(), lakukan inisialisasi model. Tambahkan baris yang ditandai dengan NEW.

lib/main.dart

import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';             // NEW

import 'app.dart';
import 'model/app_state_model.dart';                 // NEW

void main() {
 return runApp(
   ChangeNotifierProvider<AppStateModel>(            // NEW
     create: (_) => AppStateModel()..loadProducts(), // NEW
     child: CupertinoStoreApp(),                     // NEW
   ),
 );
}

cf1e10b838bf60ee.png Observations

  • Kita sedang menyiapkan AppStateModel di atas hierarki widget untuk menyediakannya ke seluruh aplikasi.
  • Kita menggunakan ChangeNotifierProvider dari paket provider, yang memantau AppStateModel jika ada notifikasi perubahan.

b2f84ff91b0e1396.png Jalankan aplikasi. Anda akan melihat layar putih berikut yang berisi menu navigasi Cupertino, judul, dan panel samping dengan 3 ikon berlabel yang mewakili tiga tab. Anda dapat beralih antara tab, tetapi ketiga halaman saat ini kosong.

35520995039d98a6.png

Terjadi masalah?

Jika aplikasi tidak berjalan dengan baik, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

Di langkah ini, tampilkan produk yang dijual di tab daftar produk.

b2f84ff91b0e1396.png Tambahkan lib/product_row_item.dart untuk menampilkan produk. Buat lib/product_row_item.dart file, dengan konten berikut:

lib/product_row_item.dart

import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';

import 'model/app_state_model.dart';
import 'model/product.dart';
import 'styles.dart';

class ProductRowItem extends StatelessWidget {
  const ProductRowItem({
    required this.product,
    required this.lastItem,
  });

  final Product product;
  final bool lastItem;

  @override
  Widget build(BuildContext context) {
    final row = SafeArea(
      top: false,
      bottom: false,
      minimum: const EdgeInsets.only(
        left: 16,
        top: 8,
        bottom: 8,
        right: 8,
      ),
      child: Row(
        children: <Widget>[
          ClipRRect(
            borderRadius: BorderRadius.circular(4),
            child: Image.asset(
              product.assetName,
              package: product.assetPackage,
              fit: BoxFit.cover,
              width: 76,
              height: 76,
            ),
          ),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 12),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.start,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text(
                    product.name,
                    style: Styles.productRowItemName,
                  ),
                  const Padding(padding: EdgeInsets.only(top: 8)),
                  Text(
                    '\$${product.price}',
                    style: Styles.productRowItemPrice,
                  )
                ],
              ),
            ),
          ),
          CupertinoButton(
            padding: EdgeInsets.zero,
            onPressed: () {
              final model = Provider.of<AppStateModel>(context, listen: false);
              model.addProductToCart(product.id);
            },
            child: const Icon(
              CupertinoIcons.plus_circled,
              semanticLabel: 'Add',
            ),
          ),
        ],
      ),
    );

    if (lastItem) {
      return row;
    }

    return Column(
      children: <Widget>[
        row,
        Padding(
          padding: const EdgeInsets.only(
            left: 100,
            right: 16,
          ),
          child: Container(
            height: 1,
            color: Styles.productRowDivider,
          ),
        ),
      ],
    );
  }
}

cf1e10b838bf60ee.png Observations

  • CupertinoSliverNavigationBar adalah cara kita mendapatkan judul perluasan gaya iOS 11 di menu navigasi. Hal ini penting untuk membuat pengguna iOS merasa nyaman dengan aplikasi tersebut.
  • Ada banyak kerumitan di file ini, karena kita mengemulasikan tampilan dan nuansa aplikasi iOS yang sangat halus. Kelebihan Flutter adalah kita dapat membuat perubahan ini di editor dan melihatnya berubah hampir secara real-time berkat Hot Reload Stateful.

b2f84ff91b0e1396.png Di lib/product_list_tab.dart, impor file product_row_item.dart.

lib/product_list_tab.dart

import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';

import 'model/app_state_model.dart';
import 'product_row_item.dart';      // NEW

b2f84ff91b0e1396.png Di metode build() untuk ProductListTab, dapatkan daftar produk dan jumlah produk. Tambahkan baris baru yang ditunjukkan di bawah:

class ProductListTab extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return CupertinoPageScaffold(
     child: Consumer<AppStateModel>(
       builder: (context, child, model) {
         final products = model.getProducts();  // NEW
         return CustomScrollView(
           semanticChildCount: products.length, // NEW
           slivers: <Widget>[
             CupertinoSliverNavigationBar(
               largeTitle: const Text('Cupertino Store'),
             ),
           ],
         );
       },
     ),
   );
 }
}

b2f84ff91b0e1396.png Selain itu, di metode build(), tambahkan sliver baru ke daftar widget sliver untuk menampung daftar produk. Tambahkan baris baru yang ditunjukkan di bawah:

class ProductListTab extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<AppStateModel>(
      builder: (context, model, child) {
        final products = model.getProducts();
        return CustomScrollView(
          semanticChildCount: products.length,
          slivers: <Widget>[
            const CupertinoSliverNavigationBar(
              largeTitle: Text('Cupertino Store'),
            ),
            SliverSafeArea(   // BEGINNING OF NEW CONTENT
              top: false,
              minimum: const EdgeInsets.only(top: 8),
              sliver: SliverList(
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    if (index < products.length) {
                      return ProductRowItem(
                        product: products[index],
                        lastItem: index == products.length - 1,
                      );
                    }

                    return null;
                  },
                ),
              ),
            )     // END OF NEW CONTENT
          ],
        );
      },
    );
  }
}

cf1e10b838bf60ee.png Observations

  • Notch dipertimbangkan oleh silver pertama (CupertinoSliverNavigationBar).
  • Silver baru dan silver pertama adalah kerabat (bukan induk-turunan), jadi silver pertama tidak mungkin berkomunikasi bahwa silver pertama sudah menggunakan notch. Oleh karena itu, silver kedua menetapkan properti top SliverSafeArea ke false sehingga notch diabaikan.
  • Properti left dan right pada SliverSafeArea tetap setelan default true jika ponsel diputar, dan tetap mempertimbangkan bottom sehingga sliver dapat men-scroll melebihi panel beranda bawah untuk menghindari gangguan ketika di-scroll ke akhir.
  • Logika ini tidak diwajibkan secara spesifik di sini karena aplikasi dibatasi agar hanya berada di posisi potret. Namun, menyertakannya menunjukkan bahwa kode ini aman untuk digunakan kembali di aplikasi yang menangani presentasi horizontal.

b2f84ff91b0e1396.png Jalankan aplikasi. Di tab produk, Anda akan melihat daftar produk dengan gambar, harga, dan tombol dengan tanda plus yang menambahkan produk ke keranjang belanja. Tombol ini akan diterapkan nanti, di langkah ketika Anda membuat keranjang belanja.

f104a94356854c24.png

Terjadi masalah?

Jika aplikasi tidak berjalan dengan baik, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

Di langkah ini, Anda akan membuat tab penelusuran dan menambahkan kemampuan untuk menelusuri produk.

b2f84ff91b0e1396.png Perbarui impor di lib/search_tab.dart.

Tambahkan impor untuk class yang akan digunakan tab penelusuran:

lib/search_tab.dart

import 'package:flutter/cupertino.dart'
import 'package:provider/provider.dart'
import 'model/app_state_model.dart'
import 'product_row_item.dart'
import 'search_bar.dart'
import 'styles.dart'

b2f84ff91b0e1396.png Perbarui metode build() di _SearchTabState.

Lakukan inisialisasi model dan ganti CustomScrollView dengan komponen individu untuk menelusuri dan memasukkan ke daftar.

class _SearchTabState extends State<SearchTab> {
// ...

  @override
  Widget build(BuildContext context) {
    final model = Provider.of<AppStateModel>(context);
    final results = model.search(_terms);

    return DecoratedBox(
      decoration: const BoxDecoration(
        color: Styles.scaffoldBackground,
      ),
      child: SafeArea(
        child: Column(
          children: [
            _buildSearchBox(),
            Expanded(
              child: ListView.builder(
                itemBuilder: (context, index) => ProductRowItem(
                  product: results[index],
                  lastItem: index == results.length - 1,
                ),
                itemCount: results.length,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

cf1e10b838bf60ee.png Observations

  • Kita sedang membuat pengalaman penelusuran gaya iOS, tetapi kita bebas menyesuaikan pengalaman pengguna.

b2f84ff91b0e1396.png Tambahkan variabel, fungsi, dan metode yang mendukung ke class _SearchTabState.

Hal ini termasuk initState(), dispose(), _onTextChanged(), dan _buildSearchBox(), seperti yang ditunjukkan di bawah:

class _SearchTabState extends State<SearchTab> {
  late final TextEditingController _controller;
  late final FocusNode _focusNode;
  String _terms = '';

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController()..addListener(_onTextChanged);
    _focusNode = FocusNode();
  }

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

  void _onTextChanged() {
    setState(() {
      _terms = _controller.text;
    });
  }

  Widget _buildSearchBox() {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: SearchBar(
        controller: _controller,
        focusNode: _focusNode,
      ),
    );
  }    // TO HERE

 @override
 Widget build(BuildContext context) {

cf1e10b838bf60ee.png Observations

  • _SearchTabState adalah tempat Anda menyimpan status khusus penelusuran. Dalam penerapan ini, kita menyimpan istilah penelusuran, dan kami menghubungkannya dengan AppStateModel untuk memenuhi kemampuan penelusuran. Jika kami menerapkan backend API, ini adalah tempat yang baik untuk mengakses jaringan Penelusuran.

b2f84ff91b0e1396.png Tambahkan class SearchBar.

Buat file baru, lib/search_bar.dart. Class SearchBar menangani penelusuran sebenarnya di daftar produk. Tambahkan konten berikut ke file:

lib/search_bar.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'styles.dart';

class SearchBar extends StatelessWidget {
  const SearchBar({
    required this.controller,
    required this.focusNode,
  });

  final TextEditingController controller;
  final FocusNode focusNode;

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: BoxDecoration(
        color: Styles.searchBackground,
        borderRadius: BorderRadius.circular(10),
      ),
      child: Padding(
        padding: const EdgeInsets.symmetric(
          horizontal: 4,
          vertical: 8,
        ),
        child: Row(
          children: [
            const Icon(
              CupertinoIcons.search,
              color: Styles.searchIconColor,
            ),
            Expanded(
              child: CupertinoTextField(
                controller: controller,
                focusNode: focusNode,
                style: Styles.searchText,
                cursorColor: Styles.searchCursorColor,
                decoration: null,
              ),
            ),
            GestureDetector(
              onTap: controller.clear,
              child: const Icon(
                CupertinoIcons.clear_thick_circled,
                color: Styles.searchIconColor,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

cf1e10b838bf60ee.png Observations

  • Antarmuka penelusuran di iOS menarik karena terdapat variasi penerapan yang cukup luas. Flutter memberi Anda kemampuan untuk menyetel tata letak dan warna penerapan dengan cepat dan mudah.

b2f84ff91b0e1396.png Jalankan aplikasi. Pilih tab penelusuran dan masukkan "kemeja" ke kolom teks. Anda akan melihat daftar 5 produk yang terdapat "kemeja" di namanya.

6f345bfa17663f9a.png

Terjadi masalah?

Jika aplikasi tidak berjalan dengan baik, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

Di tiga langkah berikutnya, Anda akan membuat tab keranjang belanja. Di langkah pertama ini, Anda akan menambahkan kolom untuk menangkap info pelanggan.

b2f84ff91b0e1396.png Perbarui file lib/shopping_cart_tab.dart.

Tambahkan metode pribadi untuk membuat kolom nama, email, dan lokasi. Kemudian tambahkan metode _buildSliverChildBuildDelegate() yang membuat bagian dari antarmuka pengguna.

lib/shopping_cart_tab.dart

class _ShoppingCartTabState extends State<ShoppingCartTab> {
  String? Name;    // ADD FROM HERE
  String? email;
  String? location;
  String? pin;
  DateTime dateTime = DateTime.now();

  Widget _buildNameField() {
    return CupertinoTextField(
      prefix: const Icon(
        CupertinoIcons.person_solid,
        color: CupertinoColors.lightBackgroundGray,
        size: 28,
      ),
      padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 12),
      clearButtonMode: OverlayVisibilityMode.editing,
      textCapitalization: TextCapitalization.words,
      autocorrect: false,
      decoration: const BoxDecoration(
        border: Border(
          bottom: BorderSide(
            width: 0,
            color: CupertinoColors.inactiveGray,
          ),
        ),
      ),
      placeholder: 'Name',
      onChanged: (newName) {
        setState(() {
          name = newName;
        });
      },
    );
  }

  Widget _buildEmailField() {
    return const CupertinoTextField(
      prefix: Icon(
        CupertinoIcons.mail_solid,
        color: CupertinoColors.lightBackgroundGray,
        size: 28,
      ),
      padding: EdgeInsets.symmetric(horizontal: 6, vertical: 12),
      clearButtonMode: OverlayVisibilityMode.editing,
      keyboardType: TextInputType.emailAddress,
      autocorrect: false,
      decoration: BoxDecoration(
        border: Border(
          bottom: BorderSide(
            width: 0,
            color: CupertinoColors.inactiveGray,
          ),
        ),
      ),
      placeholder: 'Email',
    );
  }

  Widget _buildLocationField() {
    return const CupertinoTextField(
      prefix: Icon(
        CupertinoIcons.location_solid,
        color: CupertinoColors.lightBackgroundGray,
        size: 28,
      ),
      padding: EdgeInsets.symmetric(horizontal: 6, vertical: 12),
      clearButtonMode: OverlayVisibilityMode.editing,
      textCapitalization: TextCapitalization.words,
      decoration: BoxDecoration(
        border: Border(
          bottom: BorderSide(
            width: 0,
            color: CupertinoColors.inactiveGray,
          ),
        ),
      ),
      placeholder: 'Location',
    );
  }

  SliverChildBuilderDelegate _buildSliverChildBuilderDelegate(
      AppStateModel model) {
    return SliverChildBuilderDelegate(
      (context, index) {
        switch (index) {
          case 0:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildNameField(),
            );
          case 1:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildEmailField(),
            );
          case 2:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildLocationField(),
            );
          default:
          // Do nothing. For now.
        }
        return null;
      },
    );
  }    // TO HERE

cf1e10b838bf60ee.png Observations

  • Cara utama yang membuat Flutter berbeda dari lingkungan desain Antarmuka tradisional lainnya adalah Anda memiliki kekuatan penuh bahasa pemrograman yang tepat untuk mengenalkan abstraksi. Anda dapat menambahkan fungsi ke grup fungsi, atau mengubahnya menjadi Widget yang berdiri sendiri jika Anda ingin dengan mudah menggunakannya kembali. Sebagai programmer, Anda dapat memilih cara untuk mengatur fungsi.

b2f84ff91b0e1396.png Perbarui metode build() di class _ShoppingCartTabState.

Tambahkan SliverSafeArea yang memanggil metode _buildSliverChildBuilderDelegate:

  @override
  Widget build(BuildContext context) {
    return Consumer<AppStateModel>(
      builder: (context, model, child) {
        return CustomScrollView(
          slivers: <Widget>[
            const CupertinoSliverNavigationBar(
              largeTitle: Text('Shopping Cart'),
            ),
            SliverSafeArea(
              top: false,
              minimum: const EdgeInsets.only(top: 4),
              sliver: SliverList(
                delegate: _buildSliverChildBuilderDelegate(model),
              ),
            )
          ],
        );
      },
    );
  }
}

cf1e10b838bf60ee.png Observations

  • Dengan semua antarmuka pengguna yang ditentukan di fungsi builder, metode build dapat menjadi cukup kecil.

b2f84ff91b0e1396.png Jalankan aplikasi. Pilih tab keranjang belanja. Anda akan melihat tiga kolom teks untuk mengumpulkan informasi pelanggan:

bcb97c1aff65d3d7.png

Terjadi masalah?

Jika aplikasi tidak berjalan dengan baik, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

Di langkah ini, tambahkan CupertinoDatePicker ke keranjang belanja sehingga pengguna dapat memilih tanggal pengiriman yang dipilih.

b2f84ff91b0e1396.png Tambahkan impor dan const ke lib/shopping_cart_tab.dart.

Tambahkan baris baru, seperti yang ditunjukkan:

lib/shopping_cart_tab.dart

import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';            // NEW
import 'package:provider/provider.dart';
import 'model/app_state_model.dart';
import 'styles.dart';                       // NEW

const double _kDateTimePickerHeight = 216;  // NEW

b2f84ff91b0e1396.png Tambahkan fungsi _buildDateAndTimePicker() ke widget _ShoppingCartTab.

Tambahkan fungsi, seperti berikut:

class _ShoppingCartTabState extends State<ShoppingCartTab> {
  // ...

  Widget _buildDateAndTimePicker(BuildContext context) {
    // NEW FROM HERE
    return Column(
      children: <Widget>[
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: <Widget>[
            Row(
              mainAxisAlignment: MainAxisAlignment.start,
              children: const <Widget>[
                Icon(
                  CupertinoIcons.clock,
                  color: CupertinoColors.lightBackgroundGray,
                  size: 28,
                ),
                SizedBox(width: 6),
                Text(
                  'Delivery time',
                  style: Styles.deliveryTimeLabel,
                ),
              ],
            ),
            Text(
              DateFormat.yMMMd().add_jm().format(dateTime),
              style: Styles.deliveryTime,
            ),
          ],
        ),
        Container(
          height: _kDateTimePickerHeight,
          child: CupertinoDatePicker(
            mode: CupertinoDatePickerMode.dateAndTime,
            initialDateTime: dateTime,
            onDateTimeChanged: (newDateTime) {
              setState(() {
                dateTime = newDateTime;
              });
            },
          ),
        ),
      ],
    );
  }    // TO HERE

SliverChildBuilderDelegate _buildSliverChildBuilderDelegate(
   AppStateModel model) {
  // ...

cf1e10b838bf60ee.png Observations

  • Menambahkan CupertinoDatePicker tidak memerlukan banyak waktu, dan memberi pengguna iOS cara yang intuitif untuk memasukkan tanggal dan waktu.

b2f84ff91b0e1396.png Tambahkan panggilan untuk membuat UI tanggal dan waktu, ke fungsi _buildSliverChildBuilderDelegate. Tambahkan kode baru, seperti yang ditunjukkan:

  SliverChildBuilderDelegate _buildSliverChildBuilderDelegate(
      AppStateModel model) {
    return SliverChildBuilderDelegate(
      (context, index) {
        switch (index) {
          case 0:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildNameField(),
            );
          case 1:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildEmailField(),
            );
          case 2:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildLocationField(),
            );
          case 3:                // ADD FROM HERE
            return Padding(
              padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
              child: _buildDateAndTimePicker(context),
            );                   // TO HERE
          default:
          // Do nothing. For now.
        }
        return null;
      },
    );
  }

b2f84ff91b0e1396.png Jalankan aplikasi. Pilih tab keranjang belanja. Anda akan melihat alat pilih tanggal bergaya iOS di bawah kolom teks untuk mengumpulkan info pelanggan:

ecd9ef206f1e86c7.png

Terjadi masalah?

Jika aplikasi tidak berjalan dengan baik, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

Di langkah ini, tambahkan item yang dipilih ke keranjang belanja untuk menyelesaikan aplikasi.

b2f84ff91b0e1396.png Impor paket produk di shopping_cart_tab.dart.

lib/shopping_cart_tab.dart

import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'model/app_state_model.dart';
import 'model/product.dart';              // NEW
import 'styles.dart';

b2f84ff91b0e1396.png Tambahkan format mata uang ke class _ShoppingCartTabState.

Tambahkan baris baru yang ditandai dengan NEW:

class _ShoppingCartTabState extends State<ShoppingCartTab> {
  String? name;
  String? email;
  String? location;
  String? pin;
  DateTime dateTime = DateTime.now();
  final _currencyFormat = NumberFormat.currency(symbol: '\$'); // NEW

b2f84ff91b0e1396.png Tambahkan indeks produk ke fungsi _buildSliverChildBuilderDelegate.

Tambahkan baris baru yang ditandai dengan NEW:

SliverChildBuilderDelegate _buildSliverChildBuilderDelegate(
   AppStateModel model) {
 return SliverChildBuilderDelegate(
   (context, index) {
     final productIndex = index - 4;    // NEW
     switch (index) {
  // ...

b2f84ff91b0e1396.png Di fungsi yang sama, tampilkan item yang akan dibeli.

Tambahkan kode ke bagian default: dari pernyataan tombol, seperti berikut:

switch (index) {
 case 0:
   return Padding(
     padding: const EdgeInsets.symmetric(horizontal: 16),
     child: _buildNameField(),
   );
 case 1:
   return Padding(
     padding: const EdgeInsets.symmetric(horizontal: 16),
     child: _buildEmailField(),
   );
 case 2:
   return Padding(
     padding: const EdgeInsets.symmetric(horizontal: 16),
     child: _buildLocationField(),
   );
 case 3:
   return Padding(
     padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
     child: _buildDateAndTimePicker(context),
   );
 default:                      // NEW FROM HERE
   if (model.productsInCart.length > productIndex) {
     return ShoppingCartItem(
       index: index,
       product: model.getProductById(
           model.productsInCart.keys.toList()[productIndex]),
       quantity: model.productsInCart.values.toList()[productIndex],
       lastItem: productIndex == model.productsInCart.length - 1,
       formatter: _currencyFormat,
     );
   } else if (model.productsInCart.keys.length == productIndex &&
       model.productsInCart.isNotEmpty) {
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 20),
       child: Row(
         mainAxisAlignment: MainAxisAlignment.end,
         children: <Widget>[
           Column(
             crossAxisAlignment: CrossAxisAlignment.end,
             children: <Widget>[
               Text(
                 'Shipping '
                 '${_currencyFormat.format(model.shippingCost)}',
                  style: Styles.productRowItemPrice,
               ),
               const SizedBox(height: 6),
               Text(
                 'Tax ${_currencyFormat.format(model.tax)}',
                 style: Styles.productRowItemPrice,
                ),
                const SizedBox(height: 6),
                Text(
                  'Total ${_currencyFormat.format(model.totalCost)}',
                  style: Styles.productRowTotal,
                ),
              ],
            )
          ],
        ),
      );
    }
}                       // TO HERE

b2f84ff91b0e1396.png Di bagian bawah file, tambahkan class ShoppingCartItem baru:

class ShoppingCartItem extends StatelessWidget {
  const ShoppingCartItem({
    required this.index,
    required this.product,
    required this.lastItem,
    required this.quantity,
    required this.formatter,
  });

  final Product product;
  final int index;
  final bool lastItem;
  final int quantity;
  final NumberFormat formatter;

  @override
  Widget build(BuildContext context) {
    final row = SafeArea(
      top: false,
      bottom: false,
      child: Padding(
        padding: const EdgeInsets.only(
          left: 16,
          top: 8,
          bottom: 8,
          right: 8,
        ),
        child: Row(
          children: <Widget>[
            ClipRRect(
              borderRadius: BorderRadius.circular(4),
              child: Image.asset(
                product.assetName,
                package: product.assetPackage,
                fit: BoxFit.cover,
                width: 40,
                height: 40,
              ),
            ),
            Expanded(
              child: Padding(
                padding: const EdgeInsets.symmetric(horizontal: 12),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.start,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: <Widget>[
                        Text(
                          product.name,
                          style: Styles.productRowItemName,
                        ),
                        Text(
                          '${formatter.format(quantity * product.price)}',
                          style: Styles.productRowItemName,
                        ),
                      ],
                    ),
                    const SizedBox(
                      height: 4,
                    ),
                    Text(
                      '${quantity > 1 ? '$quantity x ' : ''}'
                      '${formatter.format(product.price)}',
                      style: Styles.productRowItemPrice,
                    )
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );

    return row;
  }
}

b2f84ff91b0e1396.png Jalankan aplikasi. Dari tab produk, pilih beberapa item untuk dibeli menggunakan tombol tanda plus di sebelah kanan setiap item. Pilih tab keranjang belanja. Anda akan melihat item yang tercantum di keranjang belanja di bawah alat pilih tanggal:

28201e6fa0dc3102.png

Terjadi masalah?

Jika aplikasi tidak berjalan dengan baik, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

Selamat!

Anda telah menyelesaikan codelab dan mem-build aplikasi Flutter dengan tampilan dan nuansa Cupertino. Anda juga telah menggunakan paket provider untuk mengelola status aplikasi di seluruh layar. Jika ada waktu, Anda mungkin ingin mempelajari lebih lanjut cara mengelola status di state management documentation.

Langkah berikutnya yang lain

Codelab ini telah mem-built frontend untuk pengalaman berbelanja, tetapi langkah berikutnya dalam mewujudkannya adalah membuat backend yang menangani akun pengguna, produk, keranjang belanja, dan sebagainya. Ada beberapa cara untuk mencapai sasaran ini:

Pelajari lebih lanjut

Anda dapat menemukan info selengkapnya di link berikut: