mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-17 22:37:03 +05:30
Merge #72
72: Migrate to Ktor for network operations r=msfjarvis a=msfjarvis ~~Still a work in progress, crashes during loading right now~~ Crashes are fixed and tests were added back, good to go now. Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
commit
f602fe8d7d
23 changed files with 184 additions and 149 deletions
2
.idea/codeStyles/Project.xml
generated
2
.idea/codeStyles/Project.xml
generated
|
@ -6,7 +6,7 @@
|
||||||
<value>
|
<value>
|
||||||
<package name="java.util" alias="false" withSubpackages="false" />
|
<package name="java.util" alias="false" withSubpackages="false" />
|
||||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||||
<package name="io.ktor" alias="false" withSubpackages="true" />
|
<package name="io.ktor" alias="false" withSubpackages="false" />
|
||||||
</value>
|
</value>
|
||||||
</option>
|
</option>
|
||||||
<option name="PACKAGES_IMPORT_LAYOUT">
|
<option name="PACKAGES_IMPORT_LAYOUT">
|
||||||
|
|
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
|
@ -13,7 +13,6 @@
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
<option value="$PROJECT_DIR$/buildSrc" />
|
<option value="$PROJECT_DIR$/buildSrc" />
|
||||||
<option value="$PROJECT_DIR$/lobsters-api" />
|
|
||||||
<option value="$PROJECT_DIR$/model" />
|
<option value="$PROJECT_DIR$/model" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
|
|
|
@ -61,41 +61,45 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
kapt(Dependencies.ThirdParty.Roomigrant.compiler)
|
|
||||||
kapt(Dependencies.AndroidX.Hilt.daggerCompiler)
|
kapt(Dependencies.AndroidX.Hilt.daggerCompiler)
|
||||||
kapt(Dependencies.AndroidX.Hilt.daggerHiltCompiler)
|
kapt(Dependencies.AndroidX.Hilt.daggerHiltCompiler)
|
||||||
kapt(Dependencies.AndroidX.Room.compiler)
|
kapt(Dependencies.AndroidX.Room.compiler)
|
||||||
implementation(project(":lobsters-api"))
|
kapt(Dependencies.ThirdParty.Roomigrant.compiler)
|
||||||
implementation(project(":model"))
|
implementation(project(":model"))
|
||||||
implementation(Dependencies.AndroidX.coreKtx)
|
|
||||||
implementation(Dependencies.AndroidX.activityKtx)
|
implementation(Dependencies.AndroidX.activityKtx)
|
||||||
implementation(Dependencies.AndroidX.appCompat)
|
implementation(Dependencies.AndroidX.appCompat)
|
||||||
implementation(Dependencies.AndroidX.browser)
|
implementation(Dependencies.AndroidX.browser)
|
||||||
|
implementation(Dependencies.AndroidX.coreKtx)
|
||||||
|
implementation(Dependencies.AndroidX.material)
|
||||||
|
implementation(Dependencies.AndroidX.Compose.compiler)
|
||||||
implementation(Dependencies.AndroidX.Compose.foundation)
|
implementation(Dependencies.AndroidX.Compose.foundation)
|
||||||
implementation(Dependencies.AndroidX.Compose.foundationLayout)
|
implementation(Dependencies.AndroidX.Compose.foundationLayout)
|
||||||
implementation(Dependencies.AndroidX.Compose.foundationText)
|
implementation(Dependencies.AndroidX.Compose.foundationText)
|
||||||
implementation(Dependencies.AndroidX.Compose.runtime)
|
|
||||||
implementation(Dependencies.AndroidX.Compose.material)
|
implementation(Dependencies.AndroidX.Compose.material)
|
||||||
implementation(Dependencies.AndroidX.Compose.compiler)
|
implementation(Dependencies.AndroidX.Compose.navigation)
|
||||||
|
implementation(Dependencies.AndroidX.Compose.runtime)
|
||||||
implementation(Dependencies.AndroidX.Compose.ui)
|
implementation(Dependencies.AndroidX.Compose.ui)
|
||||||
implementation(Dependencies.AndroidX.Compose.uiTooling)
|
implementation(Dependencies.AndroidX.Compose.uiTooling)
|
||||||
implementation(Dependencies.AndroidX.Compose.uiText)
|
implementation(Dependencies.AndroidX.Compose.uiText)
|
||||||
implementation(Dependencies.AndroidX.Compose.uiTextAndroid)
|
implementation(Dependencies.AndroidX.Compose.uiTextAndroid)
|
||||||
implementation(Dependencies.AndroidX.Compose.uiUnit)
|
implementation(Dependencies.AndroidX.Compose.uiUnit)
|
||||||
|
implementation(Dependencies.AndroidX.Hilt.dagger)
|
||||||
implementation(Dependencies.AndroidX.Hilt.hiltLifecycleViewmodel)
|
implementation(Dependencies.AndroidX.Hilt.hiltLifecycleViewmodel)
|
||||||
implementation(Dependencies.AndroidX.Lifecycle.runtimeKtx)
|
implementation(Dependencies.AndroidX.Lifecycle.runtimeKtx)
|
||||||
implementation(Dependencies.AndroidX.Lifecycle.viewmodelKtx)
|
implementation(Dependencies.AndroidX.Lifecycle.viewmodelKtx)
|
||||||
implementation(Dependencies.AndroidX.Compose.navigation)
|
|
||||||
implementation(Dependencies.AndroidX.Room.runtime)
|
implementation(Dependencies.AndroidX.Room.runtime)
|
||||||
implementation(Dependencies.AndroidX.Room.ktx)
|
implementation(Dependencies.AndroidX.Room.ktx)
|
||||||
implementation(Dependencies.ThirdParty.Roomigrant.runtime)
|
|
||||||
implementation(Dependencies.AndroidX.material)
|
|
||||||
implementation(Dependencies.AndroidX.Hilt.dagger)
|
|
||||||
implementation(Dependencies.ThirdParty.accompanist)
|
|
||||||
implementation(Dependencies.Kotlin.Coroutines.android)
|
implementation(Dependencies.Kotlin.Coroutines.android)
|
||||||
|
implementation(Dependencies.Kotlin.Ktor.clientCore)
|
||||||
|
implementation(Dependencies.Kotlin.Ktor.clientJson)
|
||||||
|
implementation(Dependencies.Kotlin.Ktor.clientOkHttp)
|
||||||
|
implementation(Dependencies.Kotlin.Ktor.clientSerialization)
|
||||||
implementation(Dependencies.Kotlin.Serialization.json)
|
implementation(Dependencies.Kotlin.Serialization.json)
|
||||||
|
implementation(Dependencies.ThirdParty.accompanist)
|
||||||
implementation(Dependencies.ThirdParty.customtabs)
|
implementation(Dependencies.ThirdParty.customtabs)
|
||||||
androidTestImplementation(Dependencies.Testing.daggerHilt)
|
implementation(Dependencies.ThirdParty.Roomigrant.runtime)
|
||||||
testImplementation(Dependencies.Testing.junit)
|
testImplementation(Dependencies.Testing.junit)
|
||||||
|
testImplementation(Dependencies.Kotlin.Ktor.clientTest)
|
||||||
|
androidTestImplementation(Dependencies.Testing.daggerHilt)
|
||||||
androidTestImplementation(Dependencies.Testing.uiTest)
|
androidTestImplementation(Dependencies.Testing.uiTest)
|
||||||
}
|
}
|
||||||
|
|
4
app/proguard-rules.pro
vendored
4
app/proguard-rules.pro
vendored
|
@ -8,3 +8,7 @@
|
||||||
-keepclasseswithmembers class dev.msfjarvis.lobsters.model.** {
|
-keepclasseswithmembers class dev.msfjarvis.lobsters.model.** {
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Inline-based optimizations cause reflection to fail within Ktor (from what I can tell), so we turn
|
||||||
|
# this off for now.
|
||||||
|
-dontoptimize
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package dev.msfjarvis.lobsters.data.api
|
||||||
|
|
||||||
|
import dev.msfjarvis.lobsters.data.api.LobstersApi.Companion.BASE_URL
|
||||||
|
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ktor backed implementation of [LobstersApi]
|
||||||
|
*/
|
||||||
|
class KtorLobstersApi @Inject constructor(private val client: HttpClient) : LobstersApi {
|
||||||
|
override suspend fun getHottestPosts(page: Int): List<LobstersPost> {
|
||||||
|
return client.get("${BASE_URL}/hottest.json?page=$page")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package dev.msfjarvis.lobsters.data.api
|
||||||
|
|
||||||
|
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple interface defining an API for lobste.rs
|
||||||
|
*/
|
||||||
|
interface LobstersApi {
|
||||||
|
|
||||||
|
suspend fun getHottestPosts(page: Int): List<LobstersPost>
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val BASE_URL = "https://lobste.rs"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
package dev.msfjarvis.lobsters.injection
|
|
||||||
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.android.components.ActivityComponent
|
|
||||||
import dev.msfjarvis.lobsters.api.ApiClient
|
|
||||||
import dev.msfjarvis.lobsters.api.LobstersApi
|
|
||||||
|
|
||||||
@InstallIn(ActivityComponent::class)
|
|
||||||
@Module
|
|
||||||
object ApiModule {
|
|
||||||
const val LOBSTERS_URL = "https://lobste.rs"
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
fun provideLobstersApi(): LobstersApi {
|
|
||||||
return ApiClient.getClient(LOBSTERS_URL)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.msfjarvis.lobsters.injection
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.components.ActivityComponent
|
||||||
|
import dev.msfjarvis.lobsters.data.api.KtorLobstersApi
|
||||||
|
import dev.msfjarvis.lobsters.data.api.LobstersApi
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(ActivityComponent::class)
|
||||||
|
abstract class KtorApiModule {
|
||||||
|
@Binds abstract fun bindLobstersApi(realApi: KtorLobstersApi): LobstersApi
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package dev.msfjarvis.lobsters.injection
|
||||||
|
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.components.ApplicationComponent
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.engine.okhttp.OkHttp
|
||||||
|
import io.ktor.client.features.json.JsonFeature
|
||||||
|
import io.ktor.client.features.json.serializer.KotlinxSerializer
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(ApplicationComponent::class)
|
||||||
|
object KtorClientModule {
|
||||||
|
@Provides
|
||||||
|
fun provideClient() = HttpClient(OkHttp) {
|
||||||
|
install(JsonFeature) {
|
||||||
|
serializer = KotlinxSerializer()
|
||||||
|
}
|
||||||
|
engine {
|
||||||
|
config {
|
||||||
|
followSslRedirects(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,8 +9,8 @@ import dagger.hilt.android.qualifiers.ActivityContext
|
||||||
import dev.msfjarvis.lobsters.ui.urllauncher.UrlLauncher
|
import dev.msfjarvis.lobsters.ui.urllauncher.UrlLauncher
|
||||||
import dev.msfjarvis.lobsters.ui.urllauncher.UrlLauncherImpl
|
import dev.msfjarvis.lobsters.ui.urllauncher.UrlLauncherImpl
|
||||||
|
|
||||||
@InstallIn(ActivityComponent::class)
|
|
||||||
@Module
|
@Module
|
||||||
|
@InstallIn(ActivityComponent::class)
|
||||||
object UrlLauncherModule {
|
object UrlLauncherModule {
|
||||||
@Provides
|
@Provides
|
||||||
fun provideUrlLauncher(@ActivityContext context: Context): UrlLauncher {
|
fun provideUrlLauncher(@ActivityContext context: Context): UrlLauncher {
|
||||||
|
|
|
@ -28,7 +28,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.ui.tooling.preview.Preview
|
import androidx.ui.tooling.preview.Preview
|
||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
import dev.chrisbanes.accompanist.coil.CoilImage
|
import dev.chrisbanes.accompanist.coil.CoilImage
|
||||||
import dev.msfjarvis.lobsters.injection.ApiModule
|
|
||||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||||
import dev.msfjarvis.lobsters.model.Submitter
|
import dev.msfjarvis.lobsters.model.Submitter
|
||||||
import dev.msfjarvis.lobsters.ui.theme.LobstersTheme
|
import dev.msfjarvis.lobsters.ui.theme.LobstersTheme
|
||||||
|
@ -96,7 +95,7 @@ fun LazyItemScope.LobstersItem(
|
||||||
modifier = Modifier.wrapContentHeight(),
|
modifier = Modifier.wrapContentHeight(),
|
||||||
) {
|
) {
|
||||||
CoilImage(
|
CoilImage(
|
||||||
data = "${ApiModule.LOBSTERS_URL}/${post.submitterUser.avatarUrl}",
|
data = "https://lobste.rs/${post.submitterUser.avatarUrl}",
|
||||||
fadeIn = true,
|
fadeIn = true,
|
||||||
requestBuilder = {
|
requestBuilder = {
|
||||||
transformations(CircleCropTransformation())
|
transformations(CircleCropTransformation())
|
||||||
|
|
|
@ -3,7 +3,7 @@ package dev.msfjarvis.lobsters.ui.viewmodel
|
||||||
import androidx.hilt.lifecycle.ViewModelInject
|
import androidx.hilt.lifecycle.ViewModelInject
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dev.msfjarvis.lobsters.api.LobstersApi
|
import dev.msfjarvis.lobsters.data.api.LobstersApi
|
||||||
import dev.msfjarvis.lobsters.data.source.PostsDatabase
|
import dev.msfjarvis.lobsters.data.source.PostsDatabase
|
||||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
@ -57,7 +57,6 @@ class LobstersViewModel @ViewModelInject constructor(
|
||||||
private fun getMorePostsInternal(firstLoad: Boolean) {
|
private fun getMorePostsInternal(firstLoad: Boolean) {
|
||||||
viewModelScope.launch(coroutineExceptionHandler) {
|
viewModelScope.launch(coroutineExceptionHandler) {
|
||||||
val newPosts = lobstersApi.getHottestPosts(apiPage)
|
val newPosts = lobstersApi.getHottestPosts(apiPage)
|
||||||
.toList()
|
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
_posts.value = newPosts
|
_posts.value = newPosts
|
||||||
postsDao.deleteAllPosts()
|
postsDao.deleteAllPosts()
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
package dev.msfjarvis.lobsters.data.api
|
||||||
|
|
||||||
|
import dev.msfjarvis.lobsters.util.TestUtils
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.engine.mock.MockEngine
|
||||||
|
import io.ktor.client.engine.mock.respond
|
||||||
|
import io.ktor.client.features.json.JsonFeature
|
||||||
|
import io.ktor.client.features.json.serializer.KotlinxSerializer
|
||||||
|
import io.ktor.http.fullPath
|
||||||
|
import io.ktor.http.headersOf
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.AfterClass
|
||||||
|
import org.junit.BeforeClass
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class KtorLobstersApiTest {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
private lateinit var client: HttpClient
|
||||||
|
@JvmStatic
|
||||||
|
private lateinit var apiClient: LobstersApi
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BeforeClass
|
||||||
|
fun setUp() {
|
||||||
|
client = HttpClient(MockEngine) {
|
||||||
|
install(JsonFeature) {
|
||||||
|
serializer = KotlinxSerializer()
|
||||||
|
}
|
||||||
|
engine {
|
||||||
|
addHandler { request ->
|
||||||
|
when (request.url.fullPath) {
|
||||||
|
"/hottest.json?page=1" -> {
|
||||||
|
val responseHeaders = headersOf("Content-Type" to listOf("application/json"))
|
||||||
|
respond(TestUtils.getJson("hottest.json"), headers = responseHeaders)
|
||||||
|
}
|
||||||
|
else -> error("Unhandled ${request.url.fullPath}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apiClient = KtorLobstersApi(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@AfterClass
|
||||||
|
fun tearDown() {
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `api gets correct number of items`() = runBlocking {
|
||||||
|
val posts = apiClient.getHottestPosts(1)
|
||||||
|
assertEquals(25, posts.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `no moderator posts in test data`() = runBlocking {
|
||||||
|
val posts = apiClient.getHottestPosts(1)
|
||||||
|
val moderatorPosts = posts.asSequence()
|
||||||
|
.filter { it.submitterUser.isModerator }
|
||||||
|
.toSet()
|
||||||
|
assertTrue(moderatorPosts.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `posts with no urls`() = runBlocking {
|
||||||
|
val posts = apiClient.getHottestPosts(1)
|
||||||
|
val commentsOnlyPosts = posts.asSequence()
|
||||||
|
.filter { it.url.isEmpty() }
|
||||||
|
.toSet()
|
||||||
|
assertEquals(2, commentsOnlyPosts.size)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.msfjarvis.lobsters.api
|
package dev.msfjarvis.lobsters.util
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
|
@ -17,6 +17,16 @@ object Dependencies {
|
||||||
const val core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"
|
const val core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$version"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object Ktor {
|
||||||
|
|
||||||
|
private const val version = "1.4.0"
|
||||||
|
const val clientCore = "io.ktor:ktor-client-core:$version"
|
||||||
|
const val clientJson = "io.ktor:ktor-client-json:$version"
|
||||||
|
const val clientSerialization = "io.ktor:ktor-client-serialization:$version"
|
||||||
|
const val clientOkHttp = "io.ktor:ktor-client-okhttp:$version"
|
||||||
|
const val clientTest = "io.ktor:ktor-client-mock:$version"
|
||||||
|
}
|
||||||
|
|
||||||
object Serialization {
|
object Serialization {
|
||||||
|
|
||||||
private const val version = "1.0.1"
|
private const val version = "1.0.1"
|
||||||
|
@ -77,13 +87,6 @@ object Dependencies {
|
||||||
|
|
||||||
const val accompanist = "dev.chrisbanes.accompanist:accompanist-coil:0.3.2"
|
const val accompanist = "dev.chrisbanes.accompanist:accompanist-coil:0.3.2"
|
||||||
const val customtabs = "saschpe.android:customtabs:3.0.2"
|
const val customtabs = "saschpe.android:customtabs:3.0.2"
|
||||||
const val retrofitSerialization = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"
|
|
||||||
|
|
||||||
object Retrofit {
|
|
||||||
|
|
||||||
private const val version = "2.9.0"
|
|
||||||
const val lib = "com.squareup.retrofit2:retrofit:$version"
|
|
||||||
}
|
|
||||||
|
|
||||||
object Roomigrant {
|
object Roomigrant {
|
||||||
|
|
||||||
|
@ -98,7 +101,6 @@ object Dependencies {
|
||||||
|
|
||||||
const val daggerHilt = "com.google.dagger:hilt-android-testing:$DAGGER_HILT_VERSION"
|
const val daggerHilt = "com.google.dagger:hilt-android-testing:$DAGGER_HILT_VERSION"
|
||||||
const val junit = "junit:junit:4.13.1"
|
const val junit = "junit:junit:4.13.1"
|
||||||
const val mockWebServer = "com.squareup.okhttp3:mockwebserver:3.14.9"
|
|
||||||
const val uiTest = "androidx.ui:ui-test:$COMPOSE_VERSION"
|
const val uiTest = "androidx.ui:ui-test:$COMPOSE_VERSION"
|
||||||
|
|
||||||
object AndroidX {
|
object AndroidX {
|
||||||
|
|
1
lobsters-api/.gitignore
vendored
1
lobsters-api/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
/build
|
|
|
@ -1,16 +0,0 @@
|
||||||
plugins {
|
|
||||||
id("com.android.library")
|
|
||||||
kotlin("android")
|
|
||||||
kotlin("plugin.serialization") version "1.4.10"
|
|
||||||
`lobsters-plugin`
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":model"))
|
|
||||||
implementation(Dependencies.Kotlin.Serialization.json)
|
|
||||||
implementation(Dependencies.ThirdParty.Retrofit.lib)
|
|
||||||
implementation(Dependencies.ThirdParty.retrofitSerialization)
|
|
||||||
testImplementation(Dependencies.Testing.junit)
|
|
||||||
testImplementation(Dependencies.Kotlin.Coroutines.core)
|
|
||||||
testImplementation(Dependencies.Testing.mockWebServer)
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest package="dev.msfjarvis.lobsters.api" />
|
|
|
@ -1,16 +0,0 @@
|
||||||
package dev.msfjarvis.lobsters.api
|
|
||||||
|
|
||||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.MediaType
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
|
|
||||||
object ApiClient {
|
|
||||||
inline fun <reified T> getClient(baseUrl: String): T {
|
|
||||||
return Retrofit.Builder()
|
|
||||||
.baseUrl(baseUrl)
|
|
||||||
.addConverterFactory(Json.asConverterFactory(MediaType.get("application/json")))
|
|
||||||
.build()
|
|
||||||
.create(T::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
package dev.msfjarvis.lobsters.api
|
|
||||||
|
|
||||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
|
||||||
import retrofit2.http.GET
|
|
||||||
import retrofit2.http.Query
|
|
||||||
|
|
||||||
interface LobstersApi {
|
|
||||||
@GET("hottest.json")
|
|
||||||
suspend fun getHottestPosts(@Query("page") page: Int): List<LobstersPost>
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
package dev.msfjarvis.lobsters.api
|
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import okhttp3.mockwebserver.Dispatcher
|
|
||||||
import okhttp3.mockwebserver.MockResponse
|
|
||||||
import okhttp3.mockwebserver.MockWebServer
|
|
||||||
import okhttp3.mockwebserver.RecordedRequest
|
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class LobstersApiTest {
|
|
||||||
private val webServer = MockWebServer()
|
|
||||||
private val apiData = TestUtils.getJson("hottest.json")
|
|
||||||
private val apiClient = ApiClient.getClient<LobstersApi>("http://localhost:8080")
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setUp() {
|
|
||||||
webServer.start(8080)
|
|
||||||
webServer.dispatcher = object : Dispatcher() {
|
|
||||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
|
||||||
return MockResponse().setBody(apiData).setResponseCode(200)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `api gets correct number of items`() = runBlocking {
|
|
||||||
val posts = apiClient.getHottestPosts(1)
|
|
||||||
assertEquals(25, posts.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `no moderator posts in test data`() = runBlocking {
|
|
||||||
val posts = apiClient.getHottestPosts(1)
|
|
||||||
val moderatorPosts = posts.asSequence()
|
|
||||||
.filter { it.submitterUser.isModerator }
|
|
||||||
.toSet()
|
|
||||||
assertTrue(moderatorPosts.isEmpty())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `posts with no urls`() = runBlocking {
|
|
||||||
val posts = apiClient.getHottestPosts(1)
|
|
||||||
val commentsOnlyPosts = posts.asSequence()
|
|
||||||
.filter { it.url.isEmpty() }
|
|
||||||
.toSet()
|
|
||||||
assertEquals(2, commentsOnlyPosts.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
webServer.shutdown()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +1,3 @@
|
||||||
rootProject.name = "lobste.rs"
|
rootProject.name = "lobste.rs"
|
||||||
include(":app", ":lobsters-api", ":model")
|
include(":app", ":model")
|
||||||
enableFeaturePreview("GRADLE_METADATA")
|
enableFeaturePreview("GRADLE_METADATA")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue