Skip to content

Commit

Permalink
App implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Krzysztof Bobiński committed Feb 14, 2024
1 parent 4a6950a commit 751fe85
Show file tree
Hide file tree
Showing 69 changed files with 2,159 additions and 0 deletions.
33 changes: 33 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Android CI

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle

- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build

- name: Upload APK Release
uses: actions/upload-artifact@v4
with:
name: release-app
path: app/build/outputs/apk/release/app-release.apk
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
# DemoApp

![](https://img.shields.io/github/actions/workflow/status/krzybob/demoapp/android.yml)
![](https://www.codefactor.io/repository/github/krzybob/demoapp/badge)
![](https://img.shields.io/badge/kotlin-1.9.22-orange)
![](https://img.shields.io/badge/compose-1.6.1-orange)

Sample Android app for searching GitHub users.
You can use your access token by placing it in AppConfig.
1 change: 1 addition & 0 deletions app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
83 changes: 83 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("dagger.hilt.android.plugin")
id("com.google.devtools.ksp")
id("kotlin-parcelize")
}

android {
namespace = "pl.bobinski.demo"
compileSdk = Sdk.compile

defaultConfig {
applicationId = "pl.bobinski.demo"
minSdk = Sdk.minimum
targetSdk = Sdk.target
versionCode = 1
versionName = "1.0"

vectorDrawables {
useSupportLibrary = true
}
}

buildTypes {
release {
isDefault = true
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
sourceCompatibility = Versions.java
targetCompatibility = Versions.java
}
kotlinOptions {
jvmTarget = Versions.java.majorVersion
}
buildFeatures {
compose = true
buildConfig = true
}
composeOptions {
kotlinCompilerExtensionVersion = Versions.compose_compiler
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}

dependencies {
implementation("androidx.core:core-ktx:${Versions.core_ktx}")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}")
implementation("com.google.dagger:hilt-android:${Versions.hilt}")
ksp("com.google.dagger:hilt-compiler:${Versions.hilt}")
implementation("androidx.compose.ui:ui:${Versions.compose}")
implementation("androidx.compose.ui:ui-tooling:${Versions.compose}")
implementation("androidx.compose.foundation:foundation:${Versions.compose}")
implementation("androidx.compose.material3:material3:${Versions.compose_material3}")
implementation("androidx.activity:activity-compose:${Versions.compose_activity}")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.lifecycle}")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:${Versions.lifecycle}")
implementation("androidx.navigation:navigation-compose:${Versions.compose_navigation}")
implementation("androidx.hilt:hilt-navigation-compose:${Versions.hilt_navigation}")
implementation("androidx.paging:paging-compose:${Versions.paging}")
implementation("io.github.raamcosta.compose-destinations:core:${Versions.destinations}")
ksp("io.github.raamcosta.compose-destinations:ksp:${Versions.destinations}")
implementation("com.squareup.retrofit2:retrofit:${Versions.retrofit}")
implementation("com.squareup.retrofit2:converter-moshi:${Versions.retrofit}")
implementation("com.squareup.okhttp3:logging-interceptor:${Versions.okhttp}")
implementation("com.squareup.moshi:moshi:${Versions.moshi}")
implementation("com.squareup.moshi:moshi-adapters:${Versions.moshi}")
ksp("com.squareup.moshi:moshi-kotlin-codegen:${Versions.moshi}")
implementation("io.coil-kt:coil-compose:${Versions.coil}")
implementation("com.jakewharton.timber:timber:${Versions.timber}")
}
32 changes: 32 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

# With R8 full mode generic signatures are stripped for classes that are not
# kept. Suspend functions are wrapped in continuations where the type argument
# is used.
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response

# R8 full mode strips generic signatures from return types if not kept.
-if interface * { @retrofit2.http.* public *** *(...); }
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
32 changes: 32 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:name=".DemoApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.DemoApp"
tools:targetApi="33">

<activity
android:name=".ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.DemoApp"
android:windowSoftInputMode="adjustResize">

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
12 changes: 12 additions & 0 deletions app/src/main/java/pl/bobinski/demo/AppConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package pl.bobinski.demo

import kotlin.time.Duration.Companion.days

object AppConfig {
const val GithubApiUrl = "https://api.github.com"
val AccessToken: String? = null
val HttpCacheValidity = 1.days
const val HttpCacheMaxSizeBytes = 5L * 1024 * 1024
const val ImageCacheMemoryPercent = 0.5
const val ImageCacheMaxSizeBytes = 50L * 1024 * 1024
}
28 changes: 28 additions & 0 deletions app/src/main/java/pl/bobinski/demo/DemoApp.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package pl.bobinski.demo

import android.app.Application
import dagger.hilt.android.HiltAndroidApp
import pl.bobinski.demo.core.Initializer
import timber.log.Timber
import javax.inject.Inject

@HiltAndroidApp
class DemoApp : Application() {

@Inject
lateinit var inializers: Set<@JvmSuppressWildcards Initializer>

override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
inializers.forEach {
it.onInitialize(applicationContext)
}
}

companion object {
const val CACHE_FILE_NAME = "imageCache"
}
}
59 changes: 59 additions & 0 deletions app/src/main/java/pl/bobinski/demo/core/BaseViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package pl.bobinski.demo.core

import androidx.annotation.CallSuper
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import timber.log.Timber

abstract class BaseViewModel<Event : Any, State : Any, Effect : Any>(
initialState: State,
) : ViewModel() {

private val state = MutableStateFlow(initialState)

private val effects = Channel<Effect>(Channel.BUFFERED)

@CallSuper
protected open fun processEvent(event: Event) {
log(event)
}

protected fun updateState(update: (State) -> State) {
state.update(update)
log(state.value)
}

protected fun dispatchEffect(effect: Effect) {
log(effect)
effects.trySend(effect)
}

private fun log(any: Any) {
Timber.d("${this@BaseViewModel::class.simpleName} $any")
}

@Composable
fun use(): ViewModelContract<Event, State, Effect> {

val state by state.collectAsState()

return ViewModelContract(
processEvent = ::processEvent,
state = state,
effects = effects.receiveAsFlow()
)
}
}

data class ViewModelContract<Event, State, Effect>(
val processEvent: (Event) -> Unit,
val state: State,
val effects: Flow<Effect>
)
7 changes: 7 additions & 0 deletions app/src/main/java/pl/bobinski/demo/core/Initializer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package pl.bobinski.demo.core

import android.content.Context

interface Initializer {
fun onInitialize(applicationContext: Context)
}
16 changes: 16 additions & 0 deletions app/src/main/java/pl/bobinski/demo/data/GetUserResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package pl.bobinski.demo.data

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.util.Date

@JsonClass(generateAdapter = true)
data class GetUserResponse(
@Json(name = "login") var login: String? = null,
@Json(name = "avatar_url") var avatarUrl: String? = null,
@Json(name = "email") var email: String? = null,
@Json(name = "company") var company: String? = null,
@Json(name = "created_at") var createdAt: Date? = null,
@Json(name = "public_repos") var publicRepos: Int? = null,
@Json(name = "followers") var followers: Int? = null,
)
10 changes: 10 additions & 0 deletions app/src/main/java/pl/bobinski/demo/data/ListUsersResponseItem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package pl.bobinski.demo.data

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class ListUsersResponseItem(
@Json(name = "login") var login: String? = null,
@Json(name = "avatar_url") var avatarUrl: String? = null
)
16 changes: 16 additions & 0 deletions app/src/main/java/pl/bobinski/demo/data/SearchUsersResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package pl.bobinski.demo.data

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class SearchUsersResponse(
@Json(name = "items") val items: List<Item> = emptyList()
) {

@JsonClass(generateAdapter = true)
data class Item(
@Json(name = "login") var login: String? = null,
@Json(name = "avatar_url") var avatarUrl: String? = null
)
}
22 changes: 22 additions & 0 deletions app/src/main/java/pl/bobinski/demo/di/ApplicationModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package pl.bobinski.demo.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import pl.bobinski.demo.core.Initializer
import pl.bobinski.demo.image.CoilInitializer
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
class ApplicationModule {

@IntoSet
@Singleton
@Provides
fun provideCoilInitializer(): Initializer {
return CoilInitializer()
}
}
Loading

0 comments on commit 751fe85

Please sign in to comment.