Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/istornz/flutter_live_activities

A Flutter plugin to use iOS 16.1+ Live Activities โ›น๏ธ & iPhone Dynamic Island ๐Ÿ๏ธ features
https://github.com/istornz/flutter_live_activities

dynamic-island flutter flutter-plugin ios iphone live-activity plugin swift

Last synced: 1 day ago
JSON representation

A Flutter plugin to use iOS 16.1+ Live Activities โ›น๏ธ & iPhone Dynamic Island ๐Ÿ๏ธ features

Awesome Lists containing this project

README

        


flutter ios 16 live activities












Radion banner - Ultimate gaming app



A Flutter plugin to use iOS 16.1+ **Live Activities** & iPhone 14 Pro **Dynamic Island** features.

## ๐Ÿง What is it ?

This plugin uses the [iOS ActivityKit API](https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activities).

**live_activities** can be used to show **dynamic live notification** & implement **dynamic island** feature on iPhones that support it ๐Ÿ๏ธ

> โš ๏ธ **live_activities** is only intended to use with **iOS 16.1+** !
> It will simply do nothing on other platform & < iOS 16.1





flutter ios 16 live activities dynamic island
flutter ios 16 live activities lockscreen

flutter ios 16 live activities preview dynamic island
flutter ios 16 live activities preview action


## ๐Ÿ‘ป Getting started

Due to **technical restriction**, it's not currently possible to only use Flutter ๐Ÿซฃ.

You need to **implement** in your Flutter iOS project a **Widget Extension** & develop in _Swift_/_Objective-C_ your own **Live Activity** / **Dynamic Island** design.

> โ„น๏ธ You can check into the [**example repository**](https://github.com/istornz/live_activities/tree/main/example) for a full example app using Live Activities & Dynamic Island

- ## ๐Ÿ“ฑ Native
- Open the Xcode workspace project `ios/Runner.xcworkspace`.
- Click on `File` -> `New` -> `Target...`
- Select `Widget Extension` & click on **Next**.
- Specify the product name (e.g., `MyAppWidget`) and be sure to select "**Runner**" in "Embed in Application" dropdown.
- Click on **Finish**.
- When selecting Finish, an alert will appear, you will need to click on **Activate**.

create widget extension xcode

- Add the "Push Notifications" capabilities for the main `Runner` app **only**!.

enable push notification capabilities

- Enable live activity by adding this line in `Info.plist` for both `Runner` and your `Widget Extension`.

```xml
NSSupportsLiveActivities

```

enable live activities xcode

- Add the "App Group" capability for both `Runner` and your widget extension.
After you add the capability, check the checkmark next to the text field that contains an identifier of the form `group.example.myapp`.
This identifier will be used later and refered to as `YOUR_GROUP_ID`.

enable live activity


> โ„น๏ธ You can check on this [resource](https://levelup.gitconnected.com/how-to-create-live-activities-widget-for-ios-16-2c07889f1235) or [here](https://betterprogramming.pub/create-live-activities-with-activitykit-on-ios-16-beta-4766a347035b) for more native informations.

- Inside your extension `ExtensionNameLiveActivity.swift` file, you need to create an `ActivityAttributes` called **EXACTLY** `LiveActivitiesAppAttributes` (if you rename, activity will be created but not appear!)

```swift
struct LiveActivitiesAppAttributes: ActivityAttributes, Identifiable {
public typealias LiveDeliveryData = ContentState // don't forget to add this line, otherwise, live activity will not display it.

public struct ContentState: Codable, Hashable { }

var id = UUID()
}
```

- Create a Swift extension to handle prefixed keys at the end of the file.

```swift
extension LiveActivitiesAppAttributes {
func prefixedKey(_ key: String) -> String {
return "\(id)_\(key)"
}
}
```

- Create an `UserDefaults` with your group id to access Flutter data in your Swift code.

```swift
// Create shared default with custom group
let sharedDefault = UserDefaults(suiteName: "YOUR_GROUP_ID")!

struct FootballMatchApp: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: LiveActivitiesAppAttributes.self) { context in
// create your live activity widget extension here
// to access Flutter properties:
let myVariableFromFlutter = sharedDefault.string(forKey: context.attributes.prefixedKey("myVariableFromFlutter"))!

// [...]
}
}
}
```

- ## ๐Ÿ’™ Flutter

- Import the plugin.

```dart
import 'package:live_activities/live_activities.dart';
```

- Initialize the Plugin by passing the created **App Group Id** (created above).

```dart
final _liveActivitiesPlugin = LiveActivities();
_liveActivitiesPlugin.init(appGroupId: "YOUR_GROUP_ID");
```

- Create your dynamic activity.

```dart
final Map activityModel = {
'name': 'Margherita',
'ingredient': 'tomato, mozzarella, basil',
'quantity': 1,
};

_liveActivitiesPlugin.createActivity(activityModel);
```

> You can pass all type of data you want but keep it mind it should be compatible with [`UserDefaults`](https://developer.apple.com/documentation/foundation/userdefaults)


## Access Flutter basic data from Native ๐Ÿงต

- In your Swift extension, you need to create an `UserDefaults` instance to access data:

```swift
let sharedDefault = UserDefaults(suiteName: "YOUR_GROUP_ID")!
```

> โš ๏ธ Be sure to use the **SAME** group id in your Swift extension and your Flutter app!

- Access to your typed data:

```swift
let pizzaName = sharedDefault.string(forKey: context.attributes.prefixedKey("name"))! // put the same key as your Dart map
let pizzaPrice = sharedDefault.float(forKey: context.attributes.prefixedKey("price"))
let quantity = sharedDefault.integer(forKey: context.attributes.prefixedKey("quantity"))
// [...]
```

## Access Flutter files like pictures from Native ๐Ÿงต

- In your map, send a `LiveActivityFileFromAsset`, `LiveActivityFileFromUrl` or `LiveActivityFileFromMemory` object:

You can use the factory `.image()` to use options like resizing image.

```dart
final Map activityModel = {
'txtFile': LiveActivityFileFromAsset('assets/files/rules.txt'),
'assetKey': LiveActivityFileFromAsset.image('assets/images/pizza_chorizo.png'),
'url': LiveActivityFileFromUrl.image(
'https://cdn.pixabay.com/photo/2015/10/01/17/17/car-967387__480.png',
imageOptions: LiveActivityImageFileOptions(
resizeFactor: 0.2
)
),
};

_liveActivitiesPlugin.createActivity(activityModel);
```

โ„น๏ธ Use `LiveActivityFileFromAsset` to load a file from your Flutter asset.

โ„น๏ธ Use `LiveActivityFileFromUrl` to load a file from an external url.

โ„น๏ธ Use `LiveActivityFileFromMemory` to load a file from the memory (from a `Uint8List` object).

> โš ๏ธ File like picture need to be in a small resolution to be displayed in your live activity/dynamic island, you can use `resizeFactor` to automatically resize the image ๐Ÿ‘.

- In your Swift extension, display the image:

```swift
if let assetImage = sharedDefault.string(forKey: context.attributes.prefixedKey("assetKey")), // <-- Put your key here
let uiImage = UIImage(contentsOfFile: shop) {
Image(uiImage: uiImage)
.resizable()
.frame(width: 53, height: 53)
.cornerRadius(13)
} else {
Text("Loading")
}
```

- To display a txt file content:

```swift
let ruleFile = sharedDefault.string(forKey: context.attributes.prefixedKey("txtFile"))! // <-- Put your key here
let rule = (try? String(contentsOfFile: ruleFile, encoding: .utf8)) ?? ""

// [...] display the rule txt string variable here
```

## Communicate between native ๐Ÿงต and Flutter ๐Ÿ’™

In order to pass some useful **data** between your **native** live activity / dynamic island with your **Flutter** app you just need to setup **URL scheme**.

> โš ๏ธ It's recommended to set a custom scheme, in this example, `la` is used but keep in mind, you should use a more personalized scheme.

> **ex:** for an app named `Strava`, you could use `str`.

- Add a custom url scheme in Xcode by navigating to **Runner** > **Runner** > **URL Types** > **URL Schemes**

add url scheme xcode

- In your Swift code, just create a new **link** and open to your custom **URL Scheme**

```swift
Link(destination: URL(string: "la://my.app/order?=123")!) { // Replace "la" with your scheme
Text("See order")
}
```

> โš ๏ธ Don't forget to put the **URL Scheme** you have typed in the **previous step**. (`str://` in the previous example)

- In your Flutter App, you need to init the custom scheme you provided before

```dart
_liveActivitiesPlugin.init(
appGroupId: 'your.group.id', // replace here with your custom app group id
urlScheme: 'str' // replace here with your custom app scheme
);
```

- Finally, on the Flutter App too, you just need to listen on the **url scheme Scheme**

```dart
_liveActivitiesPlugin.urlSchemeStream().listen((schemeData) {
// do what do you want here ๐Ÿค—
});
```

## Update Live Activity with push notification ๐ŸŽฏ

You can update live activity directly in your app using the `updateActivity()` method, but if your app was killed or in the background, you canโ€™t update the notification...

To do this, you can update it using Push Notification on a server.

- Get the push token:
- Listen on the activity updates (recommended):
```dart
_liveActivitiesPlugin.activityUpdateStream.listen((event) {
event.map(
active: (activity) {
// Get the token
print(activity.activityToken);
},
ended: (activity) {},
unknown: (activity) {},
);
});
```
- Get directly the push token (not recommended, because the token may change in the future):
```dart
final activityToken = await _liveActivitiesPlugin.getPushToken(_latestActivityId!);
print(activityToken);
```
- Update your activity with the token on your server (more information can be [**found here**](https://ohdarling88.medium.com/update-dynamic-island-and-live-activity-with-push-notification-38779803c145)).

To set `matchName` for a specific notification, you just need to grab the notification id you want (ex. `35253464632`) and concatenate with your key by adding a `_`, example: `35253464632_matchName`.

That's it ๐Ÿ˜‡


## ๐Ÿ“˜ Documentation

| Name | Description | Returned value |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `.init()` | Initialize the Plugin by providing an App Group Id (see above) | `Future` When the plugin is ready to create/update an activity |
| `.createActivity()` | Create an iOS live activity | `String` The activity identifier |
| `.createOrUpdateActivity()` | Create or updates an (existing) live activity based on the provided `UUID` via `customId` | `String` The activity identifier |
| `.updateActivity()` | Update the live activity data by using the `activityId` provided | `Future` When the activity was updated |
| `.endActivity()` | End the live activity by using the `activityId` provided | `Future` When the activity was ended |
| `.getAllActivitiesIds()` | Get all activities ids created | `Future>` List of all activities ids |
| `.getAllActivities()` | Get a Map of activitiyIds and the `ActivityState` | `Future>` Map of all activitiyId -> `LiveActivityState` |
| `.endAllActivities()` | End all live activities of the app | `Future` When all activities was ended |
| `.areActivitiesEnabled()` | Check if live activities feature are supported & enabled | `Future` Live activities supported or not |
| `.getActivityState()` | Get the activity current state | `Future` An enum to know the status of the activity (`active`, `dismissed` or `ended`) |
| `.getPushToken()` | Get the activity push token synchronously (prefer using `activityUpdateStream` instead to keep push token up to date) | `String?` The activity push token (can be null) |
| `.urlSchemeStream()` | Subscription to handle every url scheme (ex: when the app is opened from a live activity / dynamic island button, you can pass data) | `Future` Url scheme data which handle `scheme` `url` `host` `path` `queryItems` |
| `.dispose()` | Remove all pictures passed in the AppGroups directory in the current session, you can use the `force` parameters to remove **all** pictures | `Future` Picture removed |
| `.activityUpdateStream` | Get notified with a stream about live activity push token & status | `Stream` Status updates for new push tokens or when the activity ends |


## ๐Ÿค” Questions

### Do I have to code in Swift?

> Yes you need to implement your activity in Swift but no worries, there is a lot of cool tutorials:
> - [https://canopas.com/integrating-live-activity-and-dynamic-island-in-i-os-a-complete-guide](https://canopas.com/integrating-live-activity-and-dynamic-island-in-i-os-a-complete-guide)
> - [https://blorenzop.medium.com/live-activities-swift-6e95ee15863e](https://blorenzop.medium.com/live-activities-swift-6e95ee15863e)
> - [https://medium.com/kinandcartacreated/how-to-build-ios-live-activity-d1b2f238819e](https://medium.com/kinandcartacreated/how-to-build-ios-live-activity-d1b2f238819e)

### I have an issue when building my app on iOS: `Error (Xcode): Cycle inside Runner; building could produce unreliable results.`

> This error occurs due to a build script ordering issue. Follow [this guide](https://stackoverflow.com/a/77178579/5078902) to resolve it.

### I can't see my live activity when I create it...

> It can be related to multiple issues, please be sure to:
>
> - App Groups Capability: Set up the `App Groups` capability for **BOTH** the `Runner` and your `extension` targets.
> - Same App Group: Use the **SAME** app group for both the `Runner` and `extension` targets.
> - Push Notification Capability: Verify that the `Push Notification` capability is enabled for the `Runner` target.
> - ActivityAttributes Definition: In your extensionโ€™s `ExtensionNameLiveActivity.swift` file, ensure you create an ActivityAttributes named **EXACTLY** `LiveActivitiesAppAttributes`.
> - Asset Size Limit: Images in live activities **must be under or equal 4 KB**. Use the resize factor argument to reduce image size if necessary.
> - Supports Live Activities: Be sure to set the `NSSupportsLiveActivities` property to `true` in `Info.plist` files for **BOTH** `Runner` and your `extension`.
> - iOS Version Requirement: The device must run **iOS 16.1 or later**.
> - Device Activity Check: Confirm that the `areActivitiesEnabled()` method returns true on your device.

### Is Android supported?

> Currently, no, but the plugin does not crash when run on Android. This means you can safely install it in a hybrid app.
>
> Simply call `areActivitiesEnabled()` before creating your activity to ensure it can be displayed on the user's device. ๐Ÿ˜Š

## ๐Ÿ‘ฅ Contributions

Contributions are welcome. Contribute by creating a PR or an issue ๐ŸŽ‰.

## ๐ŸŽฏ Roadmap

- [ ] Inject a Widget inside the notification with Flutter Engine?
- [x] Migrate to Swift Package Manager.
- [x] Support push token.
- [x] Pass media between extension & Flutter app.
- [x] Support multiple type instead of `String` (Date, Number etc.).
- [x] Pass data across native dynamic island and Flutter app.
- [x] Pass data across native live activity notification and Flutter app.
- [x] Cancel all activities.
- [x] Get all activities ids.
- [x] Check if live activities are supported.