Skip to content

Commit

Permalink
Feed Loading Strategy
Browse files Browse the repository at this point in the history
This is an idea I have to load the feed lazily given the current infrastructure.

When we request a feed, we have a list of ID's, so we can populate our LazyList with a bunch of Loading State items.

Whenever I Loading State item comes into the Composition, it launches a `SideEffect` which will call up to the ViewModel to fetch that item.
  • Loading branch information
Rahkeen committed Jul 9, 2024
1 parent 89c81f6 commit 904966b
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ import android.content.Context
import com.emergetools.hackernews.data.HackerNewsBaseClient
import com.emergetools.hackernews.data.HackerNewsSearchClient
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import java.time.Duration

class HackerNewsApplication: Application() {
private val json = Json { ignoreUnknownKeys = true }
private val httpClient = OkHttpClient.Builder()
.readTimeout(Duration.ofSeconds(30))
.build()

val baseClient = HackerNewsBaseClient(json)
val searchClient = HackerNewsSearchClient(json)
val baseClient = HackerNewsBaseClient(json, httpClient)
val searchClient = HackerNewsSearchClient(json, httpClient)
}

fun Context.baseClient(): HackerNewsBaseClient {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.emergetools.hackernews.data
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
import retrofit2.http.GET
Expand Down Expand Up @@ -34,10 +35,11 @@ interface HackerNewsBaseApi {
suspend fun getItem(@Path("id") itemId: Long): Item
}

class HackerNewsBaseClient(json: Json) {
class HackerNewsBaseClient(json: Json, client: OkHttpClient) {
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_FIREBASE_URL)
.addConverterFactory(json.asConverterFactory("application/json; charset=UTF8".toMediaType()))
.client(client)
.build()

val api = retrofit.create(HackerNewsBaseApi::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.emergetools.hackernews.data
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.kotlinx.serialization.asConverterFactory
import retrofit2.http.GET
Expand All @@ -26,10 +27,11 @@ interface HackerNewsAlgoliaApi {
suspend fun getItem(@Path("id") itemId: Long): ItemResponse
}

class HackerNewsSearchClient(json: Json) {
class HackerNewsSearchClient(json: Json, client: OkHttpClient) {
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_SEARCH_URL)
.addConverterFactory(json.asConverterFactory("application/json; charset=UTF8".toMediaType()))
.client(client)
.build()

val api: HackerNewsAlgoliaApi = retrofit.create(HackerNewsAlgoliaApi::class.java)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.emergetools.hackernews.features.stories

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.emergetools.hackernews.data.HackerNewsBaseClient
import com.emergetools.hackernews.features.comments.CommentsDestinations
import com.emergetools.hackernews.features.stories.StoriesAction.LoadStories
import com.emergetools.hackernews.features.stories.StoriesAction.LoadFeedIds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand All @@ -20,7 +21,6 @@ enum class FeedType(val label: String) {
data class StoriesState(
val stories: List<StoryItem>,
val feed: FeedType = FeedType.Top

)

sealed class StoryItem(open val id: Long) {
Expand All @@ -36,7 +36,8 @@ sealed class StoryItem(open val id: Long) {
}

sealed class StoriesAction {
data object LoadStories : StoriesAction()
data object LoadFeedIds : StoriesAction()
data class LoadStory(val id: Long): StoriesAction()
data class SelectStory(val id: Long) : StoriesAction()
data class SelectComments(val id: Long) : StoriesAction()
data class SelectFeed(val feed: FeedType) : StoriesAction()
Expand All @@ -53,12 +54,12 @@ class StoriesViewModel(private val baseClient: HackerNewsBaseClient) : ViewModel
val state = internalState.asStateFlow()

init {
actions(LoadStories)
actions(LoadFeedIds)
}

fun actions(action: StoriesAction) {
when (action) {
LoadStories -> {
LoadFeedIds -> {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val ids = when(internalState.value.feed) {
Expand All @@ -70,33 +71,11 @@ class StoriesViewModel(private val baseClient: HackerNewsBaseClient) : ViewModel
}
}

// now for each ID I need to load the item.
internalState.update { current ->
current.copy(
stories = ids.map { StoryItem.Loading(it) }
)
}
ids.forEach { id ->
val item = baseClient.api.getItem(id)
internalState.update { current ->
current.copy(
stories = current.stories.map {
if (it.id == item.id) {
StoryItem.Content(
id = item.id,
title = item.title!!,
author = item.by!!,
score = item.score ?: 0,
commentCount = item.descendants ?: 0,
url = item.url
)
} else {
it
}
}
)
}
}
}
}
}
Expand All @@ -116,7 +95,32 @@ class StoriesViewModel(private val baseClient: HackerNewsBaseClient) : ViewModel
stories = emptyList()
)
}
actions(LoadStories)
actions(LoadFeedIds)
}

is StoriesAction.LoadStory -> {
viewModelScope.launch(Dispatchers.IO) {
val item = baseClient.api.getItem(action.id)
Log.d("Feed", "Loaded item ${action.id}")
internalState.update { current ->
current.copy(
stories = current.stories.map {
if (it.id == item.id) {
StoryItem.Content(
id = item.id,
title = item.title!!,
author = item.by!!,
score = item.score ?: 0,
commentCount = item.descendants ?: 0,
url = item.url
)
} else {
it
}
}
)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.emergetools.hackernews.features.stories

import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
Expand All @@ -17,13 +18,15 @@ import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -51,6 +54,7 @@ fun StoriesScreen(
actions: (StoriesAction) -> Unit,
navigation: (StoriesNavigation) -> Unit
) {
val listState = rememberLazyListState()
Column(
modifier = modifier.background(color = MaterialTheme.colorScheme.background),
horizontalAlignment = Alignment.CenterHorizontally,
Expand All @@ -61,6 +65,7 @@ fun StoriesScreen(
onSelected = { actions(StoriesAction.SelectFeed(it)) }
)
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
Expand Down Expand Up @@ -90,6 +95,9 @@ fun StoriesScreen(
comments = CommentsDestinations.Comments(it.id)
)
)
},
onLoadRequested = {
actions(StoriesAction.LoadStory(it.id))
}
)
}
Expand Down Expand Up @@ -209,7 +217,8 @@ private fun StoryRowPreview() {
url = ""
),
onClick = {},
onCommentClicked = {}
onCommentClicked = {},
onLoadRequested = {}
)
}
}
Expand All @@ -221,7 +230,8 @@ private fun StoryRowLoadingPreview() {
StoryRow(
item = StoryItem.Loading(id = 1L),
onClick = {},
onCommentClicked = {}
onCommentClicked = {},
onLoadRequested = {}
)
}
}
Expand All @@ -231,7 +241,8 @@ fun StoryRow(
modifier: Modifier = Modifier,
item: StoryItem,
onClick: (StoryItem.Content) -> Unit,
onCommentClicked: (StoryItem.Content) -> Unit
onCommentClicked: (StoryItem.Content) -> Unit,
onLoadRequested: (StoryItem.Loading) -> Unit
) {
when (item) {
is StoryItem.Content -> {
Expand Down Expand Up @@ -311,14 +322,14 @@ fun StoryRow(
Box(
modifier = Modifier
.fillMaxWidth(0.8f)
.height(20.dp)
.height(18.dp)
.clip(CircleShape)
.background(color = Color.LightGray)
)
Box(
modifier = Modifier
.fillMaxWidth(0.45f)
.height(20.dp)
.height(18.dp)
.clip(CircleShape)
.background(color = Color.Gray)
)
Expand All @@ -343,6 +354,10 @@ fun StoryRow(
}
}
}

SideEffect {
onLoadRequested(item)
}
}
}
}

0 comments on commit 904966b

Please sign in to comment.