Blog Infos
Author
Published
Topics
, , ,
Published
Image is taken from the Internet

 

This article will continue the story about the multi-platform implementation of the in-app review. Here’s the beginning of it. After I created an implementation in KMP it was interesting for me — if it is easy to create a Flutter plugin that uses this implementation? Spoiler — really easy.

I think this article will be useful for the companies that have both Flutter and KMP teams since it allows to use the same code base (write KMP library and then wrap it with the Flutter plugin) instead of writing the plugin in pure native.

I won’t describe how to create a Flutter plugin from scratch since there’s a good documentation about it, so let’s dive into the most interesting part — integration of the KMP library.

Let’s start with the Android part. KMP module is compiled into AAR artefact for the Android. You can add it directly into the project (into libs folder), but since my artefact was already published into GitHub maven I decided to add it as the remote dependency.

For this purpose I added a repository into the android/build.gradle file of the plugin. Here’s the way how it looks like

allprojects {
repositories {
maven {
url = uri("https://maven.pkg.github.com/SergeiMikhailovskii/kmp-app-review")
credentials {
username = System.getenv("GITHUB_USER")
password = System.getenv("GITHUB_API_KEY")
}
}
}
}
view raw build.gradle hosted with ❤ by GitHub

So, as you can see, it’s just the default declaration of the repository. Of course, you can use jcenter and so on but GitHub maven was enough for me in this case. And then we add the dependency

dependencies {
implementation "com.mikhailovskii.kmp:in-app-review-kmp:1.0.16"
}
view raw build.gradle hosted with ❤ by GitHub

After the sync we are ready to use the library classes inside the android part of the plugin, so let’s do it.

package com.mikhailovskii.in_app_review
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.lifecycleScope
import com.mikhailovskii.inappreview.appGallery.AppGalleryInAppReviewInitParams
import com.mikhailovskii.inappreview.appGallery.AppGalleryInAppReviewManager
import com.mikhailovskii.inappreview.googlePlay.GooglePlayInAppReviewInitParams
import com.mikhailovskii.inappreview.googlePlay.GooglePlayInAppReviewManager
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import kotlinx.coroutines.launch
class InAppReviewPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
private lateinit var channel: MethodChannel
private var activity: FlutterFragmentActivity? = null
private val googlePlayInAppReviewManager by lazy {
GooglePlayInAppReviewManager(GooglePlayInAppReviewInitParams(requireNotNull(activity)))
}
private val appGalleryInAppReviewManager by lazy {
AppGalleryInAppReviewManager(AppGalleryInAppReviewInitParams(requireNotNull(activity)))
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "in_app_review")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"launchInAppReview" -> launchInAppReview(call, result)
"launchInMarketReview" -> launchInMarketReview(call, result)
else -> {
result.notImplemented()
}
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity as FlutterFragmentActivity
(binding.lifecycle as HiddenLifecycleReference)
.lifecycle
.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_CREATE) {
appGalleryInAppReviewManager.init()
}
})
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity as FlutterFragmentActivity
}
override fun onDetachedFromActivity() {
activity = null
}
private fun launchInAppReview(call: MethodCall, result: Result) {
val params = call.argument<Map<String, String>>("android").orEmpty()
val market = params["market"]
if (market == "googlePlay") {
launchGooglePlayInAppReview(result)
} else if (market == "appGallery") {
launchAppGalleryInAppReview(result)
}
}
private fun launchInMarketReview(call: MethodCall, result: Result) {
val params = call.argument<Map<String, String>>("android").orEmpty()
val market = params["market"]
if (market == "googlePlay") {
launchGooglePlayInMarketReview(result)
} else if (market == "appGallery") {
launchAppGalleryInMarketReview(result)
}
}
private fun launchGooglePlayInAppReview(result: Result) {
val activity = requireNotNull(activity)
activity.lifecycleScope.launch {
googlePlayInAppReviewManager.requestInAppReview().collect {
result.success(it.name)
}
}
}
private fun launchAppGalleryInAppReview(result: Result) {
val activity = requireNotNull(activity)
activity.lifecycleScope.launch {
appGalleryInAppReviewManager.requestInAppReview()
.collect {
result.success(it.name)
}
}
}
private fun launchGooglePlayInMarketReview(result: Result) {
val activity = requireNotNull(activity)
activity.lifecycleScope.launch {
googlePlayInAppReviewManager.requestInMarketReview().collect {
result.success(it.name)
}
}
}
private fun launchAppGalleryInMarketReview(result: Result) {
val activity = requireNotNull(activity)
activity.lifecycleScope.launch {
appGalleryInAppReviewManager.requestInMarketReview()
.collect {
result.success(it.name)
}
}
}
}

Now let’s discuss some interesting moments from this part of the plugin.

  1. As you can remember — both GooglePlayInAppReviewManager and AppGalleryInAppReviewManager require an instance of ComponentActivity to work, so I had to implement the ActivityAware interface to receive and store the instance of the activity. Note — by default the Flutter application inherits the MainActivity from FlutterActivity. But this variant is not suitable for my case since FlutterActivity doesn’t extend the ComponentActivity, so I had to change it to the FlutterFragmentActivity.
  2. To make the AppGallery’s implementation work well I had to call it inside the ON_CREATE state of the lifecycle. For this purpose I added a lifecycle observer (lines 57–60).
  3. To pass the result back to the Flutter part I invoke the result.success with the ReviewCode enum’s name after the event from the flow is received.

Everything other is pretty clear I think if you already have an experience in creating the Flutter plugin.

Let’s move on to the iOS part. KMP allows you to build an XCFramework. For this purpose you need to add the following lines into the build.gradle file of the KMP module:

import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework
plugins {
kotlin("multiplatform")
}
kotlin {
val xcf = XCFramework()
val iosTargets = listOf(iosX64(), iosArm64(), iosSimulatorArm64())
iosTargets.forEach {
it.binaries.framework {
baseName = "kmp-in-app-review"
xcf.add(this)
}
}
}

When you declare XCFrameworks, Kotlin Gradle plugin will register three Gradle tasks:

assembleXCFramework
assembleDebugXCFramework
assembleReleaseXCFramework

Here you can find more info about it.

Unlike the Android, for iOS I decided not to publish the artefact but to add it directly to the iOS part, so I just added the xcframework into the iOS folder. But it’s not the end — to resolve the framework classes inside the iOS part of the plugin I also had to mention the framework inside the podspec file:

s.vendored_frameworks = 'inAppReviewKMP.xcframework'

After the setup is finished we are ready to implement the iOS part:

import Flutter
import UIKit
import inAppReviewKMP
public class InAppReviewPlugin: NSObject, FlutterPlugin {
private var reviewManager: InAppReviewDelegate? = nil
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "in_app_review", binaryMessenger: registrar.messenger())
let instance = InAppReviewPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "launchInAppReview":
let params = (call.arguments as! [String: Any])["ios"] as! [String: Any]
let appStoreId = params["appStoreId"] as! String
reviewManager = AppStoreInAppReviewManager(params: AppStoreInAppReviewInitParams(appStoreId: appStoreId))
reviewManager?.requestInAppReview().collect(collector: InAppReviewCollector(result: result)) {_ in }
case "launchInMarketReview":
let params = (call.arguments as! [String: Any])["ios"] as! [String: Any]
let appStoreId = params["appStoreId"] as! String
reviewManager = AppStoreInAppReviewManager(params: AppStoreInAppReviewInitParams(appStoreId: appStoreId))
reviewManager?.requestInMarketReview().collect(collector: InAppReviewCollector(result: result)) {_ in }
default:
result(FlutterMethodNotImplemented)
}
}
}
private class InAppReviewCollector : Kotlinx_coroutines_coreFlowCollector {
let result: FlutterResult
init(result: @escaping FlutterResult) {
self.result = result
}
func emit(value: Any?, completionHandler: @escaping (Error?) -> Void) {
if let reviewCode = value as? ReviewCode {
result(reviewCode.name)
}
}
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

There’s not so much interesting inside, but I just want to highlight how to implement the flow collecting default approach — class InAppReviewCollector is responsible for it. As an alternative, you can use Swift Concurrency or this library.

Finally, we can see how it looks inside the Flutter part

class MethodChannelInAppReview extends InAppReviewPlatform {
@visibleForTesting
final methodChannel = const MethodChannel('in_app_review');
@override
Future<String?> launchInAppReview({required InAppReviewParams params}) async {
final status = await methodChannel.invokeMethod<String>(
'launchInAppReview',
params.toMap(),
);
return status;
}
@override
Future<String?> launchInMarketReview({
required InAppReviewParams params,
}) async {
final status = await methodChannel.invokeMethod<String>(
'launchInMarketReview',
params.toMap(),
);
return status;
}
}

Here’s the full project implementation

So, let’s summarise all the article — to use the KMP library inside the Flutter plugin, we need to build the AAR artefact for the Android part (add it from the remote repo or directly inside the project) and build a XCFramework/use pod for the iOS part (don’t forget to add it inside the podspec).

Thank you for reading! Feel free to ask questions and leave the feedback in comments or Linkedin.

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Collections are a set of interfaces and classes that implement highly optimised data structures.…
READ MORE
blog
With Compose Multiplatform 1.6, Jetbrains finally provides an official solution to declare string resources…
READ MORE
blog
A Journey Through Advanced and Lesser Utilized Modifiers in Jetpack Compose
READ MORE
blog
There are already so many app-level architecture and presentation layer patterns (MVC/MVP/MVVM/MVI/MVwhatever) that exist…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu