Верии ниже этой больше не совместимы (т.е не работают).

Уменьшение размера приложения.
- Существенное)

Адекватное кеширование.
- Улучшено кеширование на стороне сервера (до этого сервер не выставлял флаг отвечающий за потребность в обновлении).
- Добавлено кеширование ответов от сервера в хранилище приложения.
This commit is contained in:
2024-09-26 04:12:19 +04:00
parent f7596749e3
commit c651f4ba01
21 changed files with 331 additions and 122 deletions

View File

@@ -17,15 +17,6 @@
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="versions">
<list>
<VersionSetting>
<option name="buildVersion" value="2" />
<option name="displayName" value="1.1 (2)" />
<option name="displayVersion" value="1.1" />
</VersionSetting>
</list>
</option>
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>

View File

@@ -32,8 +32,8 @@ android {
applicationId = "ru.n08i40k.polytechnic.next"
minSdk = 26
targetSdk = 35
versionCode = 5
versionName = "1.2.2"
versionCode = 6
versionName = "1.3.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -43,7 +43,7 @@ android {
buildTypes {
release {
isMinifyEnabled = false
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"

View File

@@ -14,8 +14,8 @@
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-renamesourcefileattribute SourceFile

View File

@@ -6,6 +6,9 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ru.n08i40k.polytechnic.next.data.cache.NetworkCacheRepository
import ru.n08i40k.polytechnic.next.data.cache.impl.FakeNetworkCacheRepository
import ru.n08i40k.polytechnic.next.data.cache.impl.LocalNetworkCacheRepository
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
import ru.n08i40k.polytechnic.next.data.schedule.impl.RemoteScheduleRepository
@@ -15,16 +18,24 @@ import ru.n08i40k.polytechnic.next.data.users.impl.RemoteProfileRepository
import javax.inject.Singleton
interface AppContainer {
val applicationContext: Context
val networkCacheRepository: NetworkCacheRepository
val scheduleRepository: ScheduleRepository
val profileRepository: ProfileRepository
}
class MockAppContainer : AppContainer {
class MockAppContainer(override val applicationContext: Context) : AppContainer {
override val networkCacheRepository: NetworkCacheRepository by lazy { FakeNetworkCacheRepository() }
override val scheduleRepository: ScheduleRepository by lazy { FakeScheduleRepository() }
override val profileRepository: ProfileRepository by lazy { FakeProfileRepository() }
}
class RemoteAppContainer(private val applicationContext: Context) : AppContainer {
class RemoteAppContainer(override val applicationContext: Context) : AppContainer {
override val networkCacheRepository: NetworkCacheRepository by lazy {
LocalNetworkCacheRepository(
applicationContext
)
}
override val scheduleRepository: ScheduleRepository by lazy {
RemoteScheduleRepository(
applicationContext

View File

@@ -0,0 +1,15 @@
package ru.n08i40k.polytechnic.next.data.cache
import ru.n08i40k.polytechnic.next.CachedResponse
interface NetworkCacheRepository {
suspend fun put(url: String, data: String)
suspend fun get(url: String): CachedResponse?
suspend fun clear()
suspend fun isHashPresent(): Boolean
suspend fun setHash(hash: String)
}

View File

@@ -0,0 +1,20 @@
package ru.n08i40k.polytechnic.next.data.cache.impl
import ru.n08i40k.polytechnic.next.CachedResponse
import ru.n08i40k.polytechnic.next.data.cache.NetworkCacheRepository
class FakeNetworkCacheRepository : NetworkCacheRepository {
override suspend fun get(url: String): CachedResponse? {
return null
}
override suspend fun put(url: String, data: String) {}
override suspend fun clear() {}
override suspend fun isHashPresent(): Boolean {
return true
}
override suspend fun setHash(hash: String) {}
}

View File

@@ -0,0 +1,93 @@
package ru.n08i40k.polytechnic.next.data.cache.impl
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import ru.n08i40k.polytechnic.next.CachedResponse
import ru.n08i40k.polytechnic.next.data.cache.NetworkCacheRepository
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
import javax.inject.Inject
class LocalNetworkCacheRepository
@Inject constructor(private val applicationContext: Context) : NetworkCacheRepository {
private val cacheMap: MutableMap<String, CachedResponse> = mutableMapOf()
private var hash: String? = null
init {
cacheMap.clear()
runBlocking {
cacheMap.putAll(
applicationContext
.settingsDataStore
.data
.map { settings -> settings.cacheStorageMap }.first()
)
}
}
override suspend fun get(url: String): CachedResponse? {
if (this.hash == null)
return null
val response = cacheMap[url]
if (response?.hash != this.hash)
return null
return response
}
override suspend fun put(url: String, data: String) {
if (hash == null)
throw IllegalStateException("Не установлен хеш!")
cacheMap[url] = CachedResponse
.newBuilder()
.setHash(this.hash)
.setData(data)
.build()
save()
}
override suspend fun clear() {
this.cacheMap.clear()
this.save()
}
override suspend fun isHashPresent(): Boolean {
return this.hash != null
}
override suspend fun setHash(hash: String) {
val freshHash = this.hash == null
if (!freshHash && this.hash != hash)
clear()
this.hash = hash
if (freshHash) {
this.cacheMap
.mapNotNull { if (it.value.hash != this.hash) it.key else null }
.forEach { this.cacheMap.remove(it) }
}
}
private suspend fun save() {
withContext(Dispatchers.IO) {
runBlocking {
applicationContext.settingsDataStore.updateData {
it
.toBuilder()
.putAllCacheStorage(cacheMap)
.build()
}
}
}
}
}

View File

@@ -1,10 +1,7 @@
package ru.n08i40k.polytechnic.next.data.schedule.impl
import android.content.Context
import com.android.volley.Request
import com.android.volley.ServerError
import com.android.volley.toolbox.RequestFuture
import com.android.volley.toolbox.StringRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@@ -13,67 +10,16 @@ import kotlinx.coroutines.withContext
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
import ru.n08i40k.polytechnic.next.model.Group
import ru.n08i40k.polytechnic.next.network.NetworkConnection
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetRequest
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetRequestData
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetResponse
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateRequest
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateRequestData
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
import java.util.logging.Logger
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
class RemoteScheduleRepository(private val context: Context) : ScheduleRepository {
@OptIn(ExperimentalEncodingApi::class)
suspend fun getMainPage(): MyResult<String> {
return withContext(Dispatchers.IO) {
val mainPageFuture = RequestFuture.newFuture<String>()
val request = StringRequest(
Request.Method.GET,
"https://politehnikum-eng.ru/index/raspisanie_zanjatij/0-409",
mainPageFuture,
mainPageFuture
)
NetworkConnection.getInstance(context).addToRequestQueue(request)
try {
val encodedMainPage =
Base64.Default.encode(mainPageFuture.get().encodeToByteArray())
MyResult.Success(encodedMainPage)
} catch (exception: Exception) {
MyResult.Failure(exception)
}
}
}
suspend fun updateMainPage(): MyResult<Nothing> {
return withContext(Dispatchers.IO) {
val mainPage = getMainPage()
if (mainPage is MyResult.Failure)
return@withContext mainPage
val updateFuture = RequestFuture.newFuture<Nothing>()
ScheduleUpdateRequest(
ScheduleUpdateRequestData((mainPage as MyResult.Success<String>).data),
context,
updateFuture,
updateFuture
).send()
try {
MyResult.Success(updateFuture.get())
} catch (exception: Exception) {
MyResult.Failure(exception)
}
}
}
override suspend fun getGroup(): MyResult<Group> {
return withContext(Dispatchers.IO) {
val logger = Logger.getLogger("RemoteScheduleRepository")
val groupName = runBlocking {
context.settingsDataStore.data.map { settings -> settings.group }.first()
}
@@ -81,49 +27,16 @@ class RemoteScheduleRepository(private val context: Context) : ScheduleRepositor
if (groupName.isEmpty())
return@withContext MyResult.Failure(IllegalArgumentException("No group name provided!"))
val firstPassFuture = RequestFuture.newFuture<ScheduleGetResponse>()
val future = RequestFuture.newFuture<ScheduleGetResponse>()
ScheduleGetRequest(
ScheduleGetRequestData(groupName),
context,
firstPassFuture,
firstPassFuture
).send()
var firstPassResponse: ScheduleGetResponse? = null
try {
firstPassResponse = firstPassFuture.get()
if (!firstPassResponse.updateRequired) {
logger.info("Successfully get group schedule!")
return@withContext MyResult.Success(firstPassFuture.get().group)
}
logger.info("Successfully get group schedule, but it needs to update!")
} catch (exception: Exception) {
if (exception.cause !is ServerError)
return@withContext MyResult.Failure(exception)
logger.info("Can't get group schedule, because it needs to first update!")
}
val updateResult = updateMainPage()
if (updateResult is MyResult.Failure) {
logger.info("Can't update site main page!")
if (firstPassResponse != null)
return@withContext MyResult.Success(firstPassResponse.group)
return@withContext updateResult
}
logger.info("Site main page successfully updated!")
val secondPassFuture = RequestFuture.newFuture<ScheduleGetResponse>()
ScheduleGetRequest(
ScheduleGetRequestData(groupName),
context,
secondPassFuture,
secondPassFuture
future,
future
).send()
try {
MyResult.Success(secondPassFuture.get().group)
MyResult.Success(future.get().group)
} catch (exception: Exception) {
MyResult.Failure(exception)
}

View File

@@ -69,7 +69,7 @@ open class RequestBase(
listener: Response.Listener<String>,
errorListener: Response.ErrorListener?
) : StringRequest(method, NetworkValues.API_HOST + url, listener, errorListener) {
fun send() {
open fun send() {
Logger.getLogger("RequestBase").info("Sending request to $url")
NetworkConnection.getInstance(context).addToRequestQueue(this)
}

View File

@@ -13,7 +13,7 @@ import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
open class AuthorizedRequest(
context: Context,
method: Int,
url: String?,
url: String,
listener: Response.Listener<String>,
errorListener: Response.ErrorListener?,
private val canBeUnauthorized: Boolean = false

View File

@@ -0,0 +1,135 @@
package ru.n08i40k.polytechnic.next.network.data
import android.content.Context
import com.android.volley.Response
import com.android.volley.VolleyError
import com.android.volley.toolbox.RequestFuture
import com.android.volley.toolbox.StringRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.data.AppContainer
import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.network.NetworkConnection
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusRequest
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusResponse
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateRequest
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateRequestData
import java.util.logging.Logger
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
open class CachedRequest(
context: Context,
method: Int,
private val url: String,
private val listener: Response.Listener<String>,
errorListener: Response.ErrorListener?,
) : AuthorizedRequest(context, method, url, {
runBlocking {
(context as PolytechnicApplication)
.container.networkCacheRepository.put(url, it)
}
listener.onResponse(it)
}, errorListener) {
private val appContainer: AppContainer = (context as PolytechnicApplication).container
@OptIn(ExperimentalEncodingApi::class)
suspend fun getMainPage(): MyResult<String> {
return withContext(Dispatchers.IO) {
val mainPageFuture = RequestFuture.newFuture<String>()
val request = StringRequest(
Method.GET,
"https://politehnikum-eng.ru/index/raspisanie_zanjatij/0-409",
mainPageFuture,
mainPageFuture
)
NetworkConnection.getInstance(context).addToRequestQueue(request)
try {
val encodedMainPage =
Base64.Default.encode(mainPageFuture.get().encodeToByteArray())
MyResult.Success(encodedMainPage)
} catch (exception: Exception) {
MyResult.Failure(exception)
}
}
}
private suspend fun updateMainPage(): MyResult<ScheduleGetCacheStatusResponse> {
return withContext(Dispatchers.IO) {
val mainPage = getMainPage()
if (mainPage is MyResult.Failure)
return@withContext mainPage
val updateFuture = RequestFuture.newFuture<ScheduleGetCacheStatusResponse>()
ScheduleUpdateRequest(
ScheduleUpdateRequestData((mainPage as MyResult.Success<String>).data),
context,
updateFuture,
updateFuture
).send()
try {
MyResult.Success(updateFuture.get())
} catch (exception: Exception) {
MyResult.Failure(exception)
}
}
}
override fun send() {
val logger = Logger.getLogger("CachedRequest")
val repository = appContainer.networkCacheRepository
val future = RequestFuture.newFuture<ScheduleGetCacheStatusResponse>()
logger.info("Getting cache status...")
ScheduleGetCacheStatusRequest(context, future, future).send()
try {
val response = future.get()
logger.info("Cache status received successfully!")
if (!response.cacheUpdateRequired) {
logger.info("Cache update was not required!")
runBlocking { repository.setHash(response.cacheHash) }
} else {
logger.info("Cache update was required!")
val updateResult = runBlocking { updateMainPage() }
when (updateResult) {
is MyResult.Success -> {
logger.info("Cache update was successful!")
runBlocking { repository.setHash(updateResult.data.cacheHash) }
}
is MyResult.Failure -> {
logger.warning("Failed to update cache!")
super.getErrorListener()
?.onErrorResponse(updateResult.exception.cause as VolleyError)
return
}
}
}
} catch (exception: Exception) {
logger.warning("Failed to get cache status!")
super.getErrorListener()?.onErrorResponse(exception.cause as VolleyError)
return
}
val cachedResponse = runBlocking { repository.get(url) }
if (cachedResponse != null) {
logger.info("Found cached response!")
listener.onResponse(cachedResponse.data)
return
}
logger.info("Cached response doesn't exists!")
super.send()
}
}

View File

@@ -4,14 +4,14 @@ import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
import ru.n08i40k.polytechnic.next.network.data.CachedRequest
class ScheduleGetRequest(
private val data: ScheduleGetRequestData,
context: Context,
listener: Response.Listener<ScheduleGetResponse>,
errorListener: Response.ErrorListener? = null
) : AuthorizedRequest(
) : CachedRequest(
context, Method.POST, "schedule/get-group", Response.Listener<String> { response ->
listener.onResponse(Json.decodeFromString<ScheduleGetResponse>(response))
}, errorListener

View File

@@ -0,0 +1,16 @@
package ru.n08i40k.polytechnic.next.network.data.schedule
import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
class ScheduleGetCacheStatusRequest(
context: Context,
listener: Response.Listener<ScheduleGetCacheStatusResponse>,
errorListener: Response.ErrorListener? = null
) : AuthorizedRequest(
context, Method.GET, "schedule/cache-status", Response.Listener<String> { response ->
listener.onResponse(Json.decodeFromString<ScheduleGetCacheStatusResponse>(response))
}, errorListener
)

View File

@@ -0,0 +1,9 @@
package ru.n08i40k.polytechnic.next.network.data.schedule
import kotlinx.serialization.Serializable
@Serializable
data class ScheduleGetCacheStatusResponse(
val cacheUpdateRequired: Boolean,
val cacheHash: String,
)

View File

@@ -3,13 +3,13 @@ package ru.n08i40k.polytechnic.next.network.data.schedule
import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
import ru.n08i40k.polytechnic.next.network.data.CachedRequest
class ScheduleGetGroupNamesRequest(
context: Context,
listener: Response.Listener<ScheduleGetGroupNamesResponseData>,
errorListener: Response.ErrorListener? = null
) : AuthorizedRequest(
) : CachedRequest(
context, Method.GET, "schedule/get-group-names", Response.Listener<String> { response ->
listener.onResponse(Json.decodeFromString<ScheduleGetGroupNamesResponseData>(response))
}, errorListener

View File

@@ -7,7 +7,5 @@ import ru.n08i40k.polytechnic.next.model.Group
data class ScheduleGetResponse(
val updatedAt: String,
val group: Group,
val etag: String,
val lastChangedDays: ArrayList<Int>,
val updateRequired: Boolean
)

View File

@@ -9,11 +9,11 @@ import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
class ScheduleUpdateRequest(
private val data: ScheduleUpdateRequestData,
context: Context,
listener: Response.Listener<Nothing>,
listener: Response.Listener<ScheduleGetCacheStatusResponse>,
errorListener: Response.ErrorListener? = null
) : AuthorizedRequest(
context, Method.POST, "schedule/update-site-main-page", Response.Listener<String> {
listener.onResponse(null)
listener.onResponse(Json.decodeFromString<ScheduleGetCacheStatusResponse>(it))
}, errorListener
) {
override fun getBody(): ByteArray {

View File

@@ -7,6 +7,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@@ -21,7 +22,7 @@ import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel
@Preview(showBackground = true)
@Composable
fun ProfileScreen(
profileViewModel: ProfileViewModel = ProfileViewModel(MockAppContainer().profileRepository) {},
profileViewModel: ProfileViewModel = ProfileViewModel(MockAppContainer(LocalContext.current).profileRepository) {},
onRefreshProfile: () -> Unit = {}
) {
val uiState by profileViewModel.uiState.collectAsStateWithLifecycle()

View File

@@ -6,6 +6,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@@ -19,7 +20,7 @@ import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
@Preview(showBackground = true, showSystemUi = true)
@Composable
fun ScheduleScreen(
scheduleViewModel: ScheduleViewModel = ScheduleViewModel(MockAppContainer()),
scheduleViewModel: ScheduleViewModel = ScheduleViewModel(MockAppContainer(LocalContext.current)),
onRefreshSchedule: () -> Unit = {}
) {
val uiState by scheduleViewModel.uiState.collectAsStateWithLifecycle()

View File

@@ -3,8 +3,14 @@ syntax = "proto3";
option java_package = "ru.n08i40k.polytechnic.next";
option java_multiple_files = true;
message CachedResponse {
string hash = 1;
string data = 2;
}
message Settings {
string user_id = 1;
string access_token = 2;
string group = 3;
map<string, CachedResponse> cache_storage = 4;
}

View File

@@ -2,15 +2,15 @@
accompanistSwiperefresh = "0.36.0"
agp = "8.6.1"
firebaseBom = "33.3.0"
hiltAndroid = "2.51.1"
hiltAndroidCompiler = "2.51.1"
hiltAndroid = "2.52"
hiltAndroidCompiler = "2.52"
hiltNavigationCompose = "1.2.0"
kotlin = "2.0.10"
coreKtx = "1.13.1"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
kotlinxSerializationJson = "1.7.2"
kotlinxSerializationJson = "1.7.3"
lifecycleRuntimeKtx = "2.8.6"
activityCompose = "1.9.2"
composeBom = "2024.09.02"