Skip to content

Analytics Tools for Kotlin Multiplatform Mobile iOS and Android

License

Notifications You must be signed in to change notification settings

line/abc-kmm-analytics-tools

Repository files navigation

abc-kmm-analytics-tools: Analytics Tools for Kotlin Multiplatform Mobile iOS and android

Kotlin KMM AGP Gradle Platform

Index

Features

  • Unified handling of various event trackers is possible
  • Automatically tracking and sending events of screen view and capture
  • Provide screen name mapper
  • Remove boilerplate by binding UI elements to event triggers
  • Common interfaces available in KMM Shared

Example

Requirements

  • iOS
    • Deployment Target 10.0 or higher
  • Android
    • minSdkVersion 23

Installation

Gradle Settings

Add below gradle settings into your KMP (Kotlin Multiplatform Project)

build.gradle.kts in shared

plugins {
    id("com.android.library")
    kotlin("multiplatform")
    kotlin("native.cocoapods")
}

val analyticsTools = "com.linecorp.abc:kmm-analytics-tools:1.1.0"

kotlin {
    android()
    ios {
        binaries
            .filterIsInstance<Framework>()
            .forEach {
                it.baseName = "shared"
                it.transitiveExport = true
                it.export(analyticsTools)
            }
    }

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(analyticsTools)
            }
        }
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test-common"))
                implementation(kotlin("test-annotations-common"))
            }
        }
        val androidMain by getting {
            dependencies {
                implementation(analyticsTools)
                api(analyticsTools)
            }
        }
        val androidTest by getting {
            dependencies {
                implementation(kotlin("test-junit"))
                implementation("junit:junit:4.13.2")
                implementation("androidx.test:core:1.0.0")
                implementation("androidx.test:runner:1.1.0")
                implementation("org.robolectric:robolectric:4.2")
            }
        }
        val iosMain by getting {
            dependencies {
                implementation(analyticsTools)
                api(analyticsTools)
            }
        }
        val iosTest by getting
    }
}

Project Settings

Android

  • You can use right now without other settings.

iOS

  1. Create Podfile with below setting in your project root.
    use_frameworks!
    
    platform :ios, '10.0'
    
    install! 'cocoapods', :deterministic_uuids => false
    
    target 'iosApp' do
        pod 'shared', :path => '../shared/'
    end
  2. Run command pod install on the terminal

Configure

Screen Mapper

Screen mapper is mapping system that to map between screen class and screen name. The file ATScreenNameMapper.json that defines screen mapping table will automatically used if needed in system.

Android

  1. Create a file named ATScreenNameMapper.json in your assets folder like below.

  2. Write table as json format for screen name mapping.

{
  "MainActivity": "main"
}

iOS

  1. Create a file named ATScreenNameMapper.json in your project like below.

  2. Write table as json format for screen name mapping.

{
  "MainViewController": "main"
}

Initialization

Android

ATEventCenter.setConfiguration {
    canTrackScreenView { true }
    canTrackScreenCapture { true }
    register(TSDelegate())
    register(GoogleAnalyticsDelegate())
}

iOS

ATEventCenter.Companion().setConfiguration {
    $0.canTrackScreenView { _ in
        true
    }
    $0.canTrackScreenCapture {
        true
    }
    $0.mapScreenClass {
        $0.className()
    }
    $0.topViewController {
        UIViewController.topMost
    }
    $0.register(delegate: TSDelegate())
    $0.register(delegate: GoogleAnalyticsDelegate())
}

extension UIViewController {
    static var topMost: UIViewController? {
        UIViewControllerUtil.Companion().topMost()
    }

    var className: String {
        let regex = try! NSRegularExpression(pattern: "<.(.*)>", options: .allowCommentsAndWhitespace)
        let str = String(describing: type(of: self))
        let range = NSMakeRange(0, str.count)
        return regex.stringByReplacingMatches(in: str, options: .reportCompletion, range: range, withTemplate: "")
    }
}

Setup User Properties

This function must be called in the scope where the user properties are changed

Android or Shared

ATEventCenter.setUserProperties()

iOS

ATEventCenter.Companion().setUserProperties()

Implementation

Delegate

Delegate class is proxy for various event trackers.

Android or Shared

class GoogleAnalyticsDelegate: ATEventDelegate {
    // @optional to map key of event
    override fun mapEventKey(event: Event): String {
        event.value
    }

    // @optional to map key of parameter
    override fun mapParamKey(container: KeyValueContainer): String {
        container.key
    }

    // @required to set up event tracker
    override fun setup() {
    }

    // @required to set user properties for event tracker
    override fun setUserProperties() {
    }

    // @required to send event to the event tracker
    override fun send(event: String, params: Map<String, String>) {
    }
}

iOS

final class GoogleAnalyticsDelegate: ATEventDelegate {
    // @required to map key of parameter
    func mapEventKey(event: Event) -> String {
        event.value
    }

    // @required to map key of parameter
    func mapParamKey(container: KeyValueContainer) -> String {
        container.key
    }
    
    // @required to set up event tracker
    func setup() {
    }

    // @required to set user properties for event tracker
    func setUserProperties() {
    }
    
    // @required to send event to the event tracker
    func send(event: String, params: [String: String]) {
    }
}

Parameters

Kotlin

sealed class Param: KeyValueContainer {
    data class UserName(override val value: String): Param()
    data class ViewTime(override val value: Int): Param()
}

Swift

enum Param {
    final class UserName: AnyKeyValueContainer<NSString> {}
    final class ViewTime: AnyKeyValueContainer<KotlinInt> {}
}

If you want something more swift, use your own KeyValueContainer.

enum Param {
    final class UserName: MyKeyValueContainer<String> {}
    final class ViewTime: MyKeyValueContainer<Int> {}
}

class MyKeyValueContainer<T: Any>: KeyValueContainer {
    init(_ value: T) {
        self.value = value
    }
    
    let value: Any
    
    var key: String {
        "\(self)".components(separatedBy: ".").last?.snakenized ?? ""
    }
}

ATEventParamProvider

ATEventParamProvider is used to get extra parameters when send to events and functions are called automatically.

Android

class MainActivity : AppCompatActivity(), ATEventParamProvider {
    override fun params(event: Event, source: Any?): List<KeyValueContainer> {
        return when(event) {
            Event.VIEW -> listOf(
                Param.UserName("steve"),
                Param.ViewTime(10000))
            else -> listOf()
        }
    }
}

iOS

extension MainViewController: ATEventParamProvider {
    func params(event: Event, source: Any?) -> [KeyValueContainer] {
        switch event {
        case .view:
            return [
                Param.UserName("steve"), 
                Param.ViewTime(10000)
            ]
        default:
            return []
        }
    }
}

ATEventTriggerUICompatible (iOS Only)

This is an interface to make it easy to connect the click event of a ui element to an event trigger.

extension UIControl: ATEventTriggerUICompatible {
    public func registerTrigger(invoke: @escaping () -> Void) {
        rx.controlEvent(.touchUpInside)
            .bind { invoke() }
            .disposed(by: disposeBag)
    }
}

Integration with UI

Android

Legacy

  • Using with Event Trigger
sealed class Param: KeyValueContainer {
    data class UserName(override val value: String): Param()
}

class MainActivity : AppCompatActivity(), ATEventParamProvider {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val button = findViewById<Button>(R.id.button)
        
        button.eventTrigger
            .click("button")
            .register { listOf(Param.UserName("steve")) }

        button.setOnClickListener { v ->
            v.eventTrigger.send()
        }
    }
}
  • Using Directly
<Button
      android:id="@+id/button"
      android:text="Hello!"
      android:onClick="onClickHello"/>
fun onClickHello(view: View) {
    ATEventCenter.send(
        Event.CLICK,
        listOf(BaseParam.ClickSource("hello")))
}

iOS

SwiftUI

Button("Hello") {
    ATEventCenter.Companion().send(
        event: .click,
        params: [BaseParam.ClickSource(value: "hello")])
}

Legacy

Using with Event Trigger

  • Extension to get instance of ATEventTrigger
extension ATEventTriggerCompatible {
    var eventTrigger: ATEventTrigger<Self> {
        ATEventTriggerFactory.Companion().create(owner: self) as! ATEventTrigger<Self>
    }
}
  • Extension to make it easy to connect the click event of a ui element to an event trigger
extension UIControl: ATEventTriggerUICompatible {
    public func registerTrigger(invoke: @escaping () -> Void) {
        rx.controlEvent(.touchUpInside)
            .bind { invoke() }
            .disposed(by: disposeBag)
    }
}
  • Client side
enum Param {
    final class UserName: AnyKeyValueContainer<NSString> { }
}

override func viewDidLoad() {
    super.viewDidLoad()
    
    button.eventTrigger
        .click(source: "hello")
        .register { [Param.UserName("steve")] }
}

Using Directly

@IBAction func clicked(_ sender: UIButton) {
    ATEventCenter.Companion().send(
        event: .click,
        params: [BaseParam.ClickSource(value: "hello")])
}

Advanced

Dynamic Screen Name Mapping

You can dynamically determine the screen name by implementing ATDynamicScreenNameMappable.

Android

class MainActivity : AppCompatActivity(), ATDynamicScreenNameMappable {
    
    override fun mapScreenName(): String? {
        return "ScreenNameAsYouWant"
    }
}

iOS

extension MainViewController: ATDynamicScreenNameMappable {
    
    func mapScreenName() -> String? {
        "ScreenNameAsYouWant"
    }
}

Send with Screen Parameters Manually

Android

ATEventCenter.send(Event.CLICK, listOf(
    BaseParam.ScreenClass("screenClass"),
    BaseParam.ScreenName("screenName"),
))

iOS

ATEventCenter.Companion().send(event: .click, params: [
    BaseParam.ScreenClass(value: "screenClass"),
    BaseParam.ScreenName(value: "screenName"),
])

TODO

  • Publish to maven repository
  • Write install guide
  • Integration UI with event trigger for iOS
    • LegacyUI
    • SwiftUI
  • Integration UI with event trigger for Android
    • Legacy
      • View, Any
    • Compose