From ee9000b98a3ed2a94cd5034f409e24f07b2e31da Mon Sep 17 00:00:00 2001 From: Adrian Kuta Date: Wed, 26 Jun 2024 00:24:08 +0200 Subject: [PATCH] Cleanup --- .idea/deploymentTargetSelector.xml | 21 +++ .../unbounddragdrop/MainActivity.kt | 147 ++++++++++++++++++ .../unbounddragdrop/MainViewModel.kt | 94 +++++++++++ .../unbounddragdrop/MyApplication.kt | 13 ++ .../unbounddragdrop/SimpleGridPageAdapter.kt | 56 +++++++ .../adriankuta/unbounddragdrop/model/Task.kt | 7 + .../unbounddragdrop/model/TaskStatus.kt | 6 + .../unbounddragdrop/util/CoroutinesUtils.kt | 32 ++++ app/src/main/res/layout/activity_main.xml | 57 +++++++ app/src/main/res/layout/text_item.xml | 21 +++ .../AndroidLibraryPublishConventionPlugin.kt | 29 ++++ 11 files changed, 483 insertions(+) create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 app/src/main/java/com/github/adriankuta/unbounddragdrop/MainActivity.kt create mode 100644 app/src/main/java/com/github/adriankuta/unbounddragdrop/MainViewModel.kt create mode 100644 app/src/main/java/com/github/adriankuta/unbounddragdrop/MyApplication.kt create mode 100644 app/src/main/java/com/github/adriankuta/unbounddragdrop/SimpleGridPageAdapter.kt create mode 100644 app/src/main/java/com/github/adriankuta/unbounddragdrop/model/Task.kt create mode 100644 app/src/main/java/com/github/adriankuta/unbounddragdrop/model/TaskStatus.kt create mode 100644 app/src/main/java/com/github/adriankuta/unbounddragdrop/util/CoroutinesUtils.kt create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/text_item.xml create mode 100644 build-logic/convention/src/main/kotlin/AndroidLibraryPublishConventionPlugin.kt diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..ec00831 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/github/adriankuta/unbounddragdrop/MainActivity.kt b/app/src/main/java/com/github/adriankuta/unbounddragdrop/MainActivity.kt new file mode 100644 index 0000000..62d1fc8 --- /dev/null +++ b/app/src/main/java/com/github/adriankuta/unbounddragdrop/MainActivity.kt @@ -0,0 +1,147 @@ +package com.github.adriankuta.unbounddragdrop + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.github.adriankuta.unbounddragdrop.databinding.ActivityMainBinding +import com.github.adriankuta.unbounddragdrop.model.TaskStatus +import dev.adriankuta.unbounddragdrop.DragDropHelper +import kotlinx.coroutines.launch + +class MainActivity : AppCompatActivity() { + + private val viewModel: MainViewModel by viewModels() + private lateinit var binding: ActivityMainBinding + private lateinit var todoAdapter: SimpleGridPageAdapter + private lateinit var inProgressAdapter: SimpleGridPageAdapter + private val callback = object : DragDropHelper.Callback() { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + targetRecyclerView: RecyclerView, + targetViewHolder: RecyclerView.ViewHolder? + ): Boolean { + return onMove( + recyclerView, + viewHolder.adapterPosition, + targetRecyclerView, + targetViewHolder?.adapterPosition + ) + } + + override fun onMoved( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + targetRecyclerView: RecyclerView, + targetViewHolder: RecyclerView.ViewHolder? + ) = Unit + + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + enableEdgeToEdge() + setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + setupRecyclerViews() + setObservers() + } + + private fun setupRecyclerViews() { + todoAdapter = SimpleGridPageAdapter() + inProgressAdapter = SimpleGridPageAdapter() + + with(binding) { + setupRecyclerView(toDoRecyclerView, todoAdapter) + setupRecyclerView(inProgressRecyclerView, inProgressAdapter) + } + } + + private fun setObservers() { + lifecycleScope.launch { + viewModel.uiState.collect(::render) + } + } + + private fun render(uiState: MainUiState) { + todoAdapter.submitList(uiState.todo) + inProgressAdapter.submitList(uiState.inProgress) + } + + private fun setupRecyclerView( + recyclerView: RecyclerView, + adapter: RecyclerView.Adapter<*> + ) { + recyclerView.adapter = adapter + recyclerView.layoutManager = + GridLayoutManager(this, GRID_ROWS, GridLayoutManager.HORIZONTAL, false) + DragDropHelper(callback).attachToRecyclerView(recyclerView) + } + + private fun onMove( + recyclerView: RecyclerView, + sourcePosition: Int, + targetRecyclerView: RecyclerView, + targetPosition: Int? + ): Boolean { + if (recyclerView == targetRecyclerView) { + onChangeOrder(recyclerView, sourcePosition, targetPosition) + } else { + onChangeStatus(recyclerView, sourcePosition, targetRecyclerView, targetPosition) + } + return true + } + + private fun onChangeOrder( + recyclerView: RecyclerView, + sourcePosition: Int, + targetPosition: Int? + ) { + targetPosition ?: return + val taskStatus = when (recyclerView.id) { + R.id.to_do_recycler_view -> TaskStatus.TODO + R.id.in_progress_recycler_view -> TaskStatus.IN_PROGRESS + else -> null + } ?: return + + viewModel.onChangeOrder(taskStatus, sourcePosition, targetPosition) + } + + private fun onChangeStatus( + recyclerView: RecyclerView, + sourcePosition: Int, + targetRecyclerView: RecyclerView, + targetPosition: Int? + ) { + val fromStatus = when (recyclerView.id) { + R.id.to_do_recycler_view -> TaskStatus.TODO + R.id.in_progress_recycler_view -> TaskStatus.IN_PROGRESS + else -> null + } + val toStatus = when (targetRecyclerView.id) { + R.id.to_do_recycler_view -> TaskStatus.TODO + R.id.in_progress_recycler_view -> TaskStatus.IN_PROGRESS + else -> null + } + if (fromStatus != null && toStatus != null) { + viewModel.onChangeStatus(fromStatus, toStatus, sourcePosition, targetPosition) + } + } + + companion object { + const val GRID_ROWS = 2 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/adriankuta/unbounddragdrop/MainViewModel.kt b/app/src/main/java/com/github/adriankuta/unbounddragdrop/MainViewModel.kt new file mode 100644 index 0000000..6f93722 --- /dev/null +++ b/app/src/main/java/com/github/adriankuta/unbounddragdrop/MainViewModel.kt @@ -0,0 +1,94 @@ +package com.github.adriankuta.unbounddragdrop + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.github.adriankuta.unbounddragdrop.model.Task +import com.github.adriankuta.unbounddragdrop.model.TaskStatus +import com.github.adriankuta.unbounddragdrop.model.TaskStatus.IN_PROGRESS +import com.github.adriankuta.unbounddragdrop.model.TaskStatus.TODO +import com.github.adriankuta.unbounddragdrop.util.WhileUiSubscribed +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import java.util.Collections +import java.util.UUID + +data class MainUiState( + val todo: List = emptyList(), + val inProgress: List = emptyList(), +) + +class MainViewModel : ViewModel() { + + + private val _todoTasks = MutableStateFlow>(emptyList()) + private val _inProgressTasks = MutableStateFlow>(emptyList()) + + init { + _todoTasks.value = + List(30) { Task(UUID.randomUUID().toString(), "Task $it", TODO) } + _inProgressTasks.value = + List(4) { Task(UUID.randomUUID().toString(), "Task $it", IN_PROGRESS) } + } + + + val uiState: StateFlow = combine(_todoTasks, _inProgressTasks) { listA, listB -> + MainUiState(listA, listB) + }.stateIn( + scope = viewModelScope, + started = WhileUiSubscribed, + initialValue = MainUiState() + ) + + fun onChangeOrder(status: TaskStatus, sourcePosition: Int, targetPosition: Int) { + when (status) { + TODO -> _todoTasks.swapElements(sourcePosition, targetPosition) + IN_PROGRESS -> _inProgressTasks.swapElements(sourcePosition, targetPosition) + } + } + + fun onChangeStatus( + fromStatus: TaskStatus, + toStatus: TaskStatus, + fromPosition: Int, + toPosition: Int? + ) { + val itemToMove = when (fromStatus) { + TODO -> _todoTasks.removeAt(fromPosition) + IN_PROGRESS -> _inProgressTasks.removeAt(fromPosition) + } + + when (toStatus) { + TODO -> _todoTasks.add(toPosition ?: _todoTasks.size(), itemToMove) + IN_PROGRESS -> _inProgressTasks.add(toPosition ?: _inProgressTasks.size(), itemToMove) + } + } + + private fun MutableStateFlow>.swapElements( + sourcePosition: Int, + targetPosition: Int + ) { + val newList = value.toMutableList() + Collections.swap(newList, sourcePosition, targetPosition) + value = newList + } + + private fun MutableStateFlow>.removeAt(index: Int): T { + val newList = value.toMutableList() + val removedItem = newList.removeAt(index) + value = newList + return removedItem + } + + private fun MutableStateFlow>.add(index: Int, item: T) { + val newList = value.toMutableList() + newList.add(index, item) + value = newList + } + + private fun MutableStateFlow>.size(): Int { + return value.size + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/github/adriankuta/unbounddragdrop/MyApplication.kt b/app/src/main/java/com/github/adriankuta/unbounddragdrop/MyApplication.kt new file mode 100644 index 0000000..0e34d38 --- /dev/null +++ b/app/src/main/java/com/github/adriankuta/unbounddragdrop/MyApplication.kt @@ -0,0 +1,13 @@ +package com.github.adriankuta.unbounddragdrop + +import android.app.Application +import timber.log.Timber +import timber.log.Timber.DebugTree + +class MyApplication : Application() { + + override fun onCreate() { + super.onCreate() + Timber.plant(DebugTree()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/adriankuta/unbounddragdrop/SimpleGridPageAdapter.kt b/app/src/main/java/com/github/adriankuta/unbounddragdrop/SimpleGridPageAdapter.kt new file mode 100644 index 0000000..b5ef6fd --- /dev/null +++ b/app/src/main/java/com/github/adriankuta/unbounddragdrop/SimpleGridPageAdapter.kt @@ -0,0 +1,56 @@ +package com.github.adriankuta.unbounddragdrop + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.github.adriankuta.unbounddragdrop.databinding.TextItemBinding +import com.github.adriankuta.unbounddragdrop.model.Task + + +class SimpleGridPageAdapter : + RecyclerView.Adapter() { + + private val diffUtil = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: Task, + newItem: Task + ): Boolean { + return oldItem == newItem + } + } + + private val asyncListDiffer = AsyncListDiffer(this, diffUtil) + + fun submitList(list: List) { + asyncListDiffer.submitList(list) + } + + override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder(viewGroup.inflate()) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + val data = asyncListDiffer.currentList[position] + viewHolder.bind(data) + } + + override fun getItemCount() = asyncListDiffer.currentList.size + + class ViewHolder(private val binding: TextItemBinding) : RecyclerView.ViewHolder(binding.root) { + + fun bind(task: Task) { + binding.textView.text = task.title + binding.root.isLongClickable = true + } + } + + private fun ViewGroup.inflate() = + TextItemBinding.inflate(LayoutInflater.from(context), this, false) + +} \ No newline at end of file diff --git a/app/src/main/java/com/github/adriankuta/unbounddragdrop/model/Task.kt b/app/src/main/java/com/github/adriankuta/unbounddragdrop/model/Task.kt new file mode 100644 index 0000000..0e5b92c --- /dev/null +++ b/app/src/main/java/com/github/adriankuta/unbounddragdrop/model/Task.kt @@ -0,0 +1,7 @@ +package com.github.adriankuta.unbounddragdrop.model + +data class Task( + val id: String, + val title: String, + val taskStatus: TaskStatus +) diff --git a/app/src/main/java/com/github/adriankuta/unbounddragdrop/model/TaskStatus.kt b/app/src/main/java/com/github/adriankuta/unbounddragdrop/model/TaskStatus.kt new file mode 100644 index 0000000..bc8bbca --- /dev/null +++ b/app/src/main/java/com/github/adriankuta/unbounddragdrop/model/TaskStatus.kt @@ -0,0 +1,6 @@ +package com.github.adriankuta.unbounddragdrop.model + +enum class TaskStatus { + TODO, + IN_PROGRESS +} \ No newline at end of file diff --git a/app/src/main/java/com/github/adriankuta/unbounddragdrop/util/CoroutinesUtils.kt b/app/src/main/java/com/github/adriankuta/unbounddragdrop/util/CoroutinesUtils.kt new file mode 100644 index 0000000..a611346 --- /dev/null +++ b/app/src/main/java/com/github/adriankuta/unbounddragdrop/util/CoroutinesUtils.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * 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.github.adriankuta.unbounddragdrop.util + +import kotlinx.coroutines.flow.SharingStarted + +private const val StopTimeoutMillis: Long = 5000 + +/** + * A [SharingStarted] meant to be used with a [StateFlow] to expose data to the UI. + * + * When the UI stops observing, upstream flows stay active for some time to allow the system to + * come back from a short-lived configuration change (such as rotations). If the UI stops + * observing for longer, the cache is kept but the upstream flows are stopped. When the UI comes + * back, the latest value is replayed and the upstream flows are executed again. This is done to + * save resources when the app is in the background but let users switch between apps quickly. + */ +val WhileUiSubscribed: SharingStarted = SharingStarted.WhileSubscribed(StopTimeoutMillis) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..179b504 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/text_item.xml b/app/src/main/res/layout/text_item.xml new file mode 100644 index 0000000..2761188 --- /dev/null +++ b/app/src/main/res/layout/text_item.xml @@ -0,0 +1,21 @@ + + + + + + + + + \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryPublishConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryPublishConventionPlugin.kt new file mode 100644 index 0000000..f23b4d1 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryPublishConventionPlugin.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * 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. + */ + +import org.gradle.api.Plugin +import org.gradle.api.Project + +class AndroidLibraryPublishConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + with(pluginManager) { + apply("com.vanniktech.maven.publish") + apply("com.gradleup.nmcp") + } + } + } +}