Dimas Herjunodarpito Notoprayitno
2206081282
PBP C
-
Apa perbedaan utama antara stateless dan stateful widget dalam konteks pengembangan aplikasi Flutter?
Stateless Widgets: Merupakan widget yang statusnya tidak dapat diubah setelah dibuat. Setelah dikembangkan, widget ini tidak dapat diubah, yang berarti setiap perubahan pada variabel, ikon, tombol, atau pengambilan data tidak akan memengaruhi status aplikasi.Stateful Widgets: Merupakan widget yang statusnya dapat diubah setelah dibuat. Status ini dapat diubah, artinya dapat berubah beberapa kali selama hidupnya. Hal ini pada dasarnya berarti bahwa status aplikasi dapat berubah beberapa kali tergantung pada berbagai variabel, input, dan data. Ini digunakan ketika antarmuka pengguna dapat berubah dengan cepat. CheckBox, RadioButton, Form, dan TextField adalah beberapa contohnya.
-
Sebutkan seluruh widget yang kamu gunakan untuk menyelesaikan tugas ini dan jelaskan fungsinya masing-masing.
- MaterialApp: Widget kenyamanan yang membungkus sejumlah widget yang umumnya diperlukan untuk Material Design applications
- ThemeData: Widget untuk memanipulasi tema seluruh aplikasi yang dibuat.
- ColorScheme: Widget yang berisi satu set 30 warna berdasarkan spesifikasi Material yang dapat digunakan untuk mengonfigurasi properti warna sebagian besar komponen.
- Scaffold: Widget berupa kelas dalam flutter yang menyediakan banyak widget atau bisa dikatakan API seperti Drawer, Snack-Bar, Bottom-Navigation-Bar, Floating-Action-Button, App-Bar, dll. Scaffold akan memperluas atau menempati seluruh layar perangkat. Ini akan menempati ruang yang tersedia.
- AppBar: Widget yang biasanya merupakan komponen paling atas dari aplikasi (atau terkadang paling bawah), berisi toolbar dan beberapa tombol aksi umum lainnya.
- Text: Text widget menampilkan serangkaian teks dengan single-style. String dapat dipecah menjadi beberapa baris atau mungkin semuanya ditampilkan pada baris yang sama, tergantung pada batasan tata letak. Argumen gaya bersifat opsional.
- Padding: Widget yang menambahkan lapisan atau ruang kosong di sekitar widget atau sekelompok widget.
- Column: Widget yang digunakan untuk membuat tata letak vertikal.
- GridView.count: Widget yang membuat tata letak dengan jumlah tile tetap pada sumbu silang
- Material: Widget yang berfungsi untuk menyediakan desain visual Material Design pada widget lain.
- InkWell: Widget berupa area persegi panjang widget Material yang merespons peristiwa sentuhan dengan menampilkan percikan yang terpotong.
- Container: Kelas widget yang memungkinkan untuk menyesuaikan widget turunannya.
- Icon: Widget yang berfungsi untuk menampilkan beragam ikon dengan aneka model dan ukuran.
- SnackBar: Widget berupa pesan ringan dengan tindakan opsional yang ditampilkan secara singkat di bagian bawah layar.
-
Jelaskan bagaimana cara kamu mengimplementasikan checklist di atas secara step-by-step (bukan hanya sekadar mengikuti tutorial)
- Membuat project Flutter, lalu masuk ke dalam direktori project Flutter, dan membukanya di dalam Visual Studio Code
flutter create battlechar_mobile cd battlechar_mobile code .
- Menambahkan file dengan nama
menu.dart
di dalam direktoribattlechar_mobile/lib/
- Isi file
menu.dart
tersebut seperti berikut:import 'package:flutter/material.dart'; class MenuItem { final String name; final IconData icon; final Color color; MenuItem(this.name, this.icon, this.color); } class MyHomePage extends StatelessWidget { MyHomePage({Key? key}) : super(key: key); final List<MenuItem> items = [ MenuItem("See Operators", Icons.local_police, Colors.blue), MenuItem("Add Operators", Icons.person_add_alt_1_rounded , Colors.green), MenuItem("Logout", Icons.logout, Colors.red), ]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( 'Battlechar', style: TextStyle(color: Colors.white), ), backgroundColor: Colors.orange, ), body: SingleChildScrollView( // Widget wrapper yang dapat discroll child: Padding( padding: const EdgeInsets.all(10.0), // Set padding dari halaman child: Column( // Widget untuk menampilkan children secara vertikal children: <Widget>[ const Padding( padding: EdgeInsets.only(top: 10.0, bottom: 10.0), // Widget Text untuk menampilkan tulisan dengan alignment center dan style yang sesuai child: Text( 'Welcome to Battlechar', // Text yang menandakan toko textAlign: TextAlign.center, style: TextStyle( fontSize: 30, fontWeight: FontWeight.bold, ), ), ), // Grid layout GridView.count( // Container pada card kita. primary: true, padding: const EdgeInsets.all(20), crossAxisSpacing: 10, mainAxisSpacing: 10, crossAxisCount: 3, shrinkWrap: true, children: items.map((MenuItem item) { // Iterasi untuk setiap item return ShopCard(item); }).toList(), ), ], ), ), ), ); } } class ShopCard extends StatelessWidget { final MenuItem item; const ShopCard(this.item, {super.key}); // Constructor @override Widget build(BuildContext context) { return Material( color: item.color, child: InkWell( // Area responsive terhadap sentuhan onTap: () { // Memunculkan SnackBar ketika diklik ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar( content: Text("You've pressed the ${item.name} button!"))); }, child: Container( // Container untuk menyimpan Icon dan Text padding: const EdgeInsets.all(8), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( item.icon, color: Colors.white, size: 30.0, ), const Padding(padding: EdgeInsets.all(3)), Text( item.name, textAlign: TextAlign.center, style: const TextStyle(color: Colors.white), ), ], ), ), ), ), ); } }
- Memodifikasi file
battlechar_mobile/lib/main.dart
menjadi:import 'package:flutter/material.dart'; import 'package:battlechar_mobile/menu.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Battlechar', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange), useMaterial3: true, ), home: MyHomePage(), ); } }
- Membuat project Flutter, lalu masuk ke dalam direktori project Flutter, dan membukanya di dalam Visual Studio Code
-
Jelaskan perbedaan antara
Navigator.push()
danNavigator.pushReplacement()
, disertai dengan contoh mengenai penggunaan kedua metode tersebut yang tepat!
Navigator.push()
: Methodpush()
menambahkan suatu route ke dalam stack route yang dikelola oleh Navigator. Method ini menyebabkan route yang ditambahkan berada pada paling atas stack, sehingga route yang baru saja ditambahkan tersebut akan muncul dan ditampilkan kepada pengguna.Contoh:
... if (item.name == "Add Operators") { Navigator.push(context, MaterialPageRoute(builder: (context) => const OperatorFormPage())); } ...
Navigator.pushReplacement()
: MethodpushReplacement()
menghapus route yang sedang ditampilkan kepada pengguna dan menggantinya dengan suatu route. Method ini menyebabkan aplikasi untuk berpindah dari route yang sedang ditampilkan kepada pengguna ke suatu route yang diberikan. Pada stack route yang dikelolaNavigator
, route lama pada atas stack akan digantikan secara langsung oleh route baru yang diberikan tanpa mengubah kondisi elemen stack yang berada di bawahnya.Walaupun
push()
danpushReplacement()
sekilas terlihat mirip, namun perbedaan kedua method tersebut terletak pada apa yang dilakukan kepada route yang berada pada atas stack.push()
akan menambahkan route baru diatas route yang sudah ada pada atas stack, sedangkanpushReplacement()
menggantikan route yang sudah ada pada atas stack dengan route baru tersebut. Penting juga untuk memperhatikan kemungkinan urutan dan isi dari stack, karena jika kondisi stack kosong serta kita menekan tombol Back pada gawai, maka sistem akan keluar dari aplikasi tersebut.Contoh:
... onTap: () { Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => MyHomePage(), )); }, ...
-
Jelaskan masing-masing layout widget pada Flutter dan konteks penggunaannya masing-masing!
Single-child layout widgets:- Align: Widget yang menyelaraskan child-nya di dalam dirinya sendiri dan secara opsional menyesuaikan ukurannya berdasarkan ukuran child.
- AspectRatio: Widget yang mencoba mengukur ukuran child ke rasio aspek tertentu.
- Baseline: Container yang memposisikan child-nya sesuai dengan garis dasar child.
- Center: Alignment block yang memusatkan child-nya di dalam dirinya sendiri.
- ConstrainedBox: Widget yang memberikan batasan tambahan pada child-nya.
- Container: Widget kenyamanan yang memadukan widget pengecatan, pemosisian, dan ukuran yang umum.
- Padding: Widget yang menyisipkan child dengan padding yang diberikan.
- SizedBox: Box dengan ukuran tertentu. Jika diberi child, widget ini memaksanya untuk memiliki lebar dan/atau tinggi tertentu
- Transform: Widget yang menerapkan transformasi sebelum melukis child-nya.
Multi-child layout widgets:
- Column: Tata letak daftar widget child dalam arah vertikal.
- GridView: Grid list terdiri dari pola berulang sel yang disusun dalam tata letak vertikal dan horizontal. Widget GridView mengimplementasikan komponen ini.
- ListView: Daftar widget yang dapat digulir yang disusun secara linear. ListView adalah widget gulir yang paling sering digunakan. Widget ini menampilkan child-childnya satu demi satu dalam arah gulir. Pada sumbu silang, child-child harus mengisi ListView.
- Row: Tata letak daftar widget child dalam arah horizontal.
- Stack: Widget yang memposisikan child-childnya relatif terhadap tepi kotak. Kelas ini berguna jika kita ingin menimpa beberapa child dengan cara yang sederhana, misalnya memiliki beberapa teks dan gambar, yang ditimpa dengan gradien dan tombol yang dilampirkan di bagian bawah.
Sliver widgets:
- CupertinoSliverNavigationBar: Bilah navigasi bergaya iOS dengan judul besar bergaya iOS-11 menggunakan sliver.
-
Sebutkan apa saja elemen input pada form yang kamu pakai pada tugas kali ini dan jelaskan mengapa kamu menggunakan elemen input tersebut!
- Form: Widget ini saya gunakan agar menjadi wadah untuk berbagai input field widget yang saya buat.
- TextFormField: Widget ini saya gunakan untuk menerima input user dalam bentuk string.
-
Bagaimana penerapan clean architecture pada aplikasi Flutter?
Penerapan Clean Architecture pada aplikasi Flutter biasanya melibatkan pembagian proyek menjadi beberapa lapisan utama:- Data & Platform layer: Lapisan data berada di lapisan paling luar. Lapisan ini terdiri dari kode sumber data seperti konsumsi Rest API, akses ke database lokal, Firebase, atau sumber lain.
- Domain layer: Lapisan ini berisi entitas bisnis dan kasus penggunaan. Entitas adalah objek sederhana yang mewakili detail bisnis paling mendasar dalam aplikasi yang kita buat.
- Presentation layer: Lapisan ini berisi kode UI dan logika presentasi. Ini biasanya melibatkan kerangka kerja seperti Flutter untuk membangun antarmuka pengguna.
Dengan menerapkan Clean Architecture, kita sebagai developer dapat membuat kode yang lebih mudah dikelola dan dikembangkan. Selain itu, hal ini juga memudahkan kerja sama antar tim dalam pengembangan2.
-
Jelaskan bagaimana cara kamu mengimplementasikan checklist di atas secara step-by-step! (bukan hanya sekadar mengikuti tutorial)
- Melakukan modifikasi terhadap
main.dart
:import 'package:flutter/material.dart'; import 'package:battlechar_mobile/screens/menu.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Battlechar', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo), useMaterial3: true, ), home: MyHomePage(), ); } }
- Membuat direktori
lib/screens/
danlib/widgets
. - Memindahkan file
menu.dart
ke dalam direktorilib/screens/
. - Melakukan modifikasi terhadap file
menu.dart
:import 'package:flutter/material.dart'; import 'package:battlechar_mobile/widgets/left_drawer.dart'; import 'package:battlechar_mobile/widgets/menu_card.dart'; class MyHomePage extends StatelessWidget { MyHomePage({Key? key}) : super(key: key); final List<MenuItem> items = [ MenuItem("See Operators", Icons.local_police, Colors.blueGrey.shade900), MenuItem("Add Operators", Icons.person_add_alt_1_rounded , Colors.blue.shade900), MenuItem("Logout", Icons.logout, Colors.red.shade900), ]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( 'Battlechar', style: TextStyle(color: Colors.white), ), backgroundColor: Colors.indigo.shade900, foregroundColor: Colors.white, ), drawer: const LeftDrawer(), body: SingleChildScrollView( // Widget wrapper yang dapat discroll child: Padding( padding: const EdgeInsets.all(10.0), // Set padding dari halaman child: Column( // Widget untuk menampilkan children secara vertikal children: <Widget>[ const Padding( padding: EdgeInsets.only(top: 10.0, bottom: 10.0), // Widget Text untuk menampilkan tulisan dengan alignment center dan style yang sesuai child: Text( 'Welcome to Battlechar', // Text yang menandakan toko textAlign: TextAlign.center, style: TextStyle( fontSize: 30, fontWeight: FontWeight.bold, ), ), ), // Grid layout GridView.count( // Container pada card kita. primary: true, padding: const EdgeInsets.all(20), crossAxisSpacing: 10, mainAxisSpacing: 10, crossAxisCount: 3, shrinkWrap: true, children: items.map((MenuItem item) { // Iterasi untuk setiap item return MenuCard(item); }).toList(), ), ], ), ), ), ); } }
- Menambahkan file
left_drawer.dart
di dalam direktorilib/widgets/
:import 'package:battlechar_mobile/screens/operatorlist_items.dart'; import 'package:flutter/material.dart'; import 'package:battlechar_mobile/screens/menu.dart'; import 'package:battlechar_mobile/screens/operatorlist_form.dart'; class LeftDrawer extends StatelessWidget { const LeftDrawer({super.key}); @override Widget build(BuildContext context) { return Drawer( child: ListView( children: [ const DrawerHeader( decoration: BoxDecoration( color: Colors.indigo, ), child: Column( children: [ Text( 'Battlechar', textAlign: TextAlign.center, style: TextStyle( fontSize: 30, fontWeight: FontWeight.bold, color: Colors.white, ), ), Padding(padding: EdgeInsets.all(10)), Text( "Create and manage your operators from here!", textAlign: TextAlign.center, style: TextStyle( fontSize: 15, fontWeight: FontWeight.normal, color: Colors.white, ), ), ], ), ), // Bagian routing ListTile( leading: const Icon(Icons.home_outlined), title: const Text('Home'), // Bagian redirection ke MyHomePage onTap: () { Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => MyHomePage(), )); }, ), ListTile( leading: const Icon(Icons.person_add_alt_1_rounded), title: const Text('Add Operators'), // Bagian redirection ke ShopFormPage onTap: () { Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => const OperatorFormPage(), )); }, ), ListTile( leading: const Icon(Icons.local_police), title: const Text('See Operators'), // Bagian redirection ke ShopFormPage onTap: () { Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => const OperatorsPage(), )); }, ), ], ), ); } }
- Menambahkan file
menu_card.dart
di dalam direktorilib/widgets/
:import 'package:battlechar_mobile/screens/operatorlist_items.dart'; import 'package:flutter/material.dart'; import 'package:battlechar_mobile/screens/operatorlist_form.dart'; class MenuItem { final String name; final IconData icon; final Color color; MenuItem(this.name, this.icon, this.color); } class Operator { final String name; final int price; final String description; final String primaryWeapon; final String secondaryWeapon; final int primaryWeaponAmmo; final int secondaryWeaponAmmo; final int armor; final int speed; Operator({required this.name, required this.price, required this.description, required this.primaryWeapon, required this.secondaryWeapon, required this.primaryWeaponAmmo, required this.secondaryWeaponAmmo, required this.armor, required this.speed}); } class MenuCard extends StatelessWidget { final MenuItem item; const MenuCard(this.item, {super.key}); // Constructor @override Widget build(BuildContext context) { return Material( color: item.color, child: InkWell( // Area responsive terhadap sentuhan onTap: () { // Memunculkan SnackBar ketika diklik ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar( content: Text("You've pressed the ${item.name} button!"))); // Navigate ke route yang sesuai (tergantung jenis tombol) if (item.name == "Add Operators") { Navigator.push(context, MaterialPageRoute(builder: (context) => const OperatorFormPage())); } if (item.name == "See Operators") { Navigator.push(context, MaterialPageRoute(builder: (context) => const OperatorsPage())); } }, child: Container( // Container untuk menyimpan Icon dan Text padding: const EdgeInsets.all(8), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( item.icon, color: Colors.white, size: 30.0, ), const Padding(padding: EdgeInsets.all(3)), Text( item.name, textAlign: TextAlign.center, style: const TextStyle(color: Colors.white), ), ], ), ), ), ), ); } }
- Menambahkan file
operatorlist_form.dart
di dalam direktorilib/screens/
:import 'package:battlechar_mobile/widgets/menu_card.dart'; import 'package:flutter/material.dart'; import 'package:battlechar_mobile/widgets/left_drawer.dart'; class OperatorFormPage extends StatefulWidget { const OperatorFormPage({super.key}); @override State<OperatorFormPage> createState() => _OperatorFormPageState(); } List<Operator> operators = []; class _OperatorFormPageState extends State<OperatorFormPage> { final _formKey = GlobalKey<FormState>(); String _name = ""; int _price = 0; String _primaryWeapon = ""; int _primaryWeaponAmmo = 0; String _secondaryWeapon = ""; int _secondaryWeaponAmmo = 0; int _armor = 0; int _speed = 0; String _description = ""; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Center( child: Text( 'Add Operator Form', ), ), backgroundColor: Colors.indigo.shade900, foregroundColor: Colors.white, ), drawer: const LeftDrawer(), body: Form( key: _formKey, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Operator Name", labelText: "Operator Name", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _name = value!; }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Name cannot be empty!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Price", labelText: "Price", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _price = int.parse(value!); }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Price cannot be empty!"; } if (int.tryParse(value) == null) { return "Price must be a valid integer!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Primary Weapon", labelText: "Primary Weapon", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _primaryWeapon = value!; }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Primary weapon cannot be empty!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Primary Weapon Ammo Amount", labelText: "Primary Weapon Ammo Amount", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _primaryWeaponAmmo = int.parse(value!); }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Primary weapon ammo amount cannot be empty!"; } if (int.tryParse(value) == null) { return "Primary weapon ammo amount must be a valid integer!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Secondary Weapon", labelText: "Secondary Weapon", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _secondaryWeapon = value!; }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Secondary weapon cannot be empty!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Secondary Weapon Ammo Amount", labelText: "Secondary Weapon Ammo Amount", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _secondaryWeaponAmmo = int.parse(value!); }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Secondary weapon ammo amount cannot be empty!"; } if (int.tryParse(value) == null) { return "Secondary weapon ammo amount must be a valid integer!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Armor", labelText: "Armor", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _armor = int.parse(value!); }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Armor value cannot be empty!"; } if (int.tryParse(value) == null) { return "Armor value must be a valid integer!"; } if (int.tryParse(value)! > 3 || int.tryParse(value)! < 1) { return "Armor value must be in interval 1-to-3!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Speed", labelText: "Speed", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _speed = int.parse(value!); }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Speed value cannot be empty!"; } if (int.tryParse(value) == null) { return "Speed value must be a valid integer!"; } if (int.tryParse(value)! > 3 || int.tryParse(value)! < 1) { return "Speed value must be in interval 1-to-3!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Description", labelText: "Description", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _description = value!; }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Description cannot be empty!"; } return null; }, ), ), Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.indigo), ), onPressed: () { if (_formKey.currentState!.validate()) { operators.add(Operator( name: _name, price: _price, description: _description, primaryWeapon: _primaryWeapon, secondaryWeapon: _secondaryWeapon, primaryWeaponAmmo: _primaryWeaponAmmo, secondaryWeaponAmmo: _secondaryWeaponAmmo, armor: _armor, speed: _speed, )); showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('Operator saved successfully'), content: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Operator name: $_name'), Text('Primary weapon: $_primaryWeapon Ammo: $_primaryWeaponAmmo'), Text('Secondary weapon: $_secondaryWeapon Ammo: $_secondaryWeaponAmmo'), Text('Armor: $_armor Speed: $_speed'), Text('Price: $_price'), Text('Description: $_description'), ], ), ), actions: [ TextButton( child: const Text('OK'), onPressed: () { Navigator.pop(context); }, ), ], ); }, ); _formKey.currentState!.reset(); } }, child: const Text( "Save", style: TextStyle(color: Colors.white), ), ), ), ), ] ), ), ), ); } }
- Menambahkan file
operatorlist_items.dart
di dalam direktorilib/screens/
:import 'package:flutter/material.dart'; import 'package:battlechar_mobile/widgets/left_drawer.dart'; import 'package:battlechar_mobile/widgets/menu_card.dart'; import 'package:battlechar_mobile/screens/operatorlist_form.dart'; class OperatorsPage extends StatefulWidget { const OperatorsPage({super.key}); @override State<StatefulWidget> createState() => _OperatorsPageState(); } class _OperatorsPageState extends State<OperatorsPage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Operators'), backgroundColor: Colors.indigo.shade900, foregroundColor: Colors.white, ), drawer: LeftDrawer(), body: ListView.builder( itemCount: operators.length, itemBuilder: (context, index) { return ListTile( title: Text(operators[index].name), subtitle: Text("Primary Weapon: ${operators[index].primaryWeapon} Ammo: ${operators[index].primaryWeaponAmmo} \nSecondary Weapon: ${operators[index].secondaryWeapon} Ammo: ${operators[index].secondaryWeaponAmmo} \nArmor: ${operators[index].armor} Speed: ${operators[index].speed} \nPrice: ${operators[index].price} \nDescription: ${operators[index].description}"), onTap: () {}, ); } ), ); } }
- Melakukan modifikasi terhadap
-
Apakah bisa kita melakukan pengambilan data JSON tanpa membuat model terlebih dahulu? Jika iya, apakah hal tersebut lebih baik daripada membuat model sebelum melakukan pengambilan data JSON?
Ya, kita bisa melakukan pengambilan data JSON tanpa membuat model terlebih dahulu. Ini bisa dilakukan dengan langsung mem-parsing data JSON ke dalam struktur data seperti dictionary atau array, tergantung pada struktur data JSON itu sendiri.Namun, apakah ini lebih baik daripada membuat model terlebih dahulu? Jawabannya tergantung pada konteks dan kebutuhan aplikasi kta. Berikut adalah beberapa pertimbangan:
- Keuntungan menggunakan model:
- Validasi data: Model dapat membantu dalam validasi data. Misalnya, jika kita tahu bahwa suatu nilai harus berupa integer dan model kita mencerminkan hal ini, maka jika data JSON berisi nilai non-integer untuk bidang ini, kesalahan akan terjadi saat parsing.
- Ease of Use: Dengan model, kita dapat mengakses data menggunakan properti objek, yang bisa lebih intuitif dan mudah dibaca daripada mengakses nilai melalui kunci dictionary.
- Dokumentasi dan struktur: Model memberikan struktur yang jelas untuk data kita, yang bisa sangat membantu dalam dokumentasi dan pemahaman data.
- Keuntungan tidak menggunakan model:
- Fleksibilitas: Jika struktur data JSON sangat dinamis dan berubah-ubah, mungkin lebih mudah untuk bekerja dengan dictionary atau array daripada mencoba menyesuaikan model kita setiap kali struktur berubah.
- Kecepatan pengembangan: Dalam beberapa kasus, mungkin lebih cepat dan lebih mudah untuk mulai bekerja dengan data tanpa menghabiskan waktu untuk mendefinisikan model terlebih dahulu.
Secara umum, jika kita bekerja dengan API atau data JSON yang memiliki struktur yang konsisten dan kita ingin memanfaatkan keuntungan dari model, maka pendekatan berbasis model mungkin lebih baik. Jika kita memerlukan fleksibilitas atau kecepatan, mungkin lebih baik bekerja langsung dengan data JSON. kita harus mempertimbangkan trade-off ini dalam konteks proyek kita sendiri.
- Keuntungan menggunakan model:
-
Jelaskan fungsi dari CookieRequest dan jelaskan mengapa instance CookieRequest perlu untuk dibagikan ke semua komponen di aplikasi Flutter.
Kelas CookieRequest merupakan bagian dari implementasi manajemen otentikasi (authentication) di aplikasi Flutter dengan menggunakan cookies. Fungsionalitas utama dari kelas ini:- Inisialisasi dan Pemeliharaan Cookies:
- Kelas ini memiliki fungsi
init()
yang digunakan untuk menginisialisasi dan memelihara status cookies. Ini mencakup membaca cookies yang sudah tersimpan, memeriksa apakah sesi pengguna sudah login, dan mengelola headers yang berisi informasi cookie.
- Kelas ini memiliki fungsi
- Request HTTP dengan Otentikasi:
- Kelas ini memiliki beberapa metode untuk melakukan permintaan HTTP seperti
get
,post
, danpostJson
. Semua metode ini menyertakan informasi otentikasi dalam header HTTP jika pengguna sudah login.
- Kelas ini memiliki beberapa metode untuk melakukan permintaan HTTP seperti
- Manajemen Sesi Pengguna:
- Kelas ini menyediakan fungsionalitas untuk login dan logout. Metode
login
digunakan untuk mengirimkan permintaan login ke server, dan jika berhasil, menyimpan cookies dan status login. Metodelogout
digunakan untuk melakukan logout, menghapus cookies, dan mengubah status login.
- Kelas ini menyediakan fungsionalitas untuk login dan logout. Metode
- Penanganan Cookies:
- Kelas ini memiliki fungsi-fungsi untuk mengelola cookies, termasuk pembacaan dari penyimpanan lokal (shared preferences), pembaruan berdasarkan respons HTTP, dan pembuatan header cookie.
- Keamanan dan Pengaturan Cookies:
- Fungsi
_setCookie
digunakan untuk menguraikan dan menyimpan properti cookies, termasuk waktu kedaluwarsa (max-age). Fungsi_generateCookieHeader
digunakan untuk membuat header cookie yang akan disertakan dalam setiap permintaan HTTP.
- Fungsi
Mengapa perlu dibagikan ke semua komponen di aplikasi Flutter?
- Pemeliharaan Status Login: Dengan membagikan instance
CookieRequest
ke semua komponen, kita dapat memastikan bahwa status login pengguna dapat diakses dan diperbarui secara konsisten di seluruh aplikasi. Hal ini penting agar komponen-komponen yang berbeda dapat merespons perubahan status login dengan benar. - Pengelolaan Otentikasi: Instance
CookieRequest
menyimpan informasi otentikasi dan cookies. Dengan membagikannya ke berbagai komponen, kita dapat mengakses informasi otentikasi ini dari mana saja dalam aplikasi, memungkinkan komunikasi yang efektif dengan server dan pemeliharaan otentikasi. - Pemrosesan Permintaan HTTP: Dengan menyediakan instance
CookieRequest
ke berbagai komponen, setiap komponen dapat melakukan permintaan HTTP dengan otentikasi tanpa perlu mengulang kembali proses autentikasi. Ini meningkatkan efisiensi dan mengurangi redundansi kode. - Manajemen Cookies yang Konsisten: Melalui instance
CookieRequest
, komponen-komponen dapat mengakses dan memanipulasi cookies secara konsisten. Ini membantu dalam penanganan cookies dan menjaga konsistensi antara komponen-komponen yang berbeda.
- Inisialisasi dan Pemeliharaan Cookies:
-
Jelaskan mekanisme pengambilan data dari JSON hingga dapat ditampilkan pada Flutter.
Data diambil melalui respons URL yang telah ditentukan dalam proyek Django. Respons tersebut kemudian diuraikan (decoded) menjadi format JSON. Selanjutnya, dibuatlah sebuah daftar (list) item yang berisi elemen-elemen Item, di mana setiap elemen ini berasal dari hasil dekoding JSON yang tadi. Proses ini dilakukan dengan menggunakan iterasi, di mana setiap item dimasukkan ke dalam list item.Untuk mengambil hasil dari pengambilan item,
FutureBuilder
digunakan.FutureBuilder
memberikan kemampuan untuk memanggil hasil dari fungsi asinkron (async) yang mengambil item. Hasil dari proses ini kemudian ditampilkan menggunakanListView.builder
, yang memungkinkan pembuatan tampilan dinamis berdasarkan data yang diterima. -
Jelaskan mekanisme autentikasi dari input data akun pada Flutter ke Django hingga selesainya proses autentikasi oleh Django dan tampilnya menu pada Flutter.
Pengguna menginputkan username dan password melalui TextField dalam framework Flutter, dan data ini disimpan dalam variabel username dan password. Ketika tombol login ditekan, aplikasi mengirimkan HTTP POST request ke endpoint/auth/login/
di server Django. Data username dan password dikirimkan sebagai bagian dari body request dalam format JSON.Server Django menerima request dan melakukan proses autentikasi terhadap pengguna. Jika autentikasi berhasil, Django mengirimkan respon berisi pesan sukses dan data pengguna. Namun, jika autentikasi gagal, Django mengirimkan respon dengan pesan kesalahan. Aplikasi Flutter menerima respon dari Django dan memeriksa apakah autentikasi berhasil. Jika berhasil, aplikasi akan mengarahkan pengguna ke halaman utama (
MyHomePage
); sebaliknya, jika gagal, aplikasi akan menampilkan pesan kesalahan.Setelah pengguna berhasil login, mereka akan diarahkan ke halaman utama aplikasi di mana menu aplikasi ditampilkan.
-
Sebutkan seluruh widget yang kamu pakai pada tugas ini dan jelaskan fungsinya masing-masing.
- AppBar: The AppBar displays the toolbar widgets, leading, title, and actions, above the bottom (if any).
- Column: A widget that displays its children in a vertical array.
- Scaffold: Scaffold is a class in flutter which provides many widgets or we can say APIs like Drawer, Snack-Bar, Bottom-Navigation-Bar, Floating-Action-Button, App-Bar, etc. Scaffold will expand or occupy the whole device screen. It will occupy the available space. Scaffold will provide a framework to implement the basic material design layout of the application.
- Container: A convenience widget that combines common painting, positioning, and sizing widgets.
- TextField: A text field lets the user enter text, either with hardware keyboard or with an onscreen keyboard.
- ElevatedButton: An elevated button is a label child displayed on a Material widget whose Material.elevation increases when the button is pressed.
- FutureBuilder: A widget that builds itself based on the latest snapshot of interaction with a Future.
- SnackBar: SnackBar is a Flutter widget that enables you to temporarily display a pop-up message in your app.
- Navigator: A widget that manages a set of child widgets with a stack discipline.
- ListView.builder: The ListView.builder constructor takes an IndexedWidgetBuilder, which builds the children on demand.
-
Jelaskan bagaimana cara kamu mengimplementasikan checklist di atas secara step-by-step! (bukan hanya sekadar mengikuti tutorial).
- Membuat file
login.dart
di dalamlib/screens/
:import 'package:battlechar_mobile/screens/menu.dart'; import 'package:battlechar_mobile/screens/register.dart'; import 'package:flutter/material.dart'; import 'package:pbp_django_auth/pbp_django_auth.dart'; import 'package:provider/provider.dart'; void main() { runApp(const LoginApp()); } class LoginApp extends StatelessWidget { const LoginApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Login', theme: ThemeData( primarySwatch: Colors.indigo, ), home: const LoginPage(), ); } } class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override _LoginPageState createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { final TextEditingController _usernameController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); @override Widget build(BuildContext context) { final request = context.watch<CookieRequest>(); return Scaffold( appBar: AppBar( title: const Text( 'Login', style: TextStyle(color: Colors.white), ), backgroundColor: Colors.indigo[900], foregroundColor: Colors.white, ), body: Container( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextField( controller: _usernameController, decoration: const InputDecoration( labelText: 'Username', labelStyle: TextStyle(color: Colors.indigo), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.indigo), ), ), ), const SizedBox(height: 12.0), TextField( controller: _passwordController, decoration: const InputDecoration( labelText: 'Password', labelStyle: TextStyle(color: Colors.indigo), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.indigo), ), ), obscureText: true, ), const SizedBox(height: 24.0), ElevatedButton( onPressed: () async { String username = _usernameController.text; String password = _passwordController.text; final response = await request.login( "http://127.0.0.1:8080/auth/login/", // "http://dimas-herjunodarpito-tugas.pbp.cs.ui.ac.id/auth/login/", { 'username': username, 'password': password, }); if (request.loggedIn) { String message = response['message']; String uname = response['username']; // ignore: use_build_context_synchronously Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => MyHomePage()), ); // ignore: use_build_context_synchronously ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar( content: Text("$message Welcome, $uname."))); } else { // ignore: use_build_context_synchronously showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Login failed'), content: Text(response['message']), actions: [ TextButton( child: const Text('OK'), onPressed: () { Navigator.pop(context); }, ), ], ), ); } }, child: const Text('Login'), ), const SizedBox(height: 12.0), ElevatedButton( onPressed: () { // Navigate to Register Page Navigator.push( context, MaterialPageRoute(builder: (context) => RegisterPage()), ); }, child: const Text('Register'), ) ], ), ), ); } }
- Mengintegrasikan sistem autentikasi Django dengan proyek tugas Flutter.
- Membuat app authentication:
python manage.py startapp authentication
- Memodifikasi
requirements.txt
django gunicorn whitenoise psycopg2-binary requests urllib3 django-environ django-tailwind django-cors-headers
- Melakukan
pip install -r requirements.txt
- Memodifikasi
settings.py
project Django""" Django settings for battlechar project. Generated by 'django-admin startproject' using Django 4.2.5. For more information on this file, see https://docs.djangoproject.com/en/4.2/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.2/ref/settings/ """ from pathlib import Path import environ import os from django.core.management.commands.runserver import Command as rs rs.default_port='8080' # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent env = environ.Env() CSRF_TRUSTED_ORIGINS = ['http://dimas-herjunodarpito-tugas.pbp.cs.ui.ac.id', 'https://dimas-herjunodarpito-tugas.pbp.cs.ui.ac.id'] # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'django-insecure-t2ch*-d^38ic(-k86i8uz)c8cv+%x8ti=u6o^1b-(_psf^v(@*' PRODUCTION = env.bool('PRODUCTION', False) # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'main', 'tailwind', 'theme', 'authentication', 'corsheaders' ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'corsheaders.middleware.CorsMiddleware' ] CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_CREDENTIALS = True CSRF_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SAMESITE = 'None' SESSION_COOKIE_SAMESITE = 'None' ROOT_URLCONF = 'battlechar.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'battlechar.wsgi.application' # Database # https://docs.djangoproject.com/en/4.2/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } } if PRODUCTION: DATABASES = { 'default': env.db('DATABASE_URL') } DATABASES["default"]["ATOMIC_REQUESTS"] = True # Password validation # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'Asia/Jakarta' USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ STATIC_URL = 'static/' STATIC_ROOT = os.path.join(BASE_DIR, 'static') # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' TAILWIND_APP_NAME = 'theme' INTERNAL_IPS = [ "127.0.0.1", ]
- Memodifikasi
views.py
pada app authenticationfrom django.shortcuts import render from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.models import User @csrf_exempt def login(request): username = request.POST['username'] password = request.POST['password'] user = authenticate(username=username, password=password) if user is not None: if user.is_active: auth_login(request, user) # Status login sukses. return JsonResponse({ "username": user.username, "status": True, "message": "Login successful!" # Tambahkan data lainnya jika ingin mengirim data ke Flutter. }, status=200) else: return JsonResponse({ "status": False, "message": "Login failed, account deactivated." }, status=401) else: return JsonResponse({ "status": False, "message": "Login failed, recheck your username and password." }, status=401) @csrf_exempt def register(request): username = request.POST.get('username') password = request.POST.get('password') if User.objects.filter(username=username).exists(): return JsonResponse({"status": False, "message": "Username already used."}, status=400) user = User.objects.create_user(username=username, password=password) user.save() return JsonResponse({"username": user.username, "status": True, "message": "Register successful!"}, status=201) @csrf_exempt def logout(request): username = request.user.username try: auth_logout(request) return JsonResponse({"username": username, "status": True, "message": "Logout successful!"}, status=200) except: return JsonResponse({"status": False, "message": "Logout failed!"}, status=401)
- Membuat file
urls.py
pada app authenticationfrom django.urls import path from authentication.views import * app_name = 'authentication' urlpatterns = [ path('login/', login, name='login'), path('logout/', logout, name='logout'), path('register/', register, name='register'), ]
- Memodifikasi
urls.py
pada folder project Django""" URL configuration for battlechar project. The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/4.2/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('main.urls')), path('auth/', include('authentication.urls')), ]
- Masuk ke project Flutter dan install dua package
flutter pub add provider flutter pub add pbp_django_auth
- Memodifikasi
lib/main.dart
import 'package:battlechar_mobile/screens/login.dart'; import 'package:flutter/material.dart'; import 'package:pbp_django_auth/pbp_django_auth.dart'; import 'package:provider/provider.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return Provider(create: (_) { CookieRequest request = CookieRequest(); return request; }, child: MaterialApp( title: 'Battlechar', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo), useMaterial3: true, ), home: LoginPage(), )); } }
- Memodifikasi
lib/widgets/left_drawer.dart
import 'package:battlechar_mobile/screens/login.dart'; import 'package:battlechar_mobile/screens/operatorlist_items.dart'; import 'package:flutter/material.dart'; import 'package:battlechar_mobile/screens/menu.dart'; import 'package:battlechar_mobile/screens/operatorlist_form.dart'; import 'package:pbp_django_auth/pbp_django_auth.dart'; import 'package:provider/provider.dart'; class LeftDrawer extends StatelessWidget { const LeftDrawer({super.key}); @override Widget build(BuildContext context) { final request = context.watch<CookieRequest>(); return Drawer( child: ListView( children: [ const DrawerHeader( decoration: BoxDecoration( color: Colors.indigo, ), child: Column( children: [ Text( 'Battlechar', textAlign: TextAlign.center, style: TextStyle( fontSize: 30, fontWeight: FontWeight.bold, color: Colors.white, ), ), Padding(padding: EdgeInsets.all(10)), Text( "Create and manage your operators from here!", textAlign: TextAlign.center, style: TextStyle( fontSize: 15, fontWeight: FontWeight.normal, color: Colors.white, ), ), ], ), ), // Bagian routing ListTile( leading: const Icon(Icons.home_outlined), title: const Text('Home'), // Bagian redirection ke MyHomePage onTap: () { Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => MyHomePage(), )); }, ), ListTile( leading: const Icon(Icons.person_add_alt_1_rounded), title: const Text('Add Operators'), // Bagian redirection ke ShopFormPage onTap: () { Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => const OperatorFormPage(), )); }, ), ListTile( leading: const Icon(Icons.local_police), title: const Text('See Operators'), // Bagian redirection ke ShopFormPage onTap: () { Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => const OperatorsPage(), )); }, ), ListTile( leading: const Icon(Icons.logout), title: const Text('Logout'), // Bagian redirection ke ShopFormPage onTap: () async { final response = await request.logout( // "http://dimas-herjunodarpito-tugas.pbp.cs.ui.ac.id/auth/logout/", "http://127.0.0.1:8080/auth/logout/"); String message = response["message"]; if (response['status']) { String uname = response["username"]; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("$message See you again, $uname!"), )); Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const LoginPage()), ); } else { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("$message"), )); } }, ), ], ), ); } }
- Memodifikasi
lib/widgets/menu_card.dart
import 'package:battlechar_mobile/screens/operatorlist_items.dart'; import 'package:flutter/material.dart'; import 'package:battlechar_mobile/screens/operatorlist_form.dart'; import 'package:pbp_django_auth/pbp_django_auth.dart'; import 'package:provider/provider.dart'; import 'package:battlechar_mobile/screens/login.dart'; class MenuItem { final String name; final IconData icon; final Color color; MenuItem(this.name, this.icon, this.color); } class MenuCard extends StatelessWidget { final MenuItem item; const MenuCard(this.item, {super.key}); // Constructor @override Widget build(BuildContext context) { final request = context.watch<CookieRequest>(); return Material( color: item.color, child: InkWell( // Area responsive terhadap sentuhan onTap: () async { // Memunculkan SnackBar ketika diklik ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar( content: Text("You've pressed the ${item.name} button!"))); // Navigate ke route yang sesuai (tergantung jenis tombol) if (item.name == "Add Operators") { Navigator.push(context, MaterialPageRoute(builder: (context) => const OperatorFormPage())); } if (item.name == "See Operators") { Navigator.push(context, MaterialPageRoute(builder: (context) => const OperatorsPage())); } if (item.name == "Logout") { final response = await request.logout( // "http://dimas-herjunodarpito-tugas.pbp.cs.ui.ac.id/auth/logout/", "http://127.0.0.1:8080/auth/logout/"); String message = response["message"]; if (response['status']) { String uname = response["username"]; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("$message See you again, $uname!"), )); Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const LoginPage()), ); } else { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("$message"), )); } } }, child: Container( // Container untuk menyimpan Icon dan Text padding: const EdgeInsets.all(8), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( item.icon, color: Colors.white, size: 30.0, ), const Padding(padding: EdgeInsets.all(3)), Text( item.name, textAlign: TextAlign.center, style: const TextStyle(color: Colors.white), ), ], ), ), ), ), ); } }
- Menambah file
lib/screens/login.dart
import 'package:battlechar_mobile/screens/menu.dart'; import 'package:battlechar_mobile/screens/register.dart'; import 'package:flutter/material.dart'; import 'package:pbp_django_auth/pbp_django_auth.dart'; import 'package:provider/provider.dart'; void main() { runApp(const LoginApp()); } class LoginApp extends StatelessWidget { const LoginApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Login', theme: ThemeData( primarySwatch: Colors.indigo, ), home: const LoginPage(), ); } } class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override _LoginPageState createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { final TextEditingController _usernameController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); @override Widget build(BuildContext context) { final request = context.watch<CookieRequest>(); return Scaffold( appBar: AppBar( title: const Text( 'Login', style: TextStyle(color: Colors.white), ), backgroundColor: Colors.indigo[900], foregroundColor: Colors.white, ), body: Container( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextField( controller: _usernameController, decoration: const InputDecoration( labelText: 'Username', labelStyle: TextStyle(color: Colors.indigo), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.indigo), ), ), ), const SizedBox(height: 12.0), TextField( controller: _passwordController, decoration: const InputDecoration( labelText: 'Password', labelStyle: TextStyle(color: Colors.indigo), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.indigo), ), ), obscureText: true, ), const SizedBox(height: 24.0), ElevatedButton( onPressed: () async { String username = _usernameController.text; String password = _passwordController.text; final response = await request.login( "http://127.0.0.1:8080/auth/login/", // "http://dimas-herjunodarpito-tugas.pbp.cs.ui.ac.id/auth/login/", { 'username': username, 'password': password, }); if (request.loggedIn) { String message = response['message']; String uname = response['username']; // ignore: use_build_context_synchronously Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => MyHomePage()), ); // ignore: use_build_context_synchronously ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar( content: Text("$message Welcome, $uname."))); } else { // ignore: use_build_context_synchronously showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Login failed'), content: Text(response['message']), actions: [ TextButton( child: const Text('OK'), onPressed: () { Navigator.pop(context); }, ), ], ), ); } }, child: const Text('Login'), ), const SizedBox(height: 12.0), ElevatedButton( onPressed: () { // Navigate to Register Page Navigator.push( context, MaterialPageRoute(builder: (context) => RegisterPage()), ); }, child: const Text('Register'), ) ], ), ), ); } }
- Menambah file
lib/screens/register.dart
import 'package:battlechar_mobile/screens/login.dart'; import 'package:flutter/material.dart'; import 'package:pbp_django_auth/pbp_django_auth.dart'; import 'package:provider/provider.dart'; void main() { runApp(const RegisterApp()); } class RegisterApp extends StatelessWidget { const RegisterApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Register', theme: ThemeData( primarySwatch: Colors.indigo, ), home: const RegisterPage(), ); } } class RegisterPage extends StatefulWidget { const RegisterPage({super.key}); @override _RegisterPageState createState() => _RegisterPageState(); } class _RegisterPageState extends State<RegisterPage> { final TextEditingController _usernameController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final TextEditingController _passwordConfirmationController = TextEditingController(); @override Widget build(BuildContext context) { final request = context.watch<CookieRequest>(); return Scaffold( appBar: AppBar( title: const Text( 'Register', style: TextStyle(color: Colors.white), ), backgroundColor: Colors.indigo[900], foregroundColor: Colors.white, ), body: Container( padding: const EdgeInsets.all(16.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextField( controller: _usernameController, decoration: const InputDecoration( labelText: 'Username', labelStyle: TextStyle(color: Colors.indigo), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.indigo), ), ), ), const SizedBox(height: 12.0), TextField( controller: _passwordController, decoration: const InputDecoration( labelText: 'Password', labelStyle: TextStyle(color: Colors.indigo), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.indigo), ), ), obscureText: true, ), const SizedBox(height: 12.0), TextField( controller: _passwordConfirmationController, decoration: const InputDecoration( labelText: 'Confirm Password', labelStyle: TextStyle(color: Colors.indigo), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.indigo), ), ), obscureText: true, ), const SizedBox(height: 24.0), ElevatedButton( onPressed: () async { String username = _usernameController.text; String password = _passwordController.text; String passwordConfirmation = _passwordConfirmationController.text; if (password != passwordConfirmation) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(const SnackBar( content: Text( "Register failed, password confimation incorrect."))); return; } final response = await request.post( "http://127.0.0.1:8080/auth/register/", // "http://dimas-herjunodarpito-tugas.pbp.cs.ui.ac.id/auth/register/", { 'username': username, 'password': password, }); if (response['status']) { String message = response['message']; Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => LoginPage()), ); ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text("$message"))); } else { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Register failed.'), content: Text(response['message']), actions: [ TextButton( child: const Text('OK'), onPressed: () { Navigator.pop(context); }, ), ], ), ); } }, child: const Text('Register'), ), const SizedBox(height: 12.0), ElevatedButton( onPressed: () { // Navigate to Login Navigator.push( context, MaterialPageRoute(builder: (context) => LoginPage()), ); }, child: const Text('Login'), ) ], ), ), ); } }
- Membuat app authentication:
- Membuat file
lib/models/operator.dart
dengan bantuan website http://app.quicktype.io/// To parse this JSON data, do // // final operator = operatorFromJson(jsonString); import 'dart:convert'; Operator operatorFromJson(String str) => Operator.fromJson(json.decode(str)); String operatorToJson(Operator data) => json.encode(data.toJson()); class Operator { String model; int pk; Fields fields; Operator({ required this.model, required this.pk, required this.fields, }); factory Operator.fromJson(Map<String, dynamic> json) => Operator( model: json["model"], pk: json["pk"], fields: Fields.fromJson(json["fields"]), ); Map<String, dynamic> toJson() => { "model": model, "pk": pk, "fields": fields.toJson(), }; } class Fields { int user; String name; String unit; String primaryWeapon; String secondaryWeapon; int primaryWeaponAmmoAmount; int secondaryWeaponAmmoAmount; int armor; int speed; String description; int price; Fields({ required this.user, required this.name, required this.unit, required this.primaryWeapon, required this.secondaryWeapon, required this.primaryWeaponAmmoAmount, required this.secondaryWeaponAmmoAmount, required this.armor, required this.speed, required this.description, required this.price, }); factory Fields.fromJson(Map<String, dynamic> json) => Fields( user: json["user"], name: json["name"], unit: json["unit"], primaryWeapon: json["primary_weapon"], secondaryWeapon: json["secondary_weapon"], primaryWeaponAmmoAmount: json["primary_weapon_ammo_amount"], secondaryWeaponAmmoAmount: json["secondary_weapon_ammo_amount"], armor: json["armor"], speed: json["speed"], description: json["description"], price: json["price"], ); Map<String, dynamic> toJson() => { "user": user, "name": name, "unit": unit, "primary_weapon": primaryWeapon, "secondary_weapon": secondaryWeapon, "primary_weapon_ammo_amount": primaryWeaponAmmoAmount, "secondary_weapon_ammo_amount": secondaryWeaponAmmoAmount, "armor": armor, "speed": speed, "description": description, "price": price, }; }
- Memodifikasi
lib/screens/operatorlist_form.dart
agar bisa membuat Operator dan menyimpannya ke dalamimport 'package:flutter/material.dart'; import 'package:battlechar_mobile/widgets/left_drawer.dart'; import 'package:pbp_django_auth/pbp_django_auth.dart'; import 'package:provider/provider.dart'; import 'dart:convert'; import 'package:battlechar_mobile/screens/menu.dart'; class OperatorFormPage extends StatefulWidget { const OperatorFormPage({super.key}); @override State<OperatorFormPage> createState() => _OperatorFormPageState(); } class _OperatorFormPageState extends State<OperatorFormPage> { final _formKey = GlobalKey<FormState>(); String _name = ""; String _unit = ""; int _price = 0; String _primaryWeapon = ""; int _primaryWeaponAmmo = 0; String _secondaryWeapon = ""; int _secondaryWeaponAmmo = 0; int _armor = 0; int _speed = 0; String _description = ""; @override Widget build(BuildContext context) { final request = context.watch<CookieRequest>(); return Scaffold( appBar: AppBar( title: const Center( child: const Text( 'Add Operator', style: TextStyle(color: Colors.white), ), ), backgroundColor: Colors.indigo.shade900, foregroundColor: Colors.white, ), drawer: const LeftDrawer(), body: Form( key: _formKey, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Operator Name", labelText: "Operator Name", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _name = value!; }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Name cannot be empty!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Operator Unit", labelText: "Operator Unit", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _unit = value!; }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Unit cannot be empty!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Price", labelText: "Price", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _price = int.parse(value!); }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Price cannot be empty!"; } if (int.tryParse(value) == null) { return "Price must be a valid integer!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Primary Weapon", labelText: "Primary Weapon", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _primaryWeapon = value!; }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Primary weapon cannot be empty!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Primary Weapon Ammo Amount", labelText: "Primary Weapon Ammo Amount", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _primaryWeaponAmmo = int.parse(value!); }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Primary weapon ammo amount cannot be empty!"; } if (int.tryParse(value) == null) { return "Primary weapon ammo amount must be a valid integer!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Secondary Weapon", labelText: "Secondary Weapon", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _secondaryWeapon = value!; }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Secondary weapon cannot be empty!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Secondary Weapon Ammo Amount", labelText: "Secondary Weapon Ammo Amount", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _secondaryWeaponAmmo = int.parse(value!); }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Secondary weapon ammo amount cannot be empty!"; } if (int.tryParse(value) == null) { return "Secondary weapon ammo amount must be a valid integer!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Armor", labelText: "Armor", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _armor = int.parse(value!); }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Armor value cannot be empty!"; } if (int.tryParse(value) == null) { return "Armor value must be a valid integer!"; } if (int.tryParse(value)! > 3 || int.tryParse(value)! < 1) { return "Armor value must be in interval 1-to-3!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Speed", labelText: "Speed", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _speed = int.parse(value!); }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Speed value cannot be empty!"; } if (int.tryParse(value) == null) { return "Speed value must be a valid integer!"; } if (int.tryParse(value)! > 3 || int.tryParse(value)! < 1) { return "Speed value must be in interval 1-to-3!"; } return null; }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( decoration: InputDecoration( hintText: "Description", labelText: "Description", border: OutlineInputBorder( borderRadius: BorderRadius.circular(5.0), ), ), onChanged: (String? value) { setState(() { _description = value!; }); }, validator: (String? value) { if (value == null || value.isEmpty) { return "Description cannot be empty!"; } return null; }, ), ), Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.indigo), ), onPressed: () async { if (_formKey.currentState!.validate()) { // Kirim ke Django dan tunggu respons // DONE: Ganti URL dan jangan lupa tambahkan trailing slash (/) di akhir URL! final response = await request.postJson( "http://127.0.0.1:8080/create-flutter/", // "http://dimas-herjunodarpito-tugas.pbp.cs.ui.ac.id/create-flutter/", jsonEncode(<String, String>{ 'name': _name, 'price': _price.toString(), 'primary_weapon': _primaryWeapon, 'secondary_weapon': _secondaryWeapon, 'primary_weapon_ammo_amount': _primaryWeaponAmmo.toString(), 'secondary_weapon_ammo_amount': _secondaryWeaponAmmo.toString(), 'armor': _armor.toString(), 'speed': _speed.toString(), 'description': _description, 'unit': _unit, })); if (response['status'] == 'success') { print(response['status']); ScaffoldMessenger.of(context) .showSnackBar(const SnackBar( content: Text("New operator has been saved!"), )); Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => MyHomePage()), ); } else { ScaffoldMessenger.of(context) .showSnackBar(const SnackBar( content: Text("ERROR, please try again!"), )); } } }, child: const Text( "Save", style: TextStyle(color: Colors.white), ), ), ), ), ] ), ), ), ); } }
- Memodifikasi
lib/screens/operatorlist_items.dart
agar dapat melihat Operator yang di buat User yang sedang logged-in dari databaseimport 'package:battlechar_mobile/screens/detail_operator.dart'; import 'package:flutter/material.dart'; import 'package:battlechar_mobile/widgets/left_drawer.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'package:battlechar_mobile/models/operator.dart'; class OperatorsPage extends StatefulWidget { const OperatorsPage({super.key}); @override State<StatefulWidget> createState() => _OperatorsPageState(); } class _OperatorsPageState extends State<OperatorsPage> { Future<List<Operator>> fetchOperator() async { var url = Uri.parse('http://127.0.0.1:8080/json-by-user/'); // var url = Uri.parse('http://dimas-herjunodarpito-tugas.pbp.cs.ui.ac.id/json-by-user/'); var response = await http.get( url, headers: { "Content-Type": "application/json", }, ); // melakukan decode response menjadi bentuk json var data = jsonDecode(utf8.decode(response.bodyBytes)); // melakukan konversi data json menjadi object Operator List<Operator> listOperator = []; for (var d in data) { if (d != null) { // print(d.fields); listOperator.add(Operator.fromJson(d)); } } return listOperator; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( 'Operators', style: TextStyle(color: Colors.white), ), backgroundColor: Colors.indigo[900], foregroundColor: Colors.white, ), drawer: const LeftDrawer(), body: FutureBuilder( future: fetchOperator(), builder: (context, AsyncSnapshot snapshot) { if (snapshot.data == null) { return const Center(child: CircularProgressIndicator()); } else { if (!snapshot.hasData) { return const Column( children: [ Text( "No operators.", style: TextStyle(color: Color(0xff59A5D8), fontSize: 20), ), SizedBox(height: 8), ], ); } else { return ListView.builder( itemCount: snapshot.data!.length, itemBuilder: (_, index) => InkWell( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => DetailOperatorPage(item: snapshot.data![index]), ), ); }, child: Card( margin: const EdgeInsets.symmetric( horizontal: 16, vertical: 12), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "${snapshot.data![index].fields.name}", style: const TextStyle( fontSize: 18.0, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 10), Text( "Unit : ${snapshot.data![index].fields.unit}"), const SizedBox(height: 10), Text( "Description : ${snapshot.data![index].fields.description}") ], ), ), ), ), ); } } })); } }
- Menambah file
lib/screens/detail_operator.dart
untuk melihat detail dari Operator secara lebih mendalamimport 'package:flutter/material.dart'; import 'package:battlechar_mobile/models/operator.dart'; class DetailOperatorPage extends StatelessWidget { final Operator item; DetailOperatorPage({required this.item}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( '${item.fields.name}', style: TextStyle(color: Colors.white), ), backgroundColor: Colors.indigo[900], foregroundColor: Colors.white, ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( item.fields.name, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 10), Text("Unit: ${item.fields.unit}"), const SizedBox(height: 10), Text("Primary Weapon : ${item.fields.primaryWeapon} Ammo : ${item.fields.primaryWeaponAmmoAmount}"), const SizedBox(height: 10), Text("Secondary Weapon : ${item.fields.secondaryWeapon} Ammo : ${item.fields.secondaryWeaponAmmoAmount}"), const SizedBox(height: 10), Text("Armor : ${item.fields.armor} Speed : ${item.fields.speed}"), const SizedBox(height: 10), Text("Price : ${item.fields.price}"), const SizedBox(height: 10), Text("Description : ${item.fields.description}"), ], ), ), ); } }
- Membuat file