├── .github ├── dependabot.yml └── workflows │ ├── android.yml │ ├── auto-merge.yml │ └── update-gradle-wrapper.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro ├── signkey.jks └── src │ └── main │ ├── AndroidManifest.xml │ ├── kotlin │ └── com │ │ └── ohyooo │ │ └── demo │ │ ├── app │ │ └── App.kt │ │ ├── model │ │ └── MainUIItem.kt │ │ ├── ui │ │ ├── list │ │ │ └── ListActivity.kt │ │ ├── main │ │ │ └── MainActivity.kt │ │ └── splash │ │ │ └── SplashActivity.kt │ │ └── viewmodel │ │ └── MainViewModel.kt │ └── res │ ├── layout │ ├── activity_list.xml │ └── activity_main.xml │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── dependencies.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── com │ └── ohyooo │ └── lib │ ├── adapter │ └── ImageViewAdapter.kt │ ├── extension │ ├── ActivityExtensions.kt │ ├── ContextExtensions.kt │ ├── FragmentExtensions.kt │ └── UnitExtensions.kt │ ├── livedata │ └── SingleLiveEvent.kt │ └── mvvm │ ├── MVVMBaseActivity.kt │ ├── MVVMBaseFragment.kt │ ├── MVVMBaseViewModel.kt │ ├── MVVMLifecycle.kt │ └── MVVMViewModelFactory.kt ├── network ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── com │ └── ohyooo │ └── network │ ├── api │ └── GitHubAPIInterface.kt │ ├── factory │ └── ErrorConverterFactory.kt │ ├── model │ ├── BaseResponse.kt │ └── RateLimitResponse.kt │ └── repository │ ├── BaseRepository.kt │ └── GithubAPIRepository.kt └── settings.gradle.kts /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gradle 4 | directory: / 5 | schedule: 6 | interval: daily 7 | registries: 8 | - maven-google 9 | - gradle-plugin 10 | groups: 11 | maven-dependencies: 12 | patterns: 13 | - "*" 14 | 15 | - package-ecosystem: github-actions 16 | directory: / 17 | schedule: 18 | interval: daily 19 | groups: 20 | github-actions: 21 | patterns: 22 | - "*" 23 | 24 | registries: 25 | maven-google: 26 | type: maven-repository 27 | url: https://dl.google.com/dl/android/maven2/ 28 | gradle-plugin: 29 | type: maven-repository 30 | url: https://plugins.gradle.org/m2/ 31 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | repository_dispatch: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Get Time 17 | id: time 18 | uses: nanzm/get-time-action@v2.0 19 | with: 20 | timeZone: 8 21 | format: 'YYYYMMDDHHmmss' 22 | 23 | - name: Setup JDK 24 | uses: actions/setup-java@v4 25 | with: 26 | distribution: 'zulu' 27 | java-version: '17' 28 | java-package: jdk 29 | 30 | - name: Grant execute permission for gradlew 31 | run: chmod +x gradlew 32 | 33 | - name: Build APK 34 | run: ./gradlew assembleRelease 35 | 36 | - name: Delete workflow runs 37 | uses: GitRML/delete-workflow-runs@main 38 | with: 39 | retain_days: 3 40 | keep_minimum_runs: 2 41 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Merge 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | dependabot-merge: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} 12 | steps: 13 | - uses: peter-evans/find-comment@main 14 | id: find-comment 15 | with: 16 | token: ${{ secrets.ACTIONS_TRIGGER_PAT }} 17 | issue-number: ${{ github.event.pull_request.number }} 18 | body-includes: '@dependabot squash and merge' 19 | - uses: peter-evans/create-or-update-comment@main 20 | with: 21 | token: ${{ secrets.ACTIONS_TRIGGER_PAT }} 22 | comment-id: ${{ steps.find-comment.outputs.comment-id }} 23 | issue-number: ${{ github.event.pull_request.number }} 24 | body: '@dependabot squash and merge' 25 | edit-mode: replace 26 | -------------------------------------------------------------------------------- /.github/workflows/update-gradle-wrapper.yml: -------------------------------------------------------------------------------- 1 | name: Update Gradle Wrapper 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | 8 | jobs: 9 | update-gradle-wrapper: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@main 14 | 15 | - name: Setup JDK 16 | uses: actions/setup-java@main 17 | with: 18 | distribution: 'zulu' 19 | java-version: '21' 20 | java-package: jdk 21 | cache: 'gradle' 22 | 23 | - name: Update Gradle Wrapper 24 | uses: gradle-update/update-gradle-wrapper-action@main 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/androidstudio 3 | # Edit at https://www.gitignore.io/?templates=androidstudio 4 | 5 | ### AndroidStudio ### 6 | # Covers files to be ignored for android development using Android Studio. 7 | 8 | # Built application files 9 | *.apk 10 | *.aar 11 | *.ap_ 12 | 13 | # Files for the ART/Dalvik VM 14 | *.dex 15 | 16 | # Java class files 17 | *.class 18 | 19 | # Generated files 20 | bin/ 21 | gen/ 22 | out/ 23 | 24 | # Gradle files 25 | .gradle 26 | .gradle/ 27 | build/ 28 | 29 | # Signing files 30 | .signing/ 31 | 32 | # Local configuration file (sdk path, etc) 33 | local.properties 34 | 35 | # Proguard folder generated by Eclipse 36 | proguard/ 37 | 38 | # Log Files 39 | *.log 40 | 41 | # Android Studio 42 | /*/build/ 43 | /*/release/ 44 | /*/debug/ 45 | /*/stage/ 46 | /*/dev/ 47 | /*/local.properties 48 | /*/out 49 | /*/*/build 50 | /*/*/production 51 | captures/ 52 | .navigation/ 53 | *.ipr 54 | *~ 55 | *.swp 56 | 57 | # Android Patch 58 | gen-external-apklibs 59 | 60 | # External native build folder generated in Android Studio 2.2 and later 61 | .externalNativeBuild 62 | 63 | # NDK 64 | obj/ 65 | 66 | # IntelliJ IDEA 67 | *.iml 68 | *.iws 69 | /out/ 70 | 71 | # User-specific configurations 72 | .idea/ 73 | 74 | # OS-specific files 75 | .DS_Store 76 | .DS_Store? 77 | ._* 78 | .Spotlight-V100 79 | .Trashes 80 | ehthumbs.db 81 | Thumbs.db 82 | 83 | # Legacy Eclipse project files 84 | .classpath 85 | .project 86 | .cproject 87 | .settings/ 88 | 89 | # Mobile Tools for Java (J2ME) 90 | .mtj.tmp/ 91 | 92 | # Package Files # 93 | *.war 94 | *.ear 95 | 96 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 97 | hs_err_pid* 98 | 99 | ## Plugin-specific files: 100 | 101 | # mpeltonen/sbt-idea plugin 102 | .idea_modules/ 103 | 104 | # JIRA plugin 105 | atlassian-ide-plugin.xml 106 | 107 | # Crashlytics plugin (for Android Studio and IntelliJ) 108 | com_crashlytics_export_strings.xml 109 | crashlytics.properties 110 | crashlytics-build.properties 111 | fabric.properties 112 | 113 | ### AndroidStudio Patch ### 114 | 115 | !/gradle/wrapper/gradle-wrapper.jar 116 | 117 | # End of https://www.gitignore.io/api/androidstudio -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android MVVM Base Project 2 | 3 | ------ 4 | 5 | ## Introduction 6 | 7 | This project is intent to provide a template with basic MVVM architecture framework. You can just copy it to workspace instead of creating a new project from Android Studio. 8 | 9 | Advantages: 10 | 11 | - Simple, easy to read 12 | - Use few libs, save time for gradle syncing 13 | - No dagger or any other DI lib 14 | 15 | ## Module 16 | 17 | ```cmd 18 | ├───app 19 | │ ├───app Application 20 | │ ├───model models 21 | │ ├───ui activities & fragments 22 | │ │ ├───main MainActivity 23 | │ └───viewmodel viewmodels 24 | │ 25 | ├───lib 26 | │ ├───adapter databinding adapter 27 | │ ├───extension kotlin extensions 28 | │ └───mvvm MVVM framework 29 | │ 30 | └───network 31 | ├───api 32 | ├───model 33 | └───repository 34 | ``` 35 | 36 | ## MVVM 37 | Usually, a viwemodel can only aware the destroy of its owner in onClear() method. But after making it implements LifecycleObserver and observing owner's lifecycle in ViewModelProvider.Factory. It can use onCreate() or other lifecycle event now. 38 | Check these codes in MVVMViewModelFactory.kt 39 | 40 | ## Todo 41 | many many things.. -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | id("com.android.application") 5 | kotlin("android") 6 | } 7 | 8 | android { 9 | signingConfigs { 10 | getByName("debug") { 11 | storeFile = file("signkey.jks") 12 | storePassword = "123456" 13 | keyPassword = "123456" 14 | keyAlias = "demo" 15 | 16 | enableV3Signing = true 17 | enableV4Signing = true 18 | } 19 | } 20 | namespace = libs.versions.application.id.get() 21 | compileSdk = libs.versions.compile.sdk.get().toInt() 22 | defaultConfig { 23 | applicationId = libs.versions.application.id.get() 24 | minSdk = libs.versions.min.sdk.get().toInt() 25 | targetSdk = libs.versions.target.sdk.get().toInt() 26 | versionCode = libs.versions.version.code.get().toInt() 27 | versionName = libs.versions.target.sdk.get() + hashTag 28 | proguardFile("proguard-rules.pro") 29 | signingConfig = signingConfigs.getByName("debug") 30 | } 31 | buildTypes { 32 | release { 33 | isMinifyEnabled = true 34 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "consumer-rules.pro") 35 | } 36 | } 37 | compileOptions { 38 | sourceCompatibility = JavaVersion.VERSION_17 39 | targetCompatibility = JavaVersion.VERSION_17 40 | } 41 | kotlinOptions { 42 | jvmTarget = "17" 43 | } 44 | buildFeatures { 45 | viewBinding = true 46 | dataBinding = true 47 | } 48 | } 49 | 50 | 51 | dependencies { 52 | implementation(project(":lib")) 53 | implementation(project(":network")) 54 | implementation(libs.appcompat) 55 | implementation(libs.coreKtx) 56 | implementation(libs.fragmentKtx) 57 | implementation(libs.constraintLayout) 58 | implementation(libs.recyclerview) 59 | // 60 | implementation (libs.timber) 61 | } 62 | 63 | val hashTag: String 64 | get() { 65 | if (!File(rootDir.path + "/.git").exists()) return "" 66 | return ProcessBuilder(listOf("git", "rev-parse", "--short", "HEAD")) 67 | .directory(rootDir) 68 | .redirectOutput(ProcessBuilder.Redirect.PIPE) 69 | .redirectError(ProcessBuilder.Redirect.PIPE) 70 | .start() 71 | .apply { waitFor(5, TimeUnit.SECONDS) } 72 | .run { 73 | val error = errorStream.bufferedReader().readText().trim() 74 | if (error.isNotEmpty()) { 75 | "" 76 | } else { 77 | "-" + inputStream.bufferedReader().readText().trim() 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts.kts.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/signkey.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ohyooo/MVVMBaseProject/15ab2dfe23b187b2c8a889e568cad568e956d019/app/signkey.jks -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 18 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/ohyooo/demo/app/App.kt: -------------------------------------------------------------------------------- 1 | package com.ohyooo.demo.app 2 | 3 | import android.app.Application 4 | import timber.log.Timber 5 | 6 | class App : Application() { 7 | init { 8 | instance = this 9 | } 10 | 11 | override fun onCreate() { 12 | super.onCreate() 13 | Timber.plant(Timber.DebugTree()) 14 | } 15 | 16 | companion object { 17 | lateinit var instance: App 18 | private set 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/ohyooo/demo/model/MainUIItem.kt: -------------------------------------------------------------------------------- 1 | package com.ohyooo.demo.model 2 | 3 | 4 | class MainUIItem -------------------------------------------------------------------------------- /app/src/main/kotlin/com/ohyooo/demo/ui/list/ListActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ohyooo.demo.ui.list 2 | 3 | import android.os.Bundle 4 | import android.view.ViewGroup 5 | import android.widget.TextView 6 | import androidx.activity.ComponentActivity 7 | import androidx.recyclerview.widget.RecyclerView 8 | import com.ohyooo.demo.databinding.ActivityListBinding 9 | import com.ohyooo.demo.viewmodel.MainViewModel 10 | import com.ohyooo.lib.extension.viewModelOf 11 | 12 | class ListActivity : ComponentActivity() { 13 | 14 | private val vm: MainViewModel by viewModelOf() 15 | private val vdb by lazy { ActivityListBinding.inflate(layoutInflater).also { it.vm = vm } } 16 | 17 | private val adapter = AA() 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContentView(vdb.root) 22 | 23 | initData() 24 | initViews() 25 | } 26 | 27 | private fun initData() { 28 | } 29 | 30 | private fun initViews() { 31 | vdb.list.adapter = adapter 32 | } 33 | 34 | private class AA : RecyclerView.Adapter>() { 35 | private val list = ArrayList() 36 | 37 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { 38 | val tv = TextView(parent.context).apply { 39 | layoutParams = RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) 40 | setPadding(20, 20, 20, 0) 41 | } 42 | return VH(tv) 43 | } 44 | 45 | override fun onBindViewHolder(vh: VH, position: Int) { 46 | vh.v.text = list[position] 47 | } 48 | 49 | override fun getItemCount() = list.size 50 | 51 | fun update(data: List) { 52 | list.clear() 53 | list.addAll(data) 54 | notifyDataSetChanged() 55 | } 56 | } 57 | 58 | private class VH(val v: V) : RecyclerView.ViewHolder(v) 59 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/ohyooo/demo/ui/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ohyooo.demo.ui.main 2 | 3 | import android.os.Bundle 4 | import com.ohyooo.demo.databinding.ActivityMainBinding 5 | import com.ohyooo.demo.viewmodel.MainViewModel 6 | import com.ohyooo.lib.extension.viewModelOf 7 | import com.ohyooo.lib.mvvm.MVVMBaseActivity 8 | 9 | class MainActivity : MVVMBaseActivity() { 10 | 11 | private val vm: MainViewModel by viewModelOf() 12 | private val vdb by lazy { ActivityMainBinding.inflate(layoutInflater).also { it.vm = vm } } 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | setContentView(vdb.root) 17 | 18 | initData() 19 | initViews() 20 | } 21 | 22 | private fun initData() { 23 | } 24 | 25 | private fun initViews() { 26 | vdb.button.setOnClickListener { 27 | 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/kotlin/com/ohyooo/demo/ui/splash/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.ohyooo.demo.ui.splash 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import com.ohyooo.demo.ui.main.MainActivity 7 | 8 | class SplashActivity : Activity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | startActivity(Intent(this, MainActivity::class.java)) 12 | finish() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/kotlin/com/ohyooo/demo/viewmodel/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.ohyooo.demo.viewmodel 2 | 3 | import com.ohyooo.demo.model.MainUIItem 4 | import com.ohyooo.lib.mvvm.MVVMBaseViewModel 5 | 6 | class MainViewModel : MVVMBaseViewModel() { 7 | private val ui = MainUIItem() 8 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 19 | 20 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 18 | 19 |