Effective application analytic

Michael Spitsin
5 min readSep 23, 2017

--

With GAnalytics v1.1

Couple of months ago I’ve written about concept of constructing api layout for analytics in your application, which will solve next problems:

  • Provide strict relation between categories, actions, labels and values. Thus, the probability of sending inconsistent analytics will be reduced to minimum
  • Provide api layer between usage of analytics and sending it. Thus, we will able to write tests for it, and check does our analytics api correct or not. Also we will have all analytics in one place.
  • Provide tiny api layer. Thus, the working time and object creation excess will be minimal. Also, we will be able to create analytics double-quick.
  • Provide OOP way to work with analytics. Thus, we will able to mock analytics in test classes, and write appropriate tests with not disturbing analytics system.

One month ago I’ve released the first version of GAnalytics. This is a special library that solves all aforementioned problems. Also, as a bonus it is written on Kotlin. I’ve not written the article about release feature, since almost all of them I’ve described in one of my previous articles. Now it is time to introduce GAnalytics v.1.1. Hooray!

Actions with single label

As it appeared by using GAnalytics in real project, there are cases, when you have some event in analytics with category, action and only one label. So in that case you will need to provide a way to describe that label through arguments of action method. It will look something like:

interface CategoryInterface {
fun action(label: Label)
enum class Label { MY_LABEL_NAME }
}

And its usage will look: categoryInterface.action(CategoryInterface.Label.MY_LABEL_NAME)

Pretty verbose, hah? Moreover, to provide strongly only one label you need to define whole enum class with only one enumeration, which is not so good in terms of Android application. Especially when you have many analytics events with single label.

I’m not telling that having such events is generally a good idea, but when we already have such events, we need to find a way to nicely handle them.

So in version 1.1 more subtle solution came out. It is a @LabelFun annotation. It is used to tell GAnalytics that our method is a label and annotation will provide an action for it. Our first example will be rewritten to:

interface CategoryInterface {
@LabelFun(“action”) fun myLabelName()
}

And its usage will look: categoryInterface.label()

Main advantage of that approach is use in connection with standard way (when methods are actions). Suppose we have an pack of events with category = prices, action = show and labels = low_prices, medium_prices, high_prices or all_prices, but in the code we have an enum PriceCategory { LOW, MEDIUM, HIGH }. So our enum does not have only one variant (all_prices). But still we able to write nice api for analytics by using new annotation:

interface CategoryInterface {
fun show(label: PriceCategory)
@LabelFun(“show”, “all_prices”) showAll()
}

Moreover that approach helps us to refuse enums in some ways, when they needed only for analytics, thus we reduce number of objects. For example, aforementioned interface could be rewritten to:


interface CategoryInterface {
@LabelFun(“show”, “low_prices”) fun showLow()
@LabelFun(“show”, “medium_prices”) fun showMedium()
@LabelFun(“show”, “high_prices”) fun showHigh()
@LabelFun(“show”, “all_prices”) fun showAll()
}

Postfixes

In GAnalytics v1.0 prefixes were added. Now it is time to introduce postfixes. They looks very similar to prefixes except that they do not use some hidden logic for the case when you use single annotation without parameters. Moreover, that code will not compile:

//example of using @HasPrefix without arguments
interface CategoryInterface {
@HasPrefix fun action()
}
//will produce action = "categoryinterfaceaction"

but:

//example of using @HasPostfix without arguments
interface CategoryInterface {
@HasPostfix fun action()
}
will not compile, since @HasPostfix require to define a name of postfix

@HasPostfix adds possibility to specify some postfix for method, interface or group of interfaces. For example, if we have category interface AnalyticsRange and actions “fromOd” and “toOd”, which accept ints, then with GAnalytics v1.0 the solution will look like:

interface AnalyticsRange {
fun fromOd(value: Int)
fun toOd(value: Int)
}

In GAnalytics v1.0 the solution can be adjusted to:

@HasPostfix("Od")
interface AnalyticsRange {
fun from(value: Int)
fun to(value: Int)
}

Thus, we have more subtle names for methods.

Support of properties in Group Interfaces

If earlier we were able to write only methods without parameters in group interfaces, then from version 1.1 the support of val properties was added. Now you can declare category interfaces in group interface not in fun but also in val.

Version 1.0:

interface GroupInterface {
fun category(): AnalyticsCategory
fun otherCategory(): AnalyticsOtherCategory
}
interface AnalyticsCategory {
fun action()
}
interface AnalyticsOtherCategory {
fun action()
}
//invocation part
groupInterface.category().action()
groupInterface.otherCategory().action()

In Version 1.1 we can rewrite it to:

interface GroupInterface {
val category: AnalyticsCategory
val otherCategory: AnalyticsOtherCategory
}
interface AnalyticsCategory {
fun action()
}
interface AnalyticsOtherCategory {
fun action()
}
//invocation part
groupInterface.category.action()
groupInterface.otherCategory.action()

Thus we able to slightly improve invocation appearance in code.

Optimized memory consuming

Previously, when you had group interface with category interfaces in your project and you perform analytics sending from them (like analytics.seller().sell("ps4")), then each time you called method from group interface, new special wrapper for category interfaces was created. Moreover, each time new proxy object for category interface was created, which is not so good, especially in terms of memory consuming in android apps.

So from v1.1 this problem is solved by adding special caches for AnalyticsGroupWrapper. Now if you has, for example:

interface Analytics {
val seller: AnalyticsSeller
val customer: AnalyticsCustomer
}
interface AnalyticsSeller {
fun sell(what: String)
}
interface AnalyticsCustomer {
fun buy(what: String)
}

And you call in different places of you application:

analytics.seller.sell("something")
analytics.customer.buy("something")

several times, then for each calling seller or customer only one category interface proxy wrapper will be created. Moreover, there will be not two wrappers in total, but only one, since each of categories does not have special annotations like @Convention, @HasPrefix and e.t.c. If one of them had such annotations, then two wrappers was created: one for each category interface.

That’s all?

Almost yes. :) One small thing is supporting of hashCode, toString and equals. I understand that I probably did it a little bit hacky, but for now, if you will use debugging tools, you will see nicely explanation for Analytics interface instances (since toString is provided). Also, you be able to compare analytic interfaces, if you for some reason need it.

Afterwords

Now, that’s all. Enjoy the new version of GAnalytics and if you have any questions/proposals or remarks, please, fill free to share it with me.

--

--

Michael Spitsin
Michael Spitsin

Written by Michael Spitsin

Love being creative to solve some problems with an simple and elegant ways

No responses yet