Skip to content

Commit

Permalink
[Jetpack Compose] Migrate offline area list to Compose Lazy Column (#…
Browse files Browse the repository at this point in the history
…2698)

* Add AppTheme to preview of SyncListItem

* Create compose view for offline area list item

* Replace recycler view with lazy column

* handle clicks

* Replace toast with navigation

* Convert OfflineAreaListItemViewModel to data class OfflineAreaDetails

* Remove unused recycler view related classes

* Reformat code

* Simplify OfflineAreaDetails class

* Fix import order

* Add content description

* Remove unused style

* Update unit tests

* Refactor tests

* Apply suggestions
  • Loading branch information
shobhitagarwal1612 authored Aug 27, 2024
1 parent bb24e59 commit 4434801
Show file tree
Hide file tree
Showing 11 changed files with 170 additions and 254 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,4 @@

package com.google.android.ground.ui.offlineareas

import com.google.android.ground.model.imagery.OfflineArea
import com.google.android.ground.ui.common.Navigator

class OfflineAreaListItemViewModel(
private val navigator: Navigator,
val area: OfflineArea,
val sizeOnDisk: String,
) {
val areaName = area.name

fun onClick() {
navigator.navigate(OfflineAreasFragmentDirections.viewOfflineArea(area.id))
}
}
data class OfflineAreaDetails(val id: String, val name: String, val sizeOnDisk: String)

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.ground.ui.offlineareas

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
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.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.android.ground.R
import com.google.android.ground.ui.theme.AppTheme

@Composable
fun OfflineAreaListItem(
modifier: Modifier = Modifier,
offlineAreaDetails: OfflineAreaDetails,
itemClicked: (areaId: String) -> Unit = {},
) {
Column {
Row(
modifier =
modifier
.fillMaxWidth()
.padding(start = 16.dp, top = 4.dp, end = 24.dp, bottom = 4.dp)
.clickable { itemClicked(offlineAreaDetails.id) },
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_offline_pin),
contentDescription = stringResource(id = R.string.offline_area_list_item_icon),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp),
)

Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(0.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.Start,
) {
Text(
text = offlineAreaDetails.name,
style =
TextStyle(
fontSize = 16.sp,
lineHeight = 24.sp,
fontFamily = FontFamily(Font(R.font.text_500)),
color = MaterialTheme.colorScheme.onSurface,
),
)

Text(
text =
stringResource(
id = R.string.offline_area_list_item_size_on_disk_mb,
offlineAreaDetails.sizeOnDisk,
),
style =
TextStyle(
fontSize = 16.sp,
lineHeight = 24.sp,
fontFamily = FontFamily(Font(R.font.text_500)),
color = MaterialTheme.colorScheme.onSurfaceVariant,
),
)
}
}
}
}

@Composable
@Preview(showBackground = true, showSystemUi = true)
fun PreviewOfflineAreaListItem() {
AppTheme {
OfflineAreaListItem(
offlineAreaDetails =
OfflineAreaDetails(id = "id", name = "Region name, Country", sizeOnDisk = "12 MB")
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,20 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import com.google.android.ground.databinding.OfflineAreasFragBinding
import com.google.android.ground.ui.common.AbstractFragment
import com.google.android.ground.ui.common.Navigator
import com.google.android.ground.ui.theme.AppTheme
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

/**
* Fragment containing a list of downloaded areas on the device. An area is a set of offline raster
Expand All @@ -32,14 +42,12 @@ import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class OfflineAreasFragment : AbstractFragment() {

private lateinit var offlineAreaListAdapter: OfflineAreaListAdapter
@Inject lateinit var navigator: Navigator
private lateinit var viewModel: OfflineAreasViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = getViewModel(OfflineAreasViewModel::class.java)
offlineAreaListAdapter = OfflineAreaListAdapter()
viewModel.offlineAreas.observe(this) { offlineAreaListAdapter.update(it) }
}

override fun onCreateView(
Expand All @@ -51,13 +59,24 @@ class OfflineAreasFragment : AbstractFragment() {
val binding = OfflineAreasFragBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
binding.lifecycleOwner = this
binding.offlineAreasListComposeView.setContent { AppTheme { ShowOfflineAreas() } }

getAbstractActivity().setSupportActionBar(binding.offlineAreasToolbar)

val recyclerView = binding.offlineAreasList
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = offlineAreaListAdapter
return binding.root
}

@Composable
private fun ShowOfflineAreas() {
val list by viewModel.offlineAreas.observeAsState()
list?.let {
LazyColumn(Modifier.fillMaxSize().testTag("offline area list")) {
items(it) {
OfflineAreaListItem(offlineAreaDetails = it) { areaId ->
navigator.navigate(OfflineAreasFragmentDirections.viewOfflineArea(areaId))
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,29 +42,26 @@ internal constructor(
* unexpected error accessing the local store is encountered, emits an empty list, circumventing
* the error.
*/
val offlineAreas: LiveData<List<OfflineAreaListItemViewModel>>
val offlineAreas: LiveData<List<OfflineAreaDetails>>

val showList: LiveData<Boolean>
val showNoAreasMessage: LiveData<Boolean>
val showProgressSpinner: LiveData<Boolean>

init {
val offlineAreas = offlineAreaRepository.offlineAreas().map { toViewModel(it) }
val offlineAreas =
offlineAreaRepository.offlineAreas().map { list -> list.map { toOfflineAreaDetails(it) } }
this.offlineAreas = offlineAreas.asLiveData()
showProgressSpinner = offlineAreas.map { false }.onStart { emit(true) }.asLiveData()
showNoAreasMessage = offlineAreas.map { it.isEmpty() }.onStart { emit(false) }.asLiveData()
showList = offlineAreas.map { it.isNotEmpty() }.onStart { emit(false) }.asLiveData()
}

private fun toViewModel(offlineAreas: List<OfflineArea>): List<OfflineAreaListItemViewModel> =
offlineAreas.map { toViewModel(it) }
private fun toOfflineAreaDetails(offlineArea: OfflineArea) =
OfflineAreaDetails(offlineArea.id, offlineArea.name, offlineArea.getSizeOnDevice())

private fun toViewModel(offlineArea: OfflineArea) =
OfflineAreaListItemViewModel(
navigator,
offlineArea,
offlineAreaRepository.sizeOnDevice(offlineArea).toMb().toMbString(),
)
private fun OfflineArea.getSizeOnDevice() =
offlineAreaRepository.sizeOnDevice(this).toMb().toMbString()

/** Navigate to the area selector for offline map imagery. */
fun showOfflineAreaSelector() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import com.google.android.ground.R
import com.google.android.ground.model.job.Job
import com.google.android.ground.model.mutation.Mutation
import com.google.android.ground.model.mutation.SubmissionMutation
import com.google.android.ground.ui.theme.AppTheme
import java.util.Date

@Composable
Expand Down Expand Up @@ -150,5 +151,5 @@ fun PreviewSyncListItem(
),
)
) {
SyncListItem(Modifier, detail)
AppTheme { SyncListItem(Modifier, detail) }
}
8 changes: 4 additions & 4 deletions ground/src/main/res/layout/offline_areas_frag.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/offline_areas_list"
<androidx.compose.ui.platform.ComposeView
android:id="@+id/offline_areas_list_compose_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
Expand Down Expand Up @@ -97,11 +97,11 @@
android:layout_width="200dp"
android:layout_height="200dp"
android:contentDescription="@string/offline_map_imagery_no_areas_downloaded_image"
app:layout_constraintBottom_toTopOf="@+id/no_areas_downloaded_message"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/no_areas_downloaded_message"
app:srcCompat="@drawable/offline_map_imagery"
app:visible="@{viewModel.showNoAreasMessage}"/>
app:visible="@{viewModel.showNoAreasMessage}" />
<TextView
android:id="@+id/no_areas_downloaded_message"
style="@style/TextAppearance.App.BodyMedium"
Expand Down
Loading

0 comments on commit 4434801

Please sign in to comment.