commit 3026220cd044d2b18651cf72dc604f34c1314c6b Author: Aji Kamaludin Date: Sun Apr 3 15:02:11 2022 +0700 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..24e8b10 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..995fa7e --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..9485379 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,68 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' + id 'androidx.navigation.safeargs.kotlin' +} + +android { + compileSdk 32 + + defaultConfig { + applicationId "id.ajikamaludin.wallet" + minSdk 21 + targetSdk 32 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + buildFeatures { + dataBinding { + enabled = true + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'com.google.android.material:material:1.5.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + + // Lifecycle libraries + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1" + + // Navigation libraries + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + + // Room + implementation "androidx.room:room-runtime:2.4.2" + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + kapt "androidx.room:room-compiler:2.4.2" + implementation "androidx.room:room-ktx:2.4.2" + + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/id/ajikamaludin/wallet/ExampleInstrumentedTest.kt b/app/src/androidTest/java/id/ajikamaludin/wallet/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..18fdcea --- /dev/null +++ b/app/src/androidTest/java/id/ajikamaludin/wallet/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package id.ajikamaludin.wallet + +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("id.ajikamaludin.wallet", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7fe381b --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/id/ajikamaludin/wallet/MainActivity.kt b/app/src/main/java/id/ajikamaludin/wallet/MainActivity.kt new file mode 100644 index 0000000..09f947b --- /dev/null +++ b/app/src/main/java/id/ajikamaludin/wallet/MainActivity.kt @@ -0,0 +1,28 @@ +package id.ajikamaludin.wallet + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.NavigationUI + +class MainActivity : AppCompatActivity(R.layout.activity_main) { + + private lateinit var navController: NavController + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val navHostFragment = supportFragmentManager + .findFragmentById(R.id.nav_host_fragment) as NavHostFragment + navController = navHostFragment.navController + NavigationUI.setupActionBarWithNavController(this, navController) + } + + /** + * Handle navigation when the user chooses Up from the action bar. + */ + override fun onSupportNavigateUp(): Boolean { + return navController.navigateUp() || super.onSupportNavigateUp() + } +} \ No newline at end of file diff --git a/app/src/main/java/id/ajikamaludin/wallet/WalletApplication.kt b/app/src/main/java/id/ajikamaludin/wallet/WalletApplication.kt new file mode 100644 index 0000000..79910fb --- /dev/null +++ b/app/src/main/java/id/ajikamaludin/wallet/WalletApplication.kt @@ -0,0 +1,11 @@ +package id.ajikamaludin.wallet + +import android.app.Application +import id.ajikamaludin.wallet.database.AppDatabase + +const val ITEM_EXPENSE = 2 +const val ITEM_INCOME = 1 + +class WalletApplication: Application() { + val database: AppDatabase by lazy { AppDatabase.getDatabase(this) } +} \ No newline at end of file diff --git a/app/src/main/java/id/ajikamaludin/wallet/data/Transaction.kt b/app/src/main/java/id/ajikamaludin/wallet/data/Transaction.kt new file mode 100644 index 0000000..ea5618e --- /dev/null +++ b/app/src/main/java/id/ajikamaludin/wallet/data/Transaction.kt @@ -0,0 +1,23 @@ +package id.ajikamaludin.wallet.data + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.text.NumberFormat + + +@Entity(tableName = "transactions") +data class Transaction( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + @ColumnInfo(name = "amount") + val amount: Double, + @ColumnInfo(name = "description") + val description: String, + @ColumnInfo(name = "type") + val type: Int, + @ColumnInfo(name = "created_at", defaultValue = "CURRENT_TIMESTAMP") + val createdAt: String, +) { + fun getFormattedAmount(): String = NumberFormat.getCurrencyInstance().format(amount) +} \ No newline at end of file diff --git a/app/src/main/java/id/ajikamaludin/wallet/database/AppDatabase.kt b/app/src/main/java/id/ajikamaludin/wallet/database/AppDatabase.kt new file mode 100644 index 0000000..1a8265d --- /dev/null +++ b/app/src/main/java/id/ajikamaludin/wallet/database/AppDatabase.kt @@ -0,0 +1,31 @@ +package id.ajikamaludin.wallet.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import id.ajikamaludin.wallet.data.Transaction + +@Database(entities = [Transaction::class], version = 1, exportSchema = false) +abstract class AppDatabase : RoomDatabase() { + abstract fun transactionDao(): TransactionDao + + companion object { + @Volatile + private var INSTANCE: AppDatabase? = null + + fun getDatabase(context: Context): AppDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + "app_database" + ) + .fallbackToDestructiveMigration() + .build() + INSTANCE = instance + return instance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/id/ajikamaludin/wallet/database/TransactionDao.kt b/app/src/main/java/id/ajikamaludin/wallet/database/TransactionDao.kt new file mode 100644 index 0000000..81991fe --- /dev/null +++ b/app/src/main/java/id/ajikamaludin/wallet/database/TransactionDao.kt @@ -0,0 +1,31 @@ +package id.ajikamaludin.wallet.database + +import android.content.ClipData +import androidx.room.* +import id.ajikamaludin.wallet.ITEM_EXPENSE +import id.ajikamaludin.wallet.ITEM_INCOME +import id.ajikamaludin.wallet.data.Transaction +import kotlinx.coroutines.flow.Flow + +@Dao +interface TransactionDao { + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(transaction: Transaction) + @Update + suspend fun update(transaction: Transaction) + @Delete + suspend fun delete(transaction: Transaction) + + @Query("SELECT * from transactions ORDER BY created_at DESC") + fun getTransactions(): Flow> + + @Query("SELECT (SELECT SUM(amount) FROM transactions WHERE type = ${ITEM_INCOME}) - (SELECT SUM(amount) FROM transactions WHERE type = ${ITEM_EXPENSE})") + fun getTotalAmount(): Flow + + @Query("SELECT SUM(amount) FROM transactions WHERE type = $ITEM_INCOME") + fun getTotalIncome(): Flow + + @Query("SELECT SUM(amount) FROM transactions WHERE type = $ITEM_EXPENSE") + fun getTotalExpense(): Flow + +} \ No newline at end of file diff --git a/app/src/main/java/id/ajikamaludin/wallet/ui/AddTransactionFragment.kt b/app/src/main/java/id/ajikamaludin/wallet/ui/AddTransactionFragment.kt new file mode 100644 index 0000000..3f9995f --- /dev/null +++ b/app/src/main/java/id/ajikamaludin/wallet/ui/AddTransactionFragment.kt @@ -0,0 +1,47 @@ +package id.ajikamaludin.wallet.ui + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.AutoCompleteTextView +import androidx.fragment.app.activityViewModels +import com.google.android.material.snackbar.Snackbar +import id.ajikamaludin.wallet.R +import id.ajikamaludin.wallet.WalletApplication +import id.ajikamaludin.wallet.databinding.FragmentAddTransactionBinding +import id.ajikamaludin.wallet.databinding.FragmentTransactionListBinding + + +class AddTransactionFragment : Fragment() { + private val viewModel: TransactionViewModel by activityViewModels { + TransactionViewModelFactory( + (activity?.application as WalletApplication).database.transactionDao() + ) + } + private var _binding: FragmentAddTransactionBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + _binding = FragmentAddTransactionBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val items = listOf(getString(R.string.income), getString(R.string.expense)) + val adapter = ArrayAdapter(requireContext(), R.layout.item_type, items) + (binding.itemType as? AutoCompleteTextView)?.setAdapter(adapter) + + binding.saveAction.setOnClickListener { + Snackbar.make(this, binding.itemType.text, Snackbar.LENGTH_SHORT) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/id/ajikamaludin/wallet/ui/TransactionListAdapter.kt b/app/src/main/java/id/ajikamaludin/wallet/ui/TransactionListAdapter.kt new file mode 100644 index 0000000..2f64816 --- /dev/null +++ b/app/src/main/java/id/ajikamaludin/wallet/ui/TransactionListAdapter.kt @@ -0,0 +1,59 @@ +package id.ajikamaludin.wallet.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import id.ajikamaludin.wallet.ITEM_INCOME +import id.ajikamaludin.wallet.data.Transaction +import id.ajikamaludin.wallet.databinding.ItemTransactionBinding + +class TransactionListAdapter(private val onItemClicked: (Transaction) -> Unit): + ListAdapter(DiffCallback) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + return ItemViewHolder( + ItemTransactionBinding.inflate( + LayoutInflater.from( + parent.context + ) + ) + ) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val current = getItem(position) + holder.itemView.setOnClickListener { + onItemClicked(current) + } + holder.bind(current) + } + + class ItemViewHolder(private var binding: ItemTransactionBinding) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(transaction: Transaction) { + binding.apply { + when(transaction.type) { + ITEM_INCOME -> textAmount.text = transaction.amount.toString() + else -> textAmount.text = "-${transaction.amount.toString()}" + } + textDate.text = transaction.createdAt + textDescription.text = transaction.description + } + } + } + + companion object { + private val DiffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Transaction, newItem: Transaction): Boolean { + return oldItem === newItem + } + + override fun areContentsTheSame(oldItem: Transaction, newItem: Transaction): Boolean { + return oldItem.createdAt == newItem.createdAt + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/id/ajikamaludin/wallet/ui/TransactionListFragment.kt b/app/src/main/java/id/ajikamaludin/wallet/ui/TransactionListFragment.kt new file mode 100644 index 0000000..e0b6afa --- /dev/null +++ b/app/src/main/java/id/ajikamaludin/wallet/ui/TransactionListFragment.kt @@ -0,0 +1,55 @@ +package id.ajikamaludin.wallet.ui + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import id.ajikamaludin.wallet.R +import id.ajikamaludin.wallet.WalletApplication +import id.ajikamaludin.wallet.database.TransactionDao +import id.ajikamaludin.wallet.databinding.FragmentTransactionListBinding + +class TransactionListFragment: Fragment() { + private val viewModel: TransactionViewModel by activityViewModels { + TransactionViewModelFactory( + (activity?.application as WalletApplication).database.transactionDao() + ) + } + private var _binding: FragmentTransactionListBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + _binding = FragmentTransactionListBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.floatingActionButton.setOnClickListener { + val action = TransactionListFragmentDirections.actionTransactionListFragmentToAddTransactionFragment() + findNavController().navigate(action) + } + + binding.viewModel = viewModel + binding.lifecycleOwner = viewLifecycleOwner + + val adapter = TransactionListAdapter { + val action = TransactionListFragmentDirections.actionTransactionListFragmentToAddTransactionFragment(it.id) + findNavController().navigate(action) + } + binding.recyclerView.adapter = adapter + binding.recyclerView.layoutManager = LinearLayoutManager(context) + viewModel.transactions.observe(viewLifecycleOwner) { items -> + items.let { adapter.submitList(it) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/id/ajikamaludin/wallet/ui/TransactionViewModel.kt b/app/src/main/java/id/ajikamaludin/wallet/ui/TransactionViewModel.kt new file mode 100644 index 0000000..84a2f81 --- /dev/null +++ b/app/src/main/java/id/ajikamaludin/wallet/ui/TransactionViewModel.kt @@ -0,0 +1,22 @@ +package id.ajikamaludin.wallet.ui + +import androidx.lifecycle.* +import id.ajikamaludin.wallet.data.Transaction +import id.ajikamaludin.wallet.database.TransactionDao + +class TransactionViewModel(private val transactionDao: TransactionDao): ViewModel() { + val transactions: LiveData> = transactionDao.getTransactions().asLiveData() + val amount: LiveData = transactionDao.getTotalAmount().asLiveData() + val expense: LiveData = transactionDao.getTotalExpense().asLiveData() + val income: LiveData = transactionDao.getTotalIncome().asLiveData() +} + +class TransactionViewModelFactory(private val transactionDao: TransactionDao) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(TransactionViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return TransactionViewModel(transactionDao) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..bae1ecb --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_add_transaction.xml b/app/src/main/res/layout/fragment_add_transaction.xml new file mode 100644 index 0000000..8cb4a10 --- /dev/null +++ b/app/src/main/res/layout/fragment_add_transaction.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + +