| name | android-sdk |
| description | [Applies to: **/*.java] This guide provides definitive best practices for Android SDK development in Java, focusing on modern architecture, Jetpack libraries, performance, security, and testing. |
| source | cursor_mdc |
android-sdk Best Practices (Java)
This guide outlines the essential practices for building robust, performant, and maintainable Android applications using Java, adhering to modern standards and leveraging the AndroidX ecosystem.
1. Code Organization and Structure
1.1 Modular Architecture (MVVM)
Adopt a clean, modular architecture, with MVVM (Model-View-ViewModel) as the standard. This promotes separation of concerns, testability, and scalability.
❌ BAD: Tight Coupling
public class UserProfileActivity extends AppCompatActivity {
private TextView userName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_profile);
userName = findViewById(R.id.user_name);
new Thread(() -> {
String name = fetchUserNameFromNetwork();
runOnUiThread(() -> userName.setText(name));
}).start();
}
}
✅ GOOD: MVVM with Repository Pattern
public class UserProfileActivity extends AppCompatActivity {
private UserProfileViewModel viewModel;
private TextView userName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_profile);
userName = findViewById(R.id.user_name);
viewModel = new ViewModelProvider(this).get(UserProfileViewModel.class);
viewModel.getUserName().observe(this, name -> {
userName.setText(name);
});
}
}
public class UserProfileViewModel extends AndroidViewModel {
private UserRepository userRepository;
private MutableLiveData<String> userName = new MutableLiveData<>();
public UserProfileViewModel(@NonNull Application application) {
super(application);
this.userRepository = new UserRepository(application);
loadUserName();
}
public LiveData<String> getUserName() {
return userName;
}
private void loadUserName() {
Executors.newSingleThreadExecutor().execute(() -> {
String name = userRepository.fetchUserName();
userName.postValue(name);
});
}
}
public class UserRepository {
public String fetchUserName() {
try { Thread.sleep(2000); } catch (InterruptedException e) {}
return "John Doe";
}
}
1.2 Package Structure
Organize packages by feature or layer, not by Android component type.
❌ BAD: Component-based
com.example.app
├── activities
│ └── UserProfileActivity.java
├── fragments
│ └── SettingsFragment.java
├── adapters
│ └── UserListAdapter.java
└── utils
└── NetworkUtils.java
✅ GOOD: Feature-based (or Layer-based for larger apps)
com.example.app
├── userprofile
│ ├── UserProfileActivity.java
│ ├── UserProfileViewModel.java
│ └── UserProfileFragment.java
├── settings
│ ├── SettingsActivity.java
│ └── SettingsViewModel.java
├── data
│ ├── UserRepository.java
│ ├── UserApiService.java
│ └── UserDatabase.java
└── common
├── adapters
│ └── UserListAdapter.java
└── utils
└── NetworkUtils.java
2. Common Patterns and Anti-patterns
2.1 Jetpack Libraries
Always prefer AndroidX Jetpack libraries for modern, backward-compatible, and lifecycle-aware components.
-
Room for Persistence: Use Room for all local database storage.
❌ BAD: Raw SQLiteOpenHelper
public class MyDbHelper extends SQLiteOpenHelper { }
✅ GOOD: Room Database
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
@Entity
public class User {
@PrimaryKey public int id;
public String name;
}
@Dao
public interface UserDao {
@Query("SELECT * FROM user WHERE id = :userId")
LiveData<User> getUserById(int userId);
@Insert
void insert(User user);
}
-
View Binding (or Data Binding): Eliminate findViewById boilerplate.
❌ BAD: Manual View Lookup
public class MyActivity extends AppCompatActivity {
private Button myButton;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
myButton = findViewById(R.id.my_button);
myButton.setOnClickListener(v -> );
}
}
✅ GOOD: View Binding
public class MyActivity extends AppCompatActivity {
private ActivityMyBinding binding;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMyBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.myButton.setOnClickListener(v -> );
}
}
-
Navigation Component: Manage in-app navigation consistently.
❌ BAD: Manual Fragment Transactions
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_container, new DetailFragment())
.addToBackStack(null)
.commit();
✅ GOOD: Navigation Graph
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
navController.navigate(R.id.action_homeFragment_to_detailFragment);
2.2 Dependency Injection (Hilt)
Use Hilt (built on Dagger) for robust dependency injection, simplifying object creation and improving testability.
❌ BAD: Manual Dependency Management
public class MyRepository {
private MyApiService apiService = new MyApiService();
}
✅ GOOD: Hilt
@Module
@InstallIn(SingletonComponent.class)
public abstract class AppModule {
@Provides
@Singleton
public static MyApiService provideMyApiService() {
return new MyApiService();
}
}
@Singleton
public class MyRepository {
private final MyApiService apiService;
@Inject
public MyRepository(MyApiService apiService) {
this.apiService = apiService;
}
}
@HiltAndroidApp
public class MyApplication extends Application {}
3. Performance Considerations
3.1 Layout Optimization
Flatten view hierarchies using ConstraintLayout and ViewStub to reduce layout inflation time and memory usage.
❌ BAD: Deeply Nested Layouts
<LinearLayout orientation="vertical">
<LinearLayout orientation="horizontal">
<TextView />
<ImageView />
</LinearLayout>
<LinearLayout orientation="vertical">
<TextView />
<Button />
</LinearLayout>
</LinearLayout>
✅ GOOD: ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout>
<TextView app:layout_constraintTop_toTopOf="parent" />
<ImageView app:layout_constraintBaseline_toBaselineOf="@id/textView" />
<TextView app:layout_constraintTop_toBottomOf="@id/imageView" />
<Button app:layout_constraintTop_toBottomOf="@id/textView2" />
</androidx.constraintlayout.widget.ConstraintLayout>
3.2 Image Loading
Always use a dedicated image loading library like Glide for efficient image fetching, caching, and display.
❌ BAD: Manual Bitmap Loading
Bitmap bitmap = BitmapFactory.decodeStream(urlConnection.getInputStream());
imageView.setImageBitmap(bitmap);
✅ GOOD: Glide
Glide.with(context)
.load("https://example.com/image.jpg")
.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
.into(imageView);
3.3 Background Processing
Use WorkManager for deferrable, guaranteed background tasks. For immediate, short-lived tasks, use ExecutorService.
❌ BAD: Inconsistent Background Tasks
new AsyncTask<Void, Void, Void>() { }.execute();
✅ GOOD: WorkManager
OneTimeWorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(UploadWorker.class)
.setConstraints(new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build();
WorkManager.getInstance(context).enqueue(uploadWorkRequest);
public class UploadWorker extends Worker {
public UploadWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result doWork() {
return Result.success();
}
}
4. Common Pitfalls and Gotchas
4.1 Context Leaks
Avoid holding strong references to Activity or Fragment contexts in long-lived objects. Use ApplicationContext when a long-lived context is truly needed, or weak references.
❌ BAD: Static Context Reference
public class BadManager {
private static Context context;
public static void init(Context ctx) { context = ctx; }
}
✅ GOOD: Application Context or No Context
public class GoodManager {
private Context appContext;
public GoodManager(Context context) {
this.appContext = context.getApplicationContext();
}
public void doSomething(String data) { }
}
4.2 Security Best Practices
Implement Android's security guidelines for inter-app communication and network security.
-
Explicit Intents with Choosers: For sensitive implicit intents.
❌ BAD: Implicit Intent for Sensitive Data
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, "Sensitive data");
sendIntent.setType("text/plain");
startActivity(sendIntent);
✅ GOOD: App Chooser
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, "Sensitive data");
sendIntent.setType("text/plain");
Intent chooser = Intent.createChooser(sendIntent, getResources().getString(R.string.chooser_title));
startActivity(chooser);
-
Non-Exported Content Providers: Prevent unauthorized access.
❌ BAD: Exported Content Provider (default on older Android)
<provider android:name=".MyContentProvider" android:authorities="com.example.app.provider" />
✅ GOOD: Non-Exported Content Provider
<provider android:name=".MyContentProvider" android:authorities="com.example.app.provider" android:exported="false" />
-
Network Security Configuration: Enforce HTTPS and manage trust anchors.
❌ BAD: Allowing Cleartext Traffic
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
✅ GOOD: Disabling Cleartext Traffic
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.example.com</domain>
<domain includeSubdomains="true">secure.example.com</domain>
</domain-config>
<debug-overrides>
<trust-anchors>
<certificates src="user"/>
</trust-anchors>
</debug-overrides>
</network-security-config>
And in AndroidManifest.xml:
<application android:networkSecurityConfig="@xml/network_security_config" ...>
4.3 Ignoring Lint Warnings
Treat all lint warnings as errors. Android Studio's lint tool catches critical issues from performance to security.
❌ BAD: Shipping with Lint Warnings
> Task :app:lintDebug
...
W: /path/to/project/app/src/main/java/com/example/app/MainActivity.java:123: Warning: Hardcoded string "Hello" should use @string resource [HardcodedText]
✅ GOOD: Resolve All Lint Issues
Configure Gradle to fail builds on lint errors:
android {
lintOptions {
abortOnError true // Fail the build if lint finds errors
warningsAsErrors true // Treat warnings as errors
}
}
5. Testing Approaches
5.1 Unit Tests
Focus on testing business logic, ViewModels, and Repositories in isolation on the JVM. Use JUnit and Mockito.
public class UserProfileViewModelTest {
@Rule
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
@Mock
private UserRepository userRepository;
@Mock
private Application application;
private UserProfileViewModel viewModel;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
viewModel = new UserProfileViewModel(application);
ReflectionHelpers.setField(viewModel, "userRepository", userRepository);
}
@Test
public void getUserName_returnsCorrectName() {
when(userRepository.fetchUserName()).thenReturn("Test User");
viewModel.loadUserName();
assertEquals("Test User", viewModel.getUserName().getValue());
}
}
5.2 Integration Tests
Test interactions between components (e.g., Repository with Room database, or API service with Retrofit). Run on an Android emulator or device.
@RunWith(AndroidJUnit4.class)
public class UserDaoTest {
@Rule
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
private AppDatabase database;
private UserDao userDao;
@Before
public void createDb() {
Context context = ApplicationProvider.getApplicationContext();
database = Room.inMemoryDatabaseBuilder(context, AppDatabase.class).build();
userDao = database.userDao();
}
@After
public void closeDb() {
database.close();
}
@Test
public void writeUserAndReadInList() {
User user = new User();
user.id = 1;
user.name = "Test User";
userDao.insert(user);
User byId = LiveDataTestUtil.getValue(userDao.getUserById(1));
assertEquals(user.name, byId.name);
}
}
5.3 UI Tests (Espresso)
Verify user flows and UI correctness. Run on an Android emulator or device.
@RunWith(AndroidJUnit4.class)
@LargeTest
public class UserProfileActivityTest {
@Rule
public ActivityScenarioRule<UserProfileActivity> activityRule =
new ActivityScenarioRule<>(UserProfileActivity.class);
@Test
public void userNameIsDisplayed() {
onView(withId(R.id.user_name))
.check(matches(withText("John Doe")));
}
}
5.4 Automated Testing
Integrate all tests (unit, integration, UI, lint) into your CI/CD pipeline.
./gradlew lintDebug
./gradlew testDebugUnitTest
./gradlew connectedDebugAndroidTest