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