Skip to content

Commit

Permalink
충남대 Android_김선규 6주차 Step0 (#23)
Browse files Browse the repository at this point in the history
* Initial commit

* 충남대 Android_김선규 5주차 Step0 (#33)

* Initial commit

* 충남대 Android_김선규 4주차 Step0 (#10)

* Initial commit

* Merge : android-map-keyword into android-map-search (#8)

* 충남대 Android_김선규 3주차 과제 Step1 (#47)

* docs: add step1 requirements

* chore: set for using android api

* style: rename id in layout

* feat: remove storeInfo for using api

* feat: add connecting api for searching

* style: rename variable name proper

* 충남대 Android_김선규 3주차 과제 Step2 (#85)

* style: function rename and split

* feat: Change function to fit coroutine

* docs: add step2 requirements

* style: move from main to sub file

* chore: set it up to work in the right environment

* feat: display kakao map, when app is started

---------

Co-authored-by: MyStoryG <[email protected]>

* 충남대 Android_김선규 4주차 Step 1 제출 (#47)

* docs: add week 4 step 1 requirements

* feat: add searching by saved search keyword

* chore: relocate files proper

* feat: modify adapter to make clean code

* feat: add image for marker

* feat: add layout for displaying bottom sheet

* feat: add parcelabel for easy to send data

* feat: add displaying search result

* refactor: modify class structure

* feat: add error screen and reload button

* feat: add saving and loading last position

when app is closed, save last position
when app is opened, load last position

* 충남대 Android_김선규 4주차 Step2 수정 (#73)

* style: rename variable name

* feat: add viewModel and Repository for saving last position

* refactor: classify in more detail

* test: add android UI test

* chore: add mockk test dependency

* test: add ViewModel test

* chore: add testOptions

* test: modify android ui test

---------

Co-authored-by: MyStoryG <[email protected]>

* 충남대 Android_김선규 4주차 Step0 (#10)

* Initial commit

* Merge : android-map-keyword into android-map-search (#8)

* 충남대 Android_김선규 3주차 과제 Step1 (#47)

* docs: add step1 requirements

* chore: set for using android api

* style: rename id in layout

* feat: remove storeInfo for using api

* feat: add connecting api for searching

* style: rename variable name proper

* 충남대 Android_김선규 3주차 과제 Step2 (#85)

* style: function rename and split

* feat: Change function to fit coroutine

* docs: add step2 requirements

* style: move from main to sub file

* chore: set it up to work in the right environment

* feat: display kakao map, when app is started

---------

Co-authored-by: MyStoryG <[email protected]>

* 충남대 Android_김선규 4주차 Step 1 제출 (#47)

* docs: add week 4 step 1 requirements

* feat: add searching by saved search keyword

* chore: relocate files proper

* feat: modify adapter to make clean code

* feat: add image for marker

* feat: add layout for displaying bottom sheet

* feat: add parcelabel for easy to send data

* feat: add displaying search result

* refactor: modify class structure

* feat: add error screen and reload button

* feat: add saving and loading last position

when app is closed, save last position
when app is opened, load last position

* 충남대 Android_김선규 4주차 Step2 수정 (#73)

* style: rename variable name

* feat: add viewModel and Repository for saving last position

* refactor: classify in more detail

* test: add android UI test

* chore: add mockk test dependency

* test: add ViewModel test

* chore: add testOptions

* test: modify android ui test

* chore: add Room dependency

* feat: change SQLite to Room about DB

* chore: add Hilt dependency

* feat: add Hilt for MVVM

* chore: add dataBinding

* feat: add dataBinding

* feat: modify about Hilt

---------

Co-authored-by: MyStoryG <[email protected]>
  • Loading branch information
kimseongyu and MyStoryG authored Jul 31, 2024
1 parent 2967c62 commit 4add9de
Show file tree
Hide file tree
Showing 40 changed files with 1,618 additions and 16 deletions.
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,76 @@
# android-map-notification

## Layout requirements

**Kakao map**

Display kakao Map

- Display selected search result using `bottom sheet`
- When onMapError() is called, print error message
- Save last location before app closed, and when app is started, focusing that location

Search window button

- If Search window button is clicked, go to _Save search keyword_
- _Stack Save search keyword_ on top of the this window

**Save search keyword**

Input search keyword

- Text input window to search
- It has x button to erase

Saved search keyword list

- It has x button to erase
- It scrolls horizontally

Search result list

- Using `RecyclerView` to implement about search result list
- It scrolls vertically

## Function List

**Kakao map**

**Kakao map**

Display kakao Map

- Display selected search result using `bottom sheet`
- When onMapError() is called, print error message
- Save last location before app closed, and when app is started, focusing that location

Go to _Save serach keyword_

- If Search window button is clicked, go to _Save search keyword_
- _Stack Save search keyword_ on top of the this window

**Save search keyword**

Requirements Rule

- Using `SQLite` to save search data
- When application is restart, data is maintained
- Apply the MVVM architectural pattern

Input search keyword

- When application is restart, data is maintained
- Search every time a character is entered
- When clicked x button, string is erased

Saved search keyword list

- Keywords are not duplicated and recently serached keyword are added later
- Can search to select saved search keyword
- When clicked x button, saved search keyword is erased

Search result list

- There are at least 15 search results
- Search results have search word as categories
- Selected item is added to the saved search word list, and display location on the kakao map
21 changes: 20 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties

fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key)

plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jlleitschuh.gradle.ktlint")
id("kotlin-parcelize")
id("kotlin-kapt")
id("com.google.dagger.hilt.android")
id("com.google.gms.google-services")
// id("com.google.gms.google-services")
}

android {
Expand All @@ -20,6 +24,16 @@ android {
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

buildConfigField("String", "KAKAO_API_KEY", getApiKey("KAKAO_API_KEY"))
buildConfigField("String", "KAKAO_REST_API_KEY", getApiKey("KAKAO_REST_API_KEY"))

ndk {
abiFilters.add("arm64-v8a")
abiFilters.add("armeabi-v7a")
abiFilters.add("x86")
abiFilters.add("x86_64")
}
}

buildTypes {
Expand All @@ -43,6 +57,11 @@ android {
dataBinding = true
buildConfig = true
}
testOptions {
packaging {
resources.excludes.add("META-INF/*")
}
}
}

dependencies {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package campus.tech.kakao.map

import android.app.Instrumentation
import android.content.Intent
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import campus.tech.kakao.map.model.search.Place
import campus.tech.kakao.map.view.ActivityKeys
import campus.tech.kakao.map.view.kakaomap.KakaoMapActivity
import kotlinx.coroutines.delay
import org.hamcrest.Matchers.not
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

private const val TEST_PLACE_NAME = "Test Place"
private const val TEST_CATEGORY_NAME = "Test Category"
private const val TEST_ADDRESS_NAME = "Test Address"
private const val TEST_X = "36.37003"
private const val TEST_Y = "127.34594"

@RunWith(AndroidJUnit4::class)
class KakaoMapActivityTest {

@get: Rule
val activityRule = ActivityScenarioRule(KakaoMapActivity::class.java)

private lateinit var mDvice: UiDevice

@Before
fun setUp() {
ActivityScenario.launch(KakaoMapActivity::class.java)
mDvice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
}

@Test
fun testKakaoMapIsLoaded() {
// then
onView(withId(R.id.kakaomap_err)).check(matches(not(isDisplayed())))
}

@Test
fun testDisplaySearchWindow() {
// when
onView(withId(R.id.goto_search_window)).perform(click())

// then
onView(withId(R.id.searchWindow)).check(matches(isDisplayed()))
}

@Test
fun testDisplayPlaceOnMap() {
// given
val place = Place(TEST_PLACE_NAME, TEST_CATEGORY_NAME, TEST_ADDRESS_NAME, TEST_X, TEST_Y)
val intent = Intent(
InstrumentationRegistry.getInstrumentation().targetContext,
KakaoMapActivity::class.java
).apply {
putExtra(ActivityKeys.INTENT_PLACE, place)
}

// when
ActivityScenario.launch<KakaoMapActivity>(intent).use {
mDvice.waitForIdle(3000)

// then
onView(withId(R.id.place_info_bottom_sheet)).check(matches(isDisplayed()))
}
}

@Test
fun testDisplayKakaoMapError() {
// 잘못된 API 전송하도록 설정
// onView(withId(R.id.kakaomap_err)).check(matches(isDisplayed()))
}
}
96 changes: 96 additions & 0 deletions app/src/androidTest/java/campus/tech/kakao/map/SearchWindowTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package campus.tech.kakao.map

import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import campus.tech.kakao.map.model.search.Place
import campus.tech.kakao.map.view.ActivityKeys
import campus.tech.kakao.map.view.search.SearchWindowActivity
import org.hamcrest.Matchers.not
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

private const val TEST_SEARCH_KEYWORD = "cafe"
private const val TEST_PLACE_NAME = "충남대학교 대덕캠퍼스"

@RunWith(AndroidJUnit4::class)
class SearchWindowTest {

@get: Rule
val activityRule = ActivityScenarioRule(SearchWindowActivity::class.java)

@Before
fun setUp() {
ActivityScenario.launch(SearchWindowActivity::class.java)
Intents.init()
}

@After
fun tearDown() {
Intents.release()
}

@Test
fun testChangeSearchWindowText() {
// when
onView(withId(R.id.searchWindow)).perform(
typeText(TEST_SEARCH_KEYWORD),
closeSoftKeyboard()
)

// then
onView(withId(R.id.emptySearchResults)).check(matches(not(isDisplayed())))
onView(withId(R.id.searchResultsList)).check(matches(isDisplayed()))
}

@Test
fun testDeleteSearchKeyword() {
// when
onView(withId(R.id.delSearchKeyword)).perform(click())

// then
onView(withId(R.id.searchWindow)).check(matches(withText("")))
}

@Test
fun testClickSearchResult() {
// when
onView(withId(R.id.searchWindow)).perform(
typeText(TEST_SEARCH_KEYWORD),
closeSoftKeyboard()
)
Thread.sleep(3000)
onView(withId(R.id.searchResultsList))
.perform(actionOnItemAtPosition<RecyclerView.ViewHolder>(0, click()))

// then
Intents.intended(hasExtra(ActivityKeys.INTENT_PLACE, Place::class.java))
}

@Test
fun testClickSavedSearchKeyword() {
// recyclerview의 item view 클릭 추가
// onView(withId(R.id.searchWindow)).check(matches(withText(TEST_PLACE_NAME)))
}

@Test
fun testDeleteSavedSearchKeyword() {
// recyclerview의 item view 클릭 추가
// onView(withText(TEST_PLACE_NAME)).check(matches(not(isDisplayed())))
}
}
11 changes: 9 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.INTERNET" />

<application
android:name=".MyApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand All @@ -17,7 +18,13 @@
android:theme="@style/Theme.Map"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:name=".view.kakaomap.KakaoMapActivity"
android:exported="false" />
<activity
android:name=".view.search.SearchWindowActivity"
android:exported="false" />
<activity
android:name=".view.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand All @@ -27,4 +34,4 @@
</activity>
</application>

</manifest>
</manifest>
35 changes: 35 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/AppModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package campus.tech.kakao.map

import android.content.Context
import android.content.SharedPreferences
import campus.tech.kakao.map.model.search.SearchKeywordDao
import campus.tech.kakao.map.repository.kakaomap.LastPositionRepository
import campus.tech.kakao.map.repository.search.KakaoSearchKeywordAPI
import campus.tech.kakao.map.repository.search.SavedSearchKeywordRepository
import campus.tech.kakao.map.repository.search.SearchRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

@Provides
@Singleton
fun provideSavedSearchKeywordRepository(searchKeywordDao: SearchKeywordDao) =
SavedSearchKeywordRepository(searchKeywordDao)

@Provides
@Singleton
fun provideSearchRepository(retrofitKakaoSearchKeyword: KakaoSearchKeywordAPI): SearchRepository =
SearchRepository(retrofitKakaoSearchKeyword)

@Provides
@Singleton
fun provideLastPositionRepository(sharedPreferences: SharedPreferences) =
LastPositionRepository(sharedPreferences)
}
2 changes: 2 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package campus.tech.kakao.map

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/campus/tech/kakao/map/MyApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package campus.tech.kakao.map

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class MyApplication: Application() {
}
Loading

0 comments on commit 4add9de

Please sign in to comment.