Workshop for Android Conversation Bubbles in Flutter.
The aim of this workshop is to get you started with showing Android Conversation Bubbles in apps you build with Flutter.
In Android, Bubbles make it easier for users to see and participate in conversations. To know more about Conversation Bubbles in Android, visit the "Use bubbles for conversations" page in the Android Documentation.
This workshop uses the conversation_bubbles Flutter package to show Bubbles.
- Have the Flutter SDK installed.
- Have your favorite Flutter IDE installed (Android Studio or VS Code) and properly configured for Flutter.
- Have an Android device or emulator running Android 11 or higher.
- Get the starter code for this workshop by cloning this repository:
git clone https://github.com/keepdeploying/bubbles_in_flutter_workshop- Change into the project directory:
cd bubbles_in_flutter_workshop- Checkout to the
starterbranch:
git checkout starter-
Run
flutter pub getto get the dependencies. -
Open the project in your favorite Flutter IDE.
-
Run the app on an Android device or emulator and explore the "People" chat app.
- Add the following permission to the
AndroidManifest.xmlfile, immediately after the openingmanifestXML tag:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>- Install the permission_handler package by running the following command:
flutter pub add permission_handler-
Create a new Dart file called
notifications_permissions_service.dartin thelib/servicesdirectory. -
Add the following code to the
notifications_permissions_service.dartfile:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
/// Takes note of the number of times the notifications permission has been
/// requested. If it exceeds 2, the user is redirected to the app settings.
int _requestCount = 0;
class NotificationsPermissionService with WidgetsBindingObserver {
final _ctrl = StreamController<bool>.broadcast()..add(false);
static final instance = NotificationsPermissionService._();
NotificationsPermissionService._() {
check();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
check();
}
}
Stream<bool> get isGrantedStream => _ctrl.stream;
Future<void> check() async {
_ctrl.add((await Permission.notification.status).isGranted);
}
Future<void> request() async {
if (_requestCount > 2) {
await openAppSettings();
return;
}
_requestCount++;
await Permission.notification.request();
await check();
}
}- In
lib/screens/home_screen.dart, import thenotifications_permissions_service.dartfile:
import 'package:bubbles_in_flutter/services/notifications_permissions_service.dart';- In the
_HomeScreenStateclass, declare a reference to theNotificationsPermissionServiceinstance alongside the existingchatsvariable:
final notifService = NotificationsPermissionService.instance;- In the
actionslist of the AppBar widget, add a StreamBuilder on theisGrantedStreamof the service, that shows an IconButton that to request permissions if not granted:
StreamBuilder(
stream: notifService.isGrantedStream,
builder: (context, snap) {
// snap.data is nullable
if (snap.data != true) {
return IconButton(
icon: const Icon(Icons.notifications_on_outlined),
onPressed: notifService.request,
);
}
return const SizedBox();
},
),- If the Flutter app is running, stop it and run it again. Otherwise, just still run the app with these new changes. Request notification permissions by tapping the notification icon in the app bar.
- Add a new
intent-filterto the MainActivity in theAndroidManifest.xmlhandle app opening from a Bubble or notification. Add the following code before the closing</activity>tag in theAndroidManifest.xmlfile:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="bubbles_in_flutter.example.com"
android:pathPattern="/chat/*"
android:scheme="https" />
</intent-filter>-
Create a new file called
BubbleActivity.ktin theandroid/app/src/main/kotlin/com/example/bubbles_in_flutterdirectory alongside MainActivity.kt. -
Add the following code to the
BubbleActivity.ktfile:
package com.example.bubbles_in_flutter
import io.flutter.embedding.android.FlutterActivity
class BubbleActivity: FlutterActivity()- Add the BubbleActivity to the
AndroidManifest.xmlwith the "embeddable" and "resizeable" attributes required for Bubbles. Also add theintent-filterfor getting the Bubble intent from the BubbleActivity at the same time. Paste the following code after the closing</activity>tag in theAndroidManifest.xmlfile:
<activity
android:name=".BubbleActivity"
android:exported="true"
android:theme="@style/LaunchTheme"
android:documentLaunchMode="always"
android:allowEmbedded="true"
android:resizeableActivity="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="bubbles_in_flutter.example.com"
android:pathPattern="/chat/*"
android:scheme="https" />
</intent-filter>
</activity>- Install the conversation_bubbles package by adding it as a dependency (with git) in the
dependenciessection of thepubspec.yamlfile:
conversation_bubbles:
git:
url: https://github.com/keepdeploying/conversation_bubbles-
Run
flutter pub getto get the new dependency. -
Create a new file called
bubbles_service.dartin thelib/servicesdirectory. -
Add the following code to the
bubbles_service.dartfile:
import 'package:conversation_bubbles/conversation_bubbles.dart';
import 'package:bubbles_in_flutter/models/contact.dart';
import 'package:flutter/services.dart';
class BubblesService {
final _conversationBubblesPlugin = ConversationBubbles();
static final instance = BubblesService._();
BubblesService._();
Future<void> init() async {
_conversationBubblesPlugin.init(
appIcon: '@mipmap/ic_launcher',
fqBubbleActivity:
'com.example.bubbles_in_flutter.BubbleActivity',
);
}
Future<void> show(
Contact contact,
String messageText, {
bool shouldAutoExpand = false,
}) async {
final Contact(:id, :name) = contact;
final bytesData = await rootBundle.load('assets/$name.jpg');
final iconBytes = bytesData.buffer.asUint8List();
await _conversationBubblesPlugin.show(
notificationId: id,
body: messageText,
contentUri:
'https://bubbles_in_flutter.example.com/chat/$id',
channel: const NotificationChannel(
id: 'chat', name: 'Chat', description: 'Chat'),
person: Person(id: '$id', name: name, icon: iconBytes),
isFromUser: shouldAutoExpand,
shouldMinimize: shouldAutoExpand,
);
}
}- In
lib/main.dart, import thebubbles_service.dartfile:
import 'package:bubbles_in_flutter/services/bubbles_service.dart';- Initialize the
BubblesServicealongside the existingChatsServicein themainfunction:
await BubblesService.instance.init();- In the
lib/services/chats_service.dartfile, import thebubbles_service.dartfile:
import 'package:bubbles_in_flutter/services/bubbles_service.dart';- In the
sendmethod ofChatsService, add the following code to show a Bubble with the created reply message, after the reply has been saved to the local database:
await BubblesService.instance.show(contact, reply.text);- In the
lib/screens/chat_screen.dartfile, import thebubbles_service.dartfile:
import 'package:bubbles_in_flutter/services/bubbles_service.dart';- In the
_ChatScreenStateclass, declare a reference to theBubblesServiceinstance alongside the existingchatsvariable:
final bubbles = BubblesService.instance;- Add an "Open In New" IconButton in the
actionslist of the AppBar to bubble the chat in focus.
actions: [
IconButton(
icon: const Icon(Icons.open_in_new),
onPressed: () =>
bubbles.show(widget.contact, '', shouldAutoExpand: true),
),
],- If the Flutter app is running, stop it and run it again. Otherwise, just still run the app with these new changes. Send a message to any animal and minimize the app. See the notification show and expand the bubble. Also, go back to the Chat Screen when in the full app, tap the "Open In New" button and see how it expands the bubble.
Our Bubbles now show but they always open to the HomeScreen with all the animals listed. We need to make the Bubble open to the ChatScreen of the animal it was sent to.
To achieve that, we have to obtain the intentUri from the package and use it to navigate to the appropriate ChatScreen.
- In ChatsService, declare a private nullable Contact that could have been obtained from app launch. Also, add a getter to get the launch contact:
Contact? _launchContact;
Contact? get launchContact => _launchContact;- Import the
conversation_bubblespackage in thechats_service.dartfile:
import 'package:conversation_bubbles/conversation_bubbles.dart';- In the
initmethod of the ChatsService, after initializing the local database and setting up contacts, get the intentUri from the package and set the launch contact if it is not null:
final intentUri = await ConversationBubbles().getIntentUri();
if (intentUri != null) {
final uri = Uri.tryParse(intentUri);
if (uri != null) {
final id = int.tryParse(uri.pathSegments.last);
if (id != null) {
_launchContact = await ChatsService.instance.getContact(id);
}
}
}- In the build method of the MainApp widget in
lib/main.dartfile, declare a reference to the ChatsService instance before the top-level return statement:
final chats = ChatsService.instance;- In MaterialApp, set the onGenerateInitialRoutes property to a list that contains the ChatScreen of the launch contact if the contact is not null. We first put the HomeScreen to be sure that back button presses will work in the full app (that's if the app was opened from a notification and not a bubble).
onGenerateInitialRoutes: (_) {
return [
MaterialPageRoute(builder: (_) => const HomeScreen()),
if (chats.launchContact != null)
MaterialPageRoute(
builder: (_) => ChatScreen(contact: chats.launchContact!),
),
];
},- If the Flutter app is running, stop it and run it again. Otherwise, just still run the app with these new changes. Send a message to any animal and minimize the app. Tap the notification to open the app from the Bubble. The app should open to the ChatScreen of the animal the message was sent to.
We need to know if the app is running in a Bubble or not. This is important for the app to know if it should modify its UI based on the Bubble view or not.
- In the
bubbles_service.dartfile, declare a private bool to keep track of whether the app is in a Bubble or not. Also, add a getter to expose the value:
bool _isInBubble = false;
bool get isInBubble => _isInBubble;- In the
initmethod of theBubblesServiceclass, after initializing the package, set the_isInBubblevalue from the package's getter:
_isInBubble = await _conversationBubblesPlugin.isInBubble();- In the build method of the MainApp widget in
lib/main.dartfile, declare a reference to the BubblesService instance before the top-level return statement, alongside the already declared ChatsService instance:
final bubbles = BubblesService.instance;- In the
onGenerateInitialRoutesproperty of the MaterialApp widget, put the HomeScreen first only if the app is not in a Bubble. This is to be sure that the user can't navigate backwards to the HomeScreen from the ChatScreen when the app is in a Bubble.
Add a negative if condition to the HomeScreen route to load HomeScreen if the app is not in a bubble:
if (!bubbles.isInBubble)
MaterialPageRoute(builder: (_) => const HomeScreen()),- In the
actionslist of the AppBar widget in the ChatScreen, add a condition to show the "Open In New" icon only if the app is not in a Bubble. This prevents the user from opening a Bubble from a Bubble:
if (!bubbles.isInBubble)
IconButton(
icon: const Icon(Icons.open_in_new),
onPressed: () =>
bubbles.show(widget.contact, '', shouldAutoExpand: true),
),- If the Flutter app is running, stop it and run it again. Otherwise, just still run the app with these new changes. Send a message to any animal and minimize the app. Tap the notification to open the app from the Bubble. The app should open to the ChatScreen of the animal the message was sent to. Try to navigate back to the HomeScreen from the ChatScreen. The bubble should simply close. Also notice that the AppBar back button and the "Open In New" icon for showing a bubble in the AppBar are not shown in the Bubble view.
Click here to learn more from the Google Slides used for this workshop