- Published on
Mobile App Development dengan Flutter: Panduan untuk Developer Bekasi
- Authors
- Name
- Anonymous Developer
- @bekasidev
Mobile App Development dengan Flutter: Panduan untuk Developer Bekasi
Flutter adalah framework mobile development yang sangat populer di kalangan developer Bekasi. Dalam artikel ini, kita akan membahas mengapa Flutter menjadi pilihan utama dan bagaimana memulai journey mobile development Anda.
๐ฏ Mengapa Flutter?
Keuntungan Flutter untuk Developer Bekasi
- One Codebase, Two Platforms: Write once, run on iOS dan Android
- Fast Development: Hot reload untuk iterasi cepat
- Strong Community: Dukungan Google dan komunitas global
- Growing Job Market: Banyak perusahaan di Bekasi mencari Flutter developer
- Cost Effective: Startup dan SME lebih suka cross-platform solutions
Flutter vs Native vs React Native
Aspect | Flutter | React Native | Native |
---|---|---|---|
Performance | Near Native | Good | Best |
Development Speed | Fast | Fast | Slow |
Code Sharing | 95%+ | 80%+ | 0% |
Learning Curve | Medium | Easy (if know React) | Hard |
Job Market (Bekasi) | Growing | Stable | Established |
๐ Setup Development Environment
Prerequisites
# Check system requirements
flutter doctor
Installation Steps
1. Install Flutter SDK
# Download Flutter SDK
git clone https://github.com/flutter/flutter.git -b stable
export PATH="$PATH:`pwd`/flutter/bin"
# Verify installation
flutter --version
2. Setup IDE
VS Code (Recommended)
# Install extensions
code --install-extension Dart-Code.dart-code
code --install-extension Dart-Code.flutter
Android Studio
- Install Flutter plugin
- Install Dart plugin
- Configure Android emulator
3. Platform Setup
Android Development
# Install Android Studio
# Setup Android SDK
# Create virtual device (AVD)
flutter doctor --android-licenses
iOS Development (macOS only)
# Install Xcode
# Install CocoaPods
sudo gem install cocoapods
Create Your First App
# Create new Flutter project
flutter create bekasi_todo_app
cd bekasi_todo_app
# Run on emulator/device
flutter run
๐ฑ Building Real-World App: Bekasi Local Business Directory
Mari kita bangun aplikasi yang useful untuk komunitas Bekasi!
App Features
- ๐ Local business listings
- ๐ Search & filter businesses
- โญ Reviews & ratings
- ๐ Direct contact (call, WhatsApp, maps)
- ๐ช Business categories
- ๐ฑ Offline support
Project Structure
lib/
โโโ main.dart
โโโ app/
โ โโโ routes/
โ โโโ themes/
โ โโโ constants/
โโโ core/
โ โโโ services/
โ โโโ models/
โ โโโ utils/
โโโ features/
โ โโโ home/
โ โโโ business/
โ โโโ search/
โ โโโ profile/
โโโ shared/
โโโ widgets/
โโโ components/
โโโ helpers/
Core App Setup
1. Dependencies (pubspec.yaml)
dependencies:
flutter:
sdk: flutter
# State Management
provider: ^6.1.1
# HTTP & API
dio: ^5.3.2
# Database
sqflite: ^2.3.0
# UI Components
cached_network_image: ^3.3.0
shimmer: ^3.0.0
# Utils
url_launcher: ^6.2.1
geolocator: ^10.1.0
share_plus: ^7.2.1
2. App Configuration (main.dart)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'app/app.dart';
import 'core/services/database_service.dart';
import 'features/business/providers/business_provider.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize database
await DatabaseService.instance.init();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => BusinessProvider()),
],
child: const BekasiDirectoryApp(),
),
);
}
3. App Widget (app/app.dart)
import 'package:flutter/material.dart';
import 'routes/app_routes.dart';
import 'themes/app_theme.dart';
class BekasiDirectoryApp extends StatelessWidget {
const BekasiDirectoryApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
title: 'Bekasi Business Directory',
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
initialRoute: AppRoutes.splash,
onGenerateRoute: AppRoutes.generateRoute,
debugShowCheckedModeBanner: false,
);
}
}
Model & Data Layer
1. Business Model (core/models/business.dart)
class Business {
final String id;
final String name;
final String description;
final String category;
final String address;
final String phone;
final String? whatsapp;
final String? website;
final double? latitude;
final double? longitude;
final List<String> images;
final double rating;
final int reviewCount;
final BusinessHours hours;
Business({
required this.id,
required this.name,
required this.description,
required this.category,
required this.address,
required this.phone,
this.whatsapp,
this.website,
this.latitude,
this.longitude,
required this.images,
required this.rating,
required this.reviewCount,
required this.hours,
});
factory Business.fromJson(Map<String, dynamic> json) {
return Business(
id: json['id'],
name: json['name'],
description: json['description'],
category: json['category'],
address: json['address'],
phone: json['phone'],
whatsapp: json['whatsapp'],
website: json['website'],
latitude: json['latitude']?.toDouble(),
longitude: json['longitude']?.toDouble(),
images: List<String>.from(json['images'] ?? []),
rating: json['rating']?.toDouble() ?? 0.0,
reviewCount: json['reviewCount'] ?? 0,
hours: BusinessHours.fromJson(json['hours'] ?? {}),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'description': description,
'category': category,
'address': address,
'phone': phone,
'whatsapp': whatsapp,
'website': website,
'latitude': latitude,
'longitude': longitude,
'images': images,
'rating': rating,
'reviewCount': reviewCount,
'hours': hours.toJson(),
};
}
}
class BusinessHours {
final Map<String, String> schedule;
BusinessHours({required this.schedule});
factory BusinessHours.fromJson(Map<String, dynamic> json) {
return BusinessHours(
schedule: Map<String, String>.from(json),
);
}
Map<String, dynamic> toJson() => schedule;
bool isOpenNow() {
final now = DateTime.now();
final dayName = _getDayName(now.weekday);
final currentTime = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
final todayHours = schedule[dayName];
if (todayHours == null || todayHours == 'Closed') return false;
// Parse opening hours (format: "09:00-21:00")
final times = todayHours.split('-');
if (times.length != 2) return false;
return currentTime.compareTo(times[0]) >= 0 &&
currentTime.compareTo(times[1]) <= 0;
}
String _getDayName(int weekday) {
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
return days[weekday - 1];
}
}
2. API Service (core/services/api_service.dart)
import 'package:dio/dio.dart';
import '../models/business.dart';
class ApiService {
static final ApiService _instance = ApiService._internal();
factory ApiService() => _instance;
ApiService._internal();
final Dio _dio = Dio(BaseOptions(
baseUrl: 'https://api.bekasidirectory.com/v1',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 3),
));
Future<List<Business>> getBusinesses({
String? category,
String? query,
int page = 1,
int limit = 20,
}) async {
try {
final response = await _dio.get('/businesses', queryParameters: {
if (category != null) 'category': category,
if (query != null) 'search': query,
'page': page,
'limit': limit,
});
final List<dynamic> data = response.data['data'];
return data.map((json) => Business.fromJson(json)).toList();
} on DioException catch (e) {
throw ApiException(e.message ?? 'Unknown error occurred');
}
}
Future<Business> getBusinessById(String id) async {
try {
final response = await _dio.get('/businesses/$id');
return Business.fromJson(response.data['data']);
} on DioException catch (e) {
throw ApiException(e.message ?? 'Unknown error occurred');
}
}
Future<List<String>> getCategories() async {
try {
final response = await _dio.get('/categories');
return List<String>.from(response.data['data']);
} on DioException catch (e) {
throw ApiException(e.message ?? 'Unknown error occurred');
}
}
}
class ApiException implements Exception {
final String message;
ApiException(this.message);
}
UI Components
1. Home Screen (features/home/screens/home_screen.dart)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/business_provider.dart';
import '../../../shared/widgets/business_card.dart';
import '../../../shared/widgets/category_chip.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<BusinessProvider>().loadFeaturedBusinesses();
context.read<BusinessProvider>().loadCategories();
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Direktori Bisnis Bekasi'),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () => Navigator.pushNamed(context, '/search'),
),
],
),
body: Consumer<BusinessProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator(
onRefresh: () => provider.loadFeaturedBusinesses(),
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildWelcomeSection(),
const SizedBox(height: 24),
_buildCategoriesSection(provider),
const SizedBox(height: 24),
_buildFeaturedSection(provider),
],
),
),
);
},
),
);
}
Widget _buildWelcomeSection() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade600, Colors.blue.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Selamat Datang di Bekasi!',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'Temukan bisnis lokal terbaik di sekitar Anda',
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Navigator.pushNamed(context, '/search'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.blue.shade600,
),
child: const Text('Jelajahi Sekarang'),
),
],
),
);
}
Widget _buildCategoriesSection(BusinessProvider provider) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Kategori Populer',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
SizedBox(
height: 40,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: provider.categories.length,
itemBuilder: (context, index) {
final category = provider.categories[index];
return Padding(
padding: EdgeInsets.only(right: 8),
child: CategoryChip(
label: category,
onTap: () => _navigateToCategory(category),
),
);
},
),
),
],
);
}
Widget _buildFeaturedSection(BusinessProvider provider) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Bisnis Unggulan',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: () => Navigator.pushNamed(context, '/businesses'),
child: const Text('Lihat Semua'),
),
],
),
const SizedBox(height: 12),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: provider.featuredBusinesses.length,
itemBuilder: (context, index) {
final business = provider.featuredBusinesses[index];
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: BusinessCard(
business: business,
onTap: () => _navigateToBusinessDetail(business.id),
),
);
},
),
],
);
}
void _navigateToCategory(String category) {
Navigator.pushNamed(
context,
'/businesses',
arguments: {'category': category},
);
}
void _navigateToBusinessDetail(String businessId) {
Navigator.pushNamed(
context,
'/business-detail',
arguments: {'id': businessId},
);
}
}
2. Business Card Widget (shared/widgets/business_card.dart)
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../core/models/business.dart';
class BusinessCard extends StatelessWidget {
final Business business;
final VoidCallback? onTap;
const BusinessCard({
Key? key,
required this.business,
this.onTap,
}) : super(key: key);
Widget build(BuildContext context) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildImage(),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 8),
_buildDescription(),
const SizedBox(height: 12),
_buildInfo(),
const SizedBox(height: 12),
_buildActions(context),
],
),
),
],
),
),
);
}
Widget _buildImage() {
return ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: AspectRatio(
aspectRatio: 16 / 9,
child: business.images.isNotEmpty
? CachedNetworkImage(
imageUrl: business.images.first,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: Colors.grey.shade200,
child: const Center(
child: CircularProgressIndicator(),
),
),
errorWidget: (context, url, error) => Container(
color: Colors.grey.shade200,
child: const Icon(
Icons.image_not_supported,
size: 50,
color: Colors.grey,
),
),
)
: Container(
color: Colors.grey.shade200,
child: const Icon(
Icons.business,
size: 50,
color: Colors.grey,
),
),
),
);
}
Widget _buildHeader() {
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
business.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Text(
business.category,
style: TextStyle(
color: Colors.blue.shade700,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
_buildRating(),
],
);
}
Widget _buildRating() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.star,
size: 16,
color: Colors.orange.shade600,
),
const SizedBox(width: 4),
Text(
business.rating.toStringAsFixed(1),
style: TextStyle(
color: Colors.orange.shade600,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
Text(
' (${business.reviewCount})',
style: TextStyle(
color: Colors.orange.shade600,
fontSize: 12,
),
),
],
),
);
}
Widget _buildDescription() {
return Text(
business.description,
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
}
Widget _buildInfo() {
return Column(
children: [
_buildInfoRow(
Icons.location_on,
business.address,
Colors.red.shade400,
),
const SizedBox(height: 4),
_buildInfoRow(
Icons.access_time,
business.hours.isOpenNow() ? 'Buka Sekarang' : 'Tutup',
business.hours.isOpenNow() ? Colors.green : Colors.red,
),
],
);
}
Widget _buildInfoRow(IconData icon, String text, Color color) {
return Row(
children: [
Icon(icon, size: 16, color: color),
const SizedBox(width: 8),
Expanded(
child: Text(
text,
style: TextStyle(
color: color,
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
);
}
Widget _buildActions(BuildContext context) {
return Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => _makePhoneCall(business.phone),
icon: const Icon(Icons.phone, size: 16),
label: const Text('Telepon'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 8),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: () => _openWhatsApp(business.whatsapp ?? business.phone),
icon: const Icon(Icons.chat, size: 16),
label: const Text('WhatsApp'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 8),
),
),
),
],
);
}
Future<void> _makePhoneCall(String phoneNumber) async {
final Uri uri = Uri(scheme: 'tel', path: phoneNumber);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
}
}
Future<void> _openWhatsApp(String phoneNumber) async {
final String cleanNumber = phoneNumber.replaceAll(RegExp(r'[^0-9]'), '');
final Uri uri = Uri.parse('https://wa.me/62$cleanNumber');
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
}
๐ฆ State Management dengan Provider
Business Provider (features/business/providers/business_provider.dart)
import 'package:flutter/foundation.dart';
import '../../../core/models/business.dart';
import '../../../core/services/api_service.dart';
import '../../../core/services/database_service.dart';
class BusinessProvider extends ChangeNotifier {
final ApiService _apiService = ApiService();
final DatabaseService _dbService = DatabaseService.instance;
List<Business> _businesses = [];
List<Business> _featuredBusinesses = [];
List<String> _categories = [];
Business? _selectedBusiness;
bool _isLoading = false;
String? _error;
// Getters
List<Business> get businesses => _businesses;
List<Business> get featuredBusinesses => _featuredBusinesses;
List<String> get categories => _categories;
Business? get selectedBusiness => _selectedBusiness;
bool get isLoading => _isLoading;
String? get error => _error;
// Load featured businesses for home page
Future<void> loadFeaturedBusinesses() async {
_setLoading(true);
try {
_featuredBusinesses = await _apiService.getBusinesses(limit: 5);
_error = null;
} catch (e) {
_error = e.toString();
// Load from local cache as fallback
_featuredBusinesses = await _dbService.getCachedBusinesses(limit: 5);
}
_setLoading(false);
}
// Load all businesses with filters
Future<void> loadBusinesses({
String? category,
String? query,
int page = 1,
}) async {
if (page == 1) {
_setLoading(true);
_businesses.clear();
}
try {
final newBusinesses = await _apiService.getBusinesses(
category: category,
query: query,
page: page,
);
if (page == 1) {
_businesses = newBusinesses;
} else {
_businesses.addAll(newBusinesses);
}
// Cache to local database
await _dbService.cacheBusinesses(_businesses);
_error = null;
} catch (e) {
_error = e.toString();
if (page == 1) {
// Load from cache as fallback
_businesses = await _dbService.getCachedBusinesses(
category: category,
query: query,
);
}
}
if (page == 1) _setLoading(false);
}
// Load categories
Future<void> loadCategories() async {
try {
_categories = await _apiService.getCategories();
await _dbService.cacheCategories(_categories);
} catch (e) {
// Load from cache
_categories = await _dbService.getCachedCategories();
}
notifyListeners();
}
// Load business detail
Future<void> loadBusinessDetail(String businessId) async {
_setLoading(true);
try {
_selectedBusiness = await _apiService.getBusinessById(businessId);
await _dbService.cacheBusiness(_selectedBusiness!);
_error = null;
} catch (e) {
_error = e.toString();
// Load from cache
_selectedBusiness = await _dbService.getCachedBusiness(businessId);
}
_setLoading(false);
}
// Search businesses
Future<void> searchBusinesses(String query) async {
await loadBusinesses(query: query);
}
// Filter by category
Future<void> filterByCategory(String category) async {
await loadBusinesses(category: category);
}
// Clear data
void clearBusinesses() {
_businesses.clear();
notifyListeners();
}
void clearSelectedBusiness() {
_selectedBusiness = null;
notifyListeners();
}
// Private methods
void _setLoading(bool loading) {
_isLoading = loading;
notifyListeners();
}
}
๐งช Testing
Unit Tests
// test/providers/business_provider_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:bekasi_directory/features/business/providers/business_provider.dart';
class MockApiService extends Mock implements ApiService {}
void main() {
group('BusinessProvider Tests', () {
late BusinessProvider provider;
late MockApiService mockApiService;
setUp(() {
mockApiService = MockApiService();
provider = BusinessProvider();
});
test('should load featured businesses', () async {
// Arrange
final mockBusinesses = [
Business(id: '1', name: 'Test Business', /*...*/)
];
when(mockApiService.getBusinesses(limit: 5))
.thenAnswer((_) async => mockBusinesses);
// Act
await provider.loadFeaturedBusinesses();
// Assert
expect(provider.featuredBusinesses, equals(mockBusinesses));
expect(provider.isLoading, false);
});
});
}
Widget Tests
// test/widgets/business_card_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:bekasi_directory/shared/widgets/business_card.dart';
void main() {
testWidgets('BusinessCard displays business information', (tester) async {
// Arrange
final business = Business(
id: '1',
name: 'Test Restaurant',
description: 'Great food',
category: 'Restaurant',
// ... other required fields
);
// Act
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: BusinessCard(business: business),
),
),
);
// Assert
expect(find.text('Test Restaurant'), findsOneWidget);
expect(find.text('Restaurant'), findsOneWidget);
});
}
๐ Build & Deployment
Build for Release
# Android APK
flutter build apk --release
# Android App Bundle (untuk Play Store)
flutter build appbundle --release
# iOS (macOS only)
flutter build ios --release
Deployment ke Play Store
- Prepare Release
# Generate keystore
keytool -genkey -v -keystore ~/bekasi-directory-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias bekasi-directory
- Configure Signing (android/app/build.gradle)
android {
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
- Upload to Play Console
- Create app listing
- Upload app bundle
- Fill app information
- Submit for review
๐ฑ Best Practices untuk Performance
1. Image Optimization
// Use cached_network_image for efficient image loading
CachedNetworkImage(
imageUrl: imageUrl,
placeholder: (context, url) => const ShimmerWidget(),
memCacheWidth: 300, // Limit memory usage
memCacheHeight: 200,
)
2. List Performance
// Use ListView.builder for large lists
ListView.builder(
itemCount: businesses.length,
itemBuilder: (context, index) {
return BusinessCard(business: businesses[index]);
},
)
3. State Management
// Use Consumer instead of rebuilding entire widget tree
Consumer<BusinessProvider>(
builder: (context, provider, child) {
return provider.isLoading
? const LoadingWidget()
: BusinessList(businesses: provider.businesses);
},
)
Selamat! Anda telah mempelajari cara membangun aplikasi mobile Flutter yang lengkap untuk komunitas Bekasi. Aplikasi ini siap dikembangkan lebih lanjut dengan fitur-fitur advanced seperti push notifications, offline sync, dan machine learning.
Tutorial selanjutnya: "Advanced Flutter: State Management dengan Bloc Pattern" - coming soon!
#Flutter #MobileDev #CrossPlatform #BekasiDev #AppDevelopment