-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
54 changed files
with
2,229 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
|
||
# Ignore Gradle GUI config | ||
gradle-app.setting | ||
|
||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) | ||
!gradle-wrapper.jar | ||
|
||
# Cache of project | ||
.gradletasknamecache | ||
|
||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 | ||
# gradle/wrapper/gradle-wrapper.properties | ||
|
||
.idea | ||
*.iml | ||
.gradle | ||
/local.properties | ||
/.idea/caches | ||
/.idea/libraries | ||
/.idea/modules.xml | ||
/.idea/workspace.xml | ||
/.idea/navEditor.xml | ||
/.idea/assetWizardSettings.xml | ||
.DS_Store | ||
/build | ||
/captures | ||
|
||
|
||
*.xcworkspacedata | ||
*.xcuserstate | ||
*.xcscheme | ||
xcschememanagement.plist | ||
*.xcbkptlist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,37 @@ | ||
# MortyComposeKMM | ||
|
||
Rick & Morty app to demonstrate use of GraphQL + Jetpack Compose (heavily based on https://github.com/Dimillian/MortyUI). | ||
This is also a Kotlin Multiplatform project with GraphQL code in shared module (making use of [Apollo library's Kotlin Multiplatform support](https://www.apollographql.com/docs/android/essentials/get-started-multiplatform/)). | ||
Right now app shows list of characters and episodes but plan is to update to show detail screens as well. | ||
|
||
|
||
![BikeShare Screenshot](/art/characters_screenshot.png?raw=true) | ||
|
||
The project also makes use of Jetpack Compose's [Paging library](https://developer.android.com/jetpack/androidx/releases/paging#paging_compose_version_100_2) | ||
that allows setting up `LazyColumn` for example that's driven from `PagingSource` as shown below (that source in our case invokes Apollo GraphQL queries). | ||
|
||
```kotlin | ||
class CharacterListsViewModel(private val repository: MortyRepository): ViewModel() { | ||
|
||
val characters: Flow<PagingData<GetCharactersQuery.Result>> = Pager(PagingConfig(pageSize = 20)) { | ||
CharactersDataSource(repository) | ||
}.flow | ||
|
||
} | ||
|
||
@Composable | ||
fun CharactersListView() { | ||
val characterListsViewModel = getViewModel<CharacterListsViewModel>() | ||
val lazyCharacterList = characterListsViewModel.characters.collectAsLazyPagingItems() | ||
|
||
LazyColumn { | ||
items(lazyCharacterList) { character -> | ||
character?.let { | ||
CharactersListRowView(character) | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/build | ||
*.iml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
plugins { | ||
id("com.android.application") | ||
kotlin("android") | ||
} | ||
|
||
android { | ||
compileSdkVersion(AndroidSdk.compile) | ||
defaultConfig { | ||
applicationId = "dev.johnoreilly.mortyuicomposekmp" | ||
minSdkVersion(AndroidSdk.min) | ||
targetSdkVersion(AndroidSdk.target) | ||
|
||
versionCode = 1 | ||
versionName = "1.0" | ||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | ||
} | ||
|
||
buildFeatures { | ||
compose = true | ||
} | ||
|
||
composeOptions { | ||
kotlinCompilerVersion = "1.4.21" | ||
kotlinCompilerExtensionVersion = Versions.compose | ||
} | ||
|
||
buildTypes { | ||
getByName("release") { | ||
isMinifyEnabled = true | ||
isShrinkResources = true | ||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") | ||
} | ||
} | ||
|
||
compileOptions { | ||
sourceCompatibility = JavaVersion.VERSION_1_8 | ||
targetCompatibility = JavaVersion.VERSION_1_8 | ||
} | ||
|
||
kotlinOptions { | ||
jvmTarget = "1.8" | ||
} | ||
} | ||
|
||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { | ||
kotlinOptions { | ||
jvmTarget = JavaVersion.VERSION_1_8.toString() | ||
freeCompilerArgs = listOf("-Xallow-jvm-ir-dependencies", "-Xskip-prerelease-check", | ||
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi" | ||
) | ||
} | ||
} | ||
|
||
dependencies { | ||
implementation("androidx.appcompat:appcompat:1.2.0") | ||
implementation("com.google.android.material:material:1.2.1") | ||
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") | ||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0") | ||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0") | ||
|
||
implementation(Compose.ui) | ||
implementation(Compose.uiGraphics) | ||
implementation(Compose.uiTooling) | ||
implementation(Compose.foundationLayout) | ||
implementation(Compose.material) | ||
implementation(Compose.runtimeLiveData) | ||
implementation(Compose.navigation) | ||
implementation(Compose.paging) | ||
implementation(Compose.accompanist) | ||
|
||
implementation(Koin.core) | ||
implementation(Koin.android) | ||
implementation(Koin.androidViewModel) | ||
implementation(Koin.compose) | ||
|
||
|
||
testImplementation("junit:junit:4.13.1") | ||
testImplementation("androidx.test:core:1.3.0") | ||
testImplementation("org.robolectric:robolectric:4.4") | ||
androidTestImplementation("androidx.test:runner:1.3.0") | ||
|
||
implementation(project(":shared")) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="dev.johnoreilly.mortyuicomposekmp"> | ||
|
||
<uses-permission android:name="android.permission.INTERNET" /> | ||
|
||
<application | ||
android:name=".MortyComposeKMMApplication" | ||
android:allowBackup="false" | ||
android:supportsRtl="true" | ||
android:theme="@style/AppTheme"> | ||
<activity android:name=".ui.MainActivity"> | ||
<intent-filter> | ||
<action android:name="android.intent.action.MAIN"/> | ||
<category android:name="android.intent.category.LAUNCHER"/> | ||
</intent-filter> | ||
</activity> | ||
</application> | ||
</manifest> |
19 changes: 19 additions & 0 deletions
19
androidApp/src/main/java/dev/johnoreilly/mortyuicomposekmp/MortyComposeKMMApplication.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package dev.johnoreilly.mortyuicomposekmp | ||
|
||
import android.app.Application | ||
import dev.johnoreilly.mortyuicomposekmp.di.appModule | ||
import org.koin.android.ext.koin.androidContext | ||
import org.koin.core.context.startKoin | ||
|
||
class MortyComposeKMMApplication : Application() { | ||
|
||
override fun onCreate() { | ||
super.onCreate() | ||
|
||
startKoin { | ||
androidContext(this@MortyComposeKMMApplication) | ||
modules(appModule) | ||
} | ||
} | ||
|
||
} |
19 changes: 19 additions & 0 deletions
19
androidApp/src/main/java/dev/johnoreilly/mortyuicomposekmp/di/AppModule.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package dev.johnoreilly.mortyuicomposekmp.di | ||
|
||
import dev.johnoreilly.mortyuicomposekmp.shared.MortyRepository | ||
import dev.johnoreilly.mortyuicomposekmp.ui.characters.CharacterListsViewModel | ||
import dev.johnoreilly.mortyuicomposekmp.ui.episodes.EpisodesListViewModel | ||
import org.koin.androidx.viewmodel.dsl.viewModel | ||
import org.koin.dsl.module | ||
|
||
|
||
val mortyAppModule = module { | ||
viewModel { CharacterListsViewModel(get()) } | ||
viewModel { EpisodesListViewModel(get()) } | ||
|
||
single { MortyRepository() } | ||
} | ||
|
||
|
||
// Gather all app modules | ||
val appModule = listOf(mortyAppModule) |
90 changes: 90 additions & 0 deletions
90
androidApp/src/main/java/dev/johnoreilly/mortyuicomposekmp/ui/MainActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package dev.johnoreilly.mortyuicomposekmp.ui | ||
|
||
import androidx.appcompat.app.AppCompatActivity | ||
import android.os.Bundle | ||
import androidx.compose.material.* | ||
import androidx.compose.material.icons.Icons | ||
import androidx.compose.material.icons.filled.Face | ||
import androidx.compose.material.icons.filled.Person | ||
import androidx.compose.runtime.* | ||
import androidx.compose.ui.graphics.vector.ImageVector | ||
import androidx.compose.ui.platform.setContent | ||
import androidx.navigation.NavHostController | ||
import androidx.navigation.compose.* | ||
import dev.johnoreilly.mortyuicomposekmp.ui.characters.CharacterListsViewModel | ||
import dev.johnoreilly.mortyuicomposekmp.ui.characters.CharactersListView | ||
import dev.johnoreilly.mortyuicomposekmp.ui.episodes.EpisodesListView | ||
|
||
class MainActivity : AppCompatActivity() { | ||
|
||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
|
||
setContent { | ||
MaterialTheme { | ||
MainLayout() | ||
} | ||
} | ||
} | ||
} | ||
|
||
sealed class BottomNavigationScreens(val route: String, val label: String, val icon: ImageVector) { | ||
object CharactersScreen : BottomNavigationScreens("Characters", "Characters", Icons.Default.Person) | ||
object EpisodesScreen : BottomNavigationScreens("Episodes", "Episodes", Icons.Default.Face) | ||
} | ||
|
||
@Composable | ||
fun MainLayout() { | ||
val navController = rememberNavController() | ||
var title by remember { mutableStateOf("") } | ||
|
||
val bottomNavigationItems = listOf( | ||
BottomNavigationScreens.CharactersScreen, | ||
BottomNavigationScreens.EpisodesScreen | ||
) | ||
|
||
Scaffold( | ||
topBar = { | ||
TopAppBar(title = { Text(title) }) | ||
}, | ||
bodyContent = { | ||
NavHost(navController, startDestination = BottomNavigationScreens.CharactersScreen.route) { | ||
composable(BottomNavigationScreens.CharactersScreen.route) { | ||
title = BottomNavigationScreens.CharactersScreen.label | ||
CharactersListView() | ||
} | ||
composable(BottomNavigationScreens.EpisodesScreen.route) { | ||
title = BottomNavigationScreens.EpisodesScreen.label | ||
EpisodesListView() | ||
} | ||
} | ||
}, | ||
bottomBar = { | ||
BottomNavigation { | ||
val currentRoute = currentRoute(navController) | ||
bottomNavigationItems.forEach { screen -> | ||
BottomNavigationItem( | ||
icon = { Icon(screen.icon) }, | ||
label = { Text(screen.label) }, | ||
selected = currentRoute == screen.route, | ||
alwaysShowLabels = false, // This hides the title for the unselected items | ||
onClick = { | ||
// This if check gives us a "singleTop" behavior where we do not create a | ||
// second instance of the composable if we are already on that destination | ||
if (currentRoute != screen.route) { | ||
navController.navigate(screen.route) | ||
} | ||
} | ||
) | ||
} | ||
} | ||
} | ||
) | ||
} | ||
|
||
@Composable | ||
private fun currentRoute(navController: NavHostController): String? { | ||
val navBackStackEntry by navController.currentBackStackEntryAsState() | ||
return navBackStackEntry?.arguments?.getString(KEY_ROUTE) | ||
} | ||
|
18 changes: 18 additions & 0 deletions
18
.../src/main/java/dev/johnoreilly/mortyuicomposekmp/ui/characters/CharacterListsViewModel.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package dev.johnoreilly.mortyuicomposekmp.ui.characters | ||
|
||
import androidx.lifecycle.ViewModel | ||
import androidx.paging.Pager | ||
import androidx.paging.PagingConfig | ||
import androidx.paging.PagingData | ||
import dev.johnoreilly.mortyuicomposekmp.GetCharactersQuery | ||
import dev.johnoreilly.mortyuicomposekmp.shared.MortyRepository | ||
import kotlinx.coroutines.flow.Flow | ||
|
||
class CharacterListsViewModel(private val repository: MortyRepository): ViewModel() { | ||
|
||
val characters: Flow<PagingData<GetCharactersQuery.Result>> = Pager(PagingConfig(pageSize = 20)) { | ||
CharactersDataSource(repository) | ||
}.flow | ||
|
||
|
||
} |
19 changes: 19 additions & 0 deletions
19
...App/src/main/java/dev/johnoreilly/mortyuicomposekmp/ui/characters/CharactersDataSource.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package dev.johnoreilly.mortyuicomposekmp.ui.characters | ||
|
||
import androidx.paging.PagingSource | ||
import dev.johnoreilly.mortyuicomposekmp.GetCharactersQuery | ||
import dev.johnoreilly.mortyuicomposekmp.shared.MortyRepository | ||
|
||
class CharactersDataSource(private val repository: MortyRepository) : PagingSource<Int, GetCharactersQuery.Result>() { | ||
|
||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, GetCharactersQuery.Result> { | ||
val pageNumber = params.key ?: 0 | ||
|
||
val charactersResponse = repository.getCharacters(pageNumber) | ||
val characters = charactersResponse.data?.characters?.resultsFilterNotNull() | ||
|
||
val prevKey = if (pageNumber > 0) pageNumber - 1 else null | ||
val nextKey = charactersResponse.data?.characters?.info?.next | ||
return LoadResult.Page(data = characters ?: emptyList(), prevKey = prevKey, nextKey = nextKey) | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
...pp/src/main/java/dev/johnoreilly/mortyuicomposekmp/ui/characters/CharactersListRowView.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package dev.johnoreilly.mortyuicomposekmp.ui.characters | ||
|
||
import androidx.compose.foundation.clickable | ||
import androidx.compose.foundation.layout.* | ||
import androidx.compose.foundation.shape.CircleShape | ||
import androidx.compose.material.Card | ||
import androidx.compose.material.Divider | ||
import androidx.compose.material.MaterialTheme | ||
import androidx.compose.material.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.unit.dp | ||
import dev.chrisbanes.accompanist.coil.CoilImage | ||
import dev.johnoreilly.mortyuicomposekmp.GetCharactersQuery | ||
|
||
@Composable | ||
fun CharactersListRowView(character: GetCharactersQuery.Result) { | ||
|
||
Row(modifier = Modifier.fillMaxWidth().clickable(onClick = { }).padding(16.dp), | ||
verticalAlignment = Alignment.CenterVertically | ||
) { | ||
|
||
val imageUrl = character.image | ||
if (imageUrl != null) { | ||
Card(modifier = Modifier.preferredSize(50.dp), shape = CircleShape) { | ||
CoilImage(data = imageUrl) | ||
} | ||
} else { | ||
Spacer(modifier = Modifier.preferredSize(50.dp)) | ||
} | ||
|
||
Column(modifier = Modifier.padding(start = 8.dp, end = 8.dp)) { | ||
Text(character.name ?: "", style = MaterialTheme.typography.h6) | ||
Text("${character.episode?.size ?: 0} episode(s)", | ||
style = MaterialTheme.typography.subtitle2, color = Color.Gray) | ||
} | ||
} | ||
Divider() | ||
} | ||
|
Oops, something went wrong.