While Kotlin has become Google's preferred language for Android development, Java remains a powerful, well-supported choice for building robust mobile applications. With its vast ecosystem, extensive libraries, and mature tooling, Java continues to be an excellent platform for Android development. This article explores the fundamentals, best practices, and modern approaches to Android development using Java.
Why Choose Java for Android Development?
Advantages:
- Mature Ecosystem: Decades of libraries, frameworks, and community knowledge
- Strong Typing: Compile-time safety reduces runtime errors
- Cross-Platform Skills: Java knowledge transfers to backend, desktop, and other domains
- Stability: Well-tested language with predictable behavior
- Enterprise Ready: Ideal for large-scale applications and teams
Considerations:
- Verbose Syntax: More code compared to Kotlin for similar functionality
- Null Safety: Requires careful handling (unlike Kotlin's built-in null safety)
- Modern Features: Some newer language features arrive later than in Kotlin
Android Development Setup with Java
Required Tools:
- Android Studio - The official IDE
- Java Development Kit (JDK) 8 or later
- Android SDK - Platform tools and libraries
Project Structure:
app/ ├── src/ │ ├── main/ │ │ ├── java/com/example/app/ # Java source code │ │ ├── res/ # Resources (layouts, strings, drawables) │ │ └── AndroidManifest.xml # App configuration │ └── test/ # Unit tests ├── build.gradle # Module-level build configuration └── ...
Core Android Components with Java
1. Activities - The UI Controllers
public class MainActivity extends AppCompatActivity {
private TextView welcomeText;
private Button clickButton;
private int clickCount = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Initialize views
welcomeText = findViewById(R.id.welcome_text);
clickButton = findViewById(R.id.click_button);
// Set up button click listener
clickButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
clickCount++;
welcomeText.setText("Button clicked " + clickCount + " times");
// Start a new activity
Intent intent = new Intent(MainActivity.this, DetailActivity.class);
intent.putExtra("CLICK_COUNT", clickCount);
startActivity(intent);
}
});
}
}
2. Fragments - Reusable UI Components
public class UserProfileFragment extends Fragment {
private UserViewModel userViewModel;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_user_profile, container, false);
TextView userName = view.findViewById(R.id.user_name);
ImageView userAvatar = view.findViewById(R.id.user_avatar);
// Receive arguments
Bundle args = getArguments();
if (args != null) {
String name = args.getString("user_name");
userName.setText(name);
}
return view;
}
}
3. Services - Background Operations
public class DownloadService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// Perform background work
new Thread(new Runnable() {
@Override
public void run() {
downloadFile();
stopSelf(); // Stop service when work is complete
}
}).start();
return START_NOT_STICKY;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void downloadFile() {
// Implementation for file download
}
}
Layouts and UI Design with XML
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp"> <TextView android:id="@+id/welcome_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Welcome to Android with Java!" android:textSize="24sp" android:layout_gravity="center_horizontal" /> <Button android:id="@+id/click_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="16dp" android:text="Click Me!" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:layout_marginTop="16dp" /> </LinearLayout>
RecyclerView with Java - Displaying Lists
1. Adapter Class:
public class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserViewHolder> {
private List<User> userList;
private OnItemClickListener listener;
public interface OnItemClickListener {
void onItemClick(User user);
}
public UserAdapter(List<User> userList, OnItemClickListener listener) {
this.userList = userList;
this.listener = listener;
}
@NonNull
@Override
public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_user, parent, false);
return new UserViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull UserViewHolder holder, int position) {
User user = userList.get(position);
holder.bind(user, listener);
}
@Override
public int getItemCount() {
return userList.size();
}
// ViewHolder class
static class UserViewHolder extends RecyclerView.ViewHolder {
private TextView userName;
private TextView userEmail;
public UserViewHolder(@NonNull View itemView) {
super(itemView);
userName = itemView.findViewById(R.id.user_name);
userEmail = itemView.findViewById(R.id.user_email);
}
public void bind(final User user, final OnItemClickListener listener) {
userName.setText(user.getName());
userEmail.setText(user.getEmail());
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onItemClick(user);
}
});
}
}
}
2. Usage in Activity:
public class UserListActivity extends AppCompatActivity {
private RecyclerView recyclerView;
private UserAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_list);
recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
List<User> users = getUserList(); // Your data source
adapter = new UserAdapter(users, new UserAdapter.OnItemClickListener() {
@Override
public void onItemClick(User user) {
// Handle item click
showUserDetails(user);
}
});
recyclerView.setAdapter(adapter);
}
private void showUserDetails(User user) {
Intent intent = new Intent(this, UserDetailActivity.class);
intent.putExtra("USER_ID", user.getId());
startActivity(intent);
}
}
Networking with Retrofit and Java
1. API Interface:
public interface ApiService {
@GET("users")
Call<List<User>> getUsers();
@GET("users/{id}")
Call<User> getUserById(@Path("id") int userId);
@POST("users")
Call<User> createUser(@Body User user);
}
2. Network Operations:
public class UserRepository {
private ApiService apiService;
public UserRepository() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
apiService = retrofit.create(ApiService.class);
}
public void fetchUsers(final UserCallback callback) {
apiService.getUsers().enqueue(new Callback<List<User>>() {
@Override
- public void onResponse(Call<List<User>> call, Response<List<User>> response) {
if (response.isSuccessful()) {
callback.onSuccess(response.body());
} else {
callback.onError("Failed to fetch users");
}
}
@Override
public void onFailure(Call<List<User>> call, Throwable t) {
callback.onError(t.getMessage());
}
});
}
public interface UserCallback {
void onSuccess(List<User> users);
void onError(String error);
}
}
Data Persistence with Room Database
1. Entity Class:
@Entity(tableName = "users")
public class User {
@PrimaryKey
private int id;
@ColumnInfo(name = "name")
private String name;
@ColumnInfo(name = "email")
private String email;
// Constructors, getters, and setters
public User(int id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// Getters
public int getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
}
2. DAO Interface:
@Dao
public interface UserDao {
@Query("SELECT * FROM users")
List<User> getAllUsers();
@Query("SELECT * FROM users WHERE id = :userId")
User getUserById(int userId);
@Insert
void insertUser(User user);
@Update
void updateUser(User user);
@Delete
void deleteUser(User user);
}
3. Database Class:
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
private static volatile AppDatabase INSTANCE;
public static AppDatabase getInstance(Context context) {
if (INSTANCE == null) {
synchronized (AppDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context.getApplicationContext(),
AppDatabase.class,
"app_database"
).build();
}
}
}
return INSTANCE;
}
}
Modern Architecture: MVVM with LiveData
1. ViewModel:
public class UserViewModel extends ViewModel {
private MutableLiveData<List<User>> users = new MutableLiveData<>();
private MutableLiveData<String> error = new MutableLiveData<>();
private UserRepository userRepository;
public UserViewModel() {
userRepository = new UserRepository();
loadUsers();
}
public LiveData<List<User>> getUsers() {
return users;
}
public LiveData<String> getError() {
return error;
}
private void loadUsers() {
userRepository.fetchUsers(new UserRepository.UserCallback() {
@Override
public void onSuccess(List<User> userList) {
users.postValue(userList);
}
@Override
public void onError(String errorMessage) {
error.postValue(errorMessage);
}
});
}
}
2. Activity using ViewModel:
public class UserActivity extends AppCompatActivity {
private UserViewModel userViewModel;
private UserAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user);
userViewModel = new ViewModelProvider(this).get(UserViewModel.class);
setupRecyclerView();
setupObservers();
}
private void setupObservers() {
userViewModel.getUsers().observe(this, new Observer<List<User>>() {
@Override
public void onChanged(List<User> users) {
adapter.setUserList(users);
}
});
userViewModel.getError().observe(this, new Observer<String>() {
@Override
public void onChanged(String error) {
Toast.makeText(UserActivity.this, error, Toast.LENGTH_SHORT).show();
}
});
}
}
Dependency Injection with Dagger/Hilt
Basic Hilt Setup:
@HiltAndroidApp
public class MyApplication extends Application {
// Application class
}
@Module
@InstallIn(SingletonComponent.class)
public class AppModule {
@Provides
@Singleton
public ApiService provideApiService() {
return new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService.class);
}
}
@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
@Inject
ApiService apiService;
// Activity code...
}
Best Practices for Java Android Development
- Use AndroidX Libraries: Always use the latest AndroidX packages
- Handle Configuration Changes: Use ViewModel to survive configuration changes
- Background Processing: Use WorkManager for guaranteed background work
- Memory Management: Avoid memory leaks with weak references
- ProGuard/R8: Enable code shrinking and obfuscation for release builds
build.gradle (Module):
android {
compileSdkVersion 33
defaultConfig {
applicationId "com.example.myapp"
minSdkVersion 21
targetSdkVersion 33
versionCode 1
versionName "1.0"
javaCompileOptions {
annotationProcessorOptions {
arguments += ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.6.2'
implementation 'androidx.room:room-runtime:2.5.2'
annotationProcessor 'androidx.room:room-compiler:2.5.2'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}
Conclusion
Java remains a solid choice for Android development, offering:
- Proven Reliability: Battle-tested in millions of applications
- Rich Ecosystem: Vast collection of libraries and tools
- Strong Community: Extensive documentation and support
- Career Flexibility: Skills transfer beyond mobile development
While Kotlin offers modern language features, Java provides stability, performance, and access to a massive ecosystem. By following modern architecture patterns like MVVM, using Jetpack components, and implementing best practices, you can build high-quality, maintainable Android applications with Java that stand the test of time.
Whether you're maintaining legacy codebases or starting new projects, Java continues to be a valuable skill in the Android development landscape.