This Flutter app integrates iOS Live Activities and Dynamic Island to display a real-time countdown timer for cooking dishes. The app uses the live_activities Flutter package to manage Live Activities and Dynamic Island features on iOS 16.1+ devices.
π Read the full guide on Medium
β
Live Activity Timer: Displays a cooking countdown timer in Dynamic Island & Lock Screen.
β
Real-Time Updates: Updates the remaining time every second.
β
Flutter & Swift Integration: Uses Dart for logic and Swift for Live Activity UI.
β
iOS 16.1+ Support: Works only on iPhones with iOS 16.1 or later.
- Flutter 3.x
- Dart 3.x
- Xcode (for iOS development)
- iOS 16.1+ (Live Activities Support)
import 'dart:async';
import 'package:live_activities/live_activities.dart';
final liveActivities = LiveActivities();
String? _activityId;
Timer? timer;
@override
void initState() {
super.initState();
super.initState();
_initLiveActivities();
}
Future<void> _initLiveActivities() async {
try {
await liveActivities.init(appGroupId: "group.com.example.demoisland");
} catch (e) {
debugPrint("Error initializing Live Activities: $e");
}
}
Future<void> _startCookingTimer() async {
if (_dishNameController.text.isEmpty || _selectedMinutes == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please enter dish name & select cooking time')),
);
return;
}
// Convert the selected minutes to seconds for the countdown
double totalTimeInSeconds = (_selectedMinutes ?? 0) * 60;
// Create the activity with the initial time
final activityId = await liveActivities.createActivity({
'dishname': _dishNameController.text,
'endtime': totalTimeInSeconds, // Store time in seconds
});
setState(() {
_activityId = activityId;
});
// Start the timer for the countdown
timer = Timer.periodic(const Duration(seconds: 1), (timer) async {
if (totalTimeInSeconds <= 0) {
timer.cancel();
_stopCookingTimer();
// Stop the timer when time is up
} else {
totalTimeInSeconds -= 1; // Decrease by one second
await liveActivities.updateActivity(_activityId ?? "", {
'endtime': totalTimeInSeconds, // Update the remaining time
});
}
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('π³ Cooking timer started for ${_dishNameController.text}!'),
),
);
}
Future<void> _stopCookingTimer() async {
if (_activityId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No active cooking timer')),
);
return;
}
await liveActivities.endActivity(_activityId!);
setState(() {
_activityId = null;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Cooking timer stopped! βΉοΈ')),
);
}
Modify DemoIslandiOS.swift
:
import ActivityKit
import WidgetKit
import SwiftUI
struct LiveActivitiesAppAttributes: ActivityAttributes, Identifiable {
public typealias LiveDeliveryData = ContentState
public struct ContentState: Codable, Hashable {}
var id = UUID()
}
struct DemoIslandiOS: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: LiveActivitiesAppAttributes.self) { context in
let remainingTime = context.state.remainingTime
VStack {
Text("π³ Cooking: \(UserDefaults(suiteName: "group.com.example.myapp")!.string(forKey: "dishname") ?? "Unknown")")
.font(.title3)
.bold()
.foregroundColor(.white)
Text("β³ Remaining Time: \(formattedCountdown(from: remainingTime))")
.font(.headline)
.foregroundColor(.yellow)
}
.padding()
.background(Color.black.opacity(0.8))
.clipShape(RoundedRectangle(cornerRadius: 15))
} dynamicIsland: { context in
let remainingTime = context.state.remainingTime
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Text("π³ Cooking")
}
DynamicIslandExpandedRegion(.trailing) {
Text("\(formattedCountdown(from: remainingTime))")
}
DynamicIslandExpandedRegion(.bottom) {
Text("β Cooking in Progress... \(formattedCountdown(from: remainingTime))")
.font(.headline)
.foregroundColor(.yellow)
}
} compactLeading: {
Text("π³")
} compactTrailing: {
Text("\(formattedCountdown(from: remainingTime))")
} minimal: {
Text("β³")
}
}
}
}
// Function to format countdown time in MM:SS format
func formattedCountdown(from timeInterval: TimeInterval) -> String {
let minutes = Int(timeInterval) / 60
let seconds = Int(timeInterval) % 60
return String(format: "%02d:%02d", minutes, seconds)
}
- Flutter sends data to Swift using
UserDefaults
with the same App Group. - The Live Activity is updated every second in Swift using the data passed from Flutter.
To start the cooking timer, simply call the startCookingTimer()
function:
This will:
- Start a Live Activity for the cooking timer.
- Update the Dynamic Island every second as the countdown decreases.
- Live Activities and Dynamic Island are only available on devices with iOS 16.1 or later.
Here's the Markdown code for adding the troubleshooting section to your README.md
file:
While integrating Live Activities in Flutter, you may encounter the following errors. Here's how to fix them:
Cycle inside Runner; building could produce unreliable results. Cycle details:
This issue happens due to build dependency cycles in Xcode. Follow these steps to fix it:
- Open Xcode and navigate to your project.
- Select Runner β Build Phases.
- Move "Embed Foundation Extensions" above the "Run Script Build Phases" configuration.
- Clean the build folder:
- Click Product β Clean Build Folder (
Shift + Cmd + K
). - Run the following command in the terminal:
flutter clean
- Click Product β Clean Build Folder (
- Rebuild the app:
flutter pub get flutter build ios
πΈ Solution Screenshot
force_encoding': can't modify frozen String (FrozenError)
This error occurs because the Xcode project format.
Set it to Xcode 16.0.
πΈ Solution Screenshot
π‘Still facing issues? Open an Issue in this repository
- Ensure that both Flutter and Swift use the same App Group for sharing data.
- Test on a physical device to view the Dynamic Island behavior.
- Make sure to handle background execution and edge cases, such as when the app goes into the background.