Module 10: Legacy Project Guide & Kotlin Migration
Objective: Get a systematic guide for taking over legacy Java Android projects, learn to identify tech stacks, understand common "technical debt," and plan incremental Kotlin migration.
1. Your First Week Taking Over a Legacy Project
1.1 Project Health Checklist
After receiving the project, follow this sequence:
Step 1: Can it compile and run?
[] Clone the code
[] Check JDK version requirements (gradle-wrapper.properties -> Gradle version -> JDK version)
[] Configure local.properties (SDK path)
[] Run Gradle Sync
[] Try building a debug version: ./gradlew assembleDebug
[] Run on an emulator or physical device
[] Record all compilation errors and warningsStep 2: Basic project information
| Item | File / Location | What to Look For |
|---|---|---|
| Package name | AndroidManifest.xml | App's unique identifier |
| Minimum SDK | build.gradle -> minSdk | Range of supported devices |
| Target SDK | build.gradle -> targetSdk | Behavior adaptation |
| Version number | build.gradle -> versionCode/Name | Current version |
| Build types | build.gradle -> buildTypes | debug/release configuration |
| Dependency list | build.gradle -> dependencies | Third-party libraries |
| Permission list | AndroidManifest.xml | Which permissions are requested |
| ProGuard rules | proguard-rules.pro | Which classes are kept |
| Signing config | build.gradle -> signingConfigs | Release keystore location |
| Build variants | build.gradle -> productFlavors | Channel/brand packages |
Step 3: Tech stack identification
Run the following command to get a quick overview of dependencies:
./gradlew app:dependencies --configuration releaseRuntimeClasspath1.2 Identifying the Tech Stack Era
Determine the project's era and tech stack from its dependencies and code style:
| Indicator | Era | Description |
|---|---|---|
android.support.* package names | Pre-2018 | Uses Support Library (outdated) |
androidx.* package names | 2018+ | Migrated to AndroidX |
com.jakewharton:butterknife | 2015-2019 | ButterKnife view binding |
io.reactivex / rxjava | 2016-2020 | RxJava reactive programming |
dagger / com.google.dagger | 2016+ | Dagger dependency injection |
com.google.dagger:hilt | 2020+ | Hilt dependency injection |
| MVP pattern (Contract + Presenter) | 2016-2019 | Common architecture |
| MVVM + LiveData + ViewModel | 2019+ | Modern architecture |
com.squareup.okhttp | 2014-2016 | OkHttp 2.x (outdated) |
com.squareup.okhttp3 | 2016+ | OkHttp 3.x/4.x (modern) |
retrofit | 2016+ | Retrofit (mainstream) |
com.loopj.android:android-async-http | 2012-2015 | No longer maintained |
com.android.volley | 2013+ | Volley |
greenrobot:eventbus | 2015-2019 | EventBus event bus |
com.j256.ormlite | 2013-2017 | OrmLite (outdated) |
android.arch.persistence.room | 2017-2018 | Early Room version |
androidx.room | 2018+ | Room (AndroidX version) |
1.3 Support Library vs AndroidX
This is the first big issue you'll encounter when taking over a legacy project. In 2018, Google refactored the Support Library into AndroidX:
// Old Support Library (pre-2018)
import android.support.v7.app.AppCompatActivity;
import android.support.v4.app.Fragment;
import android.support.v7.widget.RecyclerView;
import android.support.design.widget.Snackbar;
// New AndroidX (post-2018)
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.snackbar.Snackbar;Package name mapping (common ones):
| Support Library | AndroidX |
|---|---|
android.support.v7.app.AppCompatActivity | androidx.appcompat.app.AppCompatActivity |
android.support.v4.app.Fragment | androidx.fragment.app.Fragment |
android.support.v7.widget.RecyclerView | androidx.recyclerview.widget.RecyclerView |
android.support.v7.widget.LinearLayoutManager | androidx.recyclerview.widget.LinearLayoutManager |
android.support.design.widget.* | com.google.android.material.* |
android.support.constraint.ConstraintLayout | androidx.constraintlayout.widget.ConstraintLayout |
android.support.v4.widget.SwipeRefreshLayout | androidx.swiperefreshlayout.widget.SwipeRefreshLayout |
Migration: Android Studio provides automatic migration: Refactor -> Migrate to AndroidX.
Recommendation: If the project is still using the Support Library, prioritize migrating to AndroidX. But make sure you have regression testing in place.
2. Common "Legacy Baggage" and How to Handle It
2.1 Dealing with Outdated Libraries
| Outdated Library | Replacement | Migration Difficulty |
|---|---|---|
| ButterKnife | ViewBinding | Low (replace one by one) |
| EventBus | LiveData / SharedFlow | Medium (requires refactoring communication) |
| RxJava 1.x | RxJava 3.x or Kotlin Coroutines | High |
| OrmLite | Room | High (data models differ) |
| Picasso | Glide or Coil | Low (similar APIs) |
| Volley | OkHttp + Retrofit | Medium |
| Android Annotations | Standard annotations + Hilt | High |
android-percent-support | ConstraintLayout / FlexboxLayout | Low |
com.android.support:multidex | androidx.multidex:multidex | Low |
2.2 Replacing ButterKnife with ViewBinding
// Old code: ButterKnife
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_title) TextView titleView;
@BindView(R.id.btn_submit) Button submitBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
titleView.setText("Hello");
submitBtn.setOnClickListener(v -> { /* ... */ });
}
@OnClick(R.id.btn_submit)
void onSubmit() {
// ...
}
}// New code: ViewBinding
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.tvTitle.setText("Hello");
binding.btnSubmit.setOnClickListener(v -> { /* ... */ });
}
}2.3 Replacing AsyncTask with Executor / Kotlin Coroutines
AsyncTask was deprecated in API 30 and removed on newer versions. Legacy projects often contain:
// ❌ Old code: AsyncTask
new AsyncTask<String, Void, String>() {
@Override protected String doInBackground(String... params) {
return networkRequest(params[0]);
}
@Override protected void onPostExecute(String result) {
textView.setText(result);
}
}.execute(url);// ✅ New code: ExecutorService + Handler (Java)
ExecutorService executor = Executors.newSingleThreadExecutor();
Handler handler = new Handler(Looper.getMainLooper());
executor.execute(() -> {
final String result = networkRequest(url);
handler.post(() -> textView.setText(result));
});// ✅ New code: Kotlin Coroutines
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) { networkRequest(url) }
textView.text = result
}2.4 Replacing EventBus with LiveData
// Old code: EventBus
// Post event
EventBus.getDefault().post(new UserLoginEvent(user));
// Receive event
@Subscribe(threadMode = ThreadMode.MAIN)
public void onUserLogin(UserLoginEvent event) {
updateUI(event.getUser());
}
// Register/unregister
EventBus.getDefault().register(this);
EventBus.getDefault().unregister(this);// New code: LiveData (via shared ViewModel)
// Shared ViewModel
public class AppViewModel extends ViewModel {
private final MutableLiveData<User> currentUser = new MutableLiveData<>();
public void login(User user) {
currentUser.setValue(user);
}
public LiveData<User> getCurrentUser() {
return currentUser;
}
}
// Observe (in any Fragment/Activity)
appViewModel.getCurrentUser().observe(this, user -> {
updateUI(user);
});2.5 Replacing findViewById with ViewBinding
Legacy projects are full of findViewById, a common source of null-pointer and cast errors:
// ❌ Old code
TextView tvTitle = findViewById(R.id.tv_title);
Button btnSubmit = findViewById(R.id.btn_submit);// ✅ New code
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.tvTitle.setText("Hello");
binding.btnSubmit.setOnClickListener(v -> { /* ... */ });Migration tip: Android Studio offers
Refactor -> Migrate to View Bindingto bulk-replace simple cases, but complex custom views still need manual work.
3. Incremental Modernization Strategy
3.1 Priority Matrix
High Impact | Upgrade targetSdk Fix crashes
| Security patches Performance optimization
|-------------------------------------------
Low Impact | Replace outdated Code style unification
| UI components Add comments
| Upgrade dependency versions
+-------------------------------------------
Low Risk High RiskRecommended order:
- First priority: Get it compiling and running, fix crashes
- Second priority: Upgrade targetSdk (Google Play requires this)
- Third priority: Security patches, critical dependency updates
- Fourth priority: Architecture improvements, code quality enhancements
- Fifth priority: Kotlin migration (if you decide to migrate)
3.2 targetSdk Upgrade Considerations
Each targetSdk upgrade requires checking for behavior changes:
| Upgrade to | Key Changes |
|---|---|
| API 26 (Android 8) | Background service restrictions, notification channels required |
| API 28 (Android 9) | Non-SDK interface restrictions, foreground service permissions |
| API 29 (Android 10) | Scoped Storage |
| API 30 (Android 11) | Enforced Scoped Storage, package visibility restrictions |
| API 31 (Android 12) | Must declare android:exported, exact alarm restrictions |
| API 33 (Android 13) | Granular media permissions, notification runtime permission |
| API 34 (Android 14) | Foreground service type declaration, exact alarms disabled by default |
| API 35 (Android 15) | 16 KB page size support (on some new devices) |
3.3 Gradle / AGP / JDK Compatibility Matrix
AGP version Required Gradle Compatible JDK
8.1.x 8.0+ 17
8.2.x 8.2+ 17
8.3.x 8.4+ 17
8.4.x 8.6+ 17
8.5.x 8.7+ 17-21Recommended upgrade order: JDK -> Gradle -> AGP -> third-party dependencies. Upgrade one step at a time and verify the build before continuing.
4. Java and Kotlin Coexistence
4.1 Using Kotlin in a Java Project
Gradle natively supports mixed Java/Kotlin compilation:
// app/build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android' version '1.9.22' // Add Kotlin plugin
}
android {
// ...
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}In mixed projects, Java and Kotlin files live in the same directory:
src/main/java/com/example/app/
+-- MainActivity.java # Java file
+-- UserDetailActivity.kt # Kotlin file (coexisting)
+-- model/
| +-- User.java # Java data class
| +-- Product.kt # Kotlin data class
+-- util/
+-- DateUtils.java # Java utility class
+-- StringUtils.kt # Kotlin extension functions4.2 Calling Kotlin from Java
// Kotlin file
class UserHelper {
fun getUserName(): String = "John"
@JvmStatic
fun getDefaultUser(): User = User("Default User")
}
object AppConfig {
const val API_URL = "https://api.example.com"
}// Calling from Java
UserHelper helper = new UserHelper();
String name = helper.getUserName(); // Regular method
User defaultUser = UserHelper.getDefaultUser(); // @JvmStatic method
String apiUrl = AppConfig.API_URL; // const in object4.3 Calling Java from Kotlin
// Java file
public class UserManager {
private String currentName;
public String getCurrentName() { return currentName; }
public void setCurrentName(String name) { this.currentName = name; }
public static UserManager getInstance() {
return instance;
}
}// Calling from Kotlin
val manager = UserManager.getInstance()
manager.currentName = "John" // Automatically recognizes getter/setter as properties
val name = manager.currentName4.4 Java/Kotlin Interop Pitfalls
// 1. Platform types (String! coming from Java)
// A Java method returning String is seen as String! in Kotlin and may NPE
val name: String = javaManager.name // may NPE at runtime
// Fix: declare nullability explicitly on the Kotlin side,
// or add @Nullable/@NotNull in Java
// 2. SAM conversion
// Java interfaces with a single abstract method auto-convert to lambdas
button.setOnClickListener { doSomething() }
// 3. Java generics appear invariant in Kotlin
val list: MutableList<String> = javaObject.strings
list.add("x") // may fail at compile/run time if Java returns List<String>Frontend analogy: Java/Kotlin interop is like TypeScript consuming a JavaScript library: TS introduces
any/optional types, so you must pay extra attention to runtime behavior and nulls.
5. Kotlin Migration Guide
5.1 When to Migrate?
Good candidates for migration:
- The project is planned for long-term maintenance
- The team has Kotlin experience or is willing to learn
- You need to adopt Jetpack Compose or other modern components
- Google's official documentation and the community have fully shifted to Kotlin
Not good candidates for migration:
- The project is about to be sunset
- The team has no Kotlin knowledge at all
- You're only fixing bugs, not adding new features
5.2 Recommended Migration Order
Step 1: New Kotlin files
| (write new features in Kotlin)
Step 2: Utility classes and constants
| (migrate simple, standalone files first)
Step 3: Data models (Model / Entity)
| (data class replaces POJO, significantly less code)
Step 4: Adapters and ViewHolders
| (lots of boilerplate, Kotlin simplifies noticeably)
Step 5: Fragments and Activities
| (migrate screen by screen)
Step 6: ViewModels and Repositories
| (business logic layer)
Step 7: Core framework classes (Application, BaseActivity, etc.)5.3 Kotlin Quick Reference Cheat Sheet
// Java POJO -> Kotlin data class
// Java: 50+ lines of code
public class User {
private String name;
private int age;
// getter/setter/toString/equals/hashCode...
}
// Kotlin: 1 line
data class User(val name: String, val age: Int)
// Java findViewById -> Kotlin property access
// Java
TextView tv = findViewById(R.id.tv_title);
tv.setText("Hello");
// Kotlin (with ViewBinding)
binding.tvTitle.text = "Hello"
// Java anonymous inner class -> Kotlin lambda
// Java
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doSomething();
}
});
// Kotlin
button.setOnClickListener { doSomething() }
// Java null checks -> Kotlin safe calls
// Java
if (user != null && user.getAddress() != null) {
String city = user.getAddress().getCity();
}
// Kotlin
val city = user?.address?.city
// Java switch -> Kotlin when
// Java
switch (status) {
case 0: return "pending";
case 1: return "active";
case 2: return "completed";
default: return "unknown";
}
// Kotlin
return when (status) {
0 -> "pending"
1 -> "active"
2 -> "completed"
else -> "unknown"
}
// Java for loop -> Kotlin collection operations
// Java
List<String> names = new ArrayList<>();
for (User user : users) {
if (user.getAge() > 18) {
names.add(user.getName());
}
}
// Kotlin
val names = users.filter { it.age > 18 }.map { it.name }
// Java try-catch -> Kotlin runCatching
// Java
String result;
try {
result = riskyOperation();
} catch (Exception e) {
result = "default";
}
// Kotlin
val result = runCatching { riskyOperation() }.getOrDefault("default")5.4 Android Studio Auto-Conversion
Android Studio includes a built-in Java -> Kotlin conversion tool:
- Open the Java file
Code -> Convert Java File to Kotlin File(Ctrl+Alt+Shift+K)- Review the converted code (not always accurate, manual adjustments needed)
Note: Auto-converted code may not follow idiomatic Kotlin style, but it's a useful starting point.
5.5 R8 / ProGuard After Kotlin Migration
# Common keep rules for Kotlin reflection and coroutines
-keep class kotlin.** { *; }
-keep class kotlinx.coroutines.** { *; }
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes Exceptions
-keepclassmembers class * implements android.os.Parcelable { *; }Common Release crash:
java.lang.NoSuchMethodError: No virtual method ...
# Cause: R8 over-obfuscated Kotlin stdlib or reflection calls
# Fix: add -keep rules and test with R8 full mode during build6. Dependency Injection in Legacy Projects
6.1 Identifying DI Frameworks
// Dagger 2 (most common)
@Component(modules = {AppModule.class, NetworkModule.class})
public interface AppComponent {
void inject(MainActivity activity);
}
@Module
public class NetworkModule {
@Provides
@Singleton
OkHttpClient provideOkHttpClient() {
return new OkHttpClient.Builder().build();
}
@Provides
Retrofit provideRetrofit(OkHttpClient client) {
return new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
}
// Usage
@Inject Retrofit retrofit;
// Hilt (simplified Dagger, 2020+)
@HiltAndroidApp
public class MyApplication extends Application { }
@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
@Inject UserRepository repository;
}
@Module
@InstallIn(SingletonComponent.class)
public class NetworkModule {
@Provides
@Singleton
static OkHttpClient provideOkHttpClient() {
return new OkHttpClient.Builder().build();
}
}7. Complete Takeover Workflow Summary
Day 1: Set up environment -> Compile and run -> Gather basic info
|
Day 2: Identify tech stack -> Understand architecture -> List dependencies
|
Day 3: Walk through core flows -> Map screen structure -> Trace data flow
|
Week 1: Fix compilation issues -> Run tests -> Document technical debt
|
Week 2: Create improvement plan -> Set priorities -> Begin incremental improvements8. Useful Tools
| Tool | Purpose |
|---|---|
| Android Studio Layout Inspector | Inspect UI hierarchy in real time |
| Android Studio Database Inspector | View database contents in real time |
| Stetho / Flipper | Network request and database debugging |
| LeakCanary | Memory leak detection |
| Charles / mitmproxy | Network traffic capture |
| jadx | APK decompilation to view source code |
| APK Analyzer | Analyze APK size composition |
Frontend Developer Cheat Sheet
| Frontend Project Takeover | Android Project Takeover | Notes |
|---|---|---|
Read package.json | Read build.gradle | Understand dependencies and configuration |
Inspect node_modules | ./gradlew dependencies | Dependency tree |
npm audit | Manually check outdated dependencies | Security audit |
| Check ESLint config | Check Lint config | Code quality |
| Read routing files | Read AndroidManifest.xml | Screen structure |
| Tech debt assessment | Tech stack era identification | Evaluate modernization level |
| jQuery -> React migration | Java -> Kotlin migration | Incremental migration strategy |
| Webpack -> Vite upgrade | Gradle version upgrade | Build tool upgrade |
| TypeScript strict migration | Kotlin null-safety migration | Both need incremental rollout |
Congratulations!
After completing all 10 modules, you now have:
- The ability to set up an Android Java development environment
- The ability to read and understand Java Android code
- The ability to modify and maintain legacy project features (UI, networking, data storage, etc.)
- The ability to identify project tech stacks and architecture patterns
- The ability to troubleshoot issues using debugging tools
- The ability to plan incremental modernization and Kotlin migration
Recommended next steps:
- Get your hands on a real legacy project
- Try to compile and run it from scratch, and document the issues you encounter
- Try fixing a small bug or adding a small feature
- Join Android developer communities (e.g., Stack Overflow, Reddit r/androiddev)
Good luck on your Android maintenance journey!