티스토리 뷰
0. 기본 세팅
build.gradle(:app)
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
android {
compileSdk 32
defaultConfig {
applicationId "com.example.mymemo"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion "1.2.0-beta03"
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.8.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation 'androidx.compose.material3:material3:1.0.0-alpha15'
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.activity:activity-compose:1.5.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
// compose navigation
def nav_version = "2.5.1"
implementation "androidx.navigation:navigation-compose:$nav_version"
// Room library
def room_version = "2.4.3"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// Lottie Animation
implementation 'com.airbnb.android:lottie:5.2.0'
implementation "com.airbnb.android:lottie-compose:5.2.0"
// hilt
implementation 'com.google.dagger:hilt-android:2.43'
kapt 'com.google.dagger:hilt-compiler:2.43'
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
// Lottie Animation
implementation 'com.airbnb.android:lottie:5.2.0'
implementation "com.airbnb.android:lottie-compose:5.2.0"
}
build.gradle(Mymemo)
buildscript {
ext {
compose_version = '1.3.0-alpha02'
}
dependencies {
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.43'
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.2.0' apply false
id 'com.android.library' version '7.2.0' apply false
id 'org.jetbrains.kotlin.android' version '1.6.21' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.mymemo">
<application
android:name=".MyMemo"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyMemo"
tools:targetApi="31">
<activity
android:name=".view.MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.MyMemo">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Database
@Database(
entities = [MemoEntity::class],
version = 1
)
abstract class MemoDatabase : RoomDatabase() {
abstract fun memoDao(): MemoDao
companion object {
const val DATABASE_NAME = "my_memo.db"
}
}
@Entity(
indices = [Index(
value = [
"index", "title", "contents", "isSecret",
"password", "timestamp", "colorGroup", "isImportance"], unique = true
)]
)
data class MemoEntity(
@PrimaryKey(autoGenerate = true) val index: Long = 0,
@ColumnInfo(name = "title") val title: String = "",
@ColumnInfo(name = "contents") val contents: String = "",
@ColumnInfo(name = "isSecret") val isSecret: Boolean = false,
@ColumnInfo(name = "isImportance") val isImportance: Boolean = false,
@ColumnInfo(name = "password") val password: String = "",
@ColumnInfo(name = "timestamp") val timestamp: Long = System.currentTimeMillis(),
@ColumnInfo(name = "colorGroup") val colorGroup: Int = 0
) {
fun mapper() = MemoItem(
index = index,
title = title,
contents = contents,
isSecret = isSecret,
isImportance = isImportance,
password = password,
colorGroup = colorGroup,
timestamp = timestamp
)
}
@Dao
interface MemoDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMemoItem(memoEntity: MemoEntity): Long
@Query("SELECT * FROM MemoEntity WHERE `title` LIKE :search")
fun selectAllMemo(search : String): Flow<List<MemoEntity>>
@Query("SELECT * FROM MemoEntity WHERE `index` = :index")
fun selectMemoIndex(index: Long): Flow<MemoEntity>
@Query("DELETE FROM MemoEntity WHERE `index` = :index")
suspend fun deleteMemoIndex(index: Long)
@Query("UPDATE MemoEntity SET `isImportance` = :isImportance WHERE `index` = :index")
suspend fun updateImportance(index: Long, isImportance: Boolean): Int
}
Data
data class MemoItem(
val index: Long = 0,
var title: String = "",
var contents: String = "",
var isSecret: Boolean = false,
var isImportance: Boolean = false,
var password: String = "",
var colorGroup: Int = 0,
val timestamp: Long = System.currentTimeMillis()
) {
fun mapper() = MemoEntity(
index = index,
title = title,
contents = contents,
isSecret = isSecret,
isImportance = isImportance,
password = password,
colorGroup = colorGroup,
timestamp = timestamp
)
}
수정을 위한 데이터 클래스입니다.
1. 메모 리스트 화면
기본 조건
- 사용자는 작성한 메모 리스트를 볼 수 있어야 한다.
- 메모 리스트에 노출되는 메모는 작성된 메모 문장 한 줄만 노출한다.
- 비밀 메모의 경우 메모 문장이 노출되지 않아야 한다.
- 비밀 메모는 메모 리스트에 “비밀 메모입니다" 혹은 잠금 표시로 노출되어야 한다.
- 메모 리스트에서 비밀 메모 상세 보기 클릭 시 암호를 입력해야 메모 상세화면으로 이동이 가능하다.
- 사용자는 메모를 검색할 수 있어야 한다.
- 사용자는 메모를 삭제할 수 있어야 한다.
추가 or 수정 조건
- 메모 리스트에 노출되는 문장은 메모의 제목이며 동일하게 한 줄로만 노출된다.
- 사용자는 “날짜순, 제목순, 중요글만, 비밀글만”의 조건을 선택하여 리스트의 정렬을 변경할 수 있다.
- 별 아이콘을 클릭하여 메모의 중요 메모 설정이 가능하다.
- 사용자가 지정한 색상으로 메모 리스트의 아이템 색상을 표시한다.
리스트 조회
리스트는 검색을 기반으로 조회를 하게 됩니다.
@Query("SELECT * FROM MemoEntity WHERE `title` LIKE :search")
fun selectAllMemo(search : String): Flow<List<MemoEntity>>
Room에서는 위와 같이 호출되며 search의 내용에 따라 리스트의 조회 내용이 달라지게 됩니다.
fun selectAllMemo(search: String): Flow<List<MemoItem>> =
db.selectAllMemo(search).map { it.map { entity -> entity.mapper() } }
MemoRepository에서는 MemoEntity에서 MemoItem으로 변경하여 반환합니다.
private val _list = mutableStateOf<List<MemoItem>>(emptyList())
val list : State<List<MemoItem>> = _list
init {
selectAllMemo()
}
private fun selectAllMemo(search: String = "%%") {
repository.selectAllMemo(search)
.map { orderByList(it) }
.onEach { _list.value = it }
.onEmpty { _list.value = emptyList() }
.catch { _list.value = emptyList() }
.launchIn(viewModelScope)
}
private fun orderByList(list: List<MemoItem>) : List<MemoItem> {
return when(_queryState.value) {
// 날짜순
0 -> { list.sortedBy { it.timestamp } }
// 타이틀순
1 -> { list.sortedBy { it.title } }
// 중요글만
2 -> { list.filter { it.isImportance } }
// 비밀글만
else -> { list.filter { it.isSecret } }
}
}
기본적으로 검색어는 “%%”를 사용해서 모든 리스트를 불러오게 됩니다.
map{ orderByList(it) }을 통해 정렬 조건을 지정합니다.
orderByList에서는 조건에 맞게 리스트를 sortedBy 혹은 filter를 이용해 가공하게 됩니다.
가공된 리스트를 onEach를 통해 _list 변수에 담기게 됩니다.
val list = viewModel.list.value
/** 메모 리스트 **/
if (list.isEmpty()) {
/** 빈 리스트 **/
item {
LottieAnimation(
composition = composition,
progress = { progress },
modifier = Modifier.padding(horizontal = 25.dp)
)
Text(
text = stringResource(id = R.string.empty_memo),
style = Typography.titleMedium,
color = MaterialTheme.colorScheme.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
} // 빈 리스트
} else {
/** 리스트 **/
item {
Spacer(modifier = Modifier.height(10.dp))
list.forEachIndexed { index, item ->
if (item.isSecret) {
/** 비밀메모 **/
SecretMemoItem(
secretClickListener = {
secretDialogState.value = true
secretIndex = index
},
modifier = Modifier.fillMaxWidth()
)
} else {
/** 일반 메모 **/
MemoItem(
memoItem = item,
viewModel = viewModel,
clickListener = {
routeAction.navToDetail(it)
},
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(10.dp))
}
Spacer(modifier = Modifier.height(46.dp))
} // 리스트
} // 메모 리스트
ViewModel의 list를 감지하고 있는 UI 쪽에서는 비어있을 경우에는 LottieAnimation을 이용해서 안내를 해줍니다.
메모 종류에 따라 비밀 메모, 일반 메모를 그려주게 됩니다.
val field = viewModel.searchState.value
InputBar(
modifier = Modifier.padding(vertical = 10.dp),
field = field,
hint = stringResource(id = R.string.guide_input_search),
hintColor = if (isSystemInDarkTheme()) White80 else Black80
) {
viewModel.event(ListEvent.WriteSearch(it))
}
InputBar는 OutlineTextField를 공통적으로 사용하기 위해 만들어서 사용하였습니다.
검색의 경우 TextField의 내용이 변경될 때마다 viewModel.event(ListEvent.WriteSearch(it)을 호출하게 됩니다.
fun event(event: ListEvent) {
when(event) {
is ListEvent.WriteSearch -> {
_searchState.value = event.search
selectAllMemo("%${_searchState.value}%")
}
is ListEvent.ChangeQuery -> {
_queryState.value = event.queryIndex
selectAllMemo("%${_searchState.value}%")
}
}
}
ViewModel에서는 변경된 내용을 받아서 _searchState에 저장한 뒤 다시 검색을 진행하게 됩니다.
정렬 조건 변경도 위와 같은 방식으로 이루어지게 됩니다.
그 외 기능
UI 쪽에서 클릭 등의 이벤트 발생 > ViewModel에게 이벤트 내용 전달 > ViewModel에서 Data 상태 변경 (DB 업데이트 등) > 감지하고 있던 UI 반영
기능들은 다음의 순서대로 작업을 진행하게 되며 이는 다른 페이지에서도 동일하게 적용됩니다.
2. 메모 작성 화면
기본 조건
- 사용자에게 메모를 작성/수정 기능을 제공해야 한다.
- 비밀 메모로 설정 시 비밀번호 입력창이 뜨고 비밀번호를 입력하면 비밀 메모로 설정된다.
- 일반 메모를 비밀 메모로, 비밀 메모를 일반 메모로 변경이 가능해야 한다.
- 내용을 작성할 때 작성된 메모의 글자 수가 노출되어야 한다.
추가 조건
- 메모 작성 시 제목과 내용으로 분리되며 필수로 작성하여야 한다.
- 사용자는 메모의 색상을 지정할 수 있다.
메모 작성
private val _memoItem = mutableStateOf(MemoItem())
val memoItemState : State<MemoItem> = _memoItem
init {
savedStateHandle.get<Long>("index")?.let {
selectMemo(it)
}
}
수정 시에는 NavHost에서 값을 전달받아서 해당 메모의 내용을 불러와서 저장하게 됩니다.
fun event(event: WriteEvent) {
when(event) {
is WriteEvent.WriteTitle -> {
_memoItem.value = _memoItem.value.copy(title = event.title)
}
is WriteEvent.WriteContents -> {
_memoItem.value = _memoItem.value.copy(contents = event.contents)
}
}
}
기능 구현 방식은 이전과 동일합니다.
TextField나 CheckBox, ColorSelector 등 이벤트가 발생하게 되면 변경된 값을 event()을 통해 ViewModel에게 전달하게 됩니다.
event()에서는 전달받은 값을 저장할 때 기존에 _memoItem의 값을 copy한 후 변경된 값을 변경하여 저장합니다.
_memoItem의 값이 변경되었으므로 감지하고 있던 UI 쪽에서는 다시 UI를 그리게 됩니다.
색상 선택
@Composable
fun ColorSelector(
index: Int,
colorGroup: ColorGroup,
clickListener: (Int) -> Unit
) {
Card(
shape = CircleShape,
border = BorderStroke(2.dp, if(isSystemInDarkTheme()) White else Black),
colors = CardDefaults.cardColors(
containerColor = Color(colorGroup.mainColor)
),
modifier = Modifier
.size(60.dp)
.clickable(
indication = null,
interactionSource = remember {
MutableInteractionSource()
}
) {
clickListener(index)
}
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(2.dp)
) {
Canvas(modifier = Modifier.size(58.dp)) {
drawArc(
color = Color(colorGroup.subColor),
startAngle = 270f,
sweepAngle = 180f,
useCenter = true,
style = Fill
)
}
}
}
}
전체 원 안에 반반 색상이 칠해진 UI를 구현하였습니다.
전체를 Card로 감싸고 전체 원의 모양과 색상일 지정합니다.
Canvas를 활용하여 반 원을 그려주고 색상을 적용시켜 배치를 하였습니다.
3. 메모 상세 화면
기본 조건
- 작성한 메모의 상세 내용을 볼 수 있어야 한다.
추가 조건
- 메모의 중요 체크를 변경 할 수 있어야 한다.
위의 두 개의 페이지와 구현 방식의 차이가 딱히 없으므로 내용은 생략합니다.
4. 다이얼로그
메모 삭제 다이얼로그
비밀 메모의 비밀번호 확인 다이얼로그
'안드로이드 > 코드' 카테고리의 다른 글
포켓몬 도감 만들기(2) : Fast Api, Compose, 홈 화면, 리스트 화면 (0) | 2022.08.21 |
---|---|
포켓몬 도감 만들기(1) : Fast Api (0) | 2022.08.21 |
Compose 미니 프로젝트 : Pokedex (0) | 2022.06.30 |
Compose 기초 2 : Text, Image, LazyColumn + Card (0) | 2022.06.30 |
네이버 검색 API 사용 (0) | 2022.06.09 |
- Total
- Today
- Yesterday
- 포켓몬 도감
- Fast api
- Compose 네이버 지도 api
- WebView
- Compose BottomSheetScaffold
- column
- Row
- Compose BottomSheetDialog
- 안드로이드 구글 지도
- Duplicate class fond 에러
- Compose MotionLayout
- Compose BottomSheet
- compose
- Pokedex
- Compose ConstraintLayout
- Duplicate class found error
- Retrofit
- Compose ModalBottomSheetLayout
- 안드로이드
- WorkManager
- Android
- Compose 네이버 지도
- 웹뷰
- Gradient
- Compose QRCode Scanner
- Worker
- Kotlin
- Compose Naver Map
- LazyColumn
- Android Compose
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |