Skip to content

Commit

Permalink
KMP: use compose gradle injected dep versions, locker viewmodel, watc…
Browse files Browse the repository at this point in the history
…hfaces list
  • Loading branch information
crc-32 committed Sep 18, 2024
1 parent bf66360 commit 7fc8b40
Show file tree
Hide file tree
Showing 14 changed files with 243 additions and 64 deletions.
2 changes: 2 additions & 0 deletions android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ datastore = "1.1.1"
uuidVersion = "0.8.4"
compose-lib = "1.6.11"
compose-nav = "2.7.0-alpha07"
compose-viewmodel = "2.8.0"
compose-material3 = "1.3.0"
reorderable = "2.3.3"

Expand Down Expand Up @@ -80,6 +81,7 @@ compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose-li
compose-material = { module = "org.jetbrains.compose.material3:material3", version.ref = "compose-material3" }
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose-lib" }
compose-navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "compose-nav" }
compose-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "compose-viewmodel" }
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose-lib" }
compose-components-reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }

Expand Down
9 changes: 5 additions & 4 deletions android/shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,12 @@ kotlin {
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.contentnegotiation)
implementation(libs.ktor.serialization.json)
implementation(libs.compose.runtime)
implementation(libs.compose.ui)
implementation(libs.compose.material)
implementation(libs.compose.foundation)
implementation(compose.runtime)
implementation(compose.ui)
implementation(compose.material3)
implementation(compose.foundation)
implementation(libs.compose.navigation)
implementation(libs.compose.viewmodel)
implementation(libs.compose.components.resources)
implementation(libs.compose.components.reorderable)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.rebble.cobble.shared.ui.common.RebbleIcons
import io.rebble.cobble.shared.ui.view.home.locker.Locker

Expand All @@ -13,9 +14,8 @@ enum class HomePages {

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScaffold(page: HomePages, modifier: Modifier = Modifier, topBarWindowInsets: WindowInsets = WindowInsets(0, 0, 0, 0)) {
fun HomeScaffold(page: HomePages) {
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
windowInsets = WindowInsets.statusBars,
Expand All @@ -34,10 +34,12 @@ fun HomeScaffold(page: HomePages, modifier: Modifier = Modifier, topBarWindowIns
)
}
},
) {
when (page) {
HomePages.Locker -> {
Locker()
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
when (page) {
HomePages.Locker -> {
Locker()
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
package io.rebble.cobble.shared.ui.view.home.locker

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import io.rebble.cobble.shared.database.dao.LockerDao
import io.rebble.cobble.shared.database.entity.SyncedLockerEntryWithPlatforms
import io.rebble.cobble.shared.ui.viewmodel.LockerViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
Expand All @@ -25,21 +21,13 @@ enum class LockerTabs(val label: String) {
}

@Composable
fun Locker(lockerDao: LockerDao = getKoin().get()) {
val scope = rememberCoroutineScope()
val (entries, setEntries) = remember { mutableStateOf<List<SyncedLockerEntryWithPlatforms>?>(null) }
fun Locker(lockerDao: LockerDao = getKoin().get(), viewModel: LockerViewModel = viewModel { LockerViewModel(lockerDao) }) {
val entriesState: LockerViewModel.LockerEntriesState by viewModel.entriesState.collectAsState()
val tab = remember { mutableStateOf(LockerTabs.Apps) }

scope.launch(Dispatchers.IO) {
val entries = lockerDao.getAllEntries()
setEntries(entries)
}

Column {
Surface(
modifier = Modifier.fillMaxWidth().height(100.dp)
) {
Row {
Surface {
Row(modifier = Modifier.fillMaxWidth().height(64.dp)) {
LockerTabs.entries.forEachIndexed { index, it ->
NavigationBarItem(
selected = tab.value == it,
Expand All @@ -50,17 +38,17 @@ fun Locker(lockerDao: LockerDao = getKoin().get()) {
}
}

entries?.let {
if (entriesState is LockerViewModel.LockerEntriesState.Loaded) {
when (tab.value) {
LockerTabs.Apps -> {
LockerAppList(it.filter { it.entry.type == "watchapp" })
LockerAppList(viewModel)
}

LockerTabs.Watchfaces -> {
TODO()
LockerWatchfaceList(viewModel)
}
}
} ?: run {
} else {
CircularProgressIndicator(modifier = Modifier.align(CenterHorizontally))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,33 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import io.rebble.cobble.shared.database.dao.LockerDao
import io.rebble.cobble.shared.database.entity.SyncedLockerEntryWithPlatforms
import io.rebble.cobble.shared.jobs.LockerSyncJob
import io.rebble.cobble.shared.ui.common.RebbleIcons
import io.rebble.cobble.shared.ui.viewmodel.LockerViewModel
import kotlinx.coroutines.Job
import org.koin.compose.getKoin
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LockerAppList(entries: List<SyncedLockerEntryWithPlatforms>) {
fun LockerAppList(viewModel: LockerViewModel) {
val lazyListState = rememberLazyListState()
val koin = getKoin()
val lockerDao: LockerDao = koin.get()
val entriesState by viewModel.entriesState.collectAsState()
val entries = ((entriesState as? LockerViewModel.LockerEntriesState.Loaded)?.entries ?: emptyList()).filter { it.entry.type == "watchapp" }
val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to ->
val entry = entries.first { it.entry.id == from.key }
val nwList = entries.toMutableList()
nwList.remove(entry)
nwList.add(to.index, entry)
nwList.forEachIndexed { i, e ->
lockerDao.updateOrder(e.entry.id, i+1)
}
viewModel.updateOrder(nwList)
LockerSyncJob.schedule(koin.get())
}
LazyColumn(state = lazyListState, modifier = Modifier.fillMaxSize()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.rebble.cobble.shared.ui.view.home.locker

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import io.rebble.cobble.shared.database.entity.SyncedLockerEntry
import io.rebble.cobble.shared.database.entity.SyncedLockerEntryWithPlatforms
import io.rebble.cobble.shared.ui.common.RebbleIcons
import io.rebble.cobble.shared.ui.viewmodel.LockerWatchfaceItemViewModel

@Composable
fun LockerWatchfaceItem(entry: SyncedLockerEntryWithPlatforms) {
val viewModel: LockerWatchfaceItemViewModel = viewModel { LockerWatchfaceItemViewModel(entry) }
Surface(tonalElevation = 1.dp) {
Column(modifier = Modifier.fillMaxWidth()) {
Box(modifier = Modifier.size(width = 92.dp, height = if (viewModel.circleWatchface) 92.dp else 108.dp)) { RebbleIcons.unknownApp() }
Text(viewModel.title)
Text(viewModel.developerName)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
package io.rebble.cobble.shared.ui.view.home.locker

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import io.rebble.cobble.shared.ui.viewmodel.LockerViewModel

@Composable
fun LockerWatchfaceList() {
Text("Watchface list")
fun LockerWatchfaceList(viewModel: LockerViewModel) {
val entriesState: LockerViewModel.LockerEntriesState by viewModel.entriesState.collectAsState()
val entries = ((entriesState as? LockerViewModel.LockerEntriesState.Loaded)?.entries ?: emptyList()).filter { it.entry.type == "watchface" }

LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.verticalScroll(rememberScrollState())
) {
items(entries.size) { i ->
LockerWatchfaceItem(entries[i])
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.rebble.cobble.shared.ui.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.rebble.cobble.shared.database.dao.LockerDao
import io.rebble.cobble.shared.database.entity.SyncedLockerEntryWithPlatforms
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

class LockerViewModel(private val lockerDao: LockerDao): ViewModel() {
open class LockerEntriesState {
object Loading : LockerEntriesState()
data class Loaded(val entries: List<SyncedLockerEntryWithPlatforms>) : LockerEntriesState()
}

private val _entriesState: MutableStateFlow<LockerEntriesState> = MutableStateFlow(LockerEntriesState.Loading)
val entriesState = _entriesState.asStateFlow()
private var mutex = Mutex()
private var lastJob: Job? = null

init {
viewModelScope.launch(Dispatchers.IO) {
_entriesState.value = LockerEntriesState.Loaded(lockerDao.getAllEntries())
}
}

suspend fun updateOrder(entries: List<SyncedLockerEntryWithPlatforms>) {
lastJob?.cancel()
lastJob = viewModelScope.launch(Dispatchers.IO) {
mutex.withLock {
entries.forEachIndexed { i, e ->
lockerDao.updateOrder(e.entry.id, i+1)
}
}
}
_entriesState.value = LockerEntriesState.Loaded(entries)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.rebble.cobble.shared.ui.viewmodel

import androidx.lifecycle.ViewModel
import io.rebble.cobble.shared.database.entity.SyncedLockerEntry
import io.rebble.cobble.shared.database.entity.SyncedLockerEntryWithPlatforms
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.jetbrains.skia.Image

class LockerWatchfaceItemViewModel(val entry: SyncedLockerEntryWithPlatforms): ViewModel() {
open class ImageState {
object Loading : ImageState()
data class Loaded(val image: Image) : ImageState()
}
private var _imageState = MutableStateFlow(ImageState.Loading)
val imageState = _imageState.asStateFlow()

val title: String
get() = entry.entry.title

val developerName: String
get() = entry.entry.developerName

val circleWatchface: Boolean
get() = entry.platforms.any { it.name == "chalk" } && entry.platforms.size == 1 //TODO: also display when chalk connected

init {
//TODO: Load image
}
}
1 change: 1 addition & 0 deletions ios/Runner/Pigeon/Pigeons.h
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ NSObject<FlutterMessageCodec> *KMPApiGetCodec(void);

@protocol KMPApi
- (void)updateTokenToken:(StringWrapper *)token error:(FlutterError *_Nullable *_Nonnull)error;
- (void)openLockerViewWithError:(FlutterError *_Nullable *_Nonnull)error;
@end

extern void KMPApiSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<KMPApi> *_Nullable api);
Expand Down
17 changes: 17 additions & 0 deletions ios/Runner/Pigeon/Pigeons.m
Original file line number Diff line number Diff line change
Expand Up @@ -3661,4 +3661,21 @@ void KMPApiSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<KMPApi> *a
[channel setMessageHandler:nil];
}
}
{
FlutterBasicMessageChannel *channel =
[[FlutterBasicMessageChannel alloc]
initWithName:@"dev.flutter.pigeon.KMPApi.openLockerView"
binaryMessenger:binaryMessenger
codec:KMPApiGetCodec()];
if (api) {
NSCAssert([api respondsToSelector:@selector(openLockerViewWithError:)], @"KMPApi api (%@) doesn't respond to @selector(openLockerViewWithError:)", api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
FlutterError *error;
[api openLockerViewWithError:&error];
callback(wrapResult(nil, error));
}];
} else {
[channel setMessageHandler:nil];
}
}
}
22 changes: 22 additions & 0 deletions lib/infrastructure/pigeons/pigeons.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3593,4 +3593,26 @@ class KMPApi {
return;
}
}

Future<void> openLockerView() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.KMPApi.openLockerView', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(null) as List<Object?>?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
);
} else if (replyList.length > 1) {
throw PlatformException(
code: replyList[0]! as String,
message: replyList[1] as String?,
details: replyList[2],
);
} else {
return;
}
}
}
Loading

0 comments on commit 7fc8b40

Please sign in to comment.