Myeongshin: Smart Corporate Vehicle Sharing
Managing a corporate vehicle fleet shouldn't require spreadsheets and phone calls. Myeongshin is a Flutter mobile app that brings transparency and efficiency to company car reservations, built for real-world business needs.
The Business Challenge
Corporate vehicle sharing faces unique problems:
- Booking conflicts – No centralized system to prevent double-bookings
- Manual tracking – Spreadsheets can't scale with growing fleets
- Poor visibility – Employees don't know which vehicles are available
- Missed notifications – Important updates get lost in email
- Compliance gaps – Insurance and terms acceptance isn't tracked
Myeongshin solves these challenges with a mobile-first platform that employees actually want to use.
The Solution
🚗 Complete Reservation Flow
- Browse available vehicles with real-time availability
- Select pickup/return dates with calendar interface
- Review insurance terms and company policies
- Confirm bookings with one-tap payment
- Receive instant confirmation notifications
📱 Mobile-First Experience
- Native iOS and Android from single Flutter codebase
- Offline-capable with local data caching
- Push notifications via Firebase Cloud Messaging
- Smooth animations and transitions
- Korean localization throughout
💳 Points-Based System
- Transparent credit balance display
- Point consumption per reservation
- Usage history and analytics
- Easy top-up flow
🔐 Enterprise Security
- Automatic token refresh on expiration
- Secure credential storage with flutter_secure_storage
- 401/403 handling with automatic logout
- HTTPS-only API communication
Technical Architecture
The Stack
- Flutter – Cross-platform mobile framework
- Provider – Predictable state management
- Firebase Cloud Messaging – Push notifications
- HTTP Client – RESTful API integration with auto-refresh
- flutter_secure_storage – Encrypted credential storage
- Korean Localization – Full i18n support
State Management
The app uses Provider for global state across three core domains:
// lib/main.dart
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => PointsProvider()),
ChangeNotifierProvider(create: (_) => UserProvider()),
ChangeNotifierProvider(create: (_) => DateProvider()),
],
child: const MyApp(),
),
);
PointsProvider – Manages user credit balance and transaction history
UserProvider – Handles authentication state and profile data
DateProvider – Coordinates reservation date selection across screens
Network Layer with Auto-Refresh
The HTTP client automatically refreshes expired tokens and handles errors gracefully:
// lib/utils/agent.dart
Future<http.Response> get(String endpoint) async {
await checkAndRefreshToken();
final url = Uri.parse('$_baseUrl$endpoint');
final response = await http.get(url, headers: await _headers());
_handleResponse(response);
return response;
}
void _handleResponse(http.Response response) {
if (response.body.isEmpty) return;
if (response.statusCode < 200 || response.statusCode >= 300) {
if (response.statusCode == 401 || response.statusCode == 403) {
clearAllTokens();
navigateToLogin();
}
final resBody = jsonFormatter(response);
if (response.statusCode >= 500) {
throw '다시 시도해주세요.';
}
throw (resBody['message'] ?? resBody['error'] ?? response.reasonPhrase);
}
}
This pattern ensures:
- Tokens are refreshed before expiration
- 401/403 responses trigger automatic logout
- Server errors show user-friendly Korean messages
- Network failures are handled gracefully
Firebase Push Notifications
The notification service handles three app states (foreground, background, terminated):
// lib/services/notification_service.dart
class NotificationService {
static Future<void> initialize() async {
// Request permissions
await FirebaseMessaging.instance.requestPermission();
// Foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
_showLocalNotification(message);
});
// Background messages
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// Notification tap handling
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
_handleNotificationTap(message);
});
}
}
Reservation Flow
The reservation screen validates all requirements before submission:
// lib/screens/reservation/reserve_car/index.dart
void handleOnReserve() async {
// Validate terms acceptance
if (!allChecked) {
return fireAlert(context, '이용 약관에 동의해주세요.', () {});
}
// Validate dates
if (_startDate.isAfter(_endDate) || _startDate.isBefore(DateTime.now())) {
return fireToast(context, '예약 시간을 확인하십시오', isError: true);
}
setState(() => isLoading = true);
try {
await CarService().reserveCar({
'carId': widget.car['id'],
'rentalStartDate': _startDate.toIso8601String(),
'rentalEndDate': _endDate.toIso8601String(),
'rentalLocationId': widget.car['rentalLocationId'],
'returnLocationId': widget.car['rentalLocationId'],
'isBusiness': widget.isBusiness,
});
if (!mounted) return;
// Navigate to success screen
Navigator.push(
context,
MaterialPageRoute(builder: (_) => SuccessScreen(...))
);
// Refresh points balance
await Provider.of<PointsProvider>(context, listen: false)
.fetchTotalPoints();
} catch (e) {
if (!mounted) return;
fireAlert(context, e.toString(), () {});
} finally {
setState(() {
isLoading = false;
allChecked = false;
});
}
}
The app uses Provider's listen: false pattern to
prevent unnecessary rebuilds while still accessing global state in event
handlers.
Code Highlights
Routing Configuration
The app uses MaterialApp with Korean locale and named routes:
// lib/app.dart
return MaterialApp(
title: '명신 차량 공유',
initialRoute: Routes.mainScreen,
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
theme: ThemeData(scaffoldBackgroundColor: COLORS.white),
routes: {
Routes.mainScreen: (context) => const AuthenticationFlowScreen(),
Routes.signUp: (context) => const SignUpScreen(),
Routes.signIn: (context) => const SignIn(),
Routes.forgotPassword: (context) => const SendCodeScreen(),
Routes.resetPwd: (context) => const ResetPasswordScreen(),
Routes.home: (context) => const HomeScreen(),
Routes.reserveCar: (context) => const ReserveCarScreen(),
Routes.history: (context) => const ReservationHistoryScreen(),
},
supportedLocales: const [Locale('ko', 'KR')],
locale: const Locale('ko', 'KR'),
);
Points Provider
State management for user credits with automatic refresh:
// lib/providers/points_provider.dart
class PointsProvider with ChangeNotifier {
int _totalPoints = 0;
bool _isLoading = false;
int get totalPoints => _totalPoints;
bool get isLoading => _isLoading;
Future<void> fetchTotalPoints() async {
_isLoading = true;
notifyListeners();
try {
final response = await Agent().get('/points/balance');
final data = jsonDecode(response.body);
_totalPoints = data['balance'] ?? 0;
} catch (e) {
_totalPoints = 0;
} finally {
_isLoading = false;
notifyListeners();
}
}
}
Demo
Impact & Learnings
What This Project Delivers:
✅ Eliminates booking conflicts – Real-time availability prevents double-bookings
✅ Improves employee experience – Mobile-first design beats desktop-only systems
✅ Reduces admin overhead – Automated notifications and tracking
✅ Ensures compliance – Digital terms acceptance with audit trail
Key Technical Takeaways:
- Provider scales well for medium-complexity apps without Redux overhead
- Automatic token refresh is essential for enterprise mobile apps
- Firebase FCM handles push notifications reliably across app states
- flutter_secure_storage provides platform-native credential encryption
- Korean localization requires attention to date formats and error messages
Architecture Lessons:
- Centralized HTTP client with error handling reduces boilerplate
- Named routes make navigation predictable and testable
- Provider context should use
listen: falsein event handlers - Loading states must handle
mountedchecks for async operations
This project demonstrates that Flutter can deliver production-ready enterprise apps with complex business logic, real-time updates, and robust error handling. The patterns here transfer to any corporate mobile app: expense tracking, facility booking, or equipment management.