Merge pull request #22 from msfjarvis/pagination-prep

This commit is contained in:
Harsh Shandilya 2020-09-23 22:17:13 +05:30 committed by GitHub
commit 7b42915a1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 209 additions and 28 deletions

View file

@ -1,5 +1,4 @@
plugins { plugins {
id 'kotlin-android'
id 'kotlin-kapt' id 'kotlin-kapt'
id 'dagger.hilt.android.plugin' id 'dagger.hilt.android.plugin'
} }
@ -45,17 +44,24 @@ android {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions { kotlinOptions {
freeCompilerArgs += ["-Xallow-jvm-ir-dependencies", "-Xskip-prerelease-check", "-Xopt-in=kotlin.RequiresOptIn"] freeCompilerArgs += [
"-Xallow-jvm-ir-dependencies",
"-Xskip-prerelease-check",
"-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
]
} }
} }
dependencies { dependencies {
kapt "com.google.dagger:hilt-android-compiler:$hilt_version" kapt "com.google.dagger:hilt-android-compiler:$hilt_dagger_version"
kapt "androidx.hilt:hilt-compiler:$hilt_androidx_version"
implementation(project(":data")) implementation(project(":data"))
implementation(project(":lobsters-api")) implementation(project(":lobsters-api"))
implementation(project(":model")) implementation(project(":model"))
implementation 'androidx.core:core-ktx:1.5.0-alpha03' implementation 'androidx.core:core-ktx:1.5.0-alpha03'
implementation 'androidx.activity:activity-ktx:1.2.0-alpha08'
implementation 'androidx.appcompat:appcompat:1.3.0-alpha02' implementation 'androidx.appcompat:appcompat:1.3.0-alpha02'
implementation "androidx.compose.foundation:foundation:$compose_version" implementation "androidx.compose.foundation:foundation:$compose_version"
implementation "androidx.compose.foundation:foundation-layout:$compose_version" implementation "androidx.compose.foundation:foundation-layout:$compose_version"
@ -67,10 +73,14 @@ dependencies {
implementation "androidx.compose.ui:ui-text:$compose_version" implementation "androidx.compose.ui:ui-text:$compose_version"
implementation "androidx.compose.ui:ui-text-android:$compose_version" implementation "androidx.compose.ui:ui-text-android:$compose_version"
implementation "androidx.compose.ui:ui-unit:$compose_version" implementation "androidx.compose.ui:ui-unit:$compose_version"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:$hilt_androidx_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.ui:ui-tooling:$compose_version" implementation "androidx.ui:ui-tooling:$compose_version"
implementation 'com.google.android.material:material:1.3.0-alpha02' implementation 'com.google.android.material:material:1.3.0-alpha02'
implementation "com.google.dagger:hilt-android:$hilt_version" implementation "com.google.dagger:hilt-android:$hilt_dagger_version"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_dagger_version"
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
androidTestImplementation "androidx.ui:ui-test:$compose_version" androidTestImplementation "androidx.ui:ui-test:$compose_version"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.0.10" coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.0.10"

View file

@ -1,25 +1,24 @@
package dev.msfjarvis.lobsters package dev.msfjarvis.lobsters
import android.os.Bundle import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.Text import androidx.compose.foundation.Text
import androidx.compose.foundation.lazy.LazyColumnFor import androidx.compose.foundation.lazy.LazyColumnForIndexed
import androidx.compose.material.Scaffold import androidx.compose.material.Scaffold
import androidx.compose.material.TopAppBar import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Providers import androidx.compose.runtime.Providers
import androidx.compose.runtime.ambientOf import androidx.compose.runtime.ambientOf
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.setContent import androidx.compose.ui.platform.setContent
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.msfjarvis.lobsters.api.LobstersApi import dev.msfjarvis.lobsters.api.LobstersApi
import dev.msfjarvis.lobsters.model.LobstersPost import dev.msfjarvis.lobsters.data.LobstersViewModel
import dev.msfjarvis.lobsters.ui.LobstersItem import dev.msfjarvis.lobsters.ui.LobstersItem
import dev.msfjarvis.lobsters.ui.LobstersTheme import dev.msfjarvis.lobsters.ui.LobstersTheme
import dev.msfjarvis.lobsters.urllauncher.UrlLauncher import dev.msfjarvis.lobsters.urllauncher.UrlLauncher
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
val UrlLauncherAmbient = ambientOf<UrlLauncher> { error("Needs to be provided") } val UrlLauncherAmbient = ambientOf<UrlLauncher> { error("Needs to be provided") }
@ -28,18 +27,14 @@ val UrlLauncherAmbient = ambientOf<UrlLauncher> { error("Needs to be provided")
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@Inject lateinit var urlLauncher: UrlLauncher @Inject lateinit var urlLauncher: UrlLauncher
@Inject lateinit var apiClient: LobstersApi @Inject lateinit var apiClient: LobstersApi
private val viewModel: LobstersViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
Providers(UrlLauncherAmbient provides urlLauncher) { Providers(UrlLauncherAmbient provides urlLauncher) {
LobstersTheme { LobstersTheme {
val coroutineScope = rememberCoroutineScope() LobstersApp(viewModel)
val posts = mutableStateListOf<LobstersPost>()
coroutineScope.launch {
posts.addAll(apiClient.getHottestPosts())
}
LobstersApp(posts)
} }
} }
} }
@ -48,14 +43,19 @@ class MainActivity : AppCompatActivity() {
@Composable @Composable
fun LobstersApp( fun LobstersApp(
items: List<LobstersPost>, viewModel: LobstersViewModel
) { ) {
val urlLauncher = UrlLauncherAmbient.current val urlLauncher = UrlLauncherAmbient.current
val state = viewModel.posts.collectAsState()
val lastIndex = state.value.lastIndex
Scaffold( Scaffold(
topBar = { TopAppBar({ Text(text = stringResource(R.string.app_name)) }) }, topBar = { TopAppBar({ Text(text = stringResource(R.string.app_name)) }) },
bodyContent = { bodyContent = {
LazyColumnFor(items) { item -> LazyColumnForIndexed(state.value) { index, item ->
if (lastIndex == index) {
viewModel.getMorePosts()
}
LobstersItem(item) { post -> LobstersItem(item) { post ->
urlLauncher.launch(post.url) urlLauncher.launch(post.url)
} }

View file

@ -0,0 +1,30 @@
package dev.msfjarvis.lobsters.data
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.msfjarvis.lobsters.api.LobstersApi
import dev.msfjarvis.lobsters.model.LobstersPost
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class LobstersViewModel @ViewModelInject constructor(
private val lobstersApi: LobstersApi,
) : ViewModel() {
private var apiPage = 1
private val _posts = MutableStateFlow<List<LobstersPost>>(emptyList())
val posts: StateFlow<List<LobstersPost>> get() = _posts
init {
getMorePosts()
}
fun getMorePosts() {
viewModelScope.launch {
_posts.value += lobstersApi.getHottestPosts(apiPage)
apiPage += 1
}
}
}

View file

@ -0,0 +1,22 @@
package dev.msfjarvis.lobsters.di
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import dev.msfjarvis.lobsters.data.source.PostsDatabase
@Module
@InstallIn(ActivityComponent::class)
object PersistenceModule {
@Provides
fun providePostsDatabase(@ApplicationContext context: Context): PostsDatabase {
return Room.databaseBuilder(context, PostsDatabase::class.java, "posts.db")
.fallbackToDestructiveMigration()
.build()
}
}

View file

@ -2,9 +2,13 @@
buildscript { buildscript {
ext { ext {
compose_version = '1.0.0-alpha03' compose_version = '1.0.0-alpha03'
coroutines_version = '1.3.9'
dagger_version = '2.29.1' dagger_version = '2.29.1'
hilt_version = '2.29-alpha' hilt_androidx_version = '1.0.0-alpha02'
hilt_dagger_version = '2.29-alpha'
kotlin_version = '1.4.10' kotlin_version = '1.4.10'
lifecycle_version = '2.3.0-alpha07'
moshi_version = '1.9.3'
room_version = '2.3.0-alpha02' room_version = '2.3.0-alpha02'
} }
repositories { repositories {
@ -14,7 +18,7 @@ buildscript {
dependencies { dependencies {
classpath "com.android.tools.build:gradle:4.2.0-alpha12" classpath "com.android.tools.build:gradle:4.2.0-alpha12"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_dagger_version"
} }
} }
@ -35,6 +39,7 @@ subprojects {
} else { } else {
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
} }
apply plugin: 'kotlin-android'
android { android {
compileSdkVersion 30 compileSdkVersion 30
@ -49,6 +54,13 @@ subprojects {
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
} }
configurations.all {
resolutionStrategy {
force "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
force "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
force "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
}
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions { kotlinOptions {
jvmTarget = '1.8' jvmTarget = '1.8'

View file

@ -1,10 +1,12 @@
plugins { plugins {
id 'kotlin-android'
id 'kotlin-kapt' id 'kotlin-kapt'
} }
dependencies { dependencies {
implementation project(":model")
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
api "androidx.room:room-runtime:$room_version" api "androidx.room:room-runtime:$room_version"
api "androidx.room:room-ktx:$room_version" implementation "androidx.room:room-ktx:$room_version"
implementation "com.squareup.moshi:moshi:$moshi_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
} }

View file

@ -0,0 +1,63 @@
package dev.msfjarvis.lobsters.data.source
import androidx.room.TypeConverter
import com.squareup.moshi.Moshi
import dev.msfjarvis.lobsters.model.KeybaseSignature
import dev.msfjarvis.lobsters.model.KeybaseSignatureJsonAdapter
import dev.msfjarvis.lobsters.model.LobstersPost
import dev.msfjarvis.lobsters.model.LobstersPostJsonAdapter
import dev.msfjarvis.lobsters.model.Submitter
import dev.msfjarvis.lobsters.model.SubmitterJsonAdapter
object LobstersApiTypeConverters {
private val moshi = Moshi.Builder().build()
private const val SEPARATOR = ","
@TypeConverter
@JvmStatic
fun toSubmitterUser(value: String?): Submitter? {
return value?.let { SubmitterJsonAdapter(moshi).fromJson(value) }
}
@TypeConverter
@JvmStatic
fun fromSubmitterUser(value: Submitter?): String? {
return value?.let { SubmitterJsonAdapter(moshi).toJson(value) }
}
@TypeConverter
@JvmStatic
fun toKeybaseSignature(value: String?): KeybaseSignature? {
return value?.let { KeybaseSignatureJsonAdapter(moshi).fromJson(value) }
}
@TypeConverter
@JvmStatic
fun fromKeybaseSignature(value: KeybaseSignature?): String? {
return value?.let { KeybaseSignatureJsonAdapter(moshi).toJson(value) }
}
@TypeConverter
@JvmStatic
fun toLobstersPost(value: String?): LobstersPost? {
return value?.let { LobstersPostJsonAdapter(moshi).fromJson(value) }
}
@TypeConverter
@JvmStatic
fun fromLobstersPost(value: LobstersPost?): String? {
return value?.let { LobstersPostJsonAdapter(moshi).toJson(value) }
}
@TypeConverter
@JvmStatic
fun toTagList(value: String?): List<String>? {
return value?.split(SEPARATOR)
}
@TypeConverter
@JvmStatic
fun fromTagList(value: List<String>?): String? {
return value?.joinToString(SEPARATOR)
}
}

View file

@ -0,0 +1,23 @@
package dev.msfjarvis.lobsters.data.source
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import dev.msfjarvis.lobsters.model.LobstersPost
import kotlinx.coroutines.flow.Flow
@Dao
abstract class PostsDao {
@Query("SELECT * FROM lobsters_posts")
abstract fun loadPosts(): Flow<List<LobstersPost>>
@Insert
abstract suspend fun insertPosts(vararg posts: LobstersPost)
@Delete
abstract suspend fun deletePosts(vararg posts: LobstersPost)
@Query("DELETE FROM lobsters_posts")
abstract suspend fun deleteAllPosts()
}

View file

@ -0,0 +1,21 @@
package dev.msfjarvis.lobsters.data.source
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import dev.msfjarvis.lobsters.model.LobstersPost
@Database(
entities = [
LobstersPost::class,
],
version = 1,
exportSchema = false,
)
@TypeConverters(
LobstersApiTypeConverters::class,
DateTimeTypeConverters::class,
)
abstract class PostsDatabase : RoomDatabase() {
abstract fun postsDao(): PostsDao
}

View file

@ -1,5 +1,4 @@
plugins { plugins {
id 'kotlin-android'
id 'kotlin-kapt' id 'kotlin-kapt'
} }

View file

@ -2,8 +2,9 @@ package dev.msfjarvis.lobsters.api
import dev.msfjarvis.lobsters.model.LobstersPost import dev.msfjarvis.lobsters.model.LobstersPost
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Query
interface LobstersApi { interface LobstersApi {
@GET("hottest.json") @GET("hottest.json")
suspend fun getHottestPosts(): List<LobstersPost> suspend fun getHottestPosts(@Query("page") page: Int): List<LobstersPost>
} }

View file

@ -27,7 +27,7 @@ class LobstersApiTest {
@Test @Test
fun `api gets correct number of items`() = runBlocking { fun `api gets correct number of items`() = runBlocking {
val posts = apiClient.getHottestPosts() val posts = apiClient.getHottestPosts(1)
assertEquals(25, posts.size) assertEquals(25, posts.size)
} }

View file

@ -1,10 +1,8 @@
plugins { plugins {
id 'kotlin-android'
id 'kotlin-kapt' id 'kotlin-kapt'
} }
dependencies { dependencies {
def moshi_version = "1.9.3"
api "androidx.room:room-runtime:$room_version" api "androidx.room:room-runtime:$room_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
implementation "com.squareup.moshi:moshi:$moshi_version" implementation "com.squareup.moshi:moshi:$moshi_version"

View file

@ -13,7 +13,7 @@ class Submitter(
val about: String, val about: String,
@Json(name = "is_moderator") @Json(name = "is_moderator")
val isModerator: Boolean, val isModerator: Boolean,
val karma: Long, val karma: Long = 0,
@Json(name = "avatar_url") @Json(name = "avatar_url")
val avatarUrl: String, val avatarUrl: String,
@Json(name = "invited_by_user") @Json(name = "invited_by_user")