Sitemap

Creating a Restaurant Filter Feature by Name

7 min readDec 1, 2025

--

Creating a Restaurant Filter Feature by Name from Apps Development with Flutter Path MySkill.id

Yuk subscribe untuk mendapatkan email notifikasi setiap ada artikel terbaru oleh MySkill

Press enter or click to view image in full size
Sumber : My Skill

Dalam aplikasi kuliner modern, kemampuan menemukan restoran dengan cepat menjadi faktor penentu kepuasan pengguna. Fitur pencarian dan filter berfungsi seperti peta interaktif yang memandu pengguna menemukan pilihan tepat di tengah ribuan opsi. Flutter menawarkan solusi optimal untuk membangun fitur ini dengan performa tinggi dan UI responsif.

1. Persiapan Struktur Proyek

Tambahkan struktur file berikut pada proyek sebelumnya:

lib/
├── models/
│ └── restaurant_model.dart (sudah ada)
├── screens/
│ ├── home_screen.dart (sudah ada)
│ ├── restaurant_detail_screen.dart (sudah ada)
│ └── search_screen.dart
├── widgets/
│ ├── restaurant_card.dart (sudah ada)
│ ├── search_bar.dart
│ └── filter_bottom_sheet.dart
└── utils/
└── restaurant_filter.dart

2. Implementasi Fitur Pencarian Restoran

a. Membuat Search Bar Widget (widgets/search_bar.dart)

import 'package:flutter/material.dart';

class SearchBar extends StatelessWidget {
final TextEditingController controller;
final Function(String) onChanged;

const SearchBar({
Key? key,
required this.controller,
required this.onChanged,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(12),
),
child: TextField(
controller: controller,
onChanged: onChanged,
decoration: InputDecoration(
hintText: 'Cari restoran...',
hintStyle: TextStyle(color: Colors.grey[500]),
prefixIcon: const Icon(Icons.search, color: Colors.grey),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 16),
),
),
);
}
}

Penjelasan Kode:

  • TextField: Widget input teks dengan kemampuan mendeteksi perubahan
  • onChanged: Callback yang dipanggil setiap kali teks berubah
  • InputDecoration: Mengatur tampilan field input (hint, ikon, border)
  • border: InputBorder.none: Menghilangkan border default untuk tampilan lebih bersih

b. Membuat Layar Pencarian (screens/search_screen.dart)

import 'package:flutter/material.dart';
import '../models/restaurant_model.dart';
import '../widgets/restaurant_card.dart';
import '../widgets/search_bar.dart';
import '../utils/restaurant_filter.dart';

class SearchScreen extends StatefulWidget {
final List<Restaurant> allRestaurants;

const SearchScreen({
Key? key,
required this.allRestaurants,
}) : super(key: key);

@override
_SearchScreenState createState() => _SearchScreenState();
}

class _SearchScreenState extends State<SearchScreen> {
late TextEditingController _searchController;
List<Restaurant> _filteredRestaurants = [];
final RestaurantFilter _filter = RestaurantFilter();

@override
void initState() {
super.initState();
_searchController = TextEditingController();
_filteredRestaurants = widget.allRestaurants;

// Listener untuk pencarian real-time
_searchController.addListener(() {
_filterRestaurants();
});
}

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

void _filterRestaurants() {
setState(() {
_filteredRestaurants = _filter.byName(
widget.allRestaurants,
_searchController.text,
);
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Pencarian Restoran'),
backgroundColor: Colors.deepOrange,
),
body: Column(
children: [
// Search Bar
SearchBar(
controller: _searchController,
onChanged: (value) => _filterRestaurants(),
),

// Hasil Pencarian
Expanded(
child: _filteredRestaurants.isEmpty
? const Center(
child: Text(
'Tidak ada restoran ditemukan',
style: TextStyle(fontSize: 16),
),
)
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: _filteredRestaurants.length,
itemBuilder: (context, index) {
final restaurant = _filteredRestaurants[index];
return RestaurantCard(
restaurant: restaurant,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RestaurantDetailScreen(
restaurant: restaurant,
),
),
);
},
);
},
),
),
],
),
);
}
}

Penjelasan Fitur:

  1. State Management:
  • TextEditingController mengontrol input teks
  • addListener mendeteksi perubahan teks secara real-time
  • setState memperbarui UI saat hasil filter berubah

2. Logika Pencarian:

  • _filterRestaurants() memanggil fungsi filter setiap teks berubah
  • Menampilkan pesan “Tidak ada restoran ditemukan” jika hasil kosong

3. Integrasi dengan List:

  • ListView.builder menampilkan hasil pencarian
  • Setiap item tetap bisa diklik untuk melihat detail

3. Implementasi Fitur Filter Restoran

a. Membuat Filter Utility (utils/restaurant_filter.dart)

import '../models/restaurant_model.dart';

class RestaurantFilter {
// Filter berdasarkan nama
List<Restaurant> byName(
List<Restaurant> restaurants,
String query,
) {
if (query.isEmpty) return restaurants;

return restaurants.where((restaurant) {
return restaurant.name.toLowerCase().contains(query.toLowerCase());
}).toList();
}

// Filter berdasarkan kota
List<Restaurant> byCity(
List<Restaurant> restaurants,
String city,
) {
if (city.isEmpty) return restaurants;

return restaurants.where((restaurant) {
return restaurant.city.toLowerCase() == city.toLowerCase();
}).toList();
}

// Filter berdasarkan rating minimum
List<Restaurant> byRating(
List<Restaurant> restaurants,
double minRating,
) {
return restaurants.where((restaurant) {
return restaurant.rating >= minRating;
}).toList();
}

// Filter gabungan
List<Restaurant> combined(
List<Restaurant> restaurants, {
String? nameQuery,
String? city,
double? minRating,
}) {
List<Restaurant> result = restaurants;

if (nameQuery != null && nameQuery.isNotEmpty) {
result = byName(result, nameQuery);
}

if (city != null && city.isNotEmpty) {
result = byCity(result, city);
}

if (minRating != null) {
result = byRating(result, minRating);
}

return result;
}
}

Penjelasan Logika Filter:

  1. Filter Nama:
  • Menggunakan contains() untuk pencarian partial
  • Case-insensitive dengan toLowerCase()
  • Mengembalikan semua restoran jika query kosong

2. Filter Kota:

  • Pencarian exact match untuk kota
  • Case-insensitive perbandingan

3. Filter Rating:

  • Menampilkan restoran dengan rating >= minimum
  • Menggunakan perbandingan numerik

4. Filter Gabungan:

  • Menggabungkan semua kriteria filter
  • Urutan filter: nama → kota → rating
  • Setiap filter mengurangi hasil secara bertahap

b. Membuat Filter Bottom Sheet (widgets/filter_bottom_sheet.dart)

import 'package:flutter/material.dart';

class FilterBottomSheet extends StatefulWidget {
final List<String> cities;
final Function(String? city, double? rating) onApply;

const FilterBottomSheet({
Key? key,
required this.cities,
required this.onApply,
}) : super(key: key);

@override
_FilterBottomSheetState createState() => _FilterBottomSheetState();
}

class _FilterBottomSheetState extends State<FilterBottomSheet> {
String? selectedCity;
double selectedRating = 0.0;

@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Filter Restoran',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close),
),
],
),

const SizedBox(height: 16),

// Filter Kota
const Align(
alignment: Alignment.centerLeft,
child: Text(
'Kota',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: widget.cities.map((city) {
final isSelected = selectedCity == city;
return ChoiceChip(
label: Text(city),
selected: isSelected,
onSelected: (selected) {
setState(() {
selectedCity = selected ? city : null;
});
},
backgroundColor: Colors.grey[200],
selectedColor: Colors.deepOrange[100],
);
}).toList(),
),

const SizedBox(height: 16),

// Filter Rating
const Align(
alignment: Alignment.centerLeft,
child: Text(
'Rating Minimum',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 8),
Slider(
value: selectedRating,
min: 0,
max: 5,
divisions: 10,
label: selectedRating.toString(),
onChanged: (value) {
setState(() {
selectedRating = value;
});
},
activeColor: Colors.deepOrange,
),

const SizedBox(height: 24),

// Tombol Terapkan
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
widget.onApply(selectedCity, selectedRating);
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepOrange,
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text('Terapkan Filter'),
),
),

const SizedBox(height: 16),
],
),
);
}
}

Penjelasan Komponen:

  1. ChoiceChip:
  • Widget untuk pilihan kota dengan desain chip
  • Warna berubah saat dipilih
  • Bisa memilih hanya satu kota

2. Slider:

  • Untuk memilih rating minimum
  • Rentang 0–5 dengan 10 pembagian
  • Menampilkan nilai saat slider digeser

3. Tombol Terapkan:

  • Memanggil callback onApply dengan parameter filter
  • Menutup bottom sheet setelah diterapkan

4. Integrasi di Home Screen

Update home_screen.dart untuk menambahkan fitur filter:

// Tambahkan di bagian atas file
import '../widgets/filter_bottom_sheet.dart';
import '../utils/restaurant_filter.dart';

// Tambahkan variabel state di _HomeScreenState
List<Restaurant> _filteredRestaurants = [];
final RestaurantFilter _filter = RestaurantFilter();
String? _selectedCity;
double? _minRating;

// Update initState
@override
void initState() {
super.initState();
_filteredRestaurants = restaurantList;
}

// Tambahkan method untuk menampilkan filter
void _showFilterBottomSheet() {
final cities = restaurantList.map((r) => r.city).toSet().toList();

showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => FilterBottomSheet(
cities: cities,
onApply: (city, rating) {
setState(() {
_selectedCity = city;
_minRating = rating;
_applyFilters();
});
},
),
);
}

void _applyFilters() {
setState(() {
_filteredRestaurants = _filter.combined(
restaurantList,
city: _selectedCity,
minRating: _minRating,
);
});
}

// Update bagian app bar untuk menambahkan tombol filter
AppBar(
// ... kode existing
actions: [
IconButton(
icon: const Icon(Icons.search, color: Colors.deepOrange),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SearchScreen(
allRestaurants: restaurantList,
),
),
);
},
),
IconButton(
icon: const Icon(Icons.filter_list, color: Colors.deepOrange),
onPressed: _showFilterBottomSheet,
),
],
),

// Update bagian restaurant list
Expanded(
child: _filteredRestaurants.isEmpty
? const Center(child: Text('Tidak ada restoran sesuai filter'))
: ListView.builder(
// ... kode existing
itemCount: _filteredRestaurants.length,
itemBuilder: (context, index) {
final restaurant = _filteredRestaurants[index];
// ... kode existing
},
),
),

Penjelasan Integrasi:

    Get My Skill’s stories in your inbox

    Join Medium for free to get updates from this writer.

  1. State Management:
  • _filteredRestaurants menyimpan hasil filter
  • _selectedCity dan _minRating menyimpan nilai filter aktif

2. Filter Bottom Sheet:

  • showModalBottomSheet menampilkan dialog filter
  • Mengirimkan daftar kota unik dari data restoran
  • Callback onApply memperbarui state dan menerapkan filter

3. Logika Filter Gabungan:

  • _applyFilters() menggunakan combined() dari RestaurantFilter
  • Menampilkan pesan jika tidak ada hasil

5. Optimasi Performa

a. Debounce untuk Pencarian

Tambahkan package debounce_throttle di pubspec.yaml:

dependencies:
debounce_throttle: ^2.0.0

Update search_screen.dart:

import 'package:debounce_throttle/debounce_throttle.dart';

// Di _SearchScreenState
final Debouncer<String> _debouncer = Debouncer<String>(
const Duration(milliseconds: 500),
);

// Update _filterRestaurants
void _filterRestaurants() {
_debouncer.value = _searchController.text;
}

// Di initState
@override
void initState() {
super.initState();
// ... kode existing

_debouncer.values.listen((value) {
setState(() {
_filteredRestaurants = _filter.byName(
widget.allRestaurants,
value,
);
});
});
}

Penjelasan:

  • Debounce mencegah filter berjalan setiap kali teks berubah
  • Filter hanya dijalankan 500ms setelah pengguna berhenti mengetik

b. Menggunakan Const Constructor

// Gunakan const untuk widget statis
const Icon(Icons.search)
const TextStyle(fontSize: 16)

c. Menghindari Rebuild Tidak Perlu

dart

// Pisahkan widget yang sering berubah
class RestaurantList extends StatelessWidget {
final List<Restaurant> restaurants;

const RestaurantList({Key? key, required this.restaurants}) : super(key: key);

@override
Widget build(BuildContext context) {
return ListView.builder(
// ... implementasi list
);
}
}

6. Pengujian Fitur

a. Test Case Pencarian:

  1. Ketik “sate” → Harus menampilkan restoran dengan nama mengandung “sate”
  2. Ketik “jakarta” → Tidak menampilkan apa-apa (karena filter nama, bukan kota)
  3. Hapus semua teks → Menampilkan semua restoran

b. Test Case Filter:

  1. Pilih kota “Yogyakarta” → Hanya menampilkan restoran di Yogyakarta
  2. Atur rating minimum 4.5 → Menampilkan restoran dengan rating ≥ 4.5
  3. Kombinasi kota “Padang” dan rating 4.7 → Menampilkan restoran di Padang dengan rating ≥ 4.7

c. Test Case Responsivitas:

  1. Rotasi layar → UI harus menyesuaikan dengan baik
  2. Test di berbagai ukuran layar → Layout tidak pecah

7. Pengembangan Lanjutan

a. Menambah Kategori Filter

Update restaurant_filter.dart:

// Tambahkan di model Restaurant
final String category;

// Tambahkan method filter
List<Restaurant> byCategory(
List<Restaurant> restaurants,
String category,
) {
if (category.isEmpty) return restaurants;

return restaurants.where((restaurant) {
return restaurant.category.toLowerCase() == category.toLowerCase();
}).toList();
}

b. Menyimpan Preferensi Filter

Gunakan shared_preferences:

import 'package:shared_preferences/shared_preferences.dart';

// Simpan preferensi
Future<void> saveFilterPreferences(String? city, double? rating) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('filter_city', city ?? '');
await prefs.setDouble('filter_rating', rating ?? 0.0);
}

// Muat preferensi
Future<Map<String, dynamic>> loadFilterPreferences() async {
final prefs = await SharedPreferences.getInstance();
return {
'city': prefs.getString('filter_city'),
'rating': prefs.getDouble('filter_rating') ?? 0.0,
};
}

c. Animasi Transisi

Tambahkan animasi saat membuka filter:

showModalBottomSheet(
context: context,
isScrollControlled: true,
transitionAnimationController: AnimationController(
vsync: Navigator.of(context),
duration: const Duration(milliseconds: 300),
),
builder: (context) => FilterBottomSheet(...),
);

Kesimpulan

Kita telah berhasil mengimplementasikan fitur pencarian dan filter restoran dengan Flutter yang mencakup:

  1. Pencarian Real-time:
  • Search bar dengan respons cepat
  • Debounce untuk optimasi performa
  • Pencarian partial (tidak harus exact match)

2. Filter Multi-Kriteria:

  • Filter berdasarkan kota dengan ChoiceChip
  • Filter rating dengan Slider interaktif
  • Kombinasi beberapa filter sekaligus

3. UI/UX yang Intuitif:

  • Bottom sheet untuk filter yang mudah diakses
  • Visual feedback untuk pilihan aktif
  • Pesan informatif saat tidak ada hasil

4. Arsitektur yang Terstruktur:

  • Pemisahan logika filter ke utility class
  • Widget reusable untuk komponen UI
  • State management yang efisien

Learn More Via: https://myskill.id/course/state-and-props-management

--

--