Skip to content

Commit

Permalink
Merge pull request #3 from VeceluXa/screen-map
Browse files Browse the repository at this point in the history
Screen map
  • Loading branch information
GitlHubber committed Aug 4, 2023
2 parents c174b78 + 67f6500 commit 1fdbad6
Show file tree
Hide file tree
Showing 63 changed files with 1,203 additions and 210 deletions.
43 changes: 39 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,41 @@
# Space
# Space Android App
## Mars Photos
App uses Nasa's API to display photos from Mars in a list. When clicking on a photo, details screen is opened. There user can zoom/move photo like in a gallery app. Also user can share photo with 3rd party apps.

## Setup
- Add Firebase's google-services.json to the root directory of app module

## Map

User can view map of Earth in 3 different views (default, hybrid, satellite). When user clicks on a map the dialog opens up to type marker name. Upon saving marker it will be displayed in bottom sheet dialog that is visible to user but collapsed by default.

---
# Tech stack
- Android SDK
- Clean Architecture
- Moxy MVP
- Cicerone Navigation
- RxJava3
- Retrofit, OkHttp
- Hilt
- Splash Screen API
- Google Maps SDK
- Firebase Cloud Messaging
- Moshi


# Setup
- Add Firebase's google-services.json to the root directory of app module to be able to send notification from Firebase Console
- Add [NASA_API_KEY](https://api.nasa.gov/) to local.properties
- Add [MAPS_API_KEY](https://developers.google.com/maps) to local.properties
- Add [MAPS_API_KEY](https://developers.google.com/maps) to local.properties

# Screenshots
## Main Screen
![](./assets/screen_home.jpg)

## Details Screen
![](./assets/screen_details_tutorial.jpg)
![](./assets/screen_details.jpg)

## Map Screen
![](./assets/screen_map.jpg)
![](./assets/screen_map_marker_dialog.jpg)
![](./assets/screen_map_bottom_sheet_dialog.jpg)
14 changes: 8 additions & 6 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />

<application
android:name=".presentation.App"
Expand Down Expand Up @@ -36,21 +36,23 @@
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<receiver android:name=".presentation.notifications.DeviceBootBroadcastReceiver"
<receiver
android:name=".presentation.notifications.DeviceBootBroadcastReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<provider
android:authorities="com.danilovfa.space.FileProvider"
android:name="androidx.core.content.FileProvider"
android:authorities="com.danilovfa.space.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
android:resource="@xml/file_paths" />
</provider>

<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_launcher_foreground" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.danilovfa.space.presentation.model

import com.google.android.gms.maps.model.Marker

data class MapMarker(
val label: String,
val marker: Marker
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ class HomePresenter @Inject constructor(

private var photosDisposable: Disposable? = null

fun getPhotos(rover: String = ROVER_CURIOSITY) {
var rover: String = ROVER_CURIOSITY

fun getPhotos() {
viewState.showProgressBar()
if (photos.isEmpty()) {
photosDisposable = getRoverPhotosUseCase.execute(rover)
Expand Down Expand Up @@ -65,10 +67,11 @@ class HomePresenter @Inject constructor(

}

fun selectRover(rover: String) {
fun selectRover(newRover: String) {
photos = listOf()
scrollPosition = 0
getPhotos(rover)
rover = newRover
getPhotos()
}

fun onBackPressed() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import moxy.viewstate.strategy.AddToEndSingleStrategy
import moxy.viewstate.strategy.StateStrategyType

@StateStrategyType(value = AddToEndSingleStrategy::class)
interface HomeView: MvpView {
interface HomeView : MvpView {
fun showPhotos(photos: List<MarsRoverPhoto>, scrollPosition: Int)
fun showError(message: String)
fun showError(@StringRes messageRes: Int)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.danilovfa.space.presentation.mvp.map

import com.danilovfa.space.presentation.model.MapMarker
import com.github.terrakok.cicerone.Router
import moxy.InjectViewState
import moxy.MvpPresenter
Expand All @@ -8,7 +9,20 @@ import moxy.MvpPresenter
class MapPresenter(
private val router: Router
) : MvpPresenter<MapView>() {

private val markers = mutableListOf<MapMarker>()
fun getMarkers() = markers.toList()

fun onBackPressed() {
router.exit()
}

fun deleteMarker(mapMarker: MapMarker): List<MapMarker> {
markers.remove(mapMarker)
return markers
}

fun addMarker(mapMarker: MapMarker) {
markers.add(mapMarker)
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.danilovfa.space.presentation.mvp.map

import com.danilovfa.space.presentation.model.MapMarker
import moxy.MvpView
import moxy.viewstate.strategy.AddToEndSingleStrategy
import moxy.viewstate.strategy.OneExecutionStateStrategy
import moxy.viewstate.strategy.StateStrategyType

@StateStrategyType(value = OneExecutionStateStrategy::class)
interface MapView : MvpView
@StateStrategyType(value = AddToEndSingleStrategy::class)
interface MapView : MvpView {
fun showMarkers(markers: List<MapMarker>)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import moxy.viewstate.strategy.AddToEndSingleStrategy
import moxy.viewstate.strategy.SkipStrategy
import moxy.viewstate.strategy.StateStrategyType

interface PhotoView: MvpView {
interface PhotoView : MvpView {
@StateStrategyType(value = AddToEndSingleStrategy::class)
fun showTutorial()
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.danilovfa.space.presentation.notifications
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.work.Constraints
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.danilovfa.space.presentation.ui.MainActivity
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage

class FirebaseNotificationService: FirebaseMessagingService() {
class FirebaseNotificationService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import android.app.PendingIntent
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,29 @@ class MainActivity : MvpAppCompatActivity(), MainView, RouterProvider {
supportActionBar?.title = ""

if (networkStatus)
setup()
setup(savedInstanceState)

}
private fun setup() {
val tabIdOrNull =
intent.extras?.getString(ChargingNotificationWorker.EXTRA_TAB_CONTAINER_ID)
if (tabIdOrNull != null) defaultTabId = tabIdOrNull

private fun setup(savedInstanceState: Bundle?) {
restoreSavedDefaultTab(savedInstanceState)

setBottomNavigationListener()
setOnBackPressedListener()

NotificationManager(this).setupPermissions()
}

private fun restoreSavedDefaultTab(savedInstanceState: Bundle?) {
val savedDefaultTab = savedInstanceState?.getString(SAVED_DEFAULT_TAB_ID)
if (savedDefaultTab == HOME_TAB_ID || savedDefaultTab == MAP_TAB_ID)
defaultTabId = savedDefaultTab

val tabIdOrNull =
intent.extras?.getString(ChargingNotificationWorker.EXTRA_TAB_CONTAINER_ID)
if (tabIdOrNull != null) defaultTabId = tabIdOrNull
}

private fun setBottomNavigationListener() {
binding.bottomNavigationView.apply {
setOnItemSelectedListener { item ->
Expand Down Expand Up @@ -138,6 +147,7 @@ class MainActivity : MvpAppCompatActivity(), MainView, RouterProvider {
if (currentFragment != null && newFragment != null && currentFragment == newFragment) return
val transaction = fm.beginTransaction()
if (newFragment == null) {
defaultTabId = containerId
transaction.add(
R.id.fragmentContainerView,
TabContainer(containerId).createFragment(fm.fragmentFactory),
Expand All @@ -148,6 +158,7 @@ class MainActivity : MvpAppCompatActivity(), MainView, RouterProvider {
transaction.hide(currentFragment)
}
if (newFragment != null) {
defaultTabId = containerId
transaction.show(newFragment)
}
transaction.commitNow()
Expand All @@ -172,6 +183,11 @@ class MainActivity : MvpAppCompatActivity(), MainView, RouterProvider {
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(SAVED_DEFAULT_TAB_ID, defaultTabId)
}

override fun onResumeFragments() {
super.onResumeFragments()
navigatorHolder.setNavigator(navigator)
Expand All @@ -186,4 +202,8 @@ class MainActivity : MvpAppCompatActivity(), MainView, RouterProvider {
super.onDestroy()
_binding = null
}

companion object {
private const val SAVED_DEFAULT_TAB_ID = "SavedDefaultTabId"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.danilovfa.space.presentation.ui.adapter

import android.content.Context
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.danilovfa.space.databinding.ItemMarkerBinding
import com.danilovfa.space.presentation.model.MapMarker

class MarkersAdapter :
RecyclerView.Adapter<MarkersAdapter.ViewHolder>() {

private var onMarkerDeleteListener: OnMarkerDeleteListener? = null

private val differCallback = object : DiffUtil.ItemCallback<MapMarker>() {
override fun areItemsTheSame(oldItem: MapMarker, newItem: MapMarker): Boolean {
return oldItem == newItem
}

override fun areContentsTheSame(oldItem: MapMarker, newItem: MapMarker): Boolean {
return oldItem.label == newItem.label
}
}

val differ = AsyncListDiffer(this, differCallback)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemMarkerBinding.inflate(LayoutInflater.from(parent.context))
return ViewHolder(binding, parent.context)
}

override fun getItemCount() = differ.currentList.size

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(differ.currentList[position])
}

inner class ViewHolder(private val binding: ItemMarkerBinding, private val context: Context) :
RecyclerView.ViewHolder(binding.root) {
fun bind(marker: MapMarker) {
binding.apply {
markerNameTextView.text = marker.label
markerLongitudeTextView.text = marker.marker.position.longitude.toString()
markerLatitudeTextView.text = marker.marker.position.latitude.toString()
markerDeleteImageButton.setOnClickListener {
onMarkerDeleteListener?.onMarkerDelete(marker)
}
}
}
}

fun setOnMarkerDeleteListener(listener: OnMarkerDeleteListener) {
onMarkerDeleteListener = listener
}

interface OnMarkerDeleteListener {
fun onMarkerDelete(marker: MapMarker)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import androidx.viewbinding.ViewBinding

typealias Inflate<T> = (LayoutInflater, ViewGroup?, Boolean) -> T

open class BaseDialogFragment<VB: ViewBinding>(
open class BaseDialogFragment<VB : ViewBinding>(
val inflate: Inflate<VB>
) : DialogFragment() {
private var _binding: VB? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ import com.danilovfa.space.utils.extensions.addGradient
class RadioDialogFragment(
private val title: String,
private val radioButtonsText: List<String>,
private val selectedItem: String = radioButtonsText[0],
private val onSelectItem: (text: String) -> Unit
) : BaseDialogFragment<FragmentDialogRadioBinding>(FragmentDialogRadioBinding::inflate) {
companion object {
fun display(
fragmentManager: FragmentManager,
title: String,
radioButtons: List<String>,
selectedItem: String = radioButtons[0],
onSelectItem: (text: String) -> Unit
): RadioDialogFragment {
val dialog = RadioDialogFragment(title, radioButtons, onSelectItem)
val dialog = RadioDialogFragment(title, radioButtons, selectedItem, onSelectItem)
dialog.show(fragmentManager, TAG)
return dialog
}
Expand Down Expand Up @@ -55,7 +57,10 @@ class RadioDialogFragment(
binding.radioGroup.addView(radioButton)
}

binding.radioGroup.check(radioButtons[0].id)
val selectedItemPosition = radioButtonsText.indexOf(selectedItem)
val selectedRadioButton = radioButtons[selectedItemPosition]

binding.radioGroup.check(selectedRadioButton.id)

binding.okTextView.setOnClickListener {
val checkedId = binding.radioGroup.checkedRadioButtonId
Expand Down
Loading

0 comments on commit 1fdbad6

Please sign in to comment.