android: stabilize DI graph and production api config
Some checks are pending
CI / test (push) Has started running

This commit is contained in:
Codex
2026-03-08 23:02:16 +03:00
parent 9d842c1d88
commit 4939754de8
10 changed files with 48 additions and 21 deletions

View File

@@ -57,3 +57,10 @@
- Added unit test for realtime event parsing (`RealtimeEventParserTest`). - Added unit test for realtime event parsing (`RealtimeEventParserTest`).
- Added DAO test (`ChatDaoTest`) using in-memory Room + Robolectric. - Added DAO test (`ChatDaoTest`) using in-memory Room + Robolectric.
- Updated Android checklist status in `docs/android-checklist.md`. - Updated Android checklist status in `docs/android-checklist.md`.
### Step 10 - Build stabilization fixes
- Switched Android API base URL to `https://chat.daemonlord.ru/`.
- Added cleartext traffic flag in manifest for local/dev compatibility.
- Fixed Hilt dependency cycle by separating refresh `AuthApiService` with a dedicated qualifier.
- Added `CoroutineDispatcher` DI provider and qualifier for repositories.
- Fixed Material3 experimental API opt-in and removed deprecated `StateFlow.distinctUntilChanged()` usage.

View File

@@ -17,7 +17,7 @@ android {
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "0.1.0" versionName = "0.1.0"
buildConfigField("String", "API_BASE_URL", "\"http://10.0.2.2:8000/\"") buildConfigField("String", "API_BASE_URL", "\"https://chat.daemonlord.ru/\"")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@@ -1,22 +1,23 @@
<?xml version=1.0 encoding=utf-8?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android=http://schemas.android.com/apk/res/android> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name=android.permission.INTERNET /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:name=.MessengerApplication android:name=".MessengerApplication"
android:allowBackup=true android:allowBackup="true"
android:icon=@android:drawable/sym_def_app_icon android:icon="@android:drawable/sym_def_app_icon"
android:label=@string/app_name android:label="@string/app_name"
android:roundIcon=@android:drawable/sym_def_app_icon android:roundIcon="@android:drawable/sym_def_app_icon"
android:supportsRtl=true android:supportsRtl="true"
android:theme=@android:style/Theme.Material.Light.NoActionBar> android:usesCleartextTraffic="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity <activity
android:name=.MainActivity android:name=".MainActivity"
android:exported=true> android:exported="true">
<intent-filter> <intent-filter>
<action android:name=android.intent.action.MAIN /> <action android:name="android.intent.action.MAIN" />
<category android:name=android.intent.category.LAUNCHER /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>

View File

@@ -9,6 +9,7 @@ import ru.daemonlord.messenger.core.token.TokenBundle
import ru.daemonlord.messenger.core.token.TokenRepository import ru.daemonlord.messenger.core.token.TokenRepository
import ru.daemonlord.messenger.data.auth.api.AuthApiService import ru.daemonlord.messenger.data.auth.api.AuthApiService
import ru.daemonlord.messenger.data.auth.dto.RefreshTokenRequestDto import ru.daemonlord.messenger.data.auth.dto.RefreshTokenRequestDto
import ru.daemonlord.messenger.di.RefreshAuthApi
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -16,6 +17,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class TokenRefreshAuthenticator @Inject constructor( class TokenRefreshAuthenticator @Inject constructor(
private val tokenRepository: TokenRepository, private val tokenRepository: TokenRepository,
@RefreshAuthApi
private val refreshAuthApiService: AuthApiService, private val refreshAuthApiService: AuthApiService,
) : Authenticator { ) : Authenticator {

View File

@@ -1,7 +1,6 @@
package ru.daemonlord.messenger.data.auth.repository package ru.daemonlord.messenger.data.auth.repository
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import retrofit2.HttpException import retrofit2.HttpException
import ru.daemonlord.messenger.core.token.TokenBundle import ru.daemonlord.messenger.core.token.TokenBundle
@@ -10,6 +9,7 @@ import ru.daemonlord.messenger.data.auth.api.AuthApiService
import ru.daemonlord.messenger.data.auth.dto.AuthUserDto import ru.daemonlord.messenger.data.auth.dto.AuthUserDto
import ru.daemonlord.messenger.data.auth.dto.LoginRequestDto import ru.daemonlord.messenger.data.auth.dto.LoginRequestDto
import ru.daemonlord.messenger.data.auth.dto.RefreshTokenRequestDto import ru.daemonlord.messenger.data.auth.dto.RefreshTokenRequestDto
import ru.daemonlord.messenger.di.IoDispatcher
import ru.daemonlord.messenger.domain.auth.model.AuthUser import ru.daemonlord.messenger.domain.auth.model.AuthUser
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
import ru.daemonlord.messenger.domain.common.AppError import ru.daemonlord.messenger.domain.common.AppError
@@ -22,7 +22,7 @@ import javax.inject.Singleton
class NetworkAuthRepository @Inject constructor( class NetworkAuthRepository @Inject constructor(
private val authApiService: AuthApiService, private val authApiService: AuthApiService,
private val tokenRepository: TokenRepository, private val tokenRepository: TokenRepository,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : AuthRepository { ) : AuthRepository {
override suspend fun login(email: String, password: String): AppResult<AuthUser> = withContext(ioDispatcher) { override suspend fun login(email: String, password: String): AppResult<AuthUser> = withContext(ioDispatcher) {

View File

@@ -1,7 +1,6 @@
package ru.daemonlord.messenger.data.chat.repository package ru.daemonlord.messenger.data.chat.repository
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
@@ -14,6 +13,7 @@ import ru.daemonlord.messenger.data.chat.local.dao.ChatDao
import ru.daemonlord.messenger.data.chat.mapper.toChatEntity import ru.daemonlord.messenger.data.chat.mapper.toChatEntity
import ru.daemonlord.messenger.data.chat.mapper.toDomain import ru.daemonlord.messenger.data.chat.mapper.toDomain
import ru.daemonlord.messenger.data.chat.mapper.toUserShortEntityOrNull import ru.daemonlord.messenger.data.chat.mapper.toUserShortEntityOrNull
import ru.daemonlord.messenger.di.IoDispatcher
import ru.daemonlord.messenger.domain.chat.model.ChatItem import ru.daemonlord.messenger.domain.chat.model.ChatItem
import ru.daemonlord.messenger.domain.chat.repository.ChatRepository import ru.daemonlord.messenger.domain.chat.repository.ChatRepository
import ru.daemonlord.messenger.domain.common.AppError import ru.daemonlord.messenger.domain.common.AppError
@@ -26,7 +26,7 @@ import javax.inject.Singleton
class NetworkChatRepository @Inject constructor( class NetworkChatRepository @Inject constructor(
private val chatApiService: ChatApiService, private val chatApiService: ChatApiService,
private val chatDao: ChatDao, private val chatDao: ChatDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ChatRepository { ) : ChatRepository {
override fun observeChats(archived: Boolean): Flow<List<ChatItem>> { override fun observeChats(archived: Boolean): Flow<List<ChatItem>> {

View File

@@ -4,6 +4,8 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -60,6 +62,7 @@ object NetworkModule {
@Provides @Provides
@Singleton @Singleton
@RefreshAuthApi
fun provideRefreshApiService( fun provideRefreshApiService(
@RefreshClient refreshClient: OkHttpClient, @RefreshClient refreshClient: OkHttpClient,
json: Json, json: Json,
@@ -73,6 +76,11 @@ object NetworkModule {
.create(AuthApiService::class.java) .create(AuthApiService::class.java)
} }
@Provides
@Singleton
@IoDispatcher
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@Provides @Provides
@Singleton @Singleton
fun provideApiClient( fun provideApiClient(

View File

@@ -5,3 +5,11 @@ import javax.inject.Qualifier
@Qualifier @Qualifier
@Retention(AnnotationRetention.BINARY) @Retention(AnnotationRetention.BINARY)
annotation class RefreshClient annotation class RefreshClient
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class RefreshAuthApi
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher

View File

@@ -13,6 +13,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChip
import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.AssistChipDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
@@ -45,6 +46,7 @@ fun ChatListRoute(
} }
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class)
fun ChatListScreen( fun ChatListScreen(
state: ChatListUiState, state: ChatListUiState,
onTabSelected: (ChatTab) -> Unit, onTabSelected: (ChatTab) -> Unit,

View File

@@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -59,7 +58,7 @@ class ChatListViewModel @Inject constructor(
.flatMapLatest { tab -> .flatMapLatest { tab ->
observeChatsUseCase(archived = tab == ChatTab.ARCHIVED) observeChatsUseCase(archived = tab == ChatTab.ARCHIVED)
} }
.combine(searchQuery.distinctUntilChanged()) { chats, query -> .combine(searchQuery) { chats, query ->
chats.filterByQuery(query) chats.filterByQuery(query)
} }
.collectLatest { filtered -> .collectLatest { filtered ->