Mengenal Widget Flutter #7: Membuat UI Profile Instagram

Mengenal Widget Flutter #7: Membuat UI Profile Instagram

Pendahuluan

Me-review kembali pembalajaran yang sudah kita lakukan, maka sesuai dengan pembahasan sebelumnya dimana kita akan menyelesaikan sebuah case sederhana yakni membuat UI profile Instagram menggunakan Flutter dengan menggambungkan seluruh widget yang sudah kita kenali.

Skenario yang diinginkan adalah terdapat dua buah page dimana page yang pertama adalah tampilan dari profile instagram beserta posts-nya dan page yang kedua adalah detail dari masing-masing dimana perpindahan page tersebut akan di-set transisinya.

membuat ui profile instagram di flutter

Baca Juga: Mengenal Widget Flutter #2: Container, Stack & Positioned

Membuat UI Profile Instagram

Membedah sebuah layout menjadi tugas utama kita sebelum menerjemahkannya ke dalam code, dimana dalam hal ini adalah kita akan membedah ada berapa bagian yang perlu dilengkapi dari screenshoot di atas. Saya akan membuat detail hasil pembedahan yang saya lakukan.

  1. Appbar yang berisi 3 component, pertama title, kemudian icon panah ke bawah dan yang terakhir adalah hamburger menu pada bagian actions.
  2. Last Visitor berfungsi akan menampilkan berapa pengunjung yang melihat profil kita.
  3. Profile terdiri dari 4 component, pertama adalah profile picture, kemudian 3 lainnya adalah informasi terkait posts, following dan follower.
  4. Profile Description dimana terdiri dari 4 component secara vertical, yakni nama, kategori, deskripsi dan yang terakhir adalah link ke-site pemiliknya.
  5. Shortcut Button memiliki 3 buah component berupa tombol sesuai fungsinya masing-masing.
  6. Tabbar untuk berpindah antara Posts dan Posts yang ditandai.
  7. TabbarView yang berisi grid dari lists postingan.

Sebelum memulai untuk menyelesaikan ke-7 layer di atas, install Flutter dengan command

flutter create dw_instagram

Buka file lib/main.dart dan modifikasi menjadi.

import 'package:flutter/material.dart';
import './profile.dart'; //IMPORT FILE YANG AKAN MENGHANDLE TAMPILAN PROFILE

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //DEFAULT YANG DI-LOAD ADALAH HALAMAN PROFILE
    return MaterialApp(home: Profile());
  }
}

Kemudian buat file profile.dart di dalam folder lib dan tambahkan kerangka berikut

import 'package:flutter/material.dart';

class Profile extends StatefulWidget {
  @override
  _ProfileState createState() => _ProfileState();
}

class _ProfileState extends State<Profile> with TickerProviderStateMixin {
  //VARIABLE INI NNTINYA KITA GUNAKAN UTK PROFILE PICTURE
  final urlProfile = 'https://daengweb.id/front/d-blog/img/favicon.png';

  TabController _tabController;

  @override
  void initState() {
    super.initState();
    //SET TAB CONTROLLERNYA DENGAN PROPERTI LENGTHNYA ADALAH 2
    //YANG BERARTI TAB-NYA NANTI MEMILIKI DUA BAGIAN
    _tabController = TabController(vsync: this, length: 2);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        //KARENA BAGIAN TITLE TERDIRI DARI DUA COMPONENT, MAKA KITA MASUKKAN KE DALAM ROW
        title: Row(
          children: <Widget>[
            //UNTUK TEKS DAN ICON SALING BERDEKATAN
            Text('daengwebid', style: TextStyle(color: Colors.black)),
            Icon(Icons.arrow_drop_down, color: Colors.black)
          ],
        ),
        //SEDANGKAN HUMBERGER MENU ADA DI SEBELAH KANAN MAKA KITA GUNAKAN ACTIONS DARI APPBAR
        actions: <Widget>[Icon(Icons.menu, color: Colors.black)],
        backgroundColor: Colors.white,
      ),
      body: Container(
        //JADI KITA GUNAKAN SEBUAH COLUMN KARENA ADA BANYAK  COMPONENT YANG AKAN TERSUSUN KEBAWAH
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            LastViewed(), //CLASS INI UNTUK MENAMPILKAN JUMLAH VISITOR YANG MELIHAT PROFILE
						//CLASS INI UNTUK INFORMASI PROFILE SEPERTI PP, JUMLAH POST, FOLLOWING DAN FOLLOWERS
            ProfileInformation(urlProfile: urlProfile),
            ProfileDescription(), //BAGIAN INI UNTUK NAMA PROFILE, DESC DAN KATEGORI
            ShortcutButton(), //3 TOMBOL SEBELUM POSTS
            Divider(), //BUAT GARIS UNTUK MEMISAHKAN DENGAN TAB
            ListPosts(tabController: _tabController), //TAB BESERTA LIST POSTSNYA
          ],
        ),
      ),
    );
  }
}

class ListPosts extends StatelessWidget {
  const ListPosts({
    Key key,
    @required TabController tabController,
  })  : _tabController = tabController,
        super(key: key);

  final TabController _tabController;

  @override
  Widget build(BuildContext context) {
    return null;
  }
}

class ShortcutButton extends StatelessWidget {
  const ShortcutButton({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return null;
  }
}

class ProfileDescription extends StatelessWidget {
  const ProfileDescription({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return null;
  }
}

class ProfileInformation extends StatelessWidget {
  const ProfileInformation({
    Key key,
    @required this.urlProfile,
  }) : super(key: key);

  final String urlProfile;

  @override
  Widget build(BuildContext context) {
    return null;
  }
}

class LastViewed extends StatelessWidget {
  const LastViewed({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return null;
  }
}

Note: Semuanya kita pisahkan ke dalam class masing-masing agar lebih mudah mengurainya. Masing-masing class saat ini masih me-return null karena prosesnya akan dikerjakan setelah ini.

Dari ke-7 layer pembedahan di atas, poin pertama sudah selesai dikerjakan, maka kita akan bergeser ke point ke-2 dengan membuat tampilan Last Visitor. Modifikasi class LastViewed() menjadi

class LastViewed extends StatelessWidget {
  const LastViewed({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 40, //KARENA HANYA SEBARIS TEKS MAKA KITA SET TINGGI CONTAINERNYA 40
      decoration: BoxDecoration(
        //AGAR TERLIHAT SEKATNYA, MAKA KITA TAMBAHKAN BORDER DISEKELILINGNYA
        border: Border.fromBorderSide(
          BorderSide(width: 1, color: Colors.black12),
        ),
      ),
      //KARENA DIA TERDIRI DARI DUA BAGIAN YAKNI 10 YANG BOLD DAN TEKS SELANJUTNYA
      //MAKA KITA GUNAKAN ROW
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('10', style: TextStyle(fontWeight: FontWeight.bold)),
          Text(' profile visits in the last 7 days',
              style: TextStyle(color: Colors.grey)),
        ],
      ),
    );
  }
}

Jika ingin melihat perubahan sementara, save code di atas dan hard reload. Lanjut ke poin ke-3 yakni informasi terkait profile picture dan sebagainya. Modifikasi class ProfileInformation() dengan menambahkan code berikut

class ProfileInformation extends StatelessWidget {
  //KITA MEMINTA ATAU MENERIMA DATA BERUPA URL PROFILE 
  //JIKA DILIHAT DARI KERANGKA SEBELUMNYA, DIMANA KETIKA CLASS INI DIPANGGIL
  //MAKA TERDAPAT VARIABLE YANG DIPASSING
  const ProfileInformation({
    Key key,
    @required this.urlProfile,
  }) : super(key: key);

  final String urlProfile;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(10),
      //ADA 4 BAGIAN YANG BERURUT SECARA HORIZONTAL, MAKA MENGGUNAKAN ROW
      child: Row(
        //ATTRIBUTENYA DI-SET SPACE BETWEEN AGAR TIAP COMPONENT MEMILIKI SPACE
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: <Widget>[
          //BAGIAN PERTAMA ADALAH PROFIL PIC
          Container(
            width: 100, //DENGAN UKURAN PXL ADALAH 100
            height: 100,
            //KEMUDIAN MENGUGNAKAN CLIPRREACT UNTUK MEMBUAT LINGKARAN
            child: ClipRRect(
              //SET BORDER RADIUSNYA SETENGAH DARI UKURANNYA AGAR MEMBENTUK LINGKARAN
              borderRadius: BorderRadius.circular(50),
              //AMBIL GAMBAR DARI INTERNET
              child: Image.network(
                urlProfile,
                fit: BoxFit.cover,
              ),
            ),
          ),
            
          //3 COMPONENT SELANJUTNYA MASING-MASING TERDIRI DARI 2 BAGIAN
          //YAKNI TOTAL DAN LABEL, MAKA MENGGUNAKAN COLUMN UNTUK SETIAP BAGIAN
          Column(
            children: <Widget>[
              Text(
                '14',
                style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
              ),
              Text(
                "Posts",
                style: TextStyle(color: Colors.black45),
              )
            ],
          ),
          Column(
            children: <Widget>[
              Text('131',
                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
              Text("Followers", style: TextStyle(color: Colors.black45))
            ],
          ),
          Column(
            children: <Widget>[
              Text('42',
                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
              Text("Following", style: TextStyle(color: Colors.black45))
            ],
          ),
        ],
      ),
    );
  }
}

Untuk menyelesaikan bagian profile description, maka modifikasi class ProfileDescription() menjadi.

class ProfileDescription extends StatelessWidget {
  const ProfileDescription({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(10.0),
      //ADA 4 BAGIAN YANG MEMBENTUK LIST SECARA VERTICAL, MAKA COLUMN MENGAMBIL PERANNYA
      //SEMUANYA MUDAH KARENA HANYA MENAMPILKAN TEKS BIASA SAJA
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(
            "Daeng Web ID",
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
          ),
          Text(
            "Community Organization",
            style: TextStyle(color: Colors.grey),
          ),
          Text(
            "Laravel, Flutter, Vue.js Tutorial. Blogger & Trainer about programming language",
          ),
          Text(
            "https://daengweb.id",
            style: TextStyle(color: Colors.blue),
          ),
        ],
      ),
    );
  }
}

Lanjut, point ke-5 adalah deretan 3 buah button secara horizontal. Modifikasi class ShortcutButton() dengan menambahkan code.

class ShortcutButton extends StatelessWidget {
  const ShortcutButton({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      //KONSEPNYA SAMA, KARENA DERETANNYA KE KANAN MAKA GUNAKAN ROW()
      child: Row(
        //MASING2 CHILD DIBUAT SPACE DISEKITARNYA
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: <Widget>[
          //DAN TIAP BUTTON KITA GUNAKAN RAISEDBUTTON DENGAN TEKSNYA MASING-MASING
          RaisedButton(
            onPressed: () {},
            child: Text('Edit Profile'),
            color: Colors.white,
          ),
          RaisedButton(
            onPressed: () {},
            child: Text('Promotions'),
            color: Colors.white,
          ),
          RaisedButton(
            onPressed: () {},
            child: Text('Contact'),
            color: Colors.white,
          ),
        ],
      ),
    );
  }
}

Adapun poin ke-6 dan 7 di-merge ke dalam sebuah class yang sama, modifikasi class ListPosts() menjadi code seperti di bawah ini

class ListPosts extends StatelessWidget {
  //KARENA DISINI MENGGUNAKAN TAB, MAKA KITA MINTA TABCONTROLLERNYA YANG SUDAH DI DEFINISIKAN PADA KERANGKA DIAWAL SETELAH URL PROFIL PIC
  const ListPosts({
    Key key,
    @required TabController tabController,
  })  : _tabController = tabController,
        super(key: key);

  final TabController _tabController;

  @override
  Widget build(BuildContext context) {
    return Expanded(
      //KARENA ADA DUA BAGIAN, YAKNI TABBAR DAN TABBAR VIEW MAKA KITA GUNAKAN COLUMN
      //SEBENARNYA BISA SAJA TIDAK MENGGUNAKAN COLUMN KARENA YANG MELOAD CLASS INI ADA DI DALAM COLUMN
      child: Column(
        children: <Widget>[
          //BAGIAN TABBARNYA
          TabBar(
            controller: _tabController, //KITA GUNAKAN TABBAR CONTROLLER
            //DAN MENGATUR PROPERTI LAINNYA
            labelColor: Colors.black,
            indicatorColor: Colors.black,
            unselectedLabelColor: Colors.grey,
            tabs: <Widget>[
              //KARENA LENGTH DARI TAB ADALAH 2, MAKA KITA TAMBAHKAN 2 BUAH TAB ICON
              Tab(icon: Icon(Icons.border_all)),
              Tab(icon: Icon(Icons.assignment_ind)),
            ],
          ),
          Expanded(
            //ADAPUN TABBAR VIEWNYA BERLAKU HAL YANG SAMA
            child: TabBarView(
              controller: _tabController, //CONTROLLERNYA DI ASSIGN
              children: <Widget>[
                //ADA DUA BAGIAN VIEW, UNTUK VIEW TAB PERTAMA MENGGUNAKAN GRID VIEW
                //KARENA AKAN ME-LOOPING 3 BUAH URL GAMBAR
                GridView.count(
                  physics: ScrollPhysics(), 
                  crossAxisCount: 3, //JUMLAH GRIDNYA ADA BERAPA DERETAN
                  childAspectRatio: 1.0,
                  mainAxisSpacing: 3.0, //MENGATUR JARAK OBJEK ATAS DAN BAWAH
                  crossAxisSpacing: 3.0, //MENGATUR JARAK OBJEK KIRI DAN KANAN
                  //CHILDNYA ADALAH SEBUAH LISTS ATAU ARRAY YANG DILOOPING MENGGUNAKAN MAP
                  children: <String>[
                    'https://upload.wikimedia.org/wikipedia/commons/0/01/LinuxCon_Europe_Linus_Torvalds_03_%28cropped%29.jpg',
                    'https://images-na.ssl-images-amazon.com/images/I/41dKkez-1rL._SX326_BO1,204,203,200_.jpg',
                    'https://upload.wikimedia.org/wikipedia/commons/0/01/Bill_Gates_July_2014.jpg'
                  ].map((String url) {
                    //KEMUDIAN TIAP GAMBAR DIWRAP DENGAN GESTUREDETECTOR
                    return GestureDetector(
                      //DIMANA KETIKA DITAP AKAN BERPINDAH KE HALAMAN SELANJUTNYA
                      onTap: () {
                        Navigator.of(context).push(MaterialPageRoute(builder: (context) => Post(url)));
                      },
                      //DAN KETIKA MENAMPILKAN GAMBARNYA KITA GUNAKAN WRAP DENGAN GRIDTILE
                      child: GridTile(
                        //DAN ANIMASI TRANSISINYA MENGGUNAKAN HERO SEPERTI YANG SUDAH DIJELASKAN SEBELUMNYA
                        //KEMUDIAN AMBIL GAMBAR DARI URL HASIL LOOPING DI ATAS
                        child: Hero(tag: url, child: Image.network(url, fit: BoxFit.cover)),
                      ),
                    );
                  }).toList(),
                ),
                //TAB VIEW KE DUA KITA TAMPILKAN KOTAK KOSONG SAJA 
                Container(
                  height: 100,
                  color: Colors.pinkAccent,
                  child: Center(
                    child: Text('DAENGWEB.ID'),
                  ),
                )
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Jika ingin melihat hasilnya, comment atau non aktifkan terlebih dahulu fungsi Navigator didalam GestureDector di atas karena page Post() belum tersedia.

Transisi ke Halaman Detail Posts Menggunakan Hero Animation

Masih ingat materi terkait hero animation? Materi tersebut akan diterapkan pada proses perpindahan page dari halaman profil ke halaman detail post. Page selanjutnya akan menampilkan profil pemilik post yang terdiri dari profil pic dan namanya, kemudian diikuti oleh gambar posts yang sedang diakses. Hasil akhirnya kurang lebih seperti gambar di bawah ini

hero animation di flutter

Buat file baru bernama post.dart di dalam folder lib dan tambahkan code berikut untuk menampilkan detail yang sedang ingin dilihat oleh users.

import 'package:flutter/material.dart';

class Post extends StatelessWidget {
  final urlImage;
  final urlProfile = 'https://daengweb.id/front/d-blog/img/favicon.png';

  //TERIMA DATA PROFIL PIC YANG AKAN DILIHAT
  Post(this.urlImage);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        iconTheme: IconThemeData(
            color: Colors.black,
          ),
        backgroundColor: Colors.white70,
        title: Text('Posts', style: TextStyle(color: Colors.black),),
      ),
      body: Container(
        //LAGI LAGI KITA GUNAKAN COLUMN UNTUK MENGATUR LAYOUT SECARA VERTICAL
        //DIMANA TERDIRI DARI 4 BAGIAN, PERTAMA ADALAH INFO PEMILIK
        //KEDUA ADALAH GAMBAR POST
        //KETIGA ADALAH INSIGHT DAN PROMOTE BUTTON
        //KEEMPAT ADALAH TOMBOL LOVE, COMMENT DAN SETERUSNYA
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            //BAGIAN PERTAMA TERKAIT PENGGUNA
            Padding(
              padding: const EdgeInsets.all(10.0),
              //ADA 3 ELEMEN BERDERET KE SAMPING, PERTAMA GAMBAR, TEKS DAN ICON
              //MAKA KITA GUNAKAN ROW UNTUK MENGATUR KETIGA ELEMEN TERSEBUT
              child: Row(
                children: <Widget>[
                  //BAGIAN PERTAMA GAMBAR, ADAPUN PENJELASANYA SAMA SEPERTI SEBELUMNYA YAKNI SEBUAH CONTAINER YANG SUDAH DI-SET BESAR UKURANNYA
                  Container(
                    margin: EdgeInsets.only(right: 10),
                    height: 40,
                    width: 40,
                    //KEMUDIAN GAMBARNYA DIWRAP DENGAN CLIPRREACT UNTUK MEMBUAT LENGKUNGAN LINGKARAN
                    child: ClipRRect(
                      borderRadius: BorderRadius.circular(20),
                      //DAN LOAD GAMBARNYA DARI URL YANG ADA DI VARIABLE urlprofile
                      child: Image.network(
                        urlProfile,
                        fit: BoxFit.cover,
                      ),
                    ),
                  ),
                  //NAMA PENGGUNA
                  Text(
                    "daengwebid",
                    style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
                  ),
                  //ANTAR NAMA DAN ICON DIPISAHKAN DENGAN JARAK, MAKA KITA GUNAKAN SPACER
                  Spacer(
                    flex: 1,
                  ),
                  Icon(Icons.more_vert)
                ],
              ),
            ),
            //NEXT ADALAH GAMBAR DARI POST YANG DIKLIK
            Container(
              //DIWRAP DENGAN HERO ANIMATION AGAR MEMBUAT EFEK TRANSISI
              child: Hero(tag: urlImage, child: Image.network(urlImage, fit: BoxFit.cover,)),
              //WIDTHNYA DIBUAT SELEBAR MUNGKIN
              width: double.infinity,
              //DAN TINGGINYA SEPERTIGA DARI BESAR LAYAR
              height: MediaQuery.of(context).size.height / 3,
            ),
            //BAGIAN INI ADALAH INSIGHT DAN PROMOSI, HANYA BERISI TEKS DAN TOMBOL
            Padding(
              padding: const EdgeInsets.all(8.0),
              //YANG LAGI LAGI DIBUNGKUS DENGAN ROW
              child: Row(
                //DAN ATTRIBUTENYA DI-SET SPACEBETWEEN UNTUK MEMBERI JARAK KEDUANYA
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: <Widget>[
                Text("View Insights", style: TextStyle(color: Colors.blue, fontSize: 16),),
                RaisedButton(onPressed: (){}, child: Text('Promote'), color: Colors.blue, textColor: Colors.white,)
              ],),
            ),
            //BUAT GARIS PEMISAH
            Divider(),
            //BAGIAN INI ADALAH KUMPULAN ICON YANG ADA DIBAWAH
            //KARENA ADA 4 ICON MAKA GUNAKAN ROW LAGI
            Row(children: <Widget>[
              Padding(
                padding: const EdgeInsets.only(right: 8.0, left: 8),
                child: Icon(Icons.favorite_border),
              ),
              Padding(
                padding: const EdgeInsets.only(right: 8.0),
                child: Icon(Icons.comment),
              ),
              Icon(Icons.send),
              Spacer(flex: 1,),
              Padding(
                padding: const EdgeInsets.only(right: 8.0),
                child: Icon(Icons.filter_frames),
              )
            ],),
            //YANG TERAKHIR ADALAH INFORMASI KAPAN POST TERSEBUT DIPOSTING
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Text("Desember, 24, 2018", style: TextStyle(color: Colors.grey, fontSize: 12),),
            )
          ],
        ),
      ),
    );
  }
}

Aktifkan kembali Navigator jika kamu menonaktifkannya, kemudian jangan lupa import file post.dart. Buka file profile.dart dan tambahkan code berikut pada posisi import statement

import './post.dart';

Baca Juga: Mengenal Widget Flutter #3: List View & Text Style

Kesimpulan

Seri me-review widget Flutter dengan case Membuat UI Profile Instagram dimana kita menggunakan kembali widget yang sudah dikenali atau dibahas pada materi-materi sebelumnya untuk menyelesaikan sebuah layout yang sudah ditentukan. Dari materi di atas bisa dilihat bahwa semua widget tersebut saling bahu membahu untuk mewujudkan keinginan penggunanya. Intinya dari Flutter adalah everything is widget, maka di dalam widget bisa ada widget lainnya sampai keinginan tersebut tercapai.

Adapun dokumentasi code dari artikel ini bisa dilihat di Github.

Category:
Share:

Comments