Tujuan
Pada Codelab kali ini Anda akan mempelajari bagaimana mengimplementasikan ViewModel dalam proyek Academy. Hasil dari codelab kali ini akan menjadi seperti ini:
Logika Dasar
Membuka Aplikasi → memanggil DataDummy ke ViewModel → mengirim data ke Activity → melakukan perubahan rotasi → data masih terjaga.
Codelab ViewModel
- Bukalah proyek Academy yang sudah Anda buat sebelumnya atau Anda bisa unduh di sini.
- Lihatlah terlebih dahulu susunan package dan kelas yang sudah ada:
Anda akan membuat beberapa kelas ViewModel yang nantinya akan digunakan tiap Fragment atau Activity. - Pertama, buka build.gradle level project dan tambahkan versi untuk library berikut:
ext {
Setelah itu, buka build.gradle level module: app dan tambahkan library berikut:
...
archLifecycleVersion = '2.1.0'
}dependencies {
...
//architecture component
implementation "androidx.lifecycle:lifecycle-viewmodel:$archLifecycleVersion"
} - Buatlah sebuah kelas baru di package academy dengan nama AcademyViewModel.
Kemudian pindahkan pemanggilan generateDummyCourse() dari AcademyFragment ke kelas AcademyViewModel:Kotlin class AcademyViewModel : ViewModel() {
fun getCourses(): List<CourseEntity> = DataDummy.generateDummyCourses()
}Java public class AcademyViewModel extends ViewModel {
public List<CourseEntity> getCourses() {
return DataDummy.generateDummyCourses();
}
}
- Kemudian hubungkan AcademyViewModel dengan AcademyFragment. Bukalah AcademyFragment dan tambahkan kode berikut:
Kotlin class AcademyFragment : Fragment() {
...
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (activity != null) {
val viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[AcademyViewModel::class.java]
val courses = viewModel.getCourses()
val academyAdapter = AcademyAdapter()
academyAdapter.setCourses(courses)
rv_academy.layoutManager = LinearLayoutManager(context)
rv_academy.setHasFixedSize(true)
rv_academy.adapter = academyAdapter
}
}
}Java public class AcademyFragment extends Fragment {
...
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getActivity() != null) {
AcademyViewModel viewModel = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(AcademyViewModel.class);
List<CourseEntity> courses = viewModel.getCourses();
AcademyAdapter academyAdapter = new AcademyAdapter();
academyAdapter.setCourses(courses);
rvCourse.setLayoutManager(new LinearLayoutManager(getContext()));
rvCourse.setHasFixedSize(true);
rvCourse.setAdapter(academyAdapter);
}
}
}Dengan begitu, sumber data sudah dipindahkan ke kelas ViewModel. Jalankan Aplikasi Academy, maka hasilnya akan seperti ini:
Secara tampilan tidak ada perubahan, karena Anda hanya memindahkan pengambilan data yang sebelumnya dari Activity menjadi ViewModel.
- Selanjutnya buatlah kelas ViewModel di package bookmark dan beri nama BookmarkViewModel.
Pindahkan pemanggilan generateDummyCourse() dari BookmarkFragment ke kelas BookmarkViewModel:Kotlin class BookmarkViewModel : ViewModel() {
fun getBookmarks(): List<CourseEntity> = DataDummy.generateDummyCourses()
}Java public class BookmarkViewModel extends ViewModel {
List<CourseEntity> getBookmarks() {
return DataDummy.generateDummyCourses();
}
}
- Kemudian hubungkan BookmarkAcademy dengan BookmarkFragment. Bukalah BookmarkAcademy dan tambahkan kode berikut:
Kotlin class BookmarkFragment : Fragment(), BookmarkFragmentCallback {
...
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (activity != null) {
val viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[BookmarkViewModel::class.java]
val courses = viewModel.getBookmarks()
val adapter = BookmarkAdapter(this)
adapter.setCourses(courses)
rv_bookmark.layoutManager = LinearLayoutManager(context)
rv_bookmark.setHasFixedSize(true)
rv_bookmark.adapter = adapter
}
}
...
}Java public class BookmarkFragment extends Fragment implements BookmarkFragmentCallback {
...
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getActivity() != null) {
BookmarkViewModel viewModel = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(BookmarkViewModel.class);
List<CourseEntity> courses = viewModel.getBookmarks();
BookmarkAdapter adapter = new BookmarkAdapter(this);
adapter.setCourses(courses);
rvBookmark.setLayoutManager(new LinearLayoutManager(getContext()));
rvBookmark.setHasFixedSize(true);
rvBookmark.setAdapter(adapter);
}
}
...
}Dengan begitu, sumber data sudah dipindahkan ke kelas ViewModel.
- Selanjutnya buatlah kelas ViewModel untuk DetailCourseActivity di package detail dan beri nama DetailCourseViewModel.
Tambahkan kode pada kelas tersebut untuk menetapkan atau mendapatkan courseId, mendapatkan list module dan mendapatkan CourseEntity.Kotlin class DetailCourseViewModel : ViewModel() {
private lateinit var courseId: String
fun setSelectedCourse(courseId: String) {
this.courseId = courseId
}
fun getCourse(): CourseEntity {
lateinit var course: CourseEntity
val coursesEntities = DataDummy.generateDummyCourses()
for (courseEntity in coursesEntities) {
if (courseEntity.courseId == courseId) {
course = courseEntity
}
}
return course
}
fun getModules(): List<ModuleEntity> = DataDummy.generateDummyModules(courseId)
}Java public class DetailCourseViewModel extends ViewModel {
private String courseId;
public void setSelectedCourse(String courseId) {
this.courseId = courseId;
}
public CourseEntity getCourse() {
CourseEntity course = null;
ArrayList<CourseEntity> courseEntities = DataDummy.generateDummyCourses();
for (CourseEntity courseEntity : courseEntities) {
if (courseEntity.getCourseId().equals(courseId)) {
course = courseEntity;
}
}
return course;
}
public List<ModuleEntity> getModules() {
return DataDummy.generateDummyModules(courseId);
}
}
- Selanjutnya ubahlah kode yang ada di DetailCourseActivity untuk menghubungkan DetailCourseViewModel.
Kotlin class DetailCourseActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_detail_course)
...
val viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[DetailCourseViewModel::class.java]
val extras = intent.extras
if (extras != null) {
val courseId = extras.getString(EXTRA_COURSE)
if (courseId != null) {
viewModel.setSelectedCourse(courseId)
val modules = viewModel.getModules()
adapter.setModules(modules)
populateCourse(viewModel.getCourse())
}
}
...
}
...
}Java public class DetailCourseActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail_course);
...
DetailCourseViewModel viewModel = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(DetailCourseViewModel.class);
Bundle extras = getIntent().getExtras();
if (extras != null) {
String courseId = extras.getString(EXTRA_COURSE);
if (courseId != null) {
viewModel.setSelectedCourse(courseId);
List<ModuleEntity> modules = viewModel.getModules();
adapter.setModules(modules);
populateCourse(viewModel.getCourse());
}
}
...
}
...
}Dengan bantuan kelas ViewModel, courseId akan dipertahankan sampai Activity masuk ke state onDestroy.
- Buatlah kembali kelas ViewModel yang akan digunakan untuk CourseReaderActivity. Kemudian beri nama CourseReaderViewModel.
Setelah itu tambahkanlah kode di kelas tersebut:
CourseReaderViewModel nantinya juga akan digunakan di ModuleContentFragment dan ModuleListFragment.Kotlin class CourseReaderViewModel : ViewModel() {
private lateinit var courseId: String
private lateinit var moduleId: String
fun setSelectedCourse(courseId: String) {
this.courseId = courseId
}
fun setSelectedModule(moduleId: String) {
this.moduleId = moduleId
}
fun getModules(): ArrayList<ModuleEntity> = DataDummy.generateDummyModules(courseId)
fun getSelectedModule(): ModuleEntity {
lateinit var module: ModuleEntity
val moduleEntities = getModules()
for (moduleEntity in moduleEntities) {
if (moduleEntity.moduleId == moduleId) {
module = moduleEntity
module.contentEntity = ContentEntity("<h3 class=\\\"fr-text-bordered\\\">" + module.title + "</h3><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>")
break
}
}
return module
}
}Java public class CourseReaderViewModel extends ViewModel {
private String courseId;
private String moduleId;
public void setSelectedCourse(String courseId) {
this.courseId = courseId;
}
public void setSelectedModule(String moduleId) {
this.moduleId = moduleId;
}
public ArrayList<ModuleEntity> getModules() {
return DataDummy.generateDummyModules(courseId);
}
public ModuleEntity getSelectedModule() {
ModuleEntity module = null;
ArrayList<ModuleEntity> moduleEntities = getModules();
for (ModuleEntity moduleEntity: moduleEntities) {
if (moduleEntity.getModuleId().equals(moduleId)) {
module = moduleEntity;
module.contentEntity = new ContentEntity("<h3 class=\\\"fr-text-bordered\\\">" + module.getTitle() + "</h3><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>");
break;
}
}
return module;
}
}
- Setelah membuat kelas ViewModel, bukalah CourseReaderActivity dan sesuaikanlah menjadi seperti ini:
Kotlin class CourseReaderActivity : AppCompatActivity(), CourseReaderCallback {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_course_reader)
val viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[CourseReaderViewModel::class.java]
val bundle = intent.extras
if (bundle != null) {
val courseId = bundle.getString(EXTRA_COURSE_ID)
if (courseId != null) {
viewModel.setSelectedCourse(courseId)
populateFragment()
}
}
}
}Java public class CourseReaderActivity extends AppCompatActivity implements CourseReaderCallback {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_course_reader);
CourseReaderViewModel viewModel = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(CourseReaderViewModel.class);
Bundle bundle = getIntent().getExtras();
if (bundle != null) {
String courseId = bundle.getString(EXTRA_COURSE_ID);
if (courseId != null) {
viewModel.setSelectedCourse(courseId);
populateFragment();
}
}
}
...
}
- Selanjutnya bukalah ModuleListFragment, dan ubah menjadi seperti ini:
Kotlin class ModuleListFragment : Fragment(), MyAdapterClickListener {
...
private lateinit var viewModel: CourseReaderViewModel
...
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory())[CourseReaderViewModel::class.java]
adapter = ModuleListAdapter(this)
populateRecyclerView(viewModel.getModules())
}
...
override fun onItemClicked(position: Int, moduleId: String) {
courseReaderCallback.moveTo(position, moduleId)
viewModel.setSelectedModule(moduleId)
}
...
}Java public class ModuleListFragment extends Fragment implements MyAdapterClickListener {
...
private CourseReaderViewModel viewModel;
...
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getActivity() != null) {
viewModel = new ViewModelProvider(requireActivity(), new ViewModelProvider.NewInstanceFactory()).get(CourseReaderViewModel.class);
adapter = new ModuleListAdapter(this);
populateRecyclerView(viewModel.getModules());
}
}
...
@Override
public void onItemClicked(int position, String moduleId) {
courseReaderCallback.moveTo(position, moduleId);
viewModel.setSelectedModule(moduleId);
}
}Dengan menambahkan ViewModel di atas, maka sumber data sudah dipindahkan ke ViewModel.
- Bukalah ModuleContentFragment dan sesuaikanlah kode pada kelas tersebut menjadi seperti ini:
Kotlin class ModuleContentFragment : Fragment() {
...
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (activity != null) {
val viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory())[CourseReaderViewModel::class.java]
val module = viewModel.getSelectedModule()
populateWebView(module)
}
}
private fun populateWebView(module: ModuleEntity) {
web_view.loadData(module.contentEntity?.content, "text/html", "UTF-8")
}
}Java public class ModuleContentFragment extends Fragment {
...
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (getActivity() != null) {
CourseReaderViewModel viewModel = new ViewModelProvider(requireActivity(), new ViewModelProvider.NewInstanceFactory()).get(CourseReaderViewModel.class);
ModuleEntity module = viewModel.getSelectedModule();
populateWebView(module);
}
}
private void populateWebView(ModuleEntity module) {
webView.loadData(module.contentEntity.getContent(), "text/html", "UTF-8");
}
}Mengapa ModuleContentFragment bisa langsung tahu ModuleEntity? Jika Anda lihat, tidak ada masukan courseId dan moduleId. Hal ini bisa terjadi karena courseId sudah dimasukkan di CourseReaderActivity dan moduleId dimasukkan di ModuleListFragment. Inilah yang disebut share ViewModel, membagikan ViewModel ke kelas lain. Jadi perlu diperhatikan, pemanggil ViewModel dengan Fragment tersebut:Kotlin viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory())[CourseReaderViewModel::class.java]
Java viewModel = new ViewModelProvider(requireActivity(), new ViewModelProvider.NewInstanceFactory()).get(CourseReaderViewModel.class);
Catatan:Jika Anda ganti requireActivity() dengan this, maka Fragment tidak akan mengambil ViewModel dari Activity tetapi akan membuat ViewModel baru.
- Langkah terakhir adalah menjalankan aplikasi Anda dan tampilannya akan jadi seperti ini:Jika dilihat, tidak ada perbedaan tampilan awal karena Anda hanya memindahkan data yang awalnya di Activity menjadi ViewModel. Yang berbeda yaitu tampilan di tiap modul sekarang berbeda-beda sesuai dengan modul yang dipilih, tidak seperti sebelumnya yang sama semua.
Bedah Kode
ViewModel
Perhatikan pemanggilan ViewModel berikut:
AcademyFragment
Kotlin |
val viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[AcademyViewModel::class.java] |
Java |
AcademyViewModel viewModel = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(AcademyViewModel.class); |
DetailCourseActivity
Kotlin |
val viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())[DetailCourseViewModel::class.java] |
Java |
DetailCourseViewModel viewModel = new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).get(DetailCourseViewModel.class); |
Untuk pemanggilan ViewModel antara Activity dengan Fragment itu sama. Yang membedakan ketika Fragment akan menggunakan ViewModel yang ada pada Activity (shared ViewModel). Contohnya adalah seperti ini:
ModelContentFragment
Kotlin |
val viewModel = ViewModelProvider(requireActivity(), ViewModelProvider.NewInstanceFactory())[CourseReaderViewModel::class.java] |
Java |
CourseReaderViewModel viewModel = new ViewModelProvider(requireActivity(), new ViewModelProvider.NewInstanceFactory()).get(CourseReaderViewModel.class); |
Jadi this diganti dengan requireActivity() untuk menghubungkan Fragment dengan ViewModel yang dipakai di Activity.
Anda bisa unduh proyek Academy tentang ViewModel di sini:
Codelabs selanjutnya akan membahas tentang unit testing dan instrumental testing yang ada pada proyek Academy. Tetap semangat!