diff --git a/app/build.gradle b/app/build.gradle index 4c33917..0f853e3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' apply plugin: 'dagger.hilt.android.plugin' +apply plugin: 'androidx.navigation.safeargs.kotlin' android { compileSdkVersion 30 @@ -12,16 +13,16 @@ android { minSdkVersion 19 targetSdkVersion 30 versionCode 1 - versionName "1.1" + versionName "1.2" vectorDrawables.useSupportLibrary = true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" javaCompileOptions { annotationProcessorOptions { arguments += [ - "room.schemaLocation":"$projectDir/schemas".toString(), - "room.incremental":"true", - "room.expandProjection":"true"] + "room.schemaLocation" : "$projectDir/schemas".toString(), + "room.incremental" : "true", + "room.expandProjection": "true"] } } } @@ -33,9 +34,6 @@ android { } } -// To inline the bytecode built with JVM target 1.8 into -// bytecode that is being built with JVM target 1.6. (e.g. navArgs) - compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -51,40 +49,31 @@ android { } dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) //core dependencies - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'androidx.core:core-ktx:1.3.2' - implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.appcompat:appcompat:1.3.0' + implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.preference:preference-ktx:1.1.1' implementation "androidx.multidex:multidex:2.0.1" - - // coroutines - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3' - //lifecycle dependencies - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1" - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" - implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' //Hilt dependencies - implementation "com.google.dagger:hilt-android:2.35" - kapt "com.google.dagger:hilt-android-compiler:2.35" + implementation 'com.google.dagger:hilt-android:2.37' + kapt 'com.google.dagger:hilt-compiler:2.37' //jetpack nav dependencies implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' //design dependencies - implementation 'com.google.android.material:material:1.3.0' + implementation 'com.google.android.material:material:1.4.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' //room implementation "androidx.room:room-runtime:2.3.0" kapt "androidx.room:room-compiler:2.3.0" implementation "androidx.room:room-ktx:2.3.0" //glide - implementation 'io.coil-kt:coil:1.2.0' - //api + implementation 'io.coil-kt:coil:1.3.0' + //util + implementation 'com.jakewharton.timber:timber:4.7.1' //testing dependencies - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } diff --git a/app/src/androidTest/java/com/cybershark/linkmanager/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/cybershark/linkmanager/ExampleInstrumentedTest.kt deleted file mode 100644 index 178680a..0000000 --- a/app/src/androidTest/java/com/cybershark/linkmanager/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.cybershark.linkmanager - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.cybershark.linkmanager", appContext.packageName) - } -} diff --git a/app/src/main/java/com/cybershark/linkmanager/LinkManager.kt b/app/src/main/java/com/cybershark/linkmanager/LinkManager.kt index 19992cb..2f281b3 100644 --- a/app/src/main/java/com/cybershark/linkmanager/LinkManager.kt +++ b/app/src/main/java/com/cybershark/linkmanager/LinkManager.kt @@ -2,6 +2,14 @@ package com.cybershark.linkmanager import android.app.Application import dagger.hilt.android.HiltAndroidApp +import timber.log.Timber @HiltAndroidApp -class LinkManager : Application() \ No newline at end of file +class LinkManager : Application() { + override fun onCreate() { + super.onCreate() + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cybershark/linkmanager/di/AppModule.kt b/app/src/main/java/com/cybershark/linkmanager/di/AppModule.kt index 7aeeb04..fb2289c 100644 --- a/app/src/main/java/com/cybershark/linkmanager/di/AppModule.kt +++ b/app/src/main/java/com/cybershark/linkmanager/di/AppModule.kt @@ -2,6 +2,7 @@ package com.cybershark.linkmanager.di import android.content.Context import androidx.room.Room +import com.cybershark.linkmanager.repository.IRepository import com.cybershark.linkmanager.repository.Repository import com.cybershark.linkmanager.repository.room.dao.LinkDao import com.cybershark.linkmanager.repository.room.db.LinksDB @@ -26,13 +27,9 @@ object AppModule { @Singleton @Provides - fun getRoomDao(linksDB: LinksDB): LinkDao { - return linksDB.getDAO() - } + fun getRoomDao(linksDB: LinksDB): LinkDao = linksDB.getDAO() @Singleton @Provides - fun getRepository(linkDao: LinkDao): Repository { - return Repository(linkDao) - } + fun getRepository(linkDao: LinkDao): IRepository = Repository(linkDao) } \ No newline at end of file diff --git a/app/src/main/java/com/cybershark/linkmanager/repository/IRepository.kt b/app/src/main/java/com/cybershark/linkmanager/repository/IRepository.kt new file mode 100644 index 0000000..0257ba8 --- /dev/null +++ b/app/src/main/java/com/cybershark/linkmanager/repository/IRepository.kt @@ -0,0 +1,18 @@ +package com.cybershark.linkmanager.repository + +import androidx.lifecycle.LiveData +import com.cybershark.linkmanager.repository.room.entities.LinkEntity + +interface IRepository { + val allLinks: LiveData> + + suspend fun insertLink(linkEntity: LinkEntity): Result + + suspend fun deleteAllLinks(): Result + + suspend fun updateLink(linkEntity: LinkEntity): Result + + suspend fun deleteLinkById(linkId: Int): Result + + suspend fun getAllLinksAsString(): Result +} diff --git a/app/src/main/java/com/cybershark/linkmanager/repository/Repository.kt b/app/src/main/java/com/cybershark/linkmanager/repository/Repository.kt index 2bb7e6a..0e737b0 100644 --- a/app/src/main/java/com/cybershark/linkmanager/repository/Repository.kt +++ b/app/src/main/java/com/cybershark/linkmanager/repository/Repository.kt @@ -1,17 +1,68 @@ package com.cybershark.linkmanager.repository +import androidx.lifecycle.LiveData import com.cybershark.linkmanager.repository.room.dao.LinkDao import com.cybershark.linkmanager.repository.room.entities.LinkEntity +import com.cybershark.linkmanager.ui.links.viewmodels.LinksViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext -class Repository(private val linksDao: LinkDao) { +class Repository(private val linksDao: LinkDao) : IRepository { - val allLinks = linksDao.getAllLinks() + override val allLinks: LiveData> = linksDao.getAllLinks() - suspend fun insertLink(linkEntity: LinkEntity) = linksDao.insertLink(linkEntity) + override suspend fun insertLink(linkEntity: LinkEntity): Result = + withContext(Dispatchers.IO) { + try { + linksDao.insertLink(linkEntity) + return@withContext Result.success(Unit) + } catch (e: Exception) { + return@withContext Result.failure(e) + } + } - suspend fun deleteAllLinks() = linksDao.deleteAllLinks() + override suspend fun deleteAllLinks(): Result = withContext(Dispatchers.IO) { + try { + linksDao.deleteAllLinks() + return@withContext Result.success(Unit) + } catch (e: Exception) { + return@withContext Result.failure(e) + } + } - suspend fun updateLink(linkEntity: LinkEntity) = linksDao.updateLink(linkEntity) + override suspend fun updateLink(linkEntity: LinkEntity): Result = + withContext(Dispatchers.IO) { + try { + linksDao.updateLink(linkEntity) + return@withContext Result.success(Unit) + } catch (e: Exception) { + return@withContext Result.failure(e) + } + } - suspend fun deleteLinkById(linkId: Int) = linksDao.deleteLinkById(linkId) + override suspend fun deleteLinkById(linkId: Int): Result = withContext(Dispatchers.IO) { + try { + linksDao.deleteLinkById(linkId) + return@withContext Result.success(Unit) + } catch (e: Exception) { + return@withContext Result.failure(e) + } + } + + override suspend fun getAllLinksAsString(): Result = withContext(Dispatchers.Default) { + try { + if (allLinks.value.isNullOrEmpty()) { + return@withContext Result.failure(Exception("No links added!")) + } + val string = buildString { + append(LinksViewModel.START_MESSAGE) + allLinks.value?.forEach { + append("${it.linkName} - ${it.linkURL}\n") + } + } + return@withContext Result.success(string) + } catch (e: Exception) { + return@withContext Result.failure(e) + } + } } diff --git a/app/src/main/java/com/cybershark/linkmanager/repository/room/db/LinksDB.kt b/app/src/main/java/com/cybershark/linkmanager/repository/room/db/LinksDB.kt index d7e14d5..53fffe2 100644 --- a/app/src/main/java/com/cybershark/linkmanager/repository/room/db/LinksDB.kt +++ b/app/src/main/java/com/cybershark/linkmanager/repository/room/db/LinksDB.kt @@ -7,6 +7,5 @@ import com.cybershark.linkmanager.repository.room.entities.LinkEntity @Database(entities = [LinkEntity::class], exportSchema = true, version = 1) abstract class LinksDB : RoomDatabase() { - abstract fun getDAO(): LinkDao } diff --git a/app/src/main/java/com/cybershark/linkmanager/repository/room/entities/LinkEntity.kt b/app/src/main/java/com/cybershark/linkmanager/repository/room/entities/LinkEntity.kt index 8c2bc20..b8f7c99 100644 --- a/app/src/main/java/com/cybershark/linkmanager/repository/room/entities/LinkEntity.kt +++ b/app/src/main/java/com/cybershark/linkmanager/repository/room/entities/LinkEntity.kt @@ -7,9 +7,9 @@ import androidx.room.PrimaryKey @Entity(tableName = "links") data class LinkEntity( @PrimaryKey(autoGenerate = true) - val pk : Int = 0 , - @ColumnInfo(name = "linkName",defaultValue = "") - val linkName : String, - @ColumnInfo(name = "linkURL",defaultValue = "") - val linkURL : String + val pk: Int = 0, + @ColumnInfo(name = "linkName", defaultValue = "") + val linkName: String, + @ColumnInfo(name = "linkURL", defaultValue = "") + val linkURL: String ) \ No newline at end of file diff --git a/app/src/main/java/com/cybershark/linkmanager/ui/MainActivity.kt b/app/src/main/java/com/cybershark/linkmanager/ui/MainActivity.kt index a8a02f5..9c7792a 100644 --- a/app/src/main/java/com/cybershark/linkmanager/ui/MainActivity.kt +++ b/app/src/main/java/com/cybershark/linkmanager/ui/MainActivity.kt @@ -7,9 +7,9 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.activity.viewModels -import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity import androidx.core.view.GravityCompat +import androidx.core.view.isVisible import androidx.navigation.NavController import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration @@ -23,8 +23,7 @@ import com.cybershark.linkmanager.databinding.NavHeaderMainBinding import com.cybershark.linkmanager.repository.constants.Constants import com.cybershark.linkmanager.ui.links.viewmodels.LinksViewModel import com.cybershark.linkmanager.util.UIState -import com.cybershark.linkmanager.util.makeGone -import com.cybershark.linkmanager.util.makeVisible +import com.cybershark.linkmanager.util.observe import com.cybershark.linkmanager.util.showToast import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint @@ -49,55 +48,37 @@ class MainActivity : AppCompatActivity() { setObservers() } - override fun onSupportNavigateUp(): Boolean { - return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() - } - private fun setUpNavigationDrawer() { - appBarConfiguration = AppBarConfiguration(navController.graph, binding.drawerLayout) + appBarConfiguration = + AppBarConfiguration(setOf(R.id.nav_home, R.id.nav_settings), binding.drawerLayout) binding.navView.setupWithNavController(navController) setupActionBarWithNavController(navController, appBarConfiguration) - binding.navView.setNavigationItemSelectedListener { - when (it.itemId) { - R.id.nav_bug_report -> openGithubIssues() - R.id.nav_about -> openAboutDialog() - R.id.nav_home -> openLinksFragment(it.itemId) - R.id.nav_settings -> openSettingsFragment(it.itemId) - R.id.action_share -> openShareOrCopyChooserDialog() - } - binding.drawerLayout.closeDrawer(GravityCompat.START) - true + when (it.itemId) { + R.id.nav_bug_report -> openGithubIssues() + R.id.nav_about -> openAboutDialog() + R.id.nav_home -> openLinksFragment(it.itemId) + R.id.nav_settings -> openSettingsFragment(it.itemId) + R.id.action_share -> openShareOrCopyChooserDialog() } + binding.drawerLayout.closeDrawer(GravityCompat.START) + true + } } private fun setObservers() { - linksViewModel.uiState.observe(this) { uiState -> - binding.contentLoadingScreen.makeGone() - + observe(linksViewModel.uiState) { uiState -> + binding.contentLoadingScreen.isVisible = uiState is UIState.LOADING when (uiState) { - is UIState.LOADING -> { - binding.contentLoadingScreen.makeVisible() - } - is UIState.ERROR -> { - showToast(uiState.message) - } + is UIState.ERROR -> showToast(uiState.message) is UIState.SUCCESS -> { when (uiState.taskId) { - "COPY" -> { - copyLinks(uiState.message) - } - "SHARE" -> { - shareLinks(uiState.message) - } - else -> { - showToast(uiState.message) - } + COPY_ID -> copyLinks(uiState.message) + SHARE_ID -> shareLinks(uiState.message) + else -> showToast(uiState.message) } } - else -> { - // do nothing - } + else -> Unit } } } @@ -115,26 +96,9 @@ class MainActivity : AppCompatActivity() { .show() } - private fun copyToClipBoard() { - binding.contentLoadingScreen.makeVisible() - if (linksViewModel.linksList.value.isNullOrEmpty()) { - showToast("No links added!") - binding.contentLoadingScreen.makeGone() - } else { - linksViewModel.getAllLinksAsString("COPY") - } - } + private fun shareAsText() = linksViewModel.getAllLinksAsString(SHARE_ID) - private fun shareAsText() { - binding.contentLoadingScreen.makeVisible() - - if (linksViewModel.linksList.value.isNullOrEmpty()) { - showToast("No links added!") - binding.contentLoadingScreen.makeGone() - } else { - linksViewModel.getAllLinksAsString("SHARE") - } - } + private fun copyToClipBoard() = linksViewModel.getAllLinksAsString(COPY_ID) private fun copyLinks(message: String) { val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager @@ -189,4 +153,9 @@ class MainActivity : AppCompatActivity() { githubIssuesIntent.data = Uri.parse(Constants.githubIssuesURL) startActivity(githubIssuesIntent) } + + companion object { + private const val COPY_ID = "COPY" + private const val SHARE_ID = "SHARE" + } } diff --git a/app/src/main/java/com/cybershark/linkmanager/ui/SplashActivity.kt b/app/src/main/java/com/cybershark/linkmanager/ui/SplashActivity.kt index bb45f74..4925eab 100644 --- a/app/src/main/java/com/cybershark/linkmanager/ui/SplashActivity.kt +++ b/app/src/main/java/com/cybershark/linkmanager/ui/SplashActivity.kt @@ -4,20 +4,30 @@ import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate +import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class SplashActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + lifecycleScope.launch { + setDarkOrLightTheme() + } startActivity(Intent(this, MainActivity::class.java)) finishAffinity() - setDarkOrLightTheme() } - private fun setDarkOrLightTheme() { - val themeOption = - PreferenceManager.getDefaultSharedPreferences(this).getBoolean("darkTheme", false) + private suspend fun setDarkOrLightTheme() = withContext(Dispatchers.Main) { + val themeOptionAsync = async(Dispatchers.IO) { + PreferenceManager.getDefaultSharedPreferences(this@SplashActivity) + .getBoolean("darkTheme", false) + } + val themeOption = themeOptionAsync.await() if (themeOption) AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) else diff --git a/app/src/main/java/com/cybershark/linkmanager/ui/add_edit_links/ui/AddEditLinkBottomSheet.kt b/app/src/main/java/com/cybershark/linkmanager/ui/add_edit_links/ui/AddEditLinkBottomSheet.kt index bd8ea47..7c63a07 100644 --- a/app/src/main/java/com/cybershark/linkmanager/ui/add_edit_links/ui/AddEditLinkBottomSheet.kt +++ b/app/src/main/java/com/cybershark/linkmanager/ui/add_edit_links/ui/AddEditLinkBottomSheet.kt @@ -4,7 +4,8 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.navArgs import com.cybershark.linkmanager.R import com.cybershark.linkmanager.databinding.AddEditBottomSheetBinding import com.cybershark.linkmanager.repository.room.entities.LinkEntity @@ -12,17 +13,17 @@ import com.cybershark.linkmanager.ui.links.viewmodels.LinksViewModel import com.cybershark.linkmanager.util.showToast import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint -import kotlin.properties.Delegates @AndroidEntryPoint class AddEditLinkBottomSheet : BottomSheetDialogFragment() { - private var linkId by Delegates.notNull() + private val args by navArgs() + private val linkId by lazy { args.linkId } + private val isAddDialog by lazy { args.isAddDialog } private var currentLink: LinkEntity? = null - private val linksViewModel by viewModels() + private val linksViewModel by activityViewModels() private var _binding: AddEditBottomSheetBinding? = null private val binding get() = _binding!! - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -39,15 +40,14 @@ class AddEditLinkBottomSheet : BottomSheetDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - getArgs() + setupDialog() } - private fun getArgs() { - if (arguments != null && arguments?.containsKey(EXTRAS_KEY) == true) { - linkId = requireArguments().getInt(EXTRAS_KEY) - setupEditDialog() - } else { + private fun setupDialog() { + if (isAddDialog) { setupAddDialog() + } else { + setupEditDialog() } } @@ -85,25 +85,4 @@ class AddEditLinkBottomSheet : BottomSheetDialogFragment() { } } - - - companion object { - const val TAG = "AddOrEditLinkDialog" - private const val EXTRAS_KEY = "linkId" - const val EDIT = true - const val ADD = false - - fun getInstance(action: Boolean, id: Int = 0): AddEditLinkBottomSheet { - return if (action == EDIT) { - val args = Bundle().apply { - putInt(EXTRAS_KEY, id) - } - AddEditLinkBottomSheet().apply { - arguments = args - } - } else { - AddEditLinkBottomSheet() - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/cybershark/linkmanager/ui/links/adapters/LinksAdapter.kt b/app/src/main/java/com/cybershark/linkmanager/ui/links/adapters/LinksAdapter.kt index 926bfff..716d6f0 100644 --- a/app/src/main/java/com/cybershark/linkmanager/ui/links/adapters/LinksAdapter.kt +++ b/app/src/main/java/com/cybershark/linkmanager/ui/links/adapters/LinksAdapter.kt @@ -17,8 +17,7 @@ class LinksAdapter( class LinksViewHolder( private val binding: LinkItemBinding, private val customListeners: CustomListeners - ) : - RecyclerView.ViewHolder(binding.root) { + ) : RecyclerView.ViewHolder(binding.root) { fun bind(linkEntity: LinkEntity) { binding.tvLinkName.text = linkEntity.linkName @@ -58,9 +57,7 @@ class LinksAdapter( ) } - override fun getItemCount(): Int { - return listDiffer.currentList.size - } + override fun getItemCount(): Int = listDiffer.currentList.size override fun onBindViewHolder(holder: LinksViewHolder, position: Int) { holder.bind(listDiffer.currentList[position]) diff --git a/app/src/main/java/com/cybershark/linkmanager/ui/links/viewmodels/LinksViewModel.kt b/app/src/main/java/com/cybershark/linkmanager/ui/links/viewmodels/LinksViewModel.kt index ee548f4..6f696fb 100644 --- a/app/src/main/java/com/cybershark/linkmanager/ui/links/viewmodels/LinksViewModel.kt +++ b/app/src/main/java/com/cybershark/linkmanager/ui/links/viewmodels/LinksViewModel.kt @@ -4,72 +4,85 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.cybershark.linkmanager.repository.Repository +import com.cybershark.linkmanager.repository.IRepository import com.cybershark.linkmanager.repository.room.entities.LinkEntity -import com.cybershark.linkmanager.util.UIState -import com.cybershark.linkmanager.util.getDefault -import com.cybershark.linkmanager.util.setLoading -import com.cybershark.linkmanager.util.setSuccess +import com.cybershark.linkmanager.ui.links.views.LinksFragment +import com.cybershark.linkmanager.util.* import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class LinksViewModel @Inject constructor( - private val repository: Repository + private val repository: IRepository ) : ViewModel() { - private val startMessage = "Hey Check me out here : \n" - val linksList: LiveData> = repository.allLinks private val _uiState: MutableLiveData = MutableLiveData().getDefault() - val uiState: LiveData get() = _uiState + val uiState: LiveData = _uiState fun addLink(linkName: String, linkURL: String) { _uiState.setLoading() - viewModelScope.launch(Dispatchers.IO) { - repository.insertLink(LinkEntity(linkName = linkName, linkURL = linkURL)) - _uiState.setSuccess("INSERT", "Inserted") + viewModelScope.launch { + val result = repository.insertLink(LinkEntity(linkName = linkName, linkURL = linkURL)) + if (result.isSuccess) { + _uiState.setSuccess(LinksFragment.INSERT_ID, "Inserted") + } else { + _uiState.setError(result.exceptionOrNull()) + } } } fun updateLink(pk: Int, linkName: String, linkURL: String) { _uiState.setLoading() - viewModelScope.launch(Dispatchers.IO) { - repository.updateLink(LinkEntity(pk, linkName, linkURL)) - _uiState.setSuccess("UPDATE", "Updated") + viewModelScope.launch { + val result = repository.updateLink(LinkEntity(pk, linkName, linkURL)) + if (result.isSuccess) { + _uiState.setSuccess(LinksFragment.UPDATE_ID, "Updated") + } else { + _uiState.setError(result.exceptionOrNull()) + } } } fun deleteLink(linkId: Int) { _uiState.setLoading() - viewModelScope.launch(Dispatchers.IO) { - repository.deleteLinkById(linkId) - _uiState.setSuccess("DELETE", "Deleted") + viewModelScope.launch { + val result = repository.deleteLinkById(linkId) + if (result.isSuccess) { + _uiState.setSuccess(LinksFragment.DELETE_ID, "Deleted") + } else { + _uiState.setError(result.exceptionOrNull()) + } } } fun deleteAllLinks() { _uiState.setLoading() - viewModelScope.launch(Dispatchers.IO) { - repository.deleteAllLinks() - _uiState.setSuccess("DELETEALL", "Deleted") + viewModelScope.launch { + val result = repository.deleteAllLinks() + if (result.isSuccess) { + _uiState.setSuccess(LinksFragment.DELETE_ALL_ID, "Deleted") + } else { + _uiState.setError(result.exceptionOrNull()) + } } } fun getAllLinksAsString(taskID: String) { _uiState.setLoading() - var string: String - viewModelScope.launch(Dispatchers.Default) { - string = buildString { - append(startMessage) - linksList.value?.forEach { - append("${it.linkName} - ${it.linkURL}\n") - } + viewModelScope.launch { + val result = repository.getAllLinksAsString() + if (result.isSuccess) { + _uiState.setSuccess(taskID, result.getOrDefault("")) + } else { + _uiState.setError(result.exceptionOrNull()) } - _uiState.setSuccess(taskID, string) } } + + companion object { + const val START_MESSAGE = "Hey Check me out here : \n" + } } \ No newline at end of file diff --git a/app/src/main/java/com/cybershark/linkmanager/ui/links/views/LinksFragment.kt b/app/src/main/java/com/cybershark/linkmanager/ui/links/views/LinksFragment.kt index 7ec1fb9..d79a3ca 100644 --- a/app/src/main/java/com/cybershark/linkmanager/ui/links/views/LinksFragment.kt +++ b/app/src/main/java/com/cybershark/linkmanager/ui/links/views/LinksFragment.kt @@ -12,7 +12,8 @@ import android.widget.Toast import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -20,19 +21,21 @@ import androidx.recyclerview.widget.RecyclerView import com.cybershark.linkmanager.R import com.cybershark.linkmanager.databinding.FragmentLinksBinding import com.cybershark.linkmanager.repository.room.entities.LinkEntity -import com.cybershark.linkmanager.ui.add_edit_links.ui.AddEditLinkBottomSheet import com.cybershark.linkmanager.ui.links.adapters.LinksAdapter import com.cybershark.linkmanager.ui.links.viewmodels.LinksViewModel +import com.cybershark.linkmanager.util.action +import com.cybershark.linkmanager.util.observe +import com.cybershark.linkmanager.util.shortSnackBar import com.cybershark.linkmanager.util.showToast -import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class LinksFragment : Fragment(), LinksAdapter.CustomListeners { - private val linksViewModel by viewModels() + private val linksViewModel by activityViewModels() private var _binding: FragmentLinksBinding? = null private val binding get() = _binding!! + private val navController by lazy { findNavController() } override fun onCreateView( inflater: LayoutInflater, @@ -56,13 +59,12 @@ class LinksFragment : Fragment(), LinksAdapter.CustomListeners { } private fun openAddLinkDialog() { - AddEditLinkBottomSheet.getInstance(AddEditLinkBottomSheet.ADD) - .show(childFragmentManager, AddEditLinkBottomSheet.TAG) + val action = LinksFragmentDirections.openAddEditDialog(isAddDialog = true, linkId = 0) + navController.navigate(action) } private fun setupRecyclerView() { val adapter = LinksAdapter(this) - binding.rvLinks.apply { this.adapter = adapter layoutManager = LinearLayoutManager(context) @@ -71,7 +73,7 @@ class LinksFragment : Fragment(), LinksAdapter.CustomListeners { itemAnimator = DefaultItemAnimator() } - linksViewModel.linksList.observe(viewLifecycleOwner) { + observe(linksViewModel.linksList) { binding.tvNoLinksAdded.isVisible = it.isEmpty() adapter.setList(it) } @@ -93,24 +95,19 @@ class LinksFragment : Fragment(), LinksAdapter.CustomListeners { val shareIntent = Intent(Intent.ACTION_SEND) shareIntent.type = "text/plain" shareIntent.putExtra(Intent.EXTRA_TEXT, message) - startActivity( - Intent.createChooser( - shareIntent, - context?.getString(R.string.share) - ) - ) + startActivity(Intent.createChooser(shareIntent, context?.getString(R.string.share))) } override fun onEditClick(linkId: Int) { - AddEditLinkBottomSheet.getInstance(AddEditLinkBottomSheet.EDIT, linkId) - .show(childFragmentManager, AddEditLinkBottomSheet.TAG) + val action = LinksFragmentDirections.openAddEditDialog(isAddDialog = false, linkId = linkId) + navController.navigate(action) } override fun onCopyLink(linkURL: String) { val clipBoardManager = context?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipBoardManager.setPrimaryClip(ClipData.newPlainText("link", linkURL)) - Toast.makeText(context, "Copied!", Toast.LENGTH_SHORT).show() + showToast("Copied!") } override fun onOpenLink(linkURL: String) { @@ -119,10 +116,17 @@ class LinksFragment : Fragment(), LinksAdapter.CustomListeners { override fun onDeleteLink(linkEntity: LinkEntity) { linksViewModel.deleteLink(linkEntity.pk) - Snackbar.make(binding.rvLinks, "Deleted Link!", Snackbar.LENGTH_LONG) - .setAction(R.string.undo) { + binding.rvLinks.shortSnackBar("Deleted Link!") { + action(getString(R.string.undo)) { linksViewModel.addLink(linkEntity.linkName, linkEntity.linkURL) } - .show() + } + } + + companion object { + const val INSERT_ID = "INSERT" + const val UPDATE_ID = "UPDATE" + const val DELETE_ID = "DELETE" + const val DELETE_ALL_ID = "DELETE_ALL" } } diff --git a/app/src/main/java/com/cybershark/linkmanager/ui/settings/SettingsFragment.kt b/app/src/main/java/com/cybershark/linkmanager/ui/settings/SettingsFragment.kt index fecf084..b5ab72f 100644 --- a/app/src/main/java/com/cybershark/linkmanager/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/cybershark/linkmanager/ui/settings/SettingsFragment.kt @@ -5,7 +5,7 @@ import android.os.Bundle import android.provider.Settings import android.view.View import androidx.appcompat.app.AppCompatDelegate -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.cybershark.linkmanager.BuildConfig @@ -16,7 +16,7 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class SettingsFragment : PreferenceFragmentCompat() { - private val linksViewModel by viewModels() + private val linksViewModel by activityViewModels() override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.root_preferences, rootKey) diff --git a/app/src/main/java/com/cybershark/linkmanager/util/Extensions.kt b/app/src/main/java/com/cybershark/linkmanager/util/Extensions.kt index 71bceea..de8514f 100644 --- a/app/src/main/java/com/cybershark/linkmanager/util/Extensions.kt +++ b/app/src/main/java/com/cybershark/linkmanager/util/Extensions.kt @@ -1,12 +1,12 @@ package com.cybershark.linkmanager.util -import android.content.Context import android.view.View import android.widget.Toast import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import com.google.android.material.snackbar.Snackbar fun MutableLiveData.getDefault(): MutableLiveData { return this.apply { value = UIState.IDLE } @@ -17,19 +17,15 @@ fun MutableLiveData.setLoading(): MutableLiveData { } fun MutableLiveData.setSuccess(taskId: String, message: String): MutableLiveData { - return this.apply { postValue(UIState.SUCCESS(taskId, message)) } + return this.apply { value = UIState.SUCCESS(taskId, message) } } -fun MutableLiveData.setError(message: String): MutableLiveData { - return this.apply { postValue(UIState.ERROR(message)) } +fun MutableLiveData.setError(error: Throwable?): MutableLiveData { + return this.apply { value = UIState.ERROR(error?.message ?: "An error occurred") } } -fun View.makeVisible() { - this.isVisible = true -} - -fun View.makeGone() { - this.isVisible = false +fun MutableLiveData.setError(error: String): MutableLiveData { + return this.apply { value = UIState.ERROR(error) } } internal fun AppCompatActivity.showToast(message: String, length: Int = Toast.LENGTH_SHORT) = @@ -38,5 +34,24 @@ internal fun AppCompatActivity.showToast(message: String, length: Int = Toast.LE internal fun Fragment.showToast(message: String, length: Int = Toast.LENGTH_SHORT) = Toast.makeText(context, message, length).show() -internal fun Context.showToast(message: String, length: Int = Toast.LENGTH_SHORT) = - Toast.makeText(this, message, length).show() +fun Fragment.observe(liveData: LiveData, action: (t: T) -> Unit) { + liveData.observe(viewLifecycleOwner) { t -> + action(t) + } +} + +fun AppCompatActivity.observe(liveData: LiveData, action: (t: T) -> Unit) { + liveData.observe(this) { t -> + action(t) + } +} + +internal fun View.shortSnackBar(message: String, action: (Snackbar.() -> Unit)? = null) { + val snackbar = Snackbar.make(this, message, Snackbar.LENGTH_LONG) + action?.let { snackbar.it() } + snackbar.show() +} + +internal fun Snackbar.action(message: String, action: (View) -> Unit) { + this.setAction(message, action) +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 488e878..7a42797 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -24,8 +24,8 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" - app:title="@string/app_name" - android:theme="@style/ThemeOverlay.MaterialComponents.Dark" /> + android:theme="@style/ThemeOverlay.MaterialComponents.Dark" + app:title="@string/app_name" /> @@ -52,7 +52,6 @@ app:headerLayout="@layout/nav_header_main" app:menu="@menu/activity_main_drawer" /> - + tools:layout="@layout/fragment_links"> + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/cybershark/linkmanager/ExampleUnitTest.kt b/app/src/test/java/com/cybershark/linkmanager/ExampleUnitTest.kt deleted file mode 100644 index eb8a302..0000000 --- a/app/src/test/java/com/cybershark/linkmanager/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.cybershark.linkmanager - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/build.gradle b/build.gradle index 67fef34..2f94e34 100644 --- a/build.gradle +++ b/build.gradle @@ -1,16 +1,17 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.4.31' + ext.kotlin_version = '1.5.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:4.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "com.google.dagger:hilt-android-gradle-plugin:2.35" + classpath "com.google.dagger:hilt-android-gradle-plugin:2.37" + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5" } } @@ -18,7 +19,6 @@ allprojects { repositories { google() mavenCentral() - } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6de760a..a6175d6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip