Skip to content

Rewrite TestApp in Compose #686

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 11 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ androidx-webkit = "1.13.0"

assertj = "3.27.3"

coil = "3.2.0"

dokka = "1.9.20"

google-material = "1.12.0"
Expand Down Expand Up @@ -79,6 +81,7 @@ androidx-compose-foundation = { group = "androidx.compose.foundation", name = "f
androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "androidx-compose-material" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" }
androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "androidx-compose-material-icons" }
androidx-compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" }
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "androidx-compose-runtime" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidx-compose-ui" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidx-compose-ui" }
Expand All @@ -103,6 +106,9 @@ androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "a

assertj = { group = "org.assertj", name = "assertj-core", version.ref = "assertj" }

coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
coil-network = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" }

desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
google-material = { group = "com.google.android.material", name = "material", version.ref = "google-material" }

Expand Down Expand Up @@ -141,6 +147,7 @@ compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "

[bundles]

compose = ["androidx-compose-activity", "androidx-compose-animation", "androidx-compose-foundation", "androidx-compose-material", "androidx-compose-material3", "androidx-compose-material-icons", "androidx-compose-runtime", "androidx-compose-ui", "androidx-compose-ui-tooling"]
coil = ["coil-compose", "coil-network"]
compose = ["androidx-compose-activity", "androidx-compose-animation", "androidx-compose-foundation", "androidx-compose-material", "androidx-compose-material3", "androidx-compose-material-icons", "androidx-compose-navigation", "androidx-compose-runtime", "androidx-compose-ui", "androidx-compose-ui-tooling"]
media3 = ["androidx-media3-session", "androidx-media3-common", "androidx-media3-exoplayer"]
room = ["androidx-room-runtime", "androidx-room-ktx"]
3 changes: 3 additions & 0 deletions test-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ dependencies {

implementation(libs.bundles.media3)

implementation(libs.bundles.compose)
implementation(libs.bundles.coil)

// Room database
implementation(libs.bundles.room)
ksp(libs.androidx.room.compiler)
Expand Down
41 changes: 6 additions & 35 deletions test-app/src/main/java/org/readium/r2/testapp/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,56 +7,27 @@
package org.readium.r2.testapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.snackbar.Snackbar

class MainActivity : AppCompatActivity() {
class MainActivity : ComponentActivity() {

private lateinit var navController: NavController
private val viewModel: MainViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.container)) { v, insets ->
val statusBars = insets.getInsets(WindowInsetsCompat.Type.statusBars())
v.setPadding(statusBars.left, statusBars.top, statusBars.right, statusBars.bottom)
insets
}

val navView: BottomNavigationView = findViewById(R.id.nav_view)
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController

val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.navigation_bookshelf,
R.id.navigation_catalog_list,
R.id.navigation_about
)
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
setContent {
TestApp()
}

viewModel.channel.receive(this) { handleEvent(it) }
}

override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}

private fun handleEvent(event: MainViewModel.Event) {
when (event) {
is MainViewModel.Event.ImportPublicationSuccess ->
Expand Down
18 changes: 18 additions & 0 deletions test-app/src/main/java/org/readium/r2/testapp/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,27 @@
package org.readium.r2.testapp

import android.app.Application
import androidx.compose.foundation.layout.RowScope
import androidx.compose.runtime.Composable
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import org.readium.r2.testapp.domain.Bookshelf
import org.readium.r2.testapp.domain.ImportError
import org.readium.r2.testapp.utils.EventChannel

data class TopBarState(
val title: String = "Readium",
val actions: @Composable RowScope.() -> Unit = {}
)

class MainViewModel(
application: Application,
) : AndroidViewModel(application) {
Expand All @@ -27,6 +38,13 @@ class MainViewModel(
val channel: EventChannel<Event> =
EventChannel(Channel(Channel.UNLIMITED), viewModelScope)

private val _topBarState = MutableStateFlow(TopBarState())
val topBarState: StateFlow<TopBarState> = _topBarState.asStateFlow()

fun updateTopBar(title: String, actions: @Composable RowScope.() -> Unit = {}) {
_topBarState.update { TopBarState(title, actions) }
}

init {
app.bookshelf.channel.receiveAsFlow()
.onEach { sendImportFeedback(it) }
Expand Down
144 changes: 144 additions & 0 deletions test-app/src/main/java/org/readium/r2/testapp/TestApp.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package org.readium.r2.testapp

import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Book
import androidx.compose.material.icons.filled.Explore
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import org.readium.r2.shared.publication.Publication
import org.readium.r2.testapp.about.AboutScreen
import org.readium.r2.testapp.bookshelf.BookshelfScreen
import org.readium.r2.testapp.catalogs.CatalogFeedScreen
import org.readium.r2.testapp.catalogs.CatalogScreen
import org.readium.r2.testapp.catalogs.CatalogViewModel
import org.readium.r2.testapp.catalogs.PublicationScreen
import org.readium.r2.testapp.data.model.Catalog

sealed class Screen(val route: String) {

sealed class TopLevel(
route: String,
val title: String,
val icon: ImageVector
) : Screen(route) {
object Bookshelf : TopLevel("bookshelf", "Bookshelf", Icons.Default.Book)
object Catalogs : TopLevel("catalogs", "Catalogs", Icons.Default.Explore)
object About : TopLevel("about", "About", Icons.Default.Info)
}

object CatalogDetail : Screen("catalog_detail")
object Publication : Screen("publication")
}

private val topLevelScreens = listOf(
Screen.TopLevel.Bookshelf,
Screen.TopLevel.Catalogs,
Screen.TopLevel.About,
)

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TestApp(mainViewModel: MainViewModel = viewModel()) {
val navController = rememberNavController()

val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route

val topBarState by mainViewModel.topBarState.collectAsState()

val catalogViewModel: CatalogViewModel = viewModel()

Scaffold(
topBar = {
TopAppBar(
title = { Text(topBarState.title, maxLines = 1) },
actions = topBarState.actions,
navigationIcon = {
val isTopLevelDestination = topLevelScreens.any { it.route == currentRoute }

if (!isTopLevelDestination) {
IconButton(onClick = { navController.navigateUp() }) {
Icon(
imageVector = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = "Back"
)
}
}
}
)
},
bottomBar = {
NavigationBar {
topLevelScreens.forEach { screen ->
NavigationBarItem(
icon = { Icon(screen.icon, contentDescription = null) },
label = { Text(screen.title) },
selected = currentRoute == screen.route,
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}

}
) { innerPadding ->
NavHost(
navController,
startDestination = Screen.TopLevel.Bookshelf.route,
Modifier.padding(innerPadding)
) {
composable(Screen.TopLevel.Bookshelf.route) { BookshelfScreen() }
composable(Screen.TopLevel.About.route) { AboutScreen(mainViewModel = mainViewModel) }

composable(Screen.TopLevel.Catalogs.route) {
CatalogFeedScreen(mainViewModel = mainViewModel, navController = navController)
}

composable(Screen.CatalogDetail.route) {
val catalog = navController.previousBackStackEntry
?.savedStateHandle?.get<Catalog>("catalog")

if (catalog != null) {
CatalogScreen(
catalog = catalog,
mainViewModel = mainViewModel,
catalogViewModel = catalogViewModel,
navController = navController,
onFacetClick = { /* TODO */ }
)
}
}

composable(Screen.Publication.route) {
PublicationScreen(catalogViewModel = catalogViewModel, mainViewModel = mainViewModel)
}
}
}
}

This file was deleted.

Loading