์ ๋ฌด์์ ์ฌ์ฉํ๋ ๊ด์ ๋์๋ณด๋ ํ๋ฉด์ ์ฑ์ผ๋ก ๋ง๋๋ ๊ณผ์ ์ค์์ ์๋ฆผ ๊ธฐ๋ฅ์ด ํ์ํ์ต๋๋ค.
Flutter ์ฑ์ ์ฃผ๊ธฐ์ ์ธ ๋ฐฑ๊ทธ๋ผ์ด๋ ์๋ฆผ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ ๊ณผ์ ์์ ์ํ์ฐฉ์ค, ์ ์ฒด ์ฝ๋, ํธ๋ฌ๋ธ ์ํ ๊น์ง ๊ธฐ๋กํด ๋ณด๊ฒ ์ต๋๋ค.
์ถ๊ฐํ ๊ธฐ๋ฅ์ ๊ฐ๋จํ๊ฒ 15๋ถ ๋ง๋ค ์ฃผ๊ธฐ์ ์ธ ์๋์ด ์ค๋๋ก ํ๊ธฐ + ์๋ฆผ ํ ์คํธ ๋ฒํผ์ ์ฌ์ฉํด์ ํ์ฌ ์๊ฐ ์๋ ๋ฐ๊ธฐ๋ฅผ ๊ตฌํํ์ต๋๋ค. ์ด๋ ๊ฒ ํ ์คํธ ํ ํ ์ค์ ๋ก ํ์ํ ์๋ ๊ธฐ๋ฅ์ ์์ ์์ฌ๋ก ๊ตฌํํ๋ฉด ๋๊ฒ ์ฃ .
๋ชฉ์ฐจ
- ํ์ํ ํจํค์ง ์ค์น
- Android ์ค์
- ์๋ฆผ ์๋น์ค ๊ตฌํ
- ์ฑ ์ด๊ธฐํ
- UI์ ํ ์คํธ ๋ฒํผ ์ถ๊ฐ
- ํธ๋ฌ๋ธ์ํ
1. ํ์ํ ํจํค์ง ์ค์น
pubspec.yaml ์์
dependencies:
flutter:
sdk: flutter
# ์๋ฆผ ๊ด๋ จ ํจํค์ง
flutter_local_notifications: ^17.2.3
workmanager:
git:
url: https://github.com/fluttercommunity/flutter_workmanager.git
ref: main
path: workmanager # ์ค์!
permission_handler: ^11.1.0
์ฃผ์์ฌํญ
- workmanager๋ pub.dev์ ์ต์ ๋ฒ์ (0.5.2)์ด Flutter 3.29+์ ํธํ๋์ง ์์ต๋๋ค
- GitHub์์ ์ง์ ๊ฐ์ ธ์์ผ ํ๋ฉฐ, ๋ฐ๋์ path: workmanager๋ฅผ ์ง์ ํด์ผ ํฉ๋๋ค
flutter pub get
2. Android ์ค์
2.1 build.gradle.kts ์ค์
android/app/build.gradle.kts ํ์ผ ์์
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.test_project"
compileSdk = 36 // ์ต์ SDK ์ฌ์ฉ
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true // ์ฃผ์: is ์ ๋์ฌ
}
kotlinOptions {
jvmTarget = "17"
}
defaultConfig {
applicationId = "com.example.test_project"
minSdk = flutter.minSdkVersion
targetSdk = 36
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}
์ฃผ์: Kotlin DSL์์๋ coreLibraryDesugaringEnabled ๋์ isCoreLibraryDesugaringEnabled๋ฅผ ์ฌ์ฉํฉ๋๋ค.
2.2 AndroidManifest.xml ๊ถํ ์ถ๊ฐ
android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- ๊ธฐ์กด ๊ถํ๋ค -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- ์๋ฆผ ๊ถํ ์ถ๊ฐ (Android 13+) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- ๋ฐฑ๊ทธ๋ผ์ด๋ ์์
๊ถํ -->
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<application
android:label="test_project"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- WorkManager ์๋น์ค -->
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
android:exported="false" />
<!-- ๋ถํ
์ ์๋ ์์ ๋ฆฌ์๋ฒ -->
<receiver
android:name="androidx.work.impl.utils.ForceStopRunnable$BroadcastReceiver"
android:enabled="true"
android:exported="false" />
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
3. ์๋ฆผ ์๋น์ค ๊ตฌํ
lib/data/services/notification_service.dart ์์ฑ:
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:intl/intl.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:workmanager/workmanager.dart';
class NotificationService {
static const String _channelId = 'periodic_notification';
static const String _channelName = '์ฃผ๊ธฐ์ ์๋ฆผ';
static const String _taskName = 'periodicTask';
static final FlutterLocalNotificationsPlugin _notifications =
FlutterLocalNotificationsPlugin();
/// ์๋ฆผ ์๋น์ค ์ด๊ธฐํ
static Future<void> init() async {
debugPrint('๐ต ์๋ฆผ ์๋น์ค ์ด๊ธฐํ ์์');
// Android 13 ์ด์ ์๋ฆผ ๊ถํ ์์ฒญ
await _requestPermission();
// Android ์ค์
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
final result = await _notifications.initialize(
initSettings,
onDidReceiveNotificationResponse: (details) {
debugPrint('๐ฑ ์๋ฆผ ํด๋ฆญ: ${details.payload}');
},
);
debugPrint('โ
์๋ฆผ ์ด๊ธฐํ ๊ฒฐ๊ณผ: $result');
// ์๋ฆผ ์ฑ๋ ์์ฑ
const androidChannel = AndroidNotificationChannel(
_channelId,
_channelName,
description: '์ฃผ๊ธฐ์ ์ธ ์๋ฆผ์ ๋ฐ์ต๋๋ค',
importance: Importance.max,
enableVibration: true,
playSound: true,
enableLights: true,
);
final platform = _notifications.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
if (platform != null) {
await platform.createNotificationChannel(androidChannel);
debugPrint('โ
์๋ฆผ ์ฑ๋ ์์ฑ ์๋ฃ: $_channelId');
}
}
/// ์๋ฆผ ๊ถํ ์์ฒญ
static Future<void> _requestPermission() async {
final status = await Permission.notification.status;
debugPrint('๐ฑ ํ์ฌ ์๋ฆผ ๊ถํ: $status');
if (status.isDenied || status.isPermanentlyDenied) {
final result = await Permission.notification.request();
debugPrint('๐ฑ ์๋ฆผ ๊ถํ ์์ฒญ ๊ฒฐ๊ณผ: $result');
}
}
/// ๋ฐฑ๊ทธ๋ผ์ด๋ ์์
์์
static Future<void> startBackgroundTask() async {
await Workmanager().initialize(
callbackDispatcher,
);
await Workmanager().registerPeriodicTask(
_taskName,
_taskName,
frequency: const Duration(minutes: 15), // Android ์ต์๊ฐ
constraints: Constraints(
networkType: NetworkType.notRequired,
),
);
debugPrint('โ
๋ฐฑ๊ทธ๋ผ์ด๋ ์์
์์');
}
/// ์๋ฆผ ํ์
static Future<void> showNotification() async {
debugPrint('๐ต ์๋ฆผ ํ์ ์๋');
try {
final now = DateTime.now();
final formattedDate = DateFormat('yyyy-MM-dd HH:mm:ss').format(now);
const androidDetails = AndroidNotificationDetails(
_channelId,
_channelName,
channelDescription: '์ฃผ๊ธฐ์ ์ธ ์๋ฆผ์ ๋ฐ์ต๋๋ค',
importance: Importance.max,
priority: Priority.high,
showWhen: true,
enableVibration: true,
playSound: true,
enableLights: true,
icon: '@mipmap/ic_launcher',
ticker: '์ ์๋ฆผ',
);
const details = NotificationDetails(android: androidDetails);
final id = now.millisecondsSinceEpoch ~/ 1000;
await _notifications.show(
id,
'ํ
์คํธ ์ฑ',
'์ง๊ธ์ $formattedDate์
๋๋ค.',
details,
payload: formattedDate,
);
debugPrint('โ
์๋ฆผ ํ์ ์๋ฃ: ID=$id');
} catch (e) {
debugPrint('โ ์๋ฆผ ํ์ ์คํจ: $e');
}
}
/// ๋ฐฑ๊ทธ๋ผ์ด๋ ์์
์ค์ง
static Future<void> stopBackgroundTask() async {
await Workmanager().cancelAll();
debugPrint('โ
๋ฐฑ๊ทธ๋ผ์ด๋ ์์
์ค์ง');
}
}
/// ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์คํ๋ ์ฝ๋ฐฑ
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
await NotificationService.showNotification();
return Future.value(true);
});
}
4. ์ฑ ์ด๊ธฐํ
lib/main.dart
import 'package:flutter/material.dart';
import 'app.dart';
import 'data/services/notification_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// ์๋ฆผ ์๋น์ค ์ด๊ธฐํ (await ํ์!)
await NotificationService.init();
await NotificationService.startBackgroundTask();
runApp(const App());
}
5. UI์ ํ ์คํธ ๋ฒํผ ์ถ๊ฐ
๋์๋ณด๋ ํ๋ฉด์ ํ ์คํธ ๋ฒํผ์ ์ถ๊ฐํฉ๋๋ค.
// ์๋ฆผ ํ
์คํธ ๋ฒํผ
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ElevatedButton(
onPressed: () async {
debugPrint('๐ต ์๋ฆผ ํ
์คํธ ๋ฒํผ ํด๋ฆญ๋จ');
try {
await NotificationService.showNotification();
debugPrint('โ
์๋ฆผ ์์ฒญ ์๋ฃ');
} catch (e) {
debugPrint('โ ์๋ฆผ ์์ฒญ ์คํจ: $e');
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[700],
minimumSize: const Size(double.infinity, 40),
),
child: const Text(
'์๋ฆผ ํ
์คํธ',
style: TextStyle(color: Colors.white),
),
),
),
6. ํธ๋ฌ๋ธ์ํ
๋ฌธ์ 1: workmanager ๋ฒ์ ์ถฉ๋
์๋ฌ:
Unresolved reference: shim
Unresolved reference: PluginRegistrantCallback
์์ธ: pub.dev์ workmanager 0.5.2๊ฐ Flutter 3.29+์ ํธํ๋์ง ์์
ํด๊ฒฐ: GitHub์์ ์ง์ ๊ฐ์ ธ์ค๊ธฐ
workmanager:
git:
url: https://github.com/fluttercommunity/flutter_workmanager.git
ref: main
path: workmanager # ์ด ๋ถ๋ถ ์ค์!
๋ฌธ์ 2: flutter_local_notifications ๋น๋ ์๋ฌ
์๋ฌ:
error: reference to bigLargeIcon is ambiguous
์์ธ: flutter_local_notifications 16.3.0์ด Android SDK 36๊ณผ ํธํ๋์ง ์์
ํด๊ฒฐ: ์ต์ ๋ฒ์ ์ผ๋ก ์ ๋ฐ์ดํธ
flutter_local_notifications: ^17.2.3
๋ฌธ์ 3: NetworkType.not_required ์๋ฌ
์๋ฌ:
Unresolved reference: not_required
์์ธ: workmanager ์ต์ ๋ฒ์ ์์ API๊ฐ ๋ณ๊ฒฝ๋จ
ํด๊ฒฐ: snake_case๋ฅผ camelCase๋ก ๋ณ๊ฒฝ
// ๋ณ๊ฒฝ ์
networkType: NetworkType.not_required
// ๋ณ๊ฒฝ ํ
networkType: NetworkType.notRequired
๋ฌธ์ 4: coreLibraryDesugaringEnabled ์๋ฌ
์๋ฌ:
Unresolved reference: coreLibraryDesugaringEnabled
์์ธ: Kotlin DSL์์๋ boolean ํ๋กํผํฐ์ is ์ ๋์ฌ ์ฌ์ฉ
ํด๊ฒฐ:
compileOptions {
isCoreLibraryDesugaringEnabled = true // is ์ถ๊ฐ
}
๋ฌธ์ 5: ์๋ฆผ์ด ํ์๋์ง ์์
์ฆ์: ๋ก๊ทธ๋ ์ ์์ธ๋ฐ ์๋ฆผ์ด ์ ๋ณด์
I/NotificationManager: notify(0, null, Notification...
์์ธ:
- ์๋ฆผ ๊ถํ์ด ํ์ฉ๋์ง ์์
- ์ฑ์ ์ฌ์ค์นํ์ง ์๊ณ ์ ๋ฐ์ดํธํจ
ํด๊ฒฐ:
# ์ฑ ์์ ํ ์ญ์
adb uninstall com.example.test_project
# ํด๋ฆฐ ๋น๋
flutter clean
flutter pub get
# ์ฌ์ค์น
flutter run
์ฌ์ค์น ํ ์ค์ > ์ ํ๋ฆฌ์ผ์ด์ > ์ฑ ์ด๋ฆ > ์๋ฆผ์์,
- โ ์๋ฆผ ํ์ฉ
- โ ์๋ฆฌ ๋ฐ ์ง๋
- โ ์ ๊ธํ๋ฉด์์ ๋ด์ฉ ํ์
๋ชจ๋ ์ผ์ ธ ์๋์ง ํ์ธ!
๋ฌธ์ 6: ์ผ์ฑ ๊ฐค๋ญ์ ์ถ๊ฐ ์ค์
์ผ์ฑ ๊ธฐ๊ธฐ์ ๊ฒฝ์ฐ ๋ฐฐํฐ๋ฆฌ ์ต์ ํ ๋๋ฌธ์ ๋ฐฑ๊ทธ๋ผ์ด๋ ์๋ฆผ์ด ์ ํ๋ ์ ์๋ค๊ณ ํฉ๋๋ค. ๊ทธ๋ผ ์๋์ ๊ฐ์ ์ถ๊ฐ ์ค์ ์ด ํ์ํฉ๋๋ค.
์ถ๊ฐ ์ค์ :
- ์ค์ > ๋ฐฐํฐ๋ฆฌ > ๋ฐฐํฐ๋ฆฌ ์ฌ์ฉ๋ > test_project → "์ ํ ์์"
- ์ค์ > ์ฑ > test_project > ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฌ์ฉ ์ ํ → "์ ํ ์ ํจ"
ํ ์คํธ ๋ฐฉ๋ฒ
์ฆ์ ์๋ฆผ ํ ์คํธ (๋๋ฒ๊ทธ ๋ชจ๋)
flutter run
- ์ฑ ์คํ ํ "์๋ฆผ ํ ์คํธ" ๋ฒํผ ํด๋ฆญ
- ์ฆ์ ์๋ฆผ์ด ํ์๋์ด์ผ ํจ
๋ฐฑ๊ทธ๋ผ์ด๋ ์๋ฆผ ํ ์คํธ (APK)
# Release APK ๋น๋
flutter build apk --release
# APK ์์น
# build/app/outputs/flutter-apk/app-release.apk
- APK๋ฅผ ํฐ์ ์ค์น
- ์ฑ ์คํ ํ ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ์ ํ
- 15๋ถ๋ง๋ค ์๋ฆผ ํ์ธ
๋ก๊ทธ ํ์ธ
์ ์ ๋์ ์ ๋ค์ ๋ก๊ทธ๊ฐ ํ์๋ฉ๋๋ค:
์ฑ ์ด๊ธฐํ:
๐ต ์๋ฆผ ์๋น์ค ์ด๊ธฐํ ์์
๐ฑ ํ์ฌ ์๋ฆผ ๊ถํ: granted
โ
์๋ฆผ ์ด๊ธฐํ ๊ฒฐ๊ณผ: true
โ
์๋ฆผ ์ฑ๋ ์์ฑ ์๋ฃ: periodic_notification
โ
๋ฐฑ๊ทธ๋ผ์ด๋ ์์
์์
๋ฒํผ ํด๋ฆญ:
๐ต ์๋ฆผ ํ
์คํธ ๋ฒํผ ํด๋ฆญ๋จ
๐ต ์๋ฆผ ํ์ ์๋
โ
์๋ฆผ ํ์ ์๋ฃ: ID=1728389045
โ
์๋ฆผ ์์ฒญ ์๋ฃ
๊ฒฐ๋ก
Flutter์์ ๋ฐฑ๊ทธ๋ผ์ด๋ ์๋ฆผ์ ๊ตฌํํ๋ ๊ฒ์ ์ฌ๋ฌ ๋จ๊ณ์ ์ค์ ์ด ํ์ํฉ๋๋ค:
- โ ์ต์ ํจํค์ง ๋ฒ์ ์ฌ์ฉ (ํธํ์ฑ ํ์ธ ํ์)
- โ Android ๊ถํ ๋ฐ ์๋น์ค ์ค์
- โ ์๋ฆผ ์ฑ๋ ์์ฑ
- โ ๊ถํ ์์ฒญ ๊ตฌํ
- โ ์ฑ ์์ ์ฌ์ค์น๋ก ํ ์คํธ
ํนํ Flutter 3.29+ ํ๊ฒฝ์์๋ workmanager๋ฅผ GitHub์์ ์ง์ ๊ฐ์ ธ์์ผ ํ๋ ์ ์ด ์ค์ํ์ต๋๋ค.
์ด์ ์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋์์๋ ์ฃผ๊ธฐ์ ์ผ๋ก ์๋ฆผ์ ๋ณด๋ผ ์ ์์ต๋๋ค! ๐
