Skip to content

Commit

Permalink
feat: support for custom serializer (#179)
Browse files Browse the repository at this point in the history
* dev: support custom serializer

* chores: revert minSdk to 21

* chores: fix version to 2.0.0

* refacto: Making change as non-breaking
chores: downgrade version to 1.8.0 as change is no longer breaking + adding readme

* chores: adding notice to implement kotlinx for default EmojiInitializer

* chores: spotlessApply

* chores: Adding example for Jackson and Gson + revert version.properties

* chores: rename AEmojiInitializer to AbstractEmojiInitializer

* doc: Doc for KotlinxDeserializer.kt

---------

Co-authored-by: Yoobi <>
  • Loading branch information
yoobi committed Feb 22, 2024
1 parent 9d5d043 commit 4b06463
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 63 deletions.
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ dependencies {
Don't know how to do that?? Take a look at
the [application class example](./app/src/main/java/io/wax911/emojifysample/App.kt)

**In order for EmojiInitilizer to work you need to add in your build.gradle `implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2"`

```kotlin
class App : Application() {

Expand All @@ -193,6 +195,60 @@ class App : Application() {
}
```

### Optional - Custom Serializer

`EmojiInitializer` is using kotlinx.serialization as default serializer. If you wish to use another serializer

#### Step 1. Create Custom EmojiInitializer and implement IEmojiDeserializer (moshi for this example)
```kotlin
class CustomEmojiInitializer: AbstractEmojiInitializer() {
class MoshiDeserializer: IEmojiDeserializer {
private val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()

override fun decodeFromStream(inputStream: InputStream): List<Emoji> {
val myType = Types.newParameterizedType(List::class.java, Emoji::class.java)
return moshi.adapter<List<Emoji>>(myType).fromJson(inputStream.source().buffer()) ?: listOf()
}
}

override val serializer: IEmojiDeserializer = MoshiDeserializer()
}
```

#### Step 2. Modify your application class
```kotlin
class App : Application() {

/**
* Application scope bound emojiManager, you could keep a reference to this object in a
* dependency injector framework like as a singleton in `Hilt`, `Dagger` or `Koin`
*/
internal val emojiManager: EmojiManager by lazy {
// should already be initialized if we haven't disabled initialization in manifest
// see: https://developer.android.com/topic/libraries/app-startup#disable-individual
AppInitializer.getInstance(this)
.initializeComponent(CustomEmojiInitializer::class.java)
}
}
```

#### Step 3. Modify AndroidManifest.xml of your application
```xml
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.package.CustomEmojiInitializer"
android:value="androidx.startup" />
<meta-data
android:name="io.wax911.emojify.initializer.EmojiInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
```

## Screenshots

<img src="https://github.com/wax911/android-emojify/raw/master/screenshots/device-2017-09-25-155538.png" width="365px"/> <img src="https://github.com/wax911/android-emojify/raw/master/screenshots/device-2017-09-25-155600.png" width="365px"/> <img src="https://github.com/wax911/android-emojify/raw/master/screenshots/device-2017-09-25-155617.png" width="365px"/> <img src="https://github.com/wax911/android-emojify/raw/master/screenshots/device-2017-09-25-155644.png" width="365px"/>
Expand Down
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ dependencies {
implementation(libs.google.android.material)
implementation(libs.androidx.constraintlayout)

implementation(libs.moshi.kotlin)
implementation(libs.gson)
implementation(libs.jackson.databind)

implementation(libs.jetbrains.kotlinx.coroutines.android)
implementation(libs.jetbrains.kotlinx.coroutines.core)

Expand Down
14 changes: 14 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!-- <provider-->
<!-- android:name="androidx.startup.InitializationProvider"-->
<!-- android:authorities="${applicationId}.androidx-startup"-->
<!-- android:exported="false"-->
<!-- tools:node="merge">-->
<!-- <meta-data-->
<!-- android:name="io.wax911.emojifysample.CustomEmojiInitializer"-->
<!-- android:value="androidx.startup" />-->
<!-- <meta-data-->
<!-- android:name="io.wax911.emojify.initializer.EmojiInitializer"-->
<!-- android:value="androidx.startup"-->
<!-- tools:node="remove" />-->
<!-- </provider>-->
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.wax911.emojifysample

import com.fasterxml.jackson.databind.ObjectMapper
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import io.wax911.emojify.initializer.AbstractEmojiInitializer
import io.wax911.emojify.initializer.IEmojiDeserializer
import io.wax911.emojify.model.Emoji
import okio.buffer
import okio.source
import java.io.InputStream

class CustomEmojiInitializer: AbstractEmojiInitializer() {
class MoshiDeserializer: IEmojiDeserializer {
private val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()

override fun decodeFromStream(inputStream: InputStream): List<Emoji> {
val myType = Types.newParameterizedType(List::class.java, Emoji::class.java)
return moshi.adapter<List<Emoji>>(myType).fromJson(inputStream.source().buffer()) ?: listOf()
}
}

class JacksonDeserializer: IEmojiDeserializer {
private val jackson = ObjectMapper()

override fun decodeFromStream(inputStream: InputStream): List<Emoji> {
val myType = jackson.typeFactory.constructCollectionType(List::class.java, Emoji::class.java)
return jackson.readValue(inputStream, myType)
}
}

class GsonDeserializer: IEmojiDeserializer {
private val gson = Gson()

override fun decodeFromStream(inputStream: InputStream): List<Emoji> {
val myType = TypeToken.getParameterized(List::class.java, Emoji::class.java).type
return gson.fromJson(inputStream.reader(), myType)
}
}

override val serializer: IEmojiDeserializer = MoshiDeserializer()
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ private fun DefaultConfig.applyAdditionalConfiguration(project: Project) {
internal fun Project.configureAndroid(): Unit = baseExtension().run {
compileSdkVersion(34)
defaultConfig {
minSdk = 23
minSdk = 21
targetSdk = 34
versionCode = props[PropertyTypes.CODE].toInt()
versionName = props[PropertyTypes.VERSION]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2024 AniTrend
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.wax911.emojify.deserializer

import io.wax911.emojify.initializer.IEmojiDeserializer
import io.wax911.emojify.model.Emoji
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import java.io.InputStream

/**
* Default implementation for kotlinx-serialization
*/
class KotlinxDeserializer : IEmojiDeserializer {
private val json = Json { isLenient = true }

override fun decodeFromStream(inputStream: InputStream): List<Emoji> {
val deserializer = ListSerializer(Emoji.serializer())
return json.decodeFromStream(deserializer, inputStream)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2021 AniTrend
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.wax911.emojify.initializer

import android.content.Context
import android.content.res.AssetManager
import androidx.startup.Initializer
import io.wax911.emojify.EmojiManager
import io.wax911.emojify.model.Emoji
import kotlinx.serialization.SerializationException
import java.io.IOException

/**
* Abstract the logic of Initializer<EmojiManager> so that
*/
abstract class AbstractEmojiInitializer : Initializer<EmojiManager> {
abstract val serializer: IEmojiDeserializer

/**
* Initializes emoji objects from an asset file in the library directory
*
* @param assetManager provide an assert manager
* @param path location where emoji data can be found
*
* @throws IOException when the provided [assetManager] cannot open [path]
* @throws SerializationException when an error occurs during deserialization
*/
@Throws(IOException::class, SerializationException::class)
fun initEmojiData(
assetManager: AssetManager,
path: String = DEFAULT_PATH,
): List<Emoji> {
return assetManager.open(path).use { inputStream ->
serializer.decodeFromStream(inputStream)
}
}

/**
* Initializes and a component given the application [Context]
*
* @param context The application context.
*/
override fun create(context: Context): EmojiManager {
val emojiManagerDefault = EmojiManager(emptyList())
val result =
runCatching {
val emojis = initEmojiData(context.assets)
EmojiManager(emojis)
}.onFailure { it.printStackTrace() }
return result.getOrNull() ?: emojiManagerDefault
}

/**
* @return A list of dependencies that this [Initializer] depends on. This is
* used to determine initialization order of [Initializer]s.
*
* For e.g. if a [Initializer] `B` defines another
* [Initializer] `A` as its dependency, then `A` gets initialized before `B`.
*/
override fun dependencies() = emptyList<Class<out Initializer<*>>>()

companion object {
/**
* Default location with assets where emojis can be found
*/
internal const val DEFAULT_PATH = "emoticons/emoji.json"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 AniTrend
* Copyright 2024 AniTrend
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,67 +16,15 @@

package io.wax911.emojify.initializer

import android.content.Context
import android.content.res.AssetManager
import androidx.startup.Initializer
import io.wax911.emojify.EmojiManager
import io.wax911.emojify.model.Emoji
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import java.io.IOException

class EmojiInitializer : Initializer<EmojiManager> {
/**
* Initializes emoji objects from an asset file in the library directory
*
* @param assetManager provide an assert manager
* @param path location where emoji data can be found
*
* @throws IOException when the provided [assetManager] cannot open [path]
* @throws SerializationException when an error occurs during deserialization
*/
@Throws(IOException::class, SerializationException::class)
fun initEmojiData(
assetManager: AssetManager,
path: String = DEFAULT_PATH,
): List<Emoji> {
assetManager.open(path).use { inputStream ->
val json = Json { isLenient = true }
val deserializer = ListSerializer(Emoji.serializer())
return json.decodeFromStream(deserializer, inputStream)
}
}

/**
* Initializes and a component given the application [Context]
*
* @param context The application context.
*/
override fun create(context: Context): EmojiManager {
val emojiManagerDefault = EmojiManager(emptyList())
val result =
runCatching {
val emojis = initEmojiData(context.assets)
EmojiManager(emojis)
}.onFailure { it.printStackTrace() }
return result.getOrNull() ?: emojiManagerDefault
}
import io.wax911.emojify.deserializer.KotlinxDeserializer

/**
* Default Implementation of AbstractEmojiInitializer
* **Note: You need to have kotlinx.serialization gradle implementation in your project to work**
*/
class EmojiInitializer : AbstractEmojiInitializer() {
/**
* @return A list of dependencies that this [Initializer] depends on. This is
* used to determine initialization order of [Initializer]s.
*
* For e.g. if a [Initializer] `B` defines another
* [Initializer] `A` as its dependency, then `A` gets initialized before `B`.
* Kotlinx implementation is needed in your project for this to work
*/
override fun dependencies() = emptyList<Class<out Initializer<*>>>()

companion object {
/**
* Default location with assets where emojis can be found
*/
internal const val DEFAULT_PATH = "emoticons/emoji.json"
}
override val serializer: IEmojiDeserializer = KotlinxDeserializer()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2024 AniTrend
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.wax911.emojify.initializer

import io.wax911.emojify.model.Emoji
import java.io.InputStream

/**
* Interface to implement if user wants to use a custom deserializer.
* For more information on the necessary steps refer to README.md
*/
interface IEmojiDeserializer {
/**
* Decodes the given [InputStream] to an object of type List<[Emoji]>
*/
fun decodeFromStream(inputStream: InputStream): List<Emoji>
}
Loading

0 comments on commit 4b06463

Please sign in to comment.