Flutter FCM: OnBackgroundMessage MissingPluginException
Solusi dari error MissingPluginException di FCM OnBackgroundMessage
Pengenalan
Ditulisan ini saya akan mencoba membahas lebih dalam mengenai FCM di Flutter. Salah satu hal yang ingin saya bahas ialah dibagian OnBackgroundMessage. Seperti yang kita ketahui bahwa OnBackgroundMessage adalah fungsi yang dijalankan pertama kali ketika FCM masuk dengan kondisi app dalam keadaan mati total atau terminated. Didalam OnBackgroundMessage terkadang kita bukan cuma hanya ingin menampilkan notifikasi saja melainkan kita juga ada menambahkan beberapa proses business logic didalamnya seperti kita mau menampilkan notifikasi jika si pengguna dalam keadaan sudah login atau mungkin kita mau menjalankan service tertentu. Dari 2 contoh tersebut sebenarnya bisa kita implementasikan namun, pada kenyatannya kita akan mengalami kendala dan salah satu kendala yang akan kita alami ialah kita akan mendapatkan pesan error “MissingPluginException …”. Penasaran mengapa kita bisa mendapatkan pesan error tersebut dan bagaimana solusinya mari kita bahas secara mendalam pada tulisan ini.
Persiapan
Ditulisan ini saya sudah menganggap bahwa kita semua sudah selesai melakukan setup FCM-nya di Flutter dan ready to use. Bagi kamu yang belum tahu cara setup dan menggunakannya di Flutter silakan baca tulisan saya berikut ini ya.
Contoh Projek
Pada tulisan ini saya akan memberikan contoh sederhana untuk menampilkan lokal notifikasi jika si pengguna dalam keadaan sudah login. Secara logika menurut saya hal ini sangatlah mudah. Berikut adalah gambaran umum proses logikanya.
- Pengguna melakukan login
- Simpan status login si pengguna didalam SharedPreferences atau sejenisnya.
- FCM dikirim dari Backend dan diterima oleh app
- App melakukan pengecekan apakah si pengguna sudah login
- Jika iya, maka tampilkan notifikasi
- Jika tidak, jangan tampilkan notifikasi
Sangat sederhana bukan? Untuk pengaturan pubspec.yaml-nya saya anggap kamu semua sudah hafal ya cara-caranya. Cukup tambahkan saja plugin shared_preferences dan flutter_local_notifications ke pubspec.yaml. Untuk penggunaan plugin flutter_local_notifications saya tidak akan bahas secara detail tentang penggunaannya disini tapi, ini akan saya bahas lebih detail ditulisan yang akan datang.
Buat Fitur Login
Langkah pertama, mari kita buat fitur login-nya. Untuk fitur login-nya saya tidak akan menggunakan API cukup pakai pengkondisian secara hardcode saja ya. berikut adalah kode untuk halaman login. Buka file main.dart dan ubah menjadi seperti berikut.
void main() => runApp(App());
class App extends StatefulWidget {
@override
_AppState createState() => _AppState();
}
class _AppState extends State<App> {
var isLogin = false;
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
var sharedPreferences = await SharedPreferences.getInstance();
isLogin = sharedPreferences.getBool('isLogin') ?? false;
if (isLogin) {
setState(() {});
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: !isLogin ? LoginPage() : HomePage(),
);
}
}
Future<dynamic> onBackgroundMessageHandler(Map<String, dynamic> message) async {
debugPrint('onBackgroundMessageHandler');
var sharedPreferences = await SharedPreferences.getInstance();
var isLogin = sharedPreferences.getBool('isLogin') ?? false;
if (isLogin) {
var title = '-';
var content = '-';
if (Platform.isIOS) {
title = message['title'];
content = message['content'];
} else {
title = message['data']['title'];
content = message['data']['content'];
}
_showLocalNotification(title, content);
}
return true;
}
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final scaffoldState = GlobalKey<ScaffoldState>();
final formState = GlobalKey<FormState>();
final controllerEmail = TextEditingController();
final controllerPassword = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldState,
body: SafeArea(
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 24),
child: Form(
key: formState,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Welcome Back',
style: Theme.of(context).textTheme.headline6,
),
Text(
'Sign to continue',
style: Theme.of(context).textTheme.caption,
),
SizedBox(height: 42),
TextFormField(
controller: controllerEmail,
decoration: InputDecoration(
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
isDense: true,
labelText: 'EMAIL',
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
return value.isEmpty ? 'Enter an email' : null;
},
),
SizedBox(height: 16),
TextFormField(
controller: controllerPassword,
decoration: InputDecoration(
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
isDense: true,
labelText: 'PASSWORD',
),
validator: (value) {
return value.isEmpty ? 'Enter a password' : null;
},
obscureText: true,
keyboardType: TextInputType.text,
),
SizedBox(height: 16),
SizedBox(
width: double.infinity,
height: 42,
child: RaisedButton(
onPressed: () async {
if (formState.currentState.validate()) {
var email = controllerEmail.text.trim();
var password = controllerPassword.text.trim();
if (email == 'admin' && password == 'admin') {
var sharedPreferences = await SharedPreferences.getInstance();
await sharedPreferences.setBool('isLogin', true);
Navigator.push(context, MaterialPageRoute(builder: (context) => HomePage()));
} else {
scaffoldState.currentState.showSnackBar(SnackBar(content: Text('Login failed')));
}
}
},
child: Text('LOGIN'),
color: Colors.blue,
textColor: Colors.white,
),
),
],
),
),
),
),
);
}
}
Catatan: jika masih ada error pada kode diatas mohon diabaikan saja dan lanjutkan ke langkah berikutnya.
Buat Halaman Home dan Lokal Notifikasi
Selanjutnya, kita buat halaman home dan lokal notifikasi. Kita buka kembali file main.dart dan tambahkan kodenya menjadi seperti berikut.
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final firebaseMessaging = FirebaseMessaging();
var tokenFcm = '-';
@override
void initState() {
firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) async {
_getDataFcm(message);
},
onBackgroundMessage: onBackgroundMessageHandler,
onResume: (Map<String, dynamic> message) async {
_getDataFcm(message);
},
onLaunch: (Map<String, dynamic> message) async {
_getDataFcm(message);
},
);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RaisedButton(
child: Text('GET TOKEN'),
onPressed: () {
firebaseMessaging.requestNotificationPermissions(
const IosNotificationSettings(),
);
firebaseMessaging.onIosSettingsRegistered.listen((event) {
debugPrint('IOS settings registered');
});
firebaseMessaging.getToken().then((value) => setState(() {
tokenFcm = value;
debugPrint('tokenFcm: $tokenFcm');
}));
},
),
SizedBox(width: 16),
RaisedButton(
child: Text('LOGOUT'),
onPressed: () async {
var sharedPreferences = await SharedPreferences.getInstance();
sharedPreferences.remove('isLogin');
Navigator.push(context, MaterialPageRoute(builder: (context) => LoginPage()));
},
),
],
),
Text(tokenFcm),
],
),
),
);
}
}
void _getDataFcm(Map<String, dynamic> message) {
var title = '-';
var content = '-';
if (Platform.isIOS) {
title = message['title'];
content = message['content'];
} else {
title = message['data']['title'];
content = message['data']['content'];
}
_showLocalNotification(title, content);
}
Buat Lokal Notifikasi
Sekarang kita akan membuat lokal notifikasinya. Pertama-tama, kita perlu menyalin file ic_launcher.png yang berada didalam direktori mipmap-xxxhdpi kedalam direktori drawable. Hal tersebut bertujuan agar file tersebut menjadi icon pada saat lokal notifikasi tampil. Kemudian, kita buka kembali file main.dart dan tambahkan fungsi berikut.
void _showLocalNotification(String title, String content) {
var initializationSettingsAndroid = AndroidInitializationSettings('ic_launcher');
var initializationSettingsIOS = IOSInitializationSettings();
var initializationSettings = InitializationSettings(
initializationSettingsAndroid,
initializationSettingsIOS,
);
var flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
flutterLocalNotificationsPlugin.initialize(initializationSettings);
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'test channel id',
'test channel name',
'test channel description',
importance: Importance.Max,
priority: Priority.High,
);
var iosPlatformChannelSpecifics = IOSNotificationDetails();
var platformChannelSpecifics = NotificationDetails(androidPlatformChannelSpecifics, iosPlatformChannelSpecifics);
flutterLocalNotificationsPlugin.show(1, title, content, platformChannelSpecifics);
}
Testing
Sekarang kita test apakah lokal notifikasinya sudah muncul.
Bisa kamu lihat pada video diatas bahwa ketika app-nya terminated kemudian, kita kirim FCM-nya maka lokal notifikasinya tidak tampil dan muncul pesan error kurang lebih seperti berikut.
2020-08-27 23:42:10.567 26317-26353/com.ysn.flutterfcmlogin I/flutter: onBackgroundMessageHandler
2020-08-27 23:42:10.602 26317-26353/com.ysn.flutterfcmlogin I/flutter: Unable to handle incoming background message.
2020-08-27 23:42:10.602 26317-26353/com.ysn.flutterfcmlogin I/flutter: MissingPluginException(No implementation found for method getAll on channel plugins.flutter.io/shared_preferences)
Analisa Masalah dan Solusi
Untuk penyebabnya saya belum tahu pasti tapi, menurut saya hal ini bisa terjadi dikarenakan plugin-plugin yang kita pakai didalam fungsi OnBackgroundMessage belum ter-register. Mengapa belum ter-register? Karena, OnBackgroundMessage berjalan secara background mode dan fungsi ini dijalankan pertama kali (ketika kondisi app-nya terminated). Dan proses register plugin-plugin tersebut melalui file main.dart atau fungsi void main()
. Jadi, solusinya gimana? Solusinya ialah kita perlu mendaftarkan plugin yang kita pakai didalam OnBackgroundMessage kedalam file Application.kt (Application level). Ingat, yang kita daftarkan disini ialah plugin yang benar-benar kita pakai didalam OnBackgroundMessage. Yang tidak terpakai nggak perlu didaftarkan. Sekarang buka file Application.kt ubah menjadi seperti berikut.
import com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin
import io.flutter.app.FlutterApplication
import io.flutter.plugin.common.PluginRegistry
import io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin
import io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService
import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin
class Application : FlutterApplication(), PluginRegistry.PluginRegistrantCallback {
override fun onCreate() {
super.onCreate()
FlutterFirebaseMessagingService.setPluginRegistrant(this)
}
override fun registerWith(registry: PluginRegistry) {
FirebaseMessagingPlugin.registerWith(registry.registrarFor("io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin"))
SharedPreferencesPlugin.registerWith(registry.registrarFor("io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin"))
FlutterLocalNotificationsPlugin.registerWith(registry.registrarFor("com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin"))
}
}
Untuk mengetahui dari mana nilai io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin
kita bisa melihatnya dari struktur projeknya. Berhubung pada contoh diatas kita hanya menggunakan plugin shared_preferences dan flutter_local_notifications didalam OnBackgroundMessage maka, hanya plugin tersebut yang kita daftarkan. Saran saya untuk melakukan pengeditan tersebut dilakukan di Android Studio ya biar lebih gampang.
Sekarang mari kita test lagi. Seharusnya sudah bisa ya.
Kesimpulan
Jadi, kesimpulannya ialah kita telah berhasil melakukan custom notifikasi agar hanya muncul jika si pengguna dalam keadaan sudah login. Untuk source code lengkap dari contoh projek ditulisan ini bisa kamu lihat di Github ya.