Login page dengan dart flutter

 

🎯 Kenapa Bikin Project Ini?

Project ini dibuat sebagai media belajar Flutter dengan pendekatan yang menyenangkan. Daripada hanya membuat form sederhana atau aplikasi counter, project ini mengambil inspirasi dari tampilan login Netflix yang sudah familiar di mata banyak orang, kemudian dikembangkan menjadi aplikasi kecil bernama Ngawiflix.

______________________________________________________________________________

Tools yang Dipakai

website zapp.ru

_______________________________________________________________________________

Membuat Login Page 

Membuat Login dengan Sign-In Code

Membuat Home Page (JadwalBioskop)

________________________________________________________________________________

Hasil akhir




________________________________________________________________________

inspirated by: netflix

tools: ChatGPT, zapp.run

_______________                             coba disini                    _______________________

codenya dibawahh ⬇️⬇️⬇️

________________________________________________________________________

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Netflix Login',
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: Colors.black,
        inputDecorationTheme: InputDecorationTheme(
          filled: true,
          fillColor: Colors.grey[900],
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(5),
            borderSide: const BorderSide(color: Colors.grey),
          ),
        ),
      ),
      home: const LoginPage(),
    );
  }
}

class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final TextEditingController emailController = TextEditingController();
  final TextEditingController passwordController = TextEditingController();
  bool rememberMe = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: SingleChildScrollView(
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                const SizedBox(height: 40),
                const Text(
                  "NGAWIFLIX",
                  style: TextStyle(
                    color: Colors.red,
                    fontSize: 32,
                    fontWeight: FontWeight.bold,
                    letterSpacing: 2,
                  ),
                  textAlign: TextAlign.center,
                ),
                const SizedBox(height: 40),
                const Text(
                  "Sign In",
                  style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 20),
                TextField(
                  controller: emailController,
                  decoration: const InputDecoration(
                    hintText: "Email or mobile number",
                  ),
                ),
                const SizedBox(height: 16),
                TextField(
                  controller: passwordController,
                  obscureText: true,
                  decoration: const InputDecoration(
                    hintText: "Password",
                  ),
                ),
                const SizedBox(height: 24),
                ElevatedButton(
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.red,
                    padding: const EdgeInsets.symmetric(vertical: 14),
                  ),
                  onPressed: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                          builder: (context) => JadwalBioskop()),
                    );
                  },
                  child: const Text(
                    "Sign In",
                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                  ),
                ),
                const SizedBox(height: 16),
                Row(
                  children: const [
                    Expanded(child: Divider(color: Colors.grey)),
                    Padding(
                      padding: EdgeInsets.symmetric(horizontal: 8.0),
                      child: Text("OR"),
                    ),
                    Expanded(child: Divider(color: Colors.grey)),
                  ],
                ),
                const SizedBox(height: 16),
                OutlinedButton(
                  style: OutlinedButton.styleFrom(
                    backgroundColor: Colors.grey[850],
                    padding: const EdgeInsets.symmetric(vertical: 14),
                  ),
                  onPressed: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                          builder: (context) => const LoginCodePage()),
                    );
                  },
                  child: const Text(
                    "Use a Sign-In Code",
                    style: TextStyle(color: Colors.white),
                  ),
                ),
                const SizedBox(height: 16),
                Center(
                  child: TextButton(
                    onPressed: () {},
                    child: const Text("Forgot password?"),
                  ),
                ),
                const SizedBox(height: 8),
                Row(
                  children: [
                    Checkbox(
                      value: rememberMe,
                      onChanged: (value) {
                        setState(() {
                          rememberMe = value ?? false;
                        });
                      },
                      activeColor: Colors.white,
                      checkColor: Colors.black,
                    ),
                    const Text("Remember me"),
                  ],
                ),
                const SizedBox(height: 30),
                Center(
                  child: RichText(
                    text: const TextSpan(
                      style: TextStyle(color: Colors.white),
                      children: [
                        TextSpan(text: "New to Netflix? "),
                        TextSpan(
                          text: "Sign up now.",
                          style: TextStyle(
                              fontWeight: FontWeight.bold,
                              color: Colors.white),
                        ),
                      ],
                    ),
                  ),
                ),
                const SizedBox(height: 24),
                const Text(
                  "This page is protected by Google reCAPTCHA to ensure you're not a bot. Learn more.",
                  style: TextStyle(fontSize: 12, color: Colors.grey),
                  textAlign: TextAlign.center,
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

/// LOGIN DENGAN SIGN-IN CODE
class LoginCodePage extends StatefulWidget {
  const LoginCodePage({super.key});

  @override
  State<LoginCodePage> createState() => _LoginCodePageState();
}

class _LoginCodePageState extends State<LoginCodePage> {
  final TextEditingController emailController = TextEditingController();
  bool rememberMe = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: SingleChildScrollView(
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                const SizedBox(height: 40),
                const Text(
                  "NGAWIFLIX",
                  style: TextStyle(
                    color: Colors.red,
                    fontSize: 32,
                    fontWeight: FontWeight.bold,
                    letterSpacing: 2,
                  ),
                  textAlign: TextAlign.center,
                ),
                const SizedBox(height: 40),
                const Text(
                  "Sign In",
                  style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 20),
                TextField(
                  controller: emailController,
                  decoration: const InputDecoration(
                    hintText: "Email or mobile number",
                  ),
                ),
                const SizedBox(height: 8),
                const Text(
                  "Message and data rates may apply",
                  style: TextStyle(color: Colors.grey, fontSize: 12),
                ),
                const SizedBox(height: 16),
                ElevatedButton(
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.red,
                    padding: const EdgeInsets.symmetric(vertical: 14),
                  ),
                  onPressed: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                          builder: (context) =>  JadwalBioskop()),
                    );
                  },
                  child: const Text(
                    "Send Sign-In Code",
                    style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                  ),
                ),
                const SizedBox(height: 16),
                Row(
                  children: const [
                    Expanded(child: Divider(color: Colors.grey)),
                    Padding(
                      padding: EdgeInsets.symmetric(horizontal: 8.0),
                      child: Text("OR"),
                    ),
                    Expanded(child: Divider(color: Colors.grey)),
                  ],
                ),
                const SizedBox(height: 16),
                OutlinedButton(
                  style: OutlinedButton.styleFrom(
                    backgroundColor: Colors.grey[850],
                    padding: const EdgeInsets.symmetric(vertical: 14),
                  ),
                  onPressed: () {
                    Navigator.pop(context); // balik ke login password
                  },
                  child: const Text(
                    "Use password",
                    style: TextStyle(color: Colors.white),
                  ),
                ),
                const SizedBox(height: 16),
                Center(
                  child: TextButton(
                    onPressed: () {},
                    child: const Text("Forgot Email or Phone Number?"),
                  ),
                ),
                const SizedBox(height: 8),
                Row(
                  children: [
                    Checkbox(
                      value: rememberMe,
                      onChanged: (value) {
                        setState(() {
                          rememberMe = value ?? false;
                        });
                      },
                      activeColor: Colors.white,
                      checkColor: Colors.black,
                    ),
                    const Text("Remember me"),
                  ],
                ),
                const SizedBox(height: 30),
                Center(
                  child: RichText(
                    text: const TextSpan(
                      style: TextStyle(color: Colors.white),
                      children: [
                        TextSpan(text: "New to Netflix? "),
                        TextSpan(
                          text: "Sign up now.",
                          style: TextStyle(
                              fontWeight: FontWeight.bold,
                              color: Colors.white),
                        ),
                      ],
                    ),
                  ),
                ),
                const SizedBox(height: 24),
                const Text(
                  "This page is protected by Google reCAPTCHA to ensure you're not a bot. Learn more.",
                  style: TextStyle(fontSize: 12, color: Colors.grey),
                  textAlign: TextAlign.center,
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

/// HOME PAGE
class JadwalBioskop extends StatefulWidget {
  @override
  State<JadwalBioskop> createState() => _JadwalBioskopState();
}

class _JadwalBioskopState extends State<JadwalBioskop> {
  List<Map<String, String>> filmList = [
    {
      "judul": "Ambaruwo",
      "genre": "Horror, Sci-Fi",
      "jam": "14:14, 17:17, 00:00",
      "poster": "https://p16-sign-va.tiktokcdn.com/tos-maliva-i-photomode-us/2c1bf404b3494dbe88d2cfb529a07d8f~tplv-photomode-image-v1:q70.webp?dr=1334&refresh_token=96c9dbee&x-expires=1756436400&x-signature=hy0B0aIA8wrqS8GHYZ%2Fc5d2My98%3D&t=5897f7ec&ps=b40d0ec8&shp=d05b14bd&shcp=1d1a97fc&idc=my&s=AWEME_DETAIL&biz_tag=tt_photomode&sc=image",
      "deskripsi":
          "Si Hytam kembali bangkit untuk menghytamkan kalian! Saksikan aksi seru ini di"
    },
    {
      "judul": "Ambarawuhi",
      "genre": "Horror, Adventure",
      "jam": "10:00, 13:00, 16:00",
      "poster":
          "https://p16-sign-va.tiktokcdn.com/tos-maliva-i-photomode-us/0159708740824b7395ae7b79c0ccf029~tplv-photomode-image-v1:q70.webp?dr=1334&refresh_token=58793e05&x-expires=1756436400&x-signature=7N62%2BwV6uxoottmwKwsBMBU2cF8%3D&t=5897f7ec&ps=b40d0ec8&shp=d05b14bd&shcp=1d1a97fc&idc=sg1&s=AWEME_DETAIL&biz_tag=tt_photomode&sc=image",
      "deskripsi":
          "Sekelompok KKN dari kota Ngawi yang diterror karena melakukan hal terlarang..."
    },
    {
      "judul": "AMBATUKAM: one for all",
      "genre": "Adventure",
      "jam": "10:00, 13:00, 16:00",
      "poster":
          "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSoTKE46hTPb_M8oWrv8yX7YRRAWYPOCRDEOQ&s",
      "deskripsi": "Petualangan seru melawan desa misterius..."
    },
    {
      "judul": "imut adalah maut",
      "genre": "Romance, Drama",
      "jam": "10:00, 13:00, 16:00",
      "poster":
          "https://p16-sign-va.tiktokcdn.com/tos-maliva-i-photomode-us/fff9a32b4474448482aea36881835dce~tplv-photomode-image-v1:q70.webp?dr=1334&refresh_token=6ec2b866&x-expires=1756436400&x-signature=u13SV9baoxGbHOa2UbHGp3w94kQ%3D&t=5897f7ec&ps=b40d0ec8&shp=d05b14bd&shcp=1d1a97fc&idc=my&s=AWEME_DETAIL&biz_tag=tt_photomode&sc=image",
      "deskripsi": "drama rumah tangga yang diganggu oleh siimut"
    },
    {
      "judul": "Anggrek mekar berduri",
      "genre": "Drama",
      "jam": "-",
      "poster":
          "https://p16-sign-va.tiktokcdn.com/tos-maliva-i-photomode-us/bee02b17c3aa485fb23c8d42359dca9d~tplv-photomode-image-v1:q70.webp?dr=1334&refresh_token=d5dc6895&x-expires=1756436400&x-signature=G3NJ9I%2BrORo%2FaEFoEY%2BLdQ56CJA%3D&t=5897f7ec&ps=b40d0ec8&shp=d05b14bd&shcp=1d1a97fc&idc=my&s=AWEME_DETAIL&biz_tag=tt_photomode&sc=image",
      "deskripsi": "-"
    },
    {
      "judul": "Ambabelle",
      "genre": "Horror, shounen",
      "jam": "-",
      "poster":
          "https://p16-sign-va.tiktokcdn.com/tos-maliva-i-photomode-us/033b71378e464c6ab3d974eb1b20cd86~tplv-photomode-image-v1:q70.webp?dr=1334&refresh_token=3164494f&x-expires=1756436400&x-signature=3UVFVwpJDuyDBbnSOdhSmWZIUAA%3D&t=5897f7ec&ps=b40d0ec8&shp=d05b14bd&shcp=1d1a97fc&idc=my&s=AWEME_DETAIL&biz_tag=tt_photomode&sc=image",
      "deskripsi": "Film horror luar yang dibuat ulang di ngawi city"
    },
  ];

  String searchQuery = "";

  void tambahFilm() {
    showFormDialog();
  }

  void ubahFilm(int index) {
    var film = filmList[index];
    showFormDialog(editIndex: index, film: film);
  }

  void showFormDialog({int? editIndex, Map<String, String>? film}) {
    String judul = film?["judul"] ?? "";
    String genre = film?["genre"] ?? "";
    String jam = film?["jam"] ?? "";
    String poster = film?["poster"] ?? "";
    String deskripsi = film?["deskripsi"] ?? "";

    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          backgroundColor: Colors.grey[900],
          title: Text(
            editIndex == null ? "Tambah Film" : "Ubah Film",
            style: const TextStyle(color: Colors.white),
          ),
          content: SingleChildScrollView(
            child: Column(
              children: [
                buildInput("Judul", (val) => judul = val, judul),
                buildInput("Genre", (val) => genre = val, genre),
                buildInput("Jam Tayang", (val) => jam = val, jam),
                buildInput("URL Poster", (val) => poster = val, poster),
                buildInput("Deskripsi", (val) => deskripsi = val, deskripsi,
                    maxLines: 3),
              ],
            ),
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text("Batal", style: TextStyle(color: Colors.red)),
            ),
            ElevatedButton(
              onPressed: () {
                setState(() {
                  if (editIndex == null) {
                    filmList.add({
                      "judul": judul,
                      "genre": genre,
                      "jam": jam,
                      "poster": poster,
                      "deskripsi": deskripsi,
                    });
                  } else {
                    filmList[editIndex] = {
                      "judul": judul,
                      "genre": genre,
                      "jam": jam,
                      "poster": poster,
                      "deskripsi": deskripsi,
                    };
                  }
                });
                Navigator.pop(context);
              },
              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.red[900],
              ),
              child: const Text("Simpan",
                  style: TextStyle(color: Colors.white)),
            ),
          ],
        );
      },
    );
  }

  static Widget buildInput(
      String label, Function(String) onChanged, String initialValue,
      {int maxLines = 1}) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: TextField(
        controller: TextEditingController(text: initialValue),
        style: const TextStyle(color: Colors.white),
        onChanged: onChanged,
        maxLines: maxLines,
        decoration: InputDecoration(
          labelText: label,
          labelStyle: const TextStyle(color: Colors.white),
          filled: true,
          fillColor: Colors.grey[800],
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(8),
            borderSide: BorderSide.none,
          ),
        ),
      ),
    );
  }

  void hapusFilm(int index) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        backgroundColor: Colors.grey[900],
        title: const Text("Konfirmasi",
            style: TextStyle(color: Colors.white)),
        content: const Text("Yakin ingin menghapus film ini?",
            style: TextStyle(color: Colors.white70)),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child:
                const Text("Batal", style: TextStyle(color: Colors.grey)),
          ),
          ElevatedButton(
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.red[900],
            ),
            onPressed: () {
              setState(() {
                filmList.removeAt(index);
              });
              Navigator.pop(context);
            },
            child:
                const Text("Hapus", style: TextStyle(color: Colors.white)),
          ),
        ],
      ),
    );
  }

  void detailFilm(Map<String, String> film) {
    showDialog(
      context: context,
      builder: (context) {
        final size = MediaQuery.of(context).size;
        return Dialog(
          backgroundColor: Colors.grey[900],
          shape:
              RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
          child: ConstrainedBox(
            constraints: BoxConstraints(
              maxWidth: 520,
              maxHeight: size.height * 0.85,
            ),
            child: SingleChildScrollView(
              child: Column(
                children: [
                  ClipRRect(
                    borderRadius:
                        const BorderRadius.vertical(top: Radius.circular(12)),
                    child: AspectRatio(
                      aspectRatio: 2 / 3,
                      child: Image.network(film["poster"]!,
                          fit: BoxFit.cover),
                    ),
                  ),
                  Padding(
                    padding: const EdgeInsets.all(12),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(film["judul"]!,
                            style: const TextStyle(
                                fontSize: 22,
                                fontWeight: FontWeight.bold,
                                color: Colors.white)),
                        const SizedBox(height: 4),
                        Text(film["genre"]!,
                            style: const TextStyle(color: Colors.grey)),
                        const SizedBox(height: 8),
                        Text("Jam: ${film["jam"]}",
                            style:
                                const TextStyle(color: Colors.white70)),
                        const SizedBox(height: 12),
                        Text(film["deskripsi"]!,
                            style:
                                const TextStyle(color: Colors.white)),
                        const SizedBox(height: 16),
                        Align(
                          alignment: Alignment.centerRight,
                          child: ElevatedButton(
                            style: ElevatedButton.styleFrom(
                                backgroundColor: Colors.red[900]),
                            onPressed: () => Navigator.pop(context),
                            child: const Text("Keluar",
                                style: TextStyle(color: Colors.white)),
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    final width = MediaQuery.of(context).size.width;

    List<Map<String, String>> filteredList = filmList
        .where((film) => film["judul"]!
            .toLowerCase()
            .contains(searchQuery.toLowerCase()))
        .toList();

    return Scaffold(
      backgroundColor: Colors.black,
      appBar: AppBar(
        title: const Text("Ngawiflix",
            style: TextStyle(fontWeight: FontWeight.bold)),
        backgroundColor: Colors.red[900],
        actions: [
          IconButton(
            onPressed: tambahFilm,
            icon: const Icon(Icons.add, color: Colors.white),
          ),
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              onChanged: (value) => setState(() => searchQuery = value),
              style: const TextStyle(color: Colors.white),
              decoration: InputDecoration(
                hintText: "Cari judul film...",
                hintStyle: const TextStyle(color: Colors.grey),
                prefixIcon: const Icon(Icons.search, color: Colors.white),
                filled: true,
                fillColor: Colors.grey[900],
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(8),
                  borderSide: BorderSide.none,
                ),
              ),
            ),
          ),
          Expanded(
            child: GridView.builder(
              padding: const EdgeInsets.all(12),
              gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
                maxCrossAxisExtent: width < 420 ? 360 : 240,
                crossAxisSpacing: 12,
                mainAxisSpacing: 12,
                childAspectRatio: 0.55,
              ),
              itemCount: filteredList.length,
              itemBuilder: (context, index) {
                var film = filteredList[index];
                return Card(
                  color: Colors.grey[900],
                  shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(12)),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Expanded(
                        child: InkWell(
                          onTap: () => detailFilm(film),
                          child: ClipRRect(
                            borderRadius: const BorderRadius.vertical(
                                top: Radius.circular(12)),
                            child: Image.network(
                              film["poster"]!,
                              fit: BoxFit.cover,
                              width: double.infinity,
                              errorBuilder: (_, __, ___) => const Center(
                                  child: Icon(Icons.broken_image)),
                            ),
                          ),
                        ),
                      ),
                      Padding(
                        padding: const EdgeInsets.all(8),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Text(film["judul"]!,
                                style: const TextStyle(
                                    fontSize: 16,
                                    fontWeight: FontWeight.bold,
                                    color: Colors.white),
                                maxLines: 2,
                                overflow: TextOverflow.ellipsis),
                            const SizedBox(height: 4),
                            Text(film["genre"]!,
                                style: const TextStyle(
                                    color: Colors.grey, fontSize: 12),
                                maxLines: 1,
                                overflow: TextOverflow.ellipsis),
                            const SizedBox(height: 4),
                            Text("Jam: ${film["jam"]}",
                                style: const TextStyle(
                                    color: Colors.white70, fontSize: 12),
                                maxLines: 1,
                                overflow: TextOverflow.ellipsis),
                            const SizedBox(height: 8),
                            Row(
                              mainAxisAlignment:
                                  MainAxisAlignment.spaceBetween,
                              children: [
                                IconButton(
                                  icon: const Icon(Icons.edit,
                                      color: Colors.yellow),
                                  onPressed: () => ubahFilm(index),
                                ),
                                IconButton(
                                  icon: const Icon(Icons.delete,
                                      color: Colors.red),
                                  onPressed: () => hapusFilm(index),
                                ),
                              ],
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}


Komentar

Postingan populer dari blog ini

Belajar Flutter Biar Gak Cupu: Bikin App Ada Foto + Tombol SnackBar

membuat aplikasi sederhana dengan konsep CRUD - ngawiflix

Membuat Aplikasi Flutter Daftar Film MCU dengan Navigasi & Layout responsif