Codelab 4: Pengembangan untuk Web

 Sejauh ini kita telah mengembangkan aplikasi Wisata Bandung untuk platform mobile. Dengan Flutter mengumumkan Flutter Web yang sudah masuk versi stable, artinya terdapat target platform tambahan yang perlu kita kerjakan jika ingin menyasar pengguna yang lebih luas. Bukan hanya web, pada codelab kali ini kita juga akan menerapkan layout yang responsif untuk platform mobile untuk ukuran layar yang lebih besar seperti tablet.

Hasil akhir dari codelab ini akan seperti berikut:

2021042521184504cad335f61fb247bf566b59f39d1bcf.gif

Mari kita mulai!

  1. Silakan buka project aplikasi Wisata Bandung Anda.
  2. Pastikan di dalam proyek terdapat folder web. Apabila tidak ada, jalankan perintah berikut untuk mengaktifkan konfigurasi web.
    1. flutter config --enable-web
    Kemudian, untuk menambahkan target dukungan untuk web, jalankan perintah di bawah pada terminal:
    1. flutter create .
    Folder web akan terbangkitkan secara otomatis. Folder tersebut berisikan berkas pendukung web seperti index.html, manifest.json, dan lainnya.
  3. Sekarang jalankan aplikasi Anda. Aplikasi akan berjalan dengan baik di browser. Akan tetapi kita akan menemui masalah dalam aspek ukuran, seperti list yang terlalu besar seperti berikut:
    20210425212011200cee8d5e13cee4349a47afe62b8894.jpeg
  4. Karena kita telah membuat aplikasi mobile lalu ingin membuat versi webnya, maka secara tidak langsung kita telah menerapkan mobile-first design. Sekarang kita perlu menentukan pada ukuran berapa layout mobile ini sudah tidak sesuai dan perlu diubah tampilannya. Untuk memudahkan, mari kita tampilkan lebar browser atau layar ke dalam teks AppBar.
    1. AppBar(
    2.   title:
    3.       Text('Wisata Bandung. Size: ${MediaQuery.of(context).size.width}'),
    4. ),
  5. Cobalah mengubah ukuran window browser dan cari breakpoint atau titik di mana layout sudah tidak sesuai.
    202104252129582c9a151a3718b180eb71a45f3688381e.jpegSebagai contoh, pada ukuran lebar di atas 600 sudah banyak ruang kosong yang lebih baik ditempati oleh widget lain. Maka, kita akan gunakan ukuran lebar 600 sebagai batas breakpoint ukuran mobile.
  6. Pada ukuran lebar di atas 600 kita akan buat tampilan yang berbeda menggunakan grid. Sebelumnya, mari pindahkan tampilan ListView menjadi widget tersendiri.
    1. class MainScreen extends StatelessWidget {
    2. const MainScreen({Key? key}) : super(key: key);
    3.  
    4.   @override
    5.   Widget build(BuildContext context) {
    6.     return Scaffold(
    7.       appBar: AppBar(
    8.         title:
    9.             Text('Wisata Bandung. Size: ${MediaQuery.of(context).size.width}'),
    10.       ),
    11.       body: TourismPlaceList(),
    12.     );
    13.   }
    14. }
    15.  
    16. class TourismPlaceList extends StatelessWidget {
    17.   const TourismPlaceList({Key? key}) : super(key: key);
    18.   @override
    19.   Widget build(BuildContext context) {
    20.     return ListView.builder(...);
    21.   }
    22. }
  7. Kemudian buat widget baru untuk menampilkan GridView.
    1. class TourismPlaceGrid extends StatelessWidget {
    2. const TourismPlaceGrid({Key? key}) : super(key: key);
    3.  
    4. @override
    5.   Widget build(BuildContext context) {
    6.     return Padding(
    7.       padding: const EdgeInsets.all(24.0),
    8.       child: GridView.count(
    9.         crossAxisCount: 4,
    10.         children: [],
    11.       ),
    12.     );
    13.   }
    14. }
  8. Selanjutnya ubah body dari MainScreen untuk menampilkan TourismPlaceList atau TourismPlaceGrid. Kita akan memanfaatkan widget LayoutBuilderuntuk mendapatkan ukuran layar.
    1. class MainScreen extends StatelessWidget {
    2.   @override
    3.   Widget build(BuildContext context) {
    4.     return Scaffold(
    5.       appBar: AppBar(
    6.         title:
    7.             Text('Wisata Bandung. Size: ${MediaQuery.of(context).size.width}'),
    8.       ),
    9.       body: LayoutBuilder(
    10.         builder: (BuildContext context, BoxConstraints constraints) {
    11.           if (constraints.maxWidth <= 600) {
    12.             return TourismPlaceList();
    13.           } else {
    14.             return TourismPlaceGrid();
    15.           }
    16.         },
    17. ),
    18.     );
    19.   }
    20. }
    Tampilan aplikasi di atas lebar 600 akan kosong karena kita belum mendefinisikan item untuk ditampilkan.
  9. Tampilan item grid yang akan kita buat akan seperti di bawah ini. Cobalah untuk membuat tampilan Card seperti ini dulu sebelum melihat kode pada langkah berikutnya.
    20210425213315651632e6d76ceea1b377702b23220ba2.jpeg
  10. Berikut ini adalah tampilan untuk Card di atas:
    1. GridView.count(
    2.   crossAxisCount: 4,
    3.   children: tourismPlaceList.map((place) {
    4.     return InkWell(
    5.       onTap: () {
    6.         Navigator.push(context, MaterialPageRoute(builder: (context) {
    7.           return DetailScreen(place: place);
    8.         }));
    9.       },
    10.       child: Card(
    11.         child: Column(
    12.           crossAxisAlignment: CrossAxisAlignment.stretch,
    13.           children: [
    14.             Expanded(
    15.               child: Image.asset(
    16.                 place.imageAsset,
    17.                 fit: BoxFit.cover,
    18.               ),
    19.             ),
    20.             const SizedBox(height: 8),
    21.             Padding(
    22.               padding: const EdgeInsets.only(left: 8.0),
    23.               child: Text(
    24.                 place.name,
    25.                 style: const TextStyle(
    26.                   fontSize: 16.0,
    27.                   fontWeight: FontWeight.bold,
    28.                 ),
    29.               ),
    30.             ),
    31.             Padding(
    32.               padding: const EdgeInsets.only(left: 8.0, bottom: 8.0),
    33.               child: Text(
    34.                 place.location,
    35.               ),
    36.             ),
    37.           ],
    38.         ),
    39.       ),
    40.     );
    41.   }).toList(),
    42. ),
  11. Jalankan aplikasi untuk melihat perubahan. Anda dapat melakukan duplikasi data dummy agar tampilan data menjadi semakin banyak.
    20210425213432eaeb8f3740bbd790e4e00694a0feeeca.pngNamun, untuk ukuran resolusi layar yang lebih besar lagi tampilan grid dengan empat kolom akan terlihat terlalu besar.
    202104252135262e5672e9b965e68cd1129074d325162a.pngGeser kembali ukuran jendela browser untuk menemukan titik di mana tampilan sudah harus berubah. Misalnya, kita ambil ukuran lebar di atas 1200 akan menampilkan 6 kolom.
  12. Tambahkan lagi kondisi pada LayoutBuilder. Kita akan menentukan jumlah kolom yang digunakan melalui parameter constructor.
    1. LayoutBuilder(
    2.   builder: (BuildContext context, BoxConstraints constraints) {
    3.     if (constraints.maxWidth <= 600) {
    4.       return TourismPlaceList();
    5.     } else if (constraints.maxWidth <= 1200) {
    6.       return TourismPlaceGrid(gridCount: 4);
    7.     } else {
    8.       return TourismPlaceGrid(gridCount: 6);
    9.     }
    10.   },
    11. ),
  13. Tambahkan constructor pada kelas TourismPlaceGrid.
    1. class TourismPlaceGrid extends StatelessWidget {
    2.   final int gridCount;
    3.  
    4.   const TourismPlaceGrid({Key? key, required this.gridCount}) : super(key: key);
    5.  
    6.   @override
    7.   Widget build(BuildContext context) {
    8.     return Padding(
    9.       padding: const EdgeInsets.all(16.0),
    10.       child: GridView.count(
    11.         crossAxisCount: gridCount,
    12.         children: tourismPlaceList.map((place) {
    13.           return InkWell(...);
    14.         }).toList(),
    15.       ),
    16.     );
    17.   }
    18. }
  14. Jalankan aplikasi dan lihat perubahan. Tampilan untuk lebar resolusi 1920 sudah lebih baik dari sebelumnya.
    20210425214039ed7d05ee1ed33342eff8a68008d64b13.png
  15. Selanjutnya mari tambahkan jarak antara item supaya tidak terlalu rapat.
    1. GridView.count(
    2.   crossAxisCount: gridCount,
    3.   crossAxisSpacing: 16,
    4. mainAxisSpacing: 16,
    5.   children: tourismPlaceList.map((place) {
    6.     return InkWell(...);
    7.   }).toList(),
    8. ),
  16. Jangan lupa untuk menghapus kembali teks ukuran pada AppBar.
    1. AppBar(
    2.   title: Text('Wisata Bandung'),
    3. ),
  17. Selanjutnya kita akan mulai memodifikasi tampilan halaman detail. Seperti yang Anda lihat ketika menjalankan aplikasi, halaman detail tampak kurang estetik untuk ukuran resolusi yang lebar. Untuk ukuran lebar di atas 800, kita akan membuat tampilannya seperti ini:
    2021042521424334e5fa0a54aca6abe1813edcb1470eb0.png
  18. Kita gunakan LayoutBuilderuntuk memisahkan tampilan berdasarkan breakpoint.
    1. class DetailScreen extends StatelessWidget {
    2.   final TourismPlace place;
    3.  
    4.   const DetailScreen({Key? key, required this.place}) : super(key: key);
    5.  
    6.   @override
    7.   Widget build(BuildContext context) {
    8.     return LayoutBuilder(
    9.       builder: (BuildContext context, BoxConstraints constraints) {
    10.         if (constraints.maxWidth > 800) {
    11.           return DetailWebPage(place: place);
    12.         } else {
    13.           return DetailMobilePage(place: place);
    14.         }
    15.       },
    16.     );
    17.   }
    18. }
    Pindahkan widget yang sudah ada ke dalam kelas DetailMobilePage.
    1. class DetailMobilePage extends StatelessWidget {
    2.   final TourismPlace place;
    3.   
    4.   const DetailMobilePage({Key? key, required this.place}) : super(key: key);
    5.  
    6.   @override
    7.   Widget build(BuildContext context) {
    8.     return Scaffold(...);
    9.   }
    10. }
    Buat juga kelas serupa untuk DetailWebPage.
  19. Tidak ada perbedaan konten yang ditampilkan pada halaman detail, tetapi hanya susunannya saja yang berbeda. Dari gambar sebelumnya apakah Anda dapat menganalisis bagaimana layoutnya disusun? Jika belum, maka kurang lebihnya akan seperti berikut:
    202104252144172b6214baa252be35dece119e3ebd0bc6.png
  20. Mari kita mulai dengan Column pertama. Tambahkan Text untuk judul, lalu Row untuk struktur layout di bawahnya. Tambahkan jarak menggunakan widget SizedBox.
    1. Scaffold(
    2.   body: Column(
    3.     crossAxisAlignment: CrossAxisAlignment.start,
    4.     children: [
    5.       const Text(
    6.         'Wisata Bandung',
    7.         style: TextStyle(
    8.           fontFamily: 'Staatliches',
    9.           fontSize: 32,
    10.         ),
    11.       ),
    12.       const SizedBox(height: 32),
    13.       Row(
    14.         children: [],
    15.       ),
    16.     ],
    17.   ),
    18. );
  21. Widget Row akan berisi children seperti berikut:
    1. Row(
    2.   crossAxisAlignment: CrossAxisAlignment.start,
    3.   children: [
    4.     Expanded(
    5.       child: Column(
    6.         children: [],
    7.       ),
    8.     ),
    9.     const SizedBox(width: 32),
    10.     Expanded(
    11.       child: Card(...),
    12.     ),
    13.   ],
    14. ),
    Sementara Card untuk detail informasi tempat wisata memiliki widget seperti berikut:
    1. Card(
    2.   child: Container(
    3.     padding: const EdgeInsets.all(16),
    4.     child: Column(
    5.       mainAxisSize: MainAxisSize.max,
    6.       children: <Widget>[
    7.         Container(
    8.           child: Text(
    9.             place.name,
    10.             textAlign: TextAlign.center,
    11.             style: const TextStyle(
    12.               fontSize: 30.0,
    13.               fontFamily: 'Staatliches',
    14.             ),
    15.           ),
    16.         ),
    17.         Row(
    18.           mainAxisAlignment: MainAxisAlignment.spaceBetween,
    19.           children: [
    20.             Row(
    21.               children: <Widget>[
    22.                 const Icon(Icons.calendar_today),
    23.                 const SizedBox(width: 8.0),
    24.                 Text(
    25.                   place.openDays,
    26.                   style: informationTextStyle,
    27.                 ),
    28.               ],
    29.             ),
    30.             const FavoriteButton(),
    31.           ],
    32.         ),
    33.         Row(
    34.           children: <Widget>[
    35.             const Icon(Icons.access_time),
    36.             const SizedBox(width: 8.0),
    37.             Text(
    38.               place.openTime,
    39.               style: informationTextStyle,
    40.             ),
    41.           ],
    42.         ),
    43.         const SizedBox(height: 8.0),
    44.         Row(
    45.           children: <Widget>[
    46.             const Icon(Icons.monetization_on),
    47.             const SizedBox(width: 8.0),
    48.             Text(
    49.               place.ticketPrice,
    50.               style: informationTextStyle,
    51.             ),
    52.           ],
    53.         ),
    54.         Container(
    55.           padding: const EdgeInsets.symmetric(vertical: 16.0),
    56.           child: Text(
    57.             place.description,
    58.             textAlign: TextAlign.justify,
    59.             style: const TextStyle(
    60.               fontSize: 16.0,
    61.               fontFamily: 'Oxygen',
    62.             ),
    63.           ),
    64.         ),
    65.       ],
    66.     ),
    67.   ),
    68. ),
  22. Widget Column terakhir kita gunakan untuk menampilkan gambar dari aset dan daftar gambar dari internet.
    1. Column(
    2.   children: [
    3.     ClipRRect(
    4.       child: Image.asset(place.imageAsset),
    5.       borderRadius: BorderRadius.circular(10),
    6.     ),
    7.     const SizedBox(height: 16),
    8.     Container(
    9.       height: 150,
    10.       padding: const EdgeInsets.only(bottom: 16),
    11.       child: ListView(
    12.         scrollDirection: Axis.horizontal,
    13.         children: place.imageUrls.map((url) {
    14.           return Padding(
    15.             padding: const EdgeInsets.all(4.0),
    16.             child: ClipRRect(
    17.               borderRadius: BorderRadius.circular(10),
    18.               child: Image.network(url),
    19.             ),
    20.           );
    21.         }).toList(),
    22.       ),
    23.     ),
    24.   ],
    25. ),
    Sekarang tampilan aplikasi akan seperti ini:
    202104252146170e250c800527cf4c484f2170eb260d20.png
  23. Mari tambahkan Padding untuk memberikan jarak antara konten dengan pinggir halaman.
    1. Scaffold(
    2.   body: Padding(
    3.     padding: const EdgeInsets.symmetric(
    4.       vertical: 16,
    5. horizontal: 64,
    6.     ),
    7.     child: Column(...),
    8.   ),
    9. );
  24. Kita juga dapat menentukan ukuran lebar maksimum dari konten yang tampil menggunakan SizedBox.
    1. Padding(
    2.   padding: const EdgeInsets.symmetric(
    3.     vertical: 16,
    4.     horizontal: 64,
    5.   ),
    6.   child: Center(
    7. child: SizedBox(
    8. width: 1200,
    9.       child: Column(...),
    10.     ),
    11.   ),
    12. ),
    Lihat perubahan aplikasi, sekarang halaman detail akan memiliki ukuran lebar maksimal 1200 pixel.
    202104252148180888709da1500c76c8055f582eb966b2.png
  25. Satu hal yang penting untuk kita perhatikan adalah menghadirkan pengalaman pengguna yang nyaman saat menggunakan platform apa pun. Untuk itu, tambahkan Scrollbar pada daftar gambar. Selain itu, tambahkan juga widget Container untuk memberikan ukuran tinggi pada ListView.
    1. Scrollbar(
    2.   child: Container(
    3.     height: 150,
    4. padding: const EdgeInsets.only(bottom: 16),
    5.     child: ListView(
    6.       scrollDirection: Axis.horizontal,
    7.       children: widget.place.imageUrls.map((url) {
    8.         return Padding(...);
    9.       }).toList(),
    10.     ),
    11.   ),
    12. ),
  26. Jika Anda menggunakan touchpad pada laptop, gestur swipe pada scrollbar akan berjalan dengan normal. Namun, ketika melakukan drag menggunakan mouse, fitur scrollbar tidak berjalan. Solusi yang bisa Anda lakukan adalah menggunakan ScrollController. Inisialisasikan objek ScrollController di atas method build().
    1. final _scrollController = ScrollController();
    Tambahkan _scrollController pada Scrollbar dan ListView.
    1. Scrollbar(
    2.   controller: _scrollController,
    3.   child: Container(
    4.     height: 150,
    5.     padding: const EdgeInsets.only(bottom: 16),
    6.     child: ListView(
    7.       controller: _scrollController,
    8.       scrollDirection: Axis.horizontal,
    9.       children: widget.place.imageUrls.map((url) {
    10.         return Padding(...);
    11.       }).toList(),
    12.     ),
    13.   ),
    14. ),
  27. Seperti halnya TextEditingController, ScrollController juga harus di-dispose ketika widget sudah tidak lagi digunakan. Ubahlah DetailWebPage menjadi StatefulWidget supaya kita bisa memanggil method dispose.
    20210425215045a90eb71cda047e081068c3b2be85e746.gif
  28. Tambahkan method dispose setelah method build() lalu panggil _scrollController.dispose();
    1. @override
    2. void dispose() {
    3.   _scrollController.dispose();
    4.   super.dispose();
    5. }
  29. Selamat! Sekarang tampilan aplikasi Wisata Bandung sudah lebih sesuai untuk ukuran resolusi layar yang lebih besar. Anda telah belajar banyak menyusun halaman menggunakan widget, hingga membuat halaman yang dapat responsif terhadap perbedaan ukuran layar. 

Kode selengkapnya untuk codelab ini dapat Anda temukan pada tautan: 

Popular posts from this blog

Build APK

Generic

Build IPA