Skip to content

Commit

Permalink
Merge pull request #2 from Konyaco/cache
Browse files Browse the repository at this point in the history
[Code] Support for caching.
  • Loading branch information
Konyaco authored Dec 9, 2021
2 parents 7971c41 + aeaf402 commit d847d8e
Show file tree
Hide file tree
Showing 14 changed files with 300 additions and 25 deletions.
7 changes: 4 additions & 3 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
Expand All @@ -12,9 +13,9 @@
android:theme="@style/Theme.CollinsDictionary">
<activity
android:name=".MainActivity"
android:configChanges="orientation"
android:windowSoftInputMode="adjustPan"
android:exported="true">
android:configChanges="orientation|uiMode|colorMode"
android:exported="true"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import me.konyaco.collinsdictionary.ui.MyTheme
import me.konyaco.collinsdictionary.viewmodel.AppViewModel

class MainActivity : AppCompatActivity() {
private val component by lazy { AppViewModel() }
private val component by lazy {
AppViewModel((application as MyApplication).repository)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package me.konyaco.collinsdictionary

import android.app.Application
import me.konyaco.collinsdictionary.repository.Repository
import me.konyaco.collinsdictionary.service.CollinsOnlineDictionary
import me.konyaco.collinsdictionary.service.LocalCacheDictionary
import me.konyaco.collinsdictionary.store.FileBasedLocalStorage

class MyApplication : Application() {
lateinit var repository: Repository

override fun onCreate() {
super.onCreate()
repository = Repository(
CollinsOnlineDictionary(),
LocalCacheDictionary(FileBasedLocalStorage(filesDir.resolve("cache").also {
if (!it.exists()) it.mkdir()
}))
)
}
}
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ plugins {
kotlin("android") version "1.5.31" apply false
kotlin("multiplatform") version "1.5.31" apply false
id("org.jetbrains.compose") version "1.0.0" apply false
kotlin("plugin.serialization") version "1.5.31" apply false
}

allprojects {
Expand All @@ -12,6 +13,7 @@ allprojects {
google()
}
extra["jsoup_version"] = "1.13.1"
extra["serialization_version"] = "1.3.1"
}

group = "me.konyaco.collinsdictionary"
Expand Down
3 changes: 3 additions & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
id("com.android.library")
kotlin("plugin.serialization")
}

kotlin {
Expand All @@ -11,6 +12,8 @@ kotlin {
sourceSets {
val commonMain by getting {
dependencies {
api("org.jetbrains.kotlinx:kotlinx-serialization-json:${extra["serialization_version"]}")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
api(compose.runtime)
api(compose.foundation)
api(compose.animation)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// This file is copied from jvmMain source.
package me.konyaco.collinsdictionary.store

import java.io.File

class FileBasedLocalStorage(
private val dir: File
) : LocalStorage {
private val searchDir = dir.resolve("search").also { it.mkdirIfNotExists() }
private val definitionDir = dir.resolve("definition").also { it.mkdirIfNotExists() }

override fun getSearch(word: String): String? {
return searchDir.resolve(word).takeIf { it.exists() }?.readText()
}

override fun getDefinition(word: String): String? {
return definitionDir.resolve(word).takeIf { it.exists() }?.readText()
}

override fun saveSearch(word: String, value: String) {
searchDir.resolve(word).writeText(value)
}

override fun saveDefinition(word: String, value: String) {
definitionDir.resolve(word).writeText(value)
}

override fun deleteSearch(word: String) {
searchDir.resolve(word).deleteIfExists()
}

override fun deleteDefinition(word: String) {
definitionDir.resolve(word).deleteIfExists()
}
}

private fun File.deleteIfExists() {
if (exists()) delete()
}

private fun File.mkdirIfNotExists() {
if (!exists()) mkdir()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package me.konyaco.collinsdictionary.repository

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import me.konyaco.collinsdictionary.service.CollinsOnlineDictionary
import me.konyaco.collinsdictionary.service.LocalCacheDictionary
import me.konyaco.collinsdictionary.service.SearchResult
import me.konyaco.collinsdictionary.service.Word

class Repository(
private val onlineDictionary: CollinsOnlineDictionary,
private val localCacheDictionary: LocalCacheDictionary
) {
data class Result<out T>(
val source: Source,
val data: T
) {
enum class Source {
REMOTE, LOCAL
}
}

fun search(word: String): Flow<Result<SearchResult>> = flow {
val localResult = localCacheDictionary.search(word)
when (localResult) {
is SearchResult.PreciseWord -> emit(Result(Result.Source.LOCAL, localResult))
is SearchResult.Redirect -> emit(Result(Result.Source.LOCAL, localResult))
}
val remoteResult = onlineDictionary.search(word)
localCacheDictionary.cacheSearchResult(word, remoteResult)
emit(Result(Result.Source.REMOTE, remoteResult))
}

fun getDefinition(word: String): Flow<Result<Word?>> = flow {
val localDefinition = localCacheDictionary.getDefinition(word)
if (localDefinition != null) {
emit(Result(Result.Source.LOCAL, localDefinition))
}
val remoteDefinition = onlineDictionary.getDefinition(word)
remoteDefinition?.let {
localCacheDictionary.cacheDefinition(word, remoteDefinition)
}
emit(Result(Result.Source.REMOTE, remoteDefinition))
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
package me.konyaco.collinsdictionary.service

import kotlinx.serialization.Serializable

interface CollinsDictionary {
fun search(word: String): SearchResult

fun getDefinition(word: String): Word?
}

@Serializable
sealed class SearchResult {
data class PreciseWord(val word: String) : SearchResult()
data class Redirect(val redirectTo: String): SearchResult()
data class NotFound(val alternatives: List<String>) : SearchResult()
}

@Serializable
data class Word(
val cobuildDictionary: CobuildDictionary,
// TODO: 2021/7/30 Collins British English Dictionary, Collins American English Dictionary
)

@Serializable
data class CobuildDictionary(
val sections: List<CobuildDictionarySection>
)

@Serializable
data class CobuildDictionarySection(
val word: String,
val frequency: Int?,
Expand All @@ -29,28 +35,33 @@ data class CobuildDictionarySection(
val definitionEntries: List<DefinitionEntry>
)

@Serializable
data class WordForm(
val description: String,
val spell: String
)

@Serializable
data class Pronunciation(
val ipa: String,
val soundUrl: String?
)

@Serializable
data class DefinitionEntry(
val index: Int,
val type: String,
val definition: Definition,
val extraDefinitions: List<Definition>?
)

@Serializable
data class Definition(
val def: String,
val examples: List<ExampleSentence>
)

@Serializable
data class ExampleSentence(
val sentence: String,
val grammarPattern: String?,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package me.konyaco.collinsdictionary.service

import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import me.konyaco.collinsdictionary.store.LocalStorage

class LocalCacheDictionary(private val localStorage: LocalStorage) : CollinsDictionary {
private val json = Json {
ignoreUnknownKeys = true
prettyPrint = false
}

override fun search(word: String): SearchResult {
return when (val text = localStorage.getSearch(word)) {
null -> SearchResult.NotFound(emptyList())
word -> SearchResult.PreciseWord(word)
else -> SearchResult.Redirect(text)
}
}

override fun getDefinition(word: String): Word? {
val text = localStorage.getDefinition(word) ?: return null
return json.decodeFromString<Word>(text)
}

fun cacheSearchResult(word: String, searchResult: SearchResult) {
when (searchResult) {
is SearchResult.PreciseWord -> localStorage.saveSearch(word, word)
is SearchResult.Redirect -> {
localStorage.saveSearch(word, searchResult.redirectTo)
localStorage.saveSearch(searchResult.redirectTo, searchResult.redirectTo)
}
is SearchResult.NotFound -> {}
}
}

fun cacheDefinition(word: String, definition: Word) {
localStorage.saveDefinition(word, json.encodeToString(definition))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package me.konyaco.collinsdictionary.store

interface LocalStorage {
fun getSearch(word: String): String?
fun getDefinition(word: String): String?
fun saveSearch(word: String, value: String)
fun saveDefinition(word: String, value: String)
fun deleteSearch(word: String)
fun deleteDefinition(word: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ package me.konyaco.collinsdictionary.viewmodel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import me.konyaco.collinsdictionary.service.CollinsOnlineDictionary
import me.konyaco.collinsdictionary.repository.Repository
import me.konyaco.collinsdictionary.service.SearchResult
import me.konyaco.collinsdictionary.service.Word

class AppViewModel {
private val collinsDictionary = CollinsOnlineDictionary()
class AppViewModel(private val repository: Repository) {
private val scope = CoroutineScope(Dispatchers.Default)

sealed class Result {
Expand All @@ -25,28 +26,21 @@ class AppViewModel {
scope.launch {
isSearching.emit(true)
try {
when (val result = collinsDictionary.search(word)) {
is SearchResult.PreciseWord -> {
val word = collinsDictionary.getDefinition(result.word)
if (word != null) {
queryResult.emit(Result.Succeed(word))
} else {
queryResult.emit(Result.WordNotFound)
repository.search(word).take(1).collect { result ->
when (result.data) {
is SearchResult.PreciseWord -> {
getDef(result.data.word)
}
}
is SearchResult.Redirect -> {
val word = collinsDictionary.getDefinition(result.redirectTo)
if (word != null) {
queryResult.emit(Result.Succeed(word))
} else {
is SearchResult.Redirect -> {
getDef(result.data.redirectTo)
}
is SearchResult.NotFound -> {
queryResult.emit(Result.WordNotFound)
// TODO: Use user-friendly UI to display alternatives.
}
}
is SearchResult.NotFound -> {
queryResult.emit(Result.WordNotFound)
// TODO: Use user-friendly UI to display alternatives.
}
}

} catch (e: Exception) {
queryResult.emit(Result.Failed(e.message ?: "Unknown error"))
return@launch
Expand All @@ -55,4 +49,15 @@ class AppViewModel {
}
}
}

private suspend fun getDef(word: String) {
repository.getDefinition(word).take(1).collect { result ->
if (result.data != null) {
queryResult.emit(Result.Succeed(result.data))
} else {
queryResult.emit(Result.WordNotFound)
}
}
}

}
Loading

0 comments on commit d847d8e

Please sign in to comment.