First Compose layout

This commit is contained in:
Sebastian Seedorf
2021-11-11 18:30:12 +01:00
parent ad2506333b
commit f2133abea6
12 changed files with 254 additions and 174 deletions

View File

@@ -32,10 +32,12 @@ android {
buildFeatures { buildFeatures {
viewBinding true viewBinding true
compose true
} }
dataBinding { composeOptions {
enabled = true kotlinCompilerVersion kotlin_version
kotlinCompilerExtensionVersion compose_version
} }
// To inline the bytecode built with JVM target 1.8 into // To inline the bytecode built with JVM target 1.8 into
@@ -54,7 +56,6 @@ android {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
@@ -77,4 +78,14 @@ dependencies {
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// Compose
implementation "androidx.compose.runtime:runtime:$compose_version"
implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.ui:ui-tooling:$compose_version"
implementation "androidx.compose.foundation:foundation:$compose_version"
implementation "androidx.compose.foundation:foundation-layout:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "com.google.android.material:compose-theme-adapter:$compose_version"
} }

View File

@@ -5,7 +5,7 @@ import com.beust.klaxon.KlaxonException
data class Lecturer ( data class Lecturer (
val fistName: String, val firstName: String,
val lastName: String, val lastName: String,
val email: String, val email: String,
val isResponsible: Boolean val isResponsible: Boolean

View File

@@ -1,66 +0,0 @@
package de.sebse.fuplanner2.ui.courses
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import de.sebse.fuplanner2.R
import de.sebse.fuplanner2.database.Course
import de.sebse.fuplanner2.ui.CaptionHolder
import de.sebse.fuplanner2.ui.CustomHolder
import de.sebse.fuplanner2.ui.ListItemHolder
import de.sebse.fuplanner2.ui.ViewHolderGenerator
import de.sebse.fuplanner2.utils.cast
import java.util.*
class CoursesAdapter(private val onclick: (Course) -> Unit) : RecyclerView.Adapter<CustomHolder>() {
private val positionalData: ArrayList<Any> = arrayListOf()
var dataset: List<Course> = listOf()
set(value) {
field = value
positionalData.clear()
var last: Course? = null
dataset.forEach {
if (last?.let { last -> it < last } != false)
positionalData.add(Pair(it.isSummerSemester, it.year))
positionalData.add(it)
last = it
}
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomHolder {
return ViewHolderGenerator.getHolderByType(parent, viewType)
}
override fun onBindViewHolder(holder: CustomHolder, position: Int) {
// val viewType = getItemViewType(position)
val res = holder.itemView.resources
when (holder) {
is CaptionHolder -> cast<Pair<Boolean, Int?>>(positionalData[position])?.let { (isSummer, year) ->
holder.string.text = when {
year == null -> res.getString(R.string.projects)
isSummer -> res.getString(R.string.summer_semester, year)
else -> res.getString(R.string.winter_semester, year, year+1)
}
}
is ListItemHolder -> cast<Course>(positionalData[position])?.let {
holder.title.text = it.title
holder.subLeft.text = it.lecturers.filter { lecturer -> lecturer.isResponsible }.joinToString { lecturer ->
res.getString(R.string.full_name, lecturer.fistName.substring(0, 1)+".", lecturer.lastName)
}
holder.subRight.text = it.type
holder.itemView.setOnClickListener { _ -> this.onclick(it) }
}
}
}
override fun getItemViewType(position: Int): Int {
return when (positionalData[position]) {
is Course -> ViewHolderGenerator.HolderType.ITEM.ordinal
else -> ViewHolderGenerator.HolderType.HEADER.ordinal
}
}
// Return the size of your dataset (invoked by the layout manager)
override fun getItemCount() = positionalData.size
}

View File

@@ -1,25 +1,20 @@
package de.sebse.fuplanner2.ui.courses package de.sebse.fuplanner2.ui.courses
import android.os.Bundle import android.os.Bundle
import android.provider.Settings.Global.getString
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.composethemeadapter.MdcTheme
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import de.sebse.fuplanner2.R
import de.sebse.fuplanner2.auth.AppAccounts
import de.sebse.fuplanner2.database.Course
import de.sebse.fuplanner2.databinding.ActivityMainBinding
import de.sebse.fuplanner2.databinding.FragmentRefreshRecyclerBinding import de.sebse.fuplanner2.databinding.FragmentRefreshRecyclerBinding
import de.sebse.fuplanner2.worker.AbstractAccountWorker.Companion.KEY_ACCOUNT_NAME
import de.sebse.fuplanner2.worker.CourseWorker
class CoursesFragment : Fragment() { class CoursesFragment : Fragment() {
@@ -32,42 +27,23 @@ class CoursesFragment : Fragment() {
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
val viewManager = LinearLayoutManager(context) navController = findNavController()
val viewAdapter = CoursesAdapter(this::onClick)
coursesViewModel = ViewModelProvider(this).get(CoursesViewModel::class.java) coursesViewModel = ViewModelProvider(this).get(CoursesViewModel::class.java)
binding = FragmentRefreshRecyclerBinding.inflate(inflater, container, false) binding = FragmentRefreshRecyclerBinding.inflate(inflater, container, false)
return inflater.inflate(R.layout.fragment_refresh_recycler, container, false).apply { return ComposeView(requireContext()).apply {
binding.recyclerView.apply { setContent {
//setHasFixedSize(true) MdcTheme {
layoutManager = viewManager val courses = coursesViewModel.text.observeAsState(listOf())
adapter = viewAdapter LazyColumn {
items(courses.value) { course -> CourseItem(course) {
course.uid?.let {
navController.navigate(CoursesFragmentDirections.actionNavHomeToCourseDetails(it, course.title))
}
} }
}
}
} }
binding.swipeRefreshLayout.setOnRefreshListener {
val work = OneTimeWorkRequestBuilder<CourseWorker>()
.setInputData(workDataOf(
KEY_ACCOUNT_NAME to AppAccounts.getInstance().selectedAccount?.name
))
.build()
WorkManager.getInstance(context.applicationContext)
.enqueue(work)
WorkManager.getInstance(context.applicationContext)
.getWorkInfoByIdLiveData(work.id)
.observe(viewLifecycleOwner, {
if (it.state.isFinished)
binding.swipeRefreshLayout.isRefreshing = false
})
}
coursesViewModel.text.observe(viewLifecycleOwner, Observer {
viewAdapter.dataset = it
})
navController = findNavController()
}
}
private fun onClick(course: Course) {
course.uid?.let {
navController.navigate(CoursesFragmentDirections.actionNavHomeToCourseDetails(it, course.title))
} }
} }
} }

View File

@@ -0,0 +1,154 @@
package de.sebse.fuplanner2.ui.courses
import android.content.Context
import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Person
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.google.android.material.composethemeadapter.MdcTheme
import de.sebse.fuplanner2.R
import de.sebse.fuplanner2.database.Course
import de.sebse.fuplanner2.database.Lecturer
import de.sebse.fuplanner2.utils.color.getColor
@Composable
fun CourseList(courses: List<Course>, onclick: () -> Unit) {
LazyColumn {
items(courses) { course -> CourseItem(course, onclick) }
}
}
@Composable
fun CourseItem(course: Course, onclick: () -> Unit) {
@Suppress("SimplifiableCallChain")
CourseItem(
id = course.uid,
title = course.title,
lecturers = course.lecturers
.filter { lecturer -> lecturer.isResponsible }
.map { lecturer -> stringResource(R.string.full_name, lecturer.firstName.substring(0, 1)+".", lecturer.lastName) }
.joinToString(),
type = course.type,
onclick = onclick
)
}
@Composable
fun CourseItem(id: Long?, title: String, lecturers: String, type: String, onclick: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onclick)
.padding(dimensionResource(R.dimen.card_view_margin))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(R.dimen.card_view_padding))
) {
Text(
text = title,
color = Color(getColor(LocalContext.current, id ?: 0, highContrast = true)),
style = MaterialTheme.typography.h6
)
CourseItemHint(
icon = Icons.Outlined.Person,
imageAltRes = R.string.lecturers,
text = lecturers
)
CourseItemHint(
icon = Icons.Outlined.Info,
imageAltRes = R.string.course_type,
text = type
)
}
}
}
@Composable
fun CourseItemHint(icon: ImageVector, @StringRes imageAltRes: Int, text: String) {
Row {
Icon(
icon,
contentDescription = stringResource(imageAltRes),
modifier = Modifier.padding(
start = dimensionResource(R.dimen.card_view_padding)
)
)
Text(
text = text,
style = MaterialTheme.typography.subtitle1,
modifier = Modifier.padding(
start = dimensionResource(R.dimen.card_view_padding)
)
)
}
}
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun CourseListPreview() {
val course = Course(
userId = 1,
lastRefreshed = 1,
lvNumber = hashSetOf("1000"),
title = "Kurs mit tollem Namen",
description = "Beschreibung",
internalId = "165464635",
isSummerSemester = false,
year = 2020,
lecturers = listOf(
Lecturer(
firstName = "Max",
lastName = "Maurer",
email = "some@mail.com",
isResponsible = false
),
Lecturer(
firstName = "Peter",
lastName = "Engelbert",
email = "coolio@example.com",
isResponsible = false
)
),
moduleType = 1236,
type = "Type"
)
MdcTheme {
CourseList(listOf(
course, course, course
)) { }
}
}
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview
@Composable
fun CourseItemPreview() {
MdcTheme {
CourseItem(
12411111111,
"Höhere Algorithmik",
"M. Berta, P. Parker",
"Vorlesung"
) {}
}
}

View File

@@ -61,7 +61,7 @@ class DetailsAdapter(private val onQuickLink: (ButtonTypes) -> Unit, private val
holder.btnResources.setOnClickListener { this.onQuickLink(ButtonTypes.RESOURCES) } holder.btnResources.setOnClickListener { this.onQuickLink(ButtonTypes.RESOURCES) }
} }
is MailHolder -> cast<Lecturer>(positionalData[position])?.let { type -> is MailHolder -> cast<Lecturer>(positionalData[position])?.let { type ->
holder.title.text = res.getString(R.string.full_name, type.fistName, type.lastName) holder.title.text = res.getString(R.string.full_name, type.firstName, type.lastName)
holder.subLeft.text = type.email holder.subLeft.text = type.email
holder.itemView.setOnClickListener { this.onMailTo(type) } holder.itemView.setOnClickListener { this.onMailTo(type) }
} }

View File

@@ -69,8 +69,8 @@ class DetailsFragment : Fragment() {
intent.type = "text/html" intent.type = "text/html"
intent.data = Uri.fromParts("mailto", lecturer.email, null) intent.data = Uri.fromParts("mailto", lecturer.email, null)
intent.putExtra(Intent.EXTRA_SUBJECT, this.title) intent.putExtra(Intent.EXTRA_SUBJECT, this.title)
intent.putExtra(Intent.EXTRA_TEXT, getString(R.string.email_preview, lecturer.fistName, lecturer.lastName)) intent.putExtra(Intent.EXTRA_TEXT, getString(R.string.email_preview, lecturer.firstName, lecturer.lastName))
startActivity(Intent.createChooser(intent, getString(R.string.send_email, lecturer.fistName, lecturer.lastName))) startActivity(Intent.createChooser(intent, getString(R.string.send_email, lecturer.firstName, lecturer.lastName)))
} }
private fun launchFragment(btnType: DetailsAdapter.ButtonTypes) { private fun launchFragment(btnType: DetailsAdapter.ButtonTypes) {

View File

@@ -1,24 +1,21 @@
package de.sebse.fuplanner2.ui.schedule package de.sebse.fuplanner2.ui.schedule
import android.content.Context import android.content.Context
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.view.* import android.view.*
import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.alamkanak.weekview.WeekView import com.alamkanak.weekview.WeekView
import com.alamkanak.weekview.WeekViewDisplayable
import com.alamkanak.weekview.WeekViewEntity import com.alamkanak.weekview.WeekViewEntity
import com.alamkanak.weekview.WeekViewEvent
import de.sebse.fuplanner2.R import de.sebse.fuplanner2.R
import de.sebse.fuplanner2.database.AppDatabase import de.sebse.fuplanner2.database.AppDatabase
import de.sebse.fuplanner2.database.Course import de.sebse.fuplanner2.database.Course
import de.sebse.fuplanner2.database.Event import de.sebse.fuplanner2.database.Event
import de.sebse.fuplanner2.ui.schedule.MyCustomPagingAdapter.LoadMoreHandler import de.sebse.fuplanner2.ui.schedule.MyCustomPagingAdapter.LoadMoreHandler
import de.sebse.fuplanner2.utils.color.getColor
import de.sebse.fuplanner2.utils.getHtmlSpannedString import de.sebse.fuplanner2.utils.getHtmlSpannedString
import de.sebse.fuplanner2.utils.toTimeString import de.sebse.fuplanner2.utils.toTimeString
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -26,8 +23,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.* import java.util.*
import kotlin.math.roundToInt
import kotlin.random.Random
class ScheduleFragment : Fragment() { class ScheduleFragment : Fragment() {
@@ -164,56 +159,6 @@ data class ContextEvent(val actCtx: Context, val event: Event) {
.setStyle(style) .setStyle(style)
.build() .build()
} }
@ColorInt
fun getColor(actCtx: Context, seed: Long): Int {
var h = Random(seed).nextInt(0xFFFF)
h = h * 360 / 0xffff
//int s = 0xff & encodedHash[2];
//s = s * 100 / 0xffff;
//int v = 0xff & encodedHash[3];
//v = v * 100 / 0xffff;
// range for more beautiful colors
h = h / 30 * 30
val (s, v) =
if (actCtx.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)
Pair(100, 20)//Pair(100, 80)
else
Pair(60, 100)
val (r, g, b) = hsvToRgb(h / 360.0, s / 100.0, v / 100.0)
return rgbToColorInt(r, g, b)
}
private fun hsvToRgb(
hue: Double,
saturation: Double,
value: Double
): Triple<Double, Double, Double> {
val h = (hue * 6).toInt()
val f = hue * 6 - h
val p = value * (1 - saturation)
val q = value * (1 - f * saturation)
val t = value * (1 - (1 - f) * saturation)
return when (h) {
0 -> Triple(value, t, p)
1 -> Triple(q, value, p)
2 -> Triple(p, value, t)
3 -> Triple(p, q, value)
4 -> Triple(t, p, value)
5 -> Triple(value, p, q)
else -> Triple(0.0, 0.0, 0.0)
}
}
@ColorInt
private fun rgbToColorInt(
r: Double,
g: Double,
b: Double
): Int {
return 0xFF000000.toInt() + (r*0xFF).roundToInt() * 0x10000 + (g*0xFF).roundToInt() * 0x100 + (b*0xFF).roundToInt()
}
} }
class MyCustomPagingAdapter( class MyCustomPagingAdapter(

View File

@@ -5,11 +5,13 @@ package de.sebse.fuplanner2.utils
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.annotation.TargetApi import android.annotation.TargetApi
import android.content.Context import android.content.Context
import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.text.Html import android.text.Html
import android.text.Spanned import android.text.Spanned
import android.text.format.DateFormat import android.text.format.DateFormat
import android.util.Log import android.util.Log
import androidx.annotation.ColorInt
import androidx.annotation.PluralsRes import androidx.annotation.PluralsRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
@@ -20,6 +22,8 @@ import kotlinx.coroutines.runBlocking
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.math.roundToInt
import kotlin.random.Random
object console { object console {
@@ -60,6 +64,60 @@ object xml {
} }
} }
object color {
@ColorInt
fun getColor(actCtx: Context, seed: Long, highContrast: Boolean = false): Int {
var h = Random(seed).nextInt(0xFFFF)
h = h * 360 / 0xffff
//int s = 0xff & encodedHash[2];
//s = s * 100 / 0xffff;
//int v = 0xff & encodedHash[3];
//v = v * 100 / 0xffff;
// range for more beautiful colors
h = h / 30 * 30
val isNightMode = actCtx.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
val doDarkColors = if (highContrast) !isNightMode else isNightMode
val (s, v) =
if (doDarkColors)
Pair(100, 50)//Pair(100, 80)
else
Pair(100, 70)
val (r, g, b) = hsvToRgb(h / 360.0, s / 100.0, v / 100.0)
return rgbToColorInt(r, g, b)
}
@ColorInt
private fun rgbToColorInt(
r: Double,
g: Double,
b: Double
): Int {
return 0xFF000000.toInt() + (r*0xFF).roundToInt() * 0x10000 + (g*0xFF).roundToInt() * 0x100 + (b*0xFF).roundToInt()
}
private fun hsvToRgb(
hue: Double,
saturation: Double,
value: Double
): Triple<Double, Double, Double> {
val h = (hue * 6).toInt()
val f = hue * 6 - h
val p = value * (1 - saturation)
val q = value * (1 - f * saturation)
val t = value * (1 - (1 - f) * saturation)
return when (h) {
0 -> Triple(value, t, p)
1 -> Triple(q, value, p)
2 -> Triple(p, value, t)
3 -> Triple(p, q, value)
4 -> Triple(t, p, value)
5 -> Triple(value, p, q)
else -> Triple(0.0, 0.0, 0.0)
}
}
}
inline fun <reified T: ListenableWorker> enqueueOneTimeWork(appCtx: Context, workBuilder: (OneTimeWorkRequest.Builder) -> OneTimeWorkRequest.Builder): LiveData<WorkInfo> { inline fun <reified T: ListenableWorker> enqueueOneTimeWork(appCtx: Context, workBuilder: (OneTimeWorkRequest.Builder) -> OneTimeWorkRequest.Builder): LiveData<WorkInfo> {
val work = workBuilder(OneTimeWorkRequestBuilder<T>()).build() val work = workBuilder(OneTimeWorkRequestBuilder<T>()).build()
val workManager = WorkManager.getInstance(appCtx) val workManager = WorkManager.getInstance(appCtx)

View File

@@ -59,6 +59,7 @@
<string name="dialog_location_br"><![CDATA[<b>Location:</b><br>%1$s<br>]]></string> <string name="dialog_location_br"><![CDATA[<b>Location:</b><br>%1$s<br>]]></string>
<string name="dialog_time"><![CDATA[<b>Time:</b><br>%1$s - %2$s]]></string> <string name="dialog_time"><![CDATA[<b>Time:</b><br>%1$s - %2$s]]></string>
<string name="dialog_course_br"><![CDATA[<b>Course:</b><br>%1$s<br>]]></string> <string name="dialog_course_br"><![CDATA[<b>Course:</b><br>%1$s<br>]]></string>
<string name="course_type">Course Type</string>
<plurals name="not_course_update_text"> <plurals name="not_course_update_text">
<item quantity="one">One course message</item> <item quantity="one">One course message</item>
<item quantity="other">%1$d course messages</item> <item quantity="other">%1$d course messages</item>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="FUTheme" parent="Theme.AppCompat.DayNight.DarkActionBar"> <style name="FUTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>

View File

@@ -2,6 +2,7 @@
buildscript { buildscript {
ext.kotlin_version = '1.5.31' ext.kotlin_version = '1.5.31'
ext.compose_version = '1.0.5'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()