Flutter: IndexedStack
How to use widget IndexedStack in Flutter
Pengenalan
Ketika kita ingin membuat sebuah tampilan antarmuka di Flutter pastinya kita sering menjumpai sebuah case yang mana, kita buat sebuah pengkondisian ketika kita ingin menampilkan sebuah widget. Contoh sederhananya misal, ketika si user pilih kategori A maka, akan tampil widget A. Dan jika si user pilih kategori B maka, akan tampil widget B. Nah, hal-hal seperti ini saya yakin kita semua pasti pernah mengalami case tersebut. Dan pada umumnya, untuk solusi tersebut biasanya kita buat pengkondisian secara manual menggunakan if else conditional. Di Flutter, kita sebagai developer benar-benar merasa terbantu dengan adanya kumpulan-kumpulan widget yang disediakan oleh Flutter. Jadi, di Flutter ada satu widget yang bernama IndexedStack. Widget ini berfungsi untuk membantu ketika kita ingin mengganti-ganti tampilan sebuah widget tanpa harus membuat if else conditional.
Contoh Projek
Untuk contohnya, kali ini kita akan membuat sebuah form sederhana saja yang mana didalamnya ada TextField pilih kategori, IndexedStack, dan Button. Kurang lebih kira-kira seperti inilah tampilan aplikasinya.
Buat Projek
Untuk langkah pertamanya, mari kita buat projek baru dengan nama flutter_indexed_stack.
Setup pubspec.yaml
Di projek ini kita membutuhkan sebuah plugin intl. Oleh karena itu, kita perlu menambahkan plugin tersebut kedalam projek. Untuk menambahkannya silakan jalankan perintah berikut.
flutter pub add intl
Lalu, jalankan perintah flutter pub get
.
Atur file main.dart
Langkah berikutnya ialah kita buka file main.dart dan ubah kode didalamnya menjadi seperti berikut.
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter IndexedStack',
theme: ThemeData(primarySwatch: Colors.blue),
home: const MyHomePage(title: 'Flutter IndexedStack'),
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
const MyHomePage({
Key? key,
required this.title,
}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final listCategories = <String>[
'Peminjaman Dana',
'Perjalanan Dinas',
'Pengajuan Karyawan Baru',
];
final listJobPositions = <String>[
'Product Engineer',
'Account Manager',
'Copywriter',
];
final controllerCategory = TextEditingController();
final controllerJobPosition = TextEditingController();
int? indexCategory;
var selectedCategory = '';
var widthScreen = 0.0;
var heightScreen = 0.0;
var selectedJobPosition = '';
@override
Widget build(BuildContext context) {
final mediaQueryData = MediaQuery.of(context);
widthScreen = mediaQueryData.size.width;
heightScreen = mediaQueryData.size.height;
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
child: Column(
children: [
buildWidgetTextFieldCategory(),
indexCategory == null ? Container() : IndexedStack(
index: indexCategory,
children: [
buildWidgetInputPeminjamanDana(),
buildWidgetInputPerjalananDinas(),
buildWidgetInputPengajuanKaryawanBaru(),
],
),
const SizedBox(height: 16),
buildWidgetButtonSubmit(),
],
),
),
);
}
Widget buildWidgetTextFieldCategory() {
// TODO: Buat widget text field category
return Container();
}
Widget buildWidgetInputPeminjamanDana() {
// TODO: Buat widget input peminjaman dana
return Container();
}
Widget buildWidgetInputPerjalananDinas() {
// TODO: Buat widget input perjalanan dinas
return Container();
}
Widget buildWidgetInputPengajuanKaryawanBaru() {
// TODO: Buat widget input pengajuan karyawan baru
return Container();
}
Widget buildWidgetButtonSubmit() {
// TODO: Buat widget button submit
return Container();
}
void unfocus() {
// TODO: Buat function untuk unfocus setelah pilih data
}
}
Buat StatefulWidget WidgetDialogChoose
Masih didalam file main.dart kita buat StatefulWidget baru dengan nama WidgetDialogChoose. Lalu, kita isi kode didalamnya menjadi seperti berikut.
class WidgetDialogChoose extends StatefulWidget {
final double widthScreen;
final double heightScreen;
final String title;
final List<String> listOptions;
final String? defaultSelected;
const WidgetDialogChoose({
Key? key,
required this.widthScreen,
required this.heightScreen,
required this.title,
required this.listOptions,
required this.defaultSelected,
}) : super(key: key);
@override
State<WidgetDialogChoose> createState() => _WidgetDialogChooseState();
}
class _WidgetDialogChooseState extends State<WidgetDialogChoose> {
String? selectedCategory;
@override
void setState(VoidCallback fn) {
if (mounted) {
super.setState(fn);
}
}
@override
void initState() {
selectedCategory = widget.defaultSelected;
super.initState();
}
@override
Widget build(BuildContext context) {
final children = <Widget>[];
final listWidgetRadioButton = <Widget>[];
for (final itemCategory in widget.listOptions) {
final widgetRadioButton = Radio<String>(
value: itemCategory,
groupValue: selectedCategory,
onChanged: (value) => setState(() => selectedCategory = itemCategory),
);
final widgetText = Expanded(
child: Text(
itemCategory,
style: Theme.of(context).textTheme.caption?.copyWith(
color: Colors.grey[900],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
);
final widgetItem = GestureDetector(
onTap: () => setState(() => selectedCategory = itemCategory),
child: Row(
children: [
widgetRadioButton,
widgetText,
],
),
);
listWidgetRadioButton.add(widgetItem);
}
children.addAll(listWidgetRadioButton);
return AlertDialog(
title: Text(
widget.title,
textAlign: TextAlign.start,
),
titleTextStyle: Theme.of(context).textTheme.subtitle2?.copyWith(
fontSize: 16,
),
titlePadding: const EdgeInsets.only(
left: 16,
top: 16,
),
contentPadding: const EdgeInsets.all(16),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: widget.widthScreen / 1.2,
height: widget.heightScreen / 2,
child: ListView(
shrinkWrap: true,
children: children,
),
),
Row(
children: [
Expanded(
child: buildWidgetButtonCancel(),
),
const SizedBox(width: 16),
Expanded(
child: buildWidgetButtonOk(),
),
],
),
],
),
);
}
Widget buildWidgetButtonCancel() {
return TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal'),
);
}
Widget buildWidgetButtonOk() {
return TextButton(
onPressed: selectedCategory == null || selectedCategory!.isEmpty
? null
: () {
Navigator.pop(context, selectedCategory);
},
child: const Text('OK'),
);
}
}
Jadi, widget yang kita buat tadi berfungsi untuk menampilkan dialog yang didalamnya ada sebuah radio button yang bisa dipilih oleh user.
Buat Class CurrencyIndonesiaInputFormatter
Masih didalam file main.dart silakan kita buat class baru dengan nama CurrencyIndonesiaInputFormatter dan isi kode didalamnya menjadi seperti berikut.
class CurrencyIndonesiaInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
if (newValue.text.isEmpty) {
return const TextEditingValue(
text: '0',
selection: TextSelection.collapsed(offset: 1),
);
} else if (newValue.selection.baseOffset == 0) {
return newValue;
}
var newNumberValue = double.parse(newValue.text.replaceAll(',', '').replaceAll('.', ''));
final formatter = NumberFormat('#,###');
final newText = formatter.format(newNumberValue).replaceAll(',', '.');
return newValue.copyWith(
text: newText,
selection: TextSelection.collapsed(offset: newText.length),
);
}
}
Class tersebut berfungsi untuk melakukan formatting angka rupiah ketika kita input angka didalam TextField.
Buat Widget TextField Category
Kemudian, kita akan membuat sebuah widget TextField untuk pilih kategori. Buka file main.dart dan ubah kode berikut.
Widget buildWidgetTextFieldCategory() {
// TODO: Buat widget text field category
return Container();
}
Menjadi seperti berikut.
Widget buildWidgetTextFieldCategory() {
return TextFormField(
controller: controllerCategory,
decoration: const InputDecoration(
labelText: 'Kategori',
isDense: true,
suffixIcon: Icon(
Icons.arrow_drop_down,
),
),
readOnly: true,
onTap: () async {
final chooseCategory = await showDialog<String>(
context: context,
builder: (context) {
return WidgetDialogChoose(
widthScreen: widthScreen,
heightScreen: heightScreen,
title: 'Pilih Kategori',
listOptions: listCategories,
defaultSelected: selectedCategory,
);
},
);
if (chooseCategory != null) {
setState(() {
controllerJobPosition.clear();
selectedJobPosition = '';
selectedCategory = chooseCategory;
indexCategory = listCategories.indexWhere((element) => element == selectedCategory);
controllerCategory.text = selectedCategory;
});
}
unfocus();
},
);
}
Sekarang coba kita jalankan programnya maka, outputnya akan menjadi seperti berikut.
Buat Widget Input Peminjaman Dana
Langkah berikutnya kita akan membuat inputan peminjaman dana. Jadi, inputan ini akan muncul jika si user pilih kategorinya peminjaman dana. Ubah kode berikut.
Widget buildWidgetInputPeminjamanDana() {
// TODO: Buat widget input peminjaman dana
return Container();
}
Menjadi seperti berikut.
Widget buildWidgetInputPeminjamanDana() {
return Padding(
padding: const EdgeInsets.only(top: 16),
child: TextFormField(
decoration: const InputDecoration(
labelText: 'Jumlah',
isDense: true,
prefix: Text('Rp.'),
),
keyboardType: TextInputType.number,
minLines: 1,
maxLines: 1,
inputFormatters: [
CurrencyIndonesiaInputFormatter(),
],
),
);
}
Sekarang coba kita jalankan kembali programnya. Maka, output-nya akan menjadi seperti berikut.
Buat Widget Input Perjalanan Dinas
Langkah selanjutnya, kita akan membuat widget input perjalanan dinas jika si user pilih kategori perjalanan dinas. Silakan kita ubah kode berikut.
Widget buildWidgetInputPerjalananDinas() {
// TODO: Buat widget input perjalanan dinas
return Container();
}
Menjadi seperti berikut.
Widget buildWidgetInputPerjalananDinas() {
return Padding(
padding: const EdgeInsets.only(top: 16),
child: TextFormField(
decoration: const InputDecoration(
labelText: "Lokasi",
isDense: true,
),
keyboardType: TextInputType.text,
minLines: 1,
maxLines: 1,
),
);
}
Jika dijalankan programnya maka, outputnya akan menjadi seperti berikut.
Buat Widget Input Pengajuan Karyawan Baru
Selanjutnya, kita akan membuat input pengajuan karyawan baru jika si user pilih kategori pengajuan karyawan baru. Untuk membuatnya silakan ubah kode berikut.
Widget buildWidgetInputPengajuanKaryawanBaru() {
// TODO: Buat widget input pengajuan karyawan baru
return Container();
}
Menjadi seperti berikut.
Widget buildWidgetInputPengajuanKaryawanBaru() {
return Padding(
padding: const EdgeInsets.only(top: 16),
child: TextFormField(
controller: controllerJobPosition,
decoration: const InputDecoration(
labelText: 'Posisi pekerjaan',
isDense: true,
suffixIcon: Icon(
Icons.arrow_drop_down,
),
),
readOnly: true,
onTap: () async {
final chooseJobPosition = await showDialog<String>(
context: context,
builder: (context) {
return WidgetDialogChoose(
widthScreen: widthScreen,
heightScreen: heightScreen,
title: 'Pilih Posisi Pekerjaan',
listOptions: listJobPositions,
defaultSelected: selectedJobPosition,
);
},
);
if (chooseJobPosition != null) {
setState(() {
selectedJobPosition = chooseJobPosition;
controllerJobPosition.text = selectedJobPosition;
});
}
unfocus();
},
),
);
}
Dan berikut ialah outputnya.
Buat Function unfocus
Silakan kita ubah kode berikut.
void unfocus() {
// TODO: Buat function untuk unfocus setelah pilih data
}
Menjadi seperti berikut.
void unfocus() {
final primaryFocus = FocusManager.instance.primaryFocus;
if (primaryFocus != null) {
primaryFocus.unfocus();
}
}
Jadi, function tersebut berfungsi agar ketika si user pilih kategori yang didalam TextField maka, fokus-nya itu tidak mengarah ke TextField setelah selesai pilih kategori-nya.
Buat Widget Button Submit
Langkah terakhir kita akan membuat widget button submit. Silakan kita ubah kode berikut.
Widget buildWidgetButtonSubmit() {
// TODO: Buat widget button submit
return Container();
}
Menjadi seperti berikut.
Widget buildWidgetButtonSubmit() {
return SizedBox(
width: double.infinity,
height: 42,
child: ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Data berhasil dikirimkan'),
),
);
},
child: const Text('Kirim'),
),
);
}
Kesimpulan
Jadi, kurang lebih seperti itulah cara menggunakan widget IndexedStack di Flutter. Sangat gampangkan cara pakainya. Seperti biasa untuk source code lengkapnya bisa dicek di Github ya.