Skip to content
Snippets Groups Projects
Unverified Commit 86602b4f authored by AC5636's avatar AC5636 :ghost:
Browse files

Add Employees app

parent 83a08a66
No related branches found
No related tags found
No related merge requests found
Showing
with 621 additions and 0 deletions
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
# Default ignored files
/shelf/
/workspace.xml
E08 Employees app
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<value>
<entry key="app">
<State />
</entry>
</value>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
\ No newline at end of file
App preview [video](https://youtu.be/Yt1pYpbABlw)
\ No newline at end of file
/build
\ No newline at end of file
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.e08employeesapp"
compileSdk = 34
defaultConfig {
applicationId = "com.example.e08employeesapp"
minSdk = 31
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.activity:activity-compose:1.8.1")
implementation(platform("androidx.compose:compose-bom:2023.08.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
// extra libraries
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2") // viewModel
implementation("com.squareup.retrofit2:retrofit:2.9.0") // HTTP client to fetch json
implementation("com.squareup.retrofit2:converter-gson:2.9.0") // Data converter from json kotlin object
implementation("io.coil-kt:coil-compose:2.5.0") // To load images
implementation("androidx.navigation:navigation-compose:2.7.5") // For navigation
implementation("br.com.devsrsouza.compose.icons:tabler-icons:1.1.0") // TablerIcons
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
\ No newline at end of file
# 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
\ No newline at end of file
package com.example.e08employeesapp
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.e08employeesapp", appContext.packageName)
}
}
\ No newline at end of file
<?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: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.E08EmployeesApp"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.E08EmployeesApp">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
\ No newline at end of file
package com.example.e08employeesapp
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
// https://ptm.fi/data/android_employees.json
private val apiRetrofit = Retrofit.Builder()
.baseUrl("https://ptm.fi/data/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val employeesService = apiRetrofit.create(ApiService::class.java)
interface ApiService {
@GET("android_employees.json")
suspend fun getEmployees(): EmployeesResponse
}
\ No newline at end of file
package com.example.e08employeesapp
data class EmployeeData(
val id: Int,
val firstName: String,
val lastName: String,
val email: String,
val phone: String,
val title: String,
val department: String,
val image: String
)
//{
// "id": 1,
// "firstName": "Violante",
// "lastName": "Longhi",
// "email": "vlonghi0@wikispaces.com",
// "phone": "633-736-5682",
// "title": "Structural Analysis Engineer",
// "department": "Training",
// "image": "https://randomuser.me/api/portraits/med/men/8.jpg"
//}
data class EmployeesResponse(val employees: List<EmployeeData>)
//{
// "employees": [
// {
// "id": 1,
// "firstName": "Violante",
// "lastName": "Longhi",
// "email": "vlonghi0@wikispaces.com",
// "phone": "633-736-5682",
// "title": "Structural Analysis Engineer",
// "department": "Training",
// "image": "https://randomuser.me/api/portraits/med/men/8.jpg"
// },
// ...,
// {
// "id": 200,
// "firstName": "Godfry",
// "lastName": "Lyffe",
// "email": "glyffe1@cafepress.com",
// "phone": "229-629-9209",
// "title": "Marketing Assistant",
// "department": "Business Development",
// "image": "https://randomuser.me/api/portraits/med/women/93.jpg"
// }
// ]
//}
\ No newline at end of file
package com.example.e08employeesapp
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import kotlin.Exception
class EmployeesViewModel: ViewModel() {
private val _employeesState = mutableStateOf(EmployeesState())
val employeesState: State<EmployeesState> = _employeesState
data class EmployeesState(
val isLoading: Boolean = true,
val allEmployees: List<EmployeeData> = emptyList(),
val currentEmployee: EmployeeData? = null,
val error: String? = null
)
fun setCurrentEmployee(employee: EmployeeData){
_employeesState.value = _employeesState.value.copy(currentEmployee = employee)
}
init {
if (_employeesState.value.allEmployees.isEmpty()) fetchEmployees()
}
private fun fetchEmployees(){
viewModelScope.launch {
try {
val response = employeesService.getEmployees() // fetch data
// update the state
_employeesState.value = _employeesState.value.copy(
isLoading = false,
allEmployees = response.employees,
error = null)
} catch (e: Exception){
// update and set the error message
_employeesState.value = _employeesState.value.copy(
isLoading = false,
allEmployees = emptyList(),
error="Error occurred while fetching data: ${e.message}")
}
}
}
}
\ No newline at end of file
package com.example.e08employeesapp
import android.os.Bundle
import android.util.JsonReader
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import coil.compose.rememberAsyncImagePainter
import com.example.e08employeesapp.ui.theme.E08EmployeesAppTheme
import compose.icons.TablerIcons
import compose.icons.tablericons.Building
import compose.icons.tablericons.Mail
import compose.icons.tablericons.MailForward
import compose.icons.tablericons.PhoneOutgoing
import org.json.JSONObject
import java.io.Reader
import java.net.URL
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
val appViewModel: EmployeesViewModel = viewModel()
val appState by appViewModel.employeesState
E08EmployeesAppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
App(navController, appState, onEmployeePressed = {
appViewModel.setCurrentEmployee(it)
navController.navigate("details")
})
}
}
}
}
}
@Composable
fun App(navController: NavHostController, appState: EmployeesViewModel.EmployeesState, onEmployeePressed: (EmployeeData) -> Unit){
NavHost(navController = navController, startDestination = "home") {
composable("home"){
Box(modifier = Modifier.fillMaxSize()){
when {
appState.isLoading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
appState.error != null -> {
Text(text = appState.error)
}
else -> {
HomeScreen(appState.allEmployees, onEmployeePressed)
}
}
}
}
composable("details"){
if (appState.currentEmployee!=null) DetailsScreen(employee = appState.currentEmployee)
else Text(text = "No employee selected!")
}
}
}
@Composable
fun HomeScreen(allEmployees: List<EmployeeData>, onEmployeePressed: (EmployeeData) -> Unit){
Column {
AppTitle(Modifier.padding(horizontal = 8.dp, vertical = 12.dp))
LazyColumn(){
items(allEmployees, key={it.id}){
EmployeeItem(employee = it, onEmployeePressed = onEmployeePressed)
}
}
}
}
@Composable
fun DetailsScreen(employee: EmployeeData){
val rainbowColorsBrush = remember {
Brush.sweepGradient(
listOf(
Color(0xFF9575CD),
Color(0xFFBA68C8),
Color(0xFFE57373),
Color(0xFFFFB74D),
Color(0xFFFFF176),
Color(0xFFAED581),
Color(0xFF4DD0E1),
Color(0xFF9575CD)
)
)
}
val borderWidth = 5.dp
Column(modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally) {
Image(
painter = rememberAsyncImagePainter(employee.image),
contentDescription = null,
modifier = Modifier
.width(125.dp)
.aspectRatio(1f)
.border(
BorderStroke(borderWidth, rainbowColorsBrush),
CircleShape
)
.padding(borderWidth)
.clip(CircleShape)
)
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "${employee.firstName} ${employee.lastName}", style=MaterialTheme.typography.titleLarge, modifier = Modifier.wrapContentWidth())
}
Column(modifier=Modifier.padding(bottom = 12.dp)) {
Spacer(modifier = Modifier.height(6.dp))
Text(text = employee.title, style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(6.dp))
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Icon(imageVector = TablerIcons.Building, contentDescription = null)
Text(text = employee.department)
}
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Icon(imageVector = TablerIcons.MailForward, contentDescription = null)
Text(text = employee.email)
}
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Icon(imageVector = TablerIcons.PhoneOutgoing, contentDescription = null)
Text(text = employee.phone)
}
}
Divider(thickness = 2.dp)
Column(modifier = Modifier.padding(top=12.dp)) {
Text("Details", style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(bottom = 4.dp))
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text(text = stringResource(id = R.string.employee_details), style = MaterialTheme.typography.bodyMedium)
}
}
}
}
@Composable
fun EmployeeItem(employee: EmployeeData, onEmployeePressed: (EmployeeData)->Unit){
val rainbowColorsBrush = remember {
Brush.sweepGradient(
listOf(
Color(0xFF9575CD),
Color(0xFFBA68C8),
Color(0xFFE57373),
Color(0xFFFFB74D),
Color(0xFFFFF176),
Color(0xFFAED581),
Color(0xFF4DD0E1),
Color(0xFF9575CD)
)
)
}
val borderWidth = 5.dp
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
onEmployeePressed(employee)
}.padding(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Image(
painter = rememberAsyncImagePainter(employee.image),
contentDescription = null,
modifier = Modifier
.width(125.dp)
.aspectRatio(1f)
.border(
BorderStroke(borderWidth, rainbowColorsBrush),
CircleShape
)
.padding(borderWidth)
.clip(CircleShape)
)
Column(modifier=Modifier.padding(top = 10.dp, bottom = 10.dp)) {
Text(text = "${employee.firstName} ${employee.lastName}", style=MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(6.dp))
Text(text = employee.title, style = MaterialTheme.typography.titleSmall)
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Icon(imageVector = TablerIcons.MailForward, contentDescription = null)
Text(text = employee.email, style = MaterialTheme.typography.bodyMedium)
}
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Icon(imageVector = TablerIcons.PhoneOutgoing, contentDescription = null)
Text(text = employee.phone, style=MaterialTheme.typography.bodyMedium)
}
}
}
}
@Composable
fun AppTitle(modifier: Modifier = Modifier) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primary)
.then(modifier)
)
{
Text(
text = stringResource(id = R.string.app_name),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.titleLarge
)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment