Refactor Page into smaller component

This commit is contained in:
Thomas Andres Gomez 2022-04-22 11:22:41 +02:00
parent 0ee7de3cde
commit de64718e10
18 changed files with 286 additions and 228 deletions

View file

@ -8,8 +8,8 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.pixelized.biblib.ui.screen.home.BooksPage
import com.pixelized.biblib.ui.screen.home.NewsPage
import com.pixelized.biblib.ui.screen.home.page.books.BooksPage
import com.pixelized.biblib.ui.screen.home.page.news.NewsPage
val LocalPageNavHostController = compositionLocalOf<NavHostController> {
error("LocalHomePageNavHostController is not ready yet.")

View file

@ -6,7 +6,18 @@ sealed class Screen(
object Authentication : Screen(
route = "authentication"
)
object Home : Screen(
route = "home"
)
class BookDetail(id: Int) : Screen(
route = "$ROOT/$id"
) {
companion object {
const val ROOT = "detail"
const val ARG_BOOK_ID = "id"
const val route = "$ROOT/{$ARG_BOOK_ID}"
}
}
}

View file

@ -9,6 +9,7 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.pixelized.biblib.ui.screen.authentication.AuthenticationScreen
import com.pixelized.biblib.ui.screen.detail.DetailScreen
import com.pixelized.biblib.ui.screen.home.HomeScreen
val LocalScreenNavHostController = compositionLocalOf<NavHostController> {
@ -33,6 +34,9 @@ fun ScreenNavHost(
composable(Screen.Home.route) {
HomeScreen()
}
composable(Screen.BookDetail.route) {
DetailScreen()
}
}
}
}

View file

@ -0,0 +1,30 @@
package com.pixelized.biblib.ui.screen.detail
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.ui.old.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.theme.BibLibTheme
@Composable
fun DetailScreen(
booksViewModel: BooksViewModel = hiltViewModel()
) {
}
@Composable
private fun DetailScreenContent() {
}
@Composable
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
private fun DetailScreenContentPreview() {
BibLibTheme {
DetailScreenContent()
}
}

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.screen.home.item
package com.pixelized.biblib.ui.screen.home.common.composable
import android.content.res.Configuration
import androidx.compose.animation.*
@ -16,8 +16,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.screen.home.uio.BookThumbnailUio
import com.pixelized.biblib.ui.screen.home.uio.CoverUio
import com.pixelized.biblib.ui.screen.home.common.uio.BookThumbnailUio
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.utils.extention.bibLib

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.screen.home.common
package com.pixelized.biblib.ui.screen.home.common.composable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
@ -19,9 +19,8 @@ import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.screen.home.item.BookThumbnail
import com.pixelized.biblib.ui.screen.home.uio.BookThumbnailUio
import com.pixelized.biblib.ui.screen.home.uio.CoverUio
import com.pixelized.biblib.ui.screen.home.common.uio.BookThumbnailUio
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.utils.extention.bibLib
import kotlinx.coroutines.flow.flowOf

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.screen.home.uio
package com.pixelized.biblib.ui.screen.home.common.uio
import androidx.compose.runtime.State

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.screen.home.uio
package com.pixelized.biblib.ui.screen.home.common.uio
import com.pixelized.biblib.network.client.IBibLibClient.Companion.COVER_URL
import java.net.URL

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.screen.home.uio
package com.pixelized.biblib.ui.screen.home.common.uio
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable

View file

@ -0,0 +1,34 @@
package com.pixelized.biblib.ui.screen.home.common.viewModel
import android.app.Application
import androidx.compose.ui.graphics.painter.Painter
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.R
import com.pixelized.biblib.utils.BitmapCache
import com.pixelized.biblib.utils.extention.context
import com.pixelized.biblib.utils.painterResource
import kotlinx.coroutines.CoroutineScope
import java.net.URL
abstract class ACoverViewModel(
application: Application,
private val cache: BitmapCache,
) : AndroidViewModel(application) {
fun cover(
cache: BitmapCache = this.cache,
coroutineScope: CoroutineScope = viewModelScope,
placeHolder: Painter = painterResource(context, R.drawable.ic_baseline_auto_stories_24),
url: URL,
) = cache.download(
placeHolder = placeHolder,
coroutineScope = coroutineScope,
url = url,
)
companion object {
const val PAGING_SIZE = 30
}
}

View file

@ -1,11 +1,11 @@
package com.pixelized.biblib.ui.screen.home
package com.pixelized.biblib.ui.screen.home.page.books
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.ui.screen.home.common.LazyBookThumbnailColumn
import com.pixelized.biblib.ui.screen.home.viewModel.BooksViewModel
import com.pixelized.biblib.ui.screen.home.page.news.NewsPage
import com.pixelized.biblib.ui.screen.home.common.composable.LazyBookThumbnailColumn
import com.pixelized.biblib.ui.theme.BibLibTheme
@Composable

View file

@ -0,0 +1,46 @@
package com.pixelized.biblib.ui.screen.home.page.books
import android.app.Application
import androidx.compose.runtime.Composable
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.compose.collectAsLazyPagingItems
import com.pixelized.biblib.model.book.Book
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.repository.book.IBookRepository
import com.pixelized.biblib.ui.screen.home.common.uio.BookThumbnailUio
import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel
import com.pixelized.biblib.utils.BitmapCache
import com.pixelized.biblib.utils.extention.longDate
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import java.net.URL
import javax.inject.Inject
@HiltViewModel
class BooksViewModel @Inject constructor(
application: Application,
bookRepository: IBookRepository,
cache: BitmapCache,
) : ACoverViewModel(application, cache) {
private val booksSource = Pager(
config = PagingConfig(pageSize = PAGING_SIZE),
pagingSourceFactory = bookRepository.getBooksSource()
.map { it.toThumbnail() }
.asPagingSourceFactory(Dispatchers.IO)
).flow
val books @Composable get() = booksSource.collectAsLazyPagingItems()
private fun Book.toThumbnail() = BookThumbnailUio(
id = id,
genre = genre?.joinToString { it.name } ?: "",
title = title,
author = author.joinToString { it.name },
date = releaseDate.longDate(),
isNew = isNew,
cover = cover(url = URL("${IBibLibClient.THUMBNAIL_URL}/$id.jpg"))
)
}

View file

@ -0,0 +1,48 @@
package com.pixelized.biblib.ui.screen.home.page.news
import android.app.Application
import androidx.compose.runtime.Composable
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import com.pixelized.biblib.model.book.Book
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.repository.book.IBookRepository
import com.pixelized.biblib.ui.screen.home.common.uio.BookThumbnailUio
import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel
import com.pixelized.biblib.utils.BitmapCache
import com.pixelized.biblib.utils.extention.longDate
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import java.net.URL
import javax.inject.Inject
@HiltViewModel
class NewsBookViewModel @Inject constructor(
application: Application,
bookRepository: IBookRepository,
cache: BitmapCache,
) : ACoverViewModel(application, cache) {
private val newsSource: Flow<PagingData<BookThumbnailUio>> = Pager(
config = PagingConfig(pageSize = PAGING_SIZE),
pagingSourceFactory = bookRepository.getNewsSource()
.map { it.toThumbnail() }
.asPagingSourceFactory(Dispatchers.IO)
).flow
val news @Composable get() = newsSource.collectAsLazyPagingItems()
private fun Book.toThumbnail() = BookThumbnailUio(
id = id,
genre = genre?.joinToString { it.name } ?: "",
title = title,
author = author.joinToString { it.name },
date = releaseDate.longDate(),
isNew = isNew,
cover = cover(url = URL("${IBibLibClient.THUMBNAIL_URL}/$id.jpg"))
)
}

View file

@ -1,16 +1,15 @@
package com.pixelized.biblib.ui.screen.home
package com.pixelized.biblib.ui.screen.home.page.news
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.ui.screen.home.common.LazyBookThumbnailColumn
import com.pixelized.biblib.ui.screen.home.viewModel.BooksViewModel
import com.pixelized.biblib.ui.screen.home.common.composable.LazyBookThumbnailColumn
import com.pixelized.biblib.ui.theme.BibLibTheme
@Composable
fun NewsPage(
booksViewModel: BooksViewModel = hiltViewModel()
booksViewModel: NewsBookViewModel = hiltViewModel()
) {
LazyBookThumbnailColumn(
books = booksViewModel.news

View file

@ -1,208 +0,0 @@
package com.pixelized.biblib.ui.screen.home.viewModel
import android.app.Application
import android.util.Log
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.drawablepainter.DrawablePainter
import com.pixelized.biblib.R
import com.pixelized.biblib.model.book.Book
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.factory.BookFactory
import com.pixelized.biblib.repository.apiCache.IAPICacheRepository
import com.pixelized.biblib.repository.book.IBookRepository
import com.pixelized.biblib.ui.composable.StateUio
import com.pixelized.biblib.ui.screen.home.uio.BookThumbnailUio
import com.pixelized.biblib.ui.screen.home.uio.BookUio
import com.pixelized.biblib.ui.screen.home.uio.CoverUio
import com.pixelized.biblib.utils.BitmapCache
import com.pixelized.biblib.utils.extention.capitalize
import com.pixelized.biblib.utils.extention.context
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import java.net.URL
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
/**
* TODO: there is some book related code that should be inside a Repository // DataSource.
*/
@HiltViewModel
class BooksViewModel @Inject constructor(
application: Application,
private val bookRepository: IBookRepository,
private val client: IBibLibClient,
private val apiCache: IAPICacheRepository,
private val cache: BitmapCache,
) : AndroidViewModel(application) {
private val formatterLong = SimpleDateFormat("MMMM yyyy", Locale.getDefault())
private val formatterShort = SimpleDateFormat("MMM yyyy", Locale.getDefault())
private val _state = mutableStateOf<StateUio<Boolean>?>(null)
val state: State<StateUio<Boolean>?> = _state
private val booksSource = Pager(
config = PagingConfig(pageSize = PAGING_SIZE),
pagingSourceFactory = bookRepository.getBooksSource()
.map { it.toThumbnail() }
.asPagingSourceFactory(Dispatchers.IO)
).flow
val books @Composable get() = booksSource.collectAsLazyPagingItems()
private val newsSource: Flow<PagingData<BookThumbnailUio>> = Pager(
config = PagingConfig(pageSize = PAGING_SIZE),
pagingSourceFactory = bookRepository.getNewsSource()
.map { it.toThumbnail() }
.asPagingSourceFactory(Dispatchers.IO)
).flow
val news @Composable get() = newsSource.collectAsLazyPagingItems()
fun updateBooks() {
viewModelScope.launch(Dispatchers.IO) {
_state.value = StateUio.Progress()
try {
val updated = loadNewBooks() && loadAllBooks()
_state.value = StateUio.Success(updated)
} catch (exception: Exception) {
Log.e("BooksViewModel", exception.message, exception)
_state.value = StateUio.Failure(exception = exception)
}
}
}
fun getBookDetail(id: Int): State<BookUio?> {
val data = mutableStateOf<BookUio?>(null)
viewModelScope.launch(Dispatchers.IO) {
val factory = BookFactory()
val response = client.service.detail(id)
val book = factory.fromDetailResponseToBook(response)
data.value = book.toUio()
}
return data
}
fun send(id: Int, mail: String) {
viewModelScope.launch(Dispatchers.IO) {
client.service.send(bookId = id, mail = mail)
}
}
private suspend fun loadNewBooks(): Boolean {
val cached = apiCache.new
val updated = client.service.new()
return if (cached != updated) {
apiCache.new = updated
true
} else {
false
}
}
private suspend fun loadAllBooks(): Boolean {
client.service.list().let { response ->
val newIds = apiCache.new?.data?.map { it.book_id } ?: listOf()
val factory = BookFactory()
val books = response.data?.map { dto ->
val isNew = newIds.contains(dto.book_id)
val index = newIds.indexOf(dto.book_id)
factory.fromListResponseToBook(dto, isNew, index)
}
books?.let { data -> bookRepository.update(data) }
}
return true
}
//////////////////////////////////////
// region: UIO conversion
private fun Book.toThumbnail() = BookThumbnailUio(
id = id,
genre = genre?.joinToString { it.name } ?: "",
title = title,
author = author.joinToString { it.name },
date = if (releaseDate.time < 0) {
null
} else {
formatterLong.format(releaseDate).capitalize()
},
isNew = isNew,
cover = cover(url = URL("${IBibLibClient.THUMBNAIL_URL}/$id.jpg"))
)
private fun Book.toUio() = BookUio(
id = id,
title = title,
author = author.joinToString { it.name },
rating = rating?.toFloat() ?: 0.0f,
language = language?.displayLanguage?.capitalize() ?: "",
date = if (releaseDate.time < 0) {
null
} else {
formatterShort.format(releaseDate).capitalize()
},
series = series?.name,
description = synopsis ?: "",
)
// endregion
private fun cover(
cache: BitmapCache = this.cache,
placeHolder: Painter = DrawablePainter(
drawable = AppCompatResources.getDrawable(
context,
R.drawable.ic_baseline_auto_stories_24
)!!
),
coroutineScope: CoroutineScope = viewModelScope,
url: URL,
): State<CoverUio> {
if (cache.exist(url)) {
val bitmap = cache.readFromDisk(url) ?: throw RuntimeException("")
val resource = BitmapPainter(bitmap.asImageBitmap())
return mutableStateOf(CoverUio(painter = resource))
} else {
val state = mutableStateOf(
CoverUio(
contentScale = ContentScale.None,
painter = placeHolder
)
)
coroutineScope.launch {
val resource = cache.readFromDisk(url)?.let { BitmapPainter(it.asImageBitmap()) }
if (resource != null) {
state.value = CoverUio(painter = resource)
} else {
val downloaded = cache.download(url)
if (downloaded != null) {
cache.writeToDisk(url, downloaded)
state.value = CoverUio(painter = BitmapPainter(downloaded.asImageBitmap()))
}
}
}
return state
}
}
companion object {
private const val PAGING_SIZE = 30
}
}

View file

@ -4,7 +4,16 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
@ -40,7 +49,7 @@ class BitmapCache @Inject constructor(context: Context) {
}
suspend fun download(url: URL): Bitmap? {
Log.v("BitmapCache","download: $url")
Log.v("BitmapCache", "download: $url")
return withContext(Dispatchers.IO) {
try {
BitmapFactory.decodeStream(url.openStream())
@ -51,5 +60,38 @@ class BitmapCache @Inject constructor(context: Context) {
}
}
fun download(
coroutineScope: CoroutineScope,
placeHolder: Painter,
url: URL,
): State<CoverUio> {
if (exist(url)) {
val bitmap = readFromDisk(url) ?: throw RuntimeException("")
val resource = BitmapPainter(bitmap.asImageBitmap())
return mutableStateOf(CoverUio(painter = resource))
} else {
val state = mutableStateOf(
CoverUio(
contentScale = ContentScale.None,
painter = placeHolder
)
)
coroutineScope.launch {
val resource = readFromDisk(url)?.let { BitmapPainter(it.asImageBitmap()) }
if (resource != null) {
state.value = CoverUio(painter = resource)
} else {
val downloaded = download(url)
if (downloaded != null) {
writeToDisk(url, downloaded)
state.value = CoverUio(painter = BitmapPainter(downloaded.asImageBitmap()))
}
}
}
return state
}
}
private fun file(url: URL): File = File(cache?.absolutePath + url.file)
}

View file

@ -0,0 +1,32 @@
package com.pixelized.biblib.utils.extention
import java.text.Format
import java.text.SimpleDateFormat
import java.util.*
fun Date.longDate(
default: String? = null,
formatter: Format = SimpleDateFormat("MMMM yyyy", Locale.getDefault()),
): String? = format(
default = default,
formatter = formatter
)
fun Date.shortDate(
default: String? = null,
formatter: Format = SimpleDateFormat("MMM yyyy", Locale.getDefault()),
): String? = format(
default = default,
formatter = formatter
)
private fun Date.format(
default: String?,
formatter: Format,
): String? {
return if (time < 0) {
default
} else {
formatter.format(this).capitalize()
}
}

View file

@ -0,0 +1,21 @@
package com.pixelized.biblib.utils
import android.content.Context
import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import com.google.accompanist.drawablepainter.DrawablePainter
@Composable
fun painterResource(@DrawableRes res: Int): Painter {
val context = LocalContext.current
val drawable = AppCompatResources.getDrawable(context, res)
return DrawablePainter(drawable = drawable ?: error(""))
}
fun painterResource(context: Context, @DrawableRes res: Int): Painter {
val drawable = AppCompatResources.getDrawable(context, res)
return DrawablePainter(drawable = drawable ?: error(""))
}