Having problems trying to change architecture to MVVM using Dagger

0

I have been currently studying about Android architecture and I'm trying to build a simple app using MVVM following the Google guide. I followed the tutorial until I got to the cached part (which is not important to me at the moment). The problem is that I am getting a couple of problems that I am not able to resolve.

This is my fragment:

public class OutboundFragment extends Fragment {

    private OutboundFlightsViewModel viewModel;

    public OutboundFragment() {
        // Required empty public constructor
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        viewModel = ViewModelProviders.of(this).get(OutboundFlightsViewModel.class);
        viewModel.init();
        viewModel.getFlights().observe(this, flights -> {
            // Update UI.
        });
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_outbound, container, false);
    }

}

This is my Repository:

@Singleton
public class FlightsRepository {

    public LiveData<Flights> getFlights() {

        final MutableLiveData<Flights> data = new MutableLiveData<>();

        ApiInterface apiService =
                ApiClient.getClient().create(ApiInterface.class);

        Call<Flights> call = apiService.getFlights();
        call.enqueue(new Callback<Flights>() {
            @Override
            public void onResponse(Call<Flights>call, Response<Flights> response) {
                data.setValue(response.body());
            }

            @Override
            public void onFailure(Call<Flights>call, Throwable t) {
                // Log error here since request failed
            }
        });

        return data;
    }
}

This is my ViewModel:

public class OutboundFlightsViewModel extends ViewModel {

    private LiveData<Flights> flights;
    private FlightsRepository flightsRepo;

    @Inject
    public OutboundFlightsViewModel(FlightsRepository flightsRepo) {
        this.flightsRepo = flightsRepo;
    }

    public OutboundFlightsViewModel(){}

    public void init() {
        if (this.flights != null) {
            return;
        }
        if (flightsRepo != null) {
            flights = flightsRepo.getFlights();
        }
    }

    public LiveData<Flights> getFlights() {
        return this.flights;
    }
}

These are the dependencies I have in my gradle file:

    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support:design:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation "android.arch.lifecycle:extensions:1.1.1"
    implementation "android.arch.lifecycle:viewmodel:1.1.1"
    // Dagger
    implementation 'com.google.dagger:dagger:2.20'
    implementation 'com.google.dagger:dagger-android-support:2.20'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.20'
    // Retrofit, gson
    implementation 'com.google.code.gson:gson:2.8.2'
    implementation 'com.squareup.retrofit2:retrofit:2.0.2'
    implementation 'com.squareup.retrofit2:converter-gson:2.0.2'
    // RecyclerView
    implementation 'com.android.support:recyclerview-v7:28.0.0'
    // butter knife
    implementation 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

Finally this is the logcat containing both of the problems:

2019-02-20 13:19:12.864 3864-27637/? E/ExternalAccountType: Unsupported attribute viewStreamItemActivity
2019-02-20 13:19:13.069 1181-1181/? E/LoadedApk: Unable to instantiate appComponentFactory
    java.lang.ClassNotFoundException: Didn't find class "android.support.v4.app.CoreComponentFactory" on path: DexPathList[[],nativeLibraryDirectories=[/system/app/OPBackup/lib/arm64, /system/app/OPBackup/OPBackup.apk!/lib/arm64-v8a, /system/lib64, /system/lib64]]
        at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:169)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
        at android.app.LoadedApk.createAppFactory(LoadedApk.java:226)
        at android.app.LoadedApk.updateApplicationInfo(LoadedApk.java:346)
        at android.app.ActivityThread.handleDispatchPackageBroadcast(ActivityThread.java:5524)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1831)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:193)
        at com.android.server.SystemServer.run(SystemServer.java:482)
        at com.android.server.SystemServer.main(SystemServer.java:322)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:537)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:838)
2019-02-20 13:19:13.092 10596-10622/com.example.mguimaraes.maxmilhas E/libc: Access denied finding property "vendor.debug.egl.profiler"
2019-02-20 13:19:13.093 10596-10622/com.example.mguimaraes.maxmilhas E/libc: Access denied finding property "vendor.debug.prerotation.disable"
2019-02-20 13:19:13.073 1181-1181/? E/LoadedApk: Unable to instantiate appComponentFactory
    java.lang.ClassNotFoundException: Didn't find class "android.support.v4.app.CoreComponentFactory" on path: DexPathList[[],nativeLibraryDirectories=[/system/app/OPBackup/lib/arm64, /system/app/OPBackup/OPBackup.apk!/lib/arm64-v8a, /system/lib64, /system/lib64]]
        at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:169)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
        at android.app.LoadedApk.createAppFactory(LoadedApk.java:226)
        at android.app.LoadedApk.updateApplicationInfo(LoadedApk.java:346)
        at android.app.ActivityThread.handleDispatchPackageBroadcast(ActivityThread.java:5524)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1831)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:193)
        at com.android.server.SystemServer.run(SystemServer.java:482)
        at com.android.server.SystemServer.main(SystemServer.java:322)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:537)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:838)
2019-02-20 13:19:13.149 12490-12516/? E/neplus.launche: Invalid ID 0x00000000.
2019-02-20 13:19:13.153 12490-12516/? E/neplus.launche: Invalid ID 0x00000000.
2019-02-20 13:19:13.172 12490-12516/? E/neplus.launche: Invalid ID 0x00000000.
2019-02-20 13:19:13.172 12490-12516/? E/neplus.launche: Invalid ID 0x00000000.
2019-02-20 13:19:13.184 12490-12516/? E/neplus.launche: Invalid ID 0x00000000.
2019-02-20 13:19:13.184 12490-12516/? E/neplus.launche: Invalid ID 0x00000000.
2019-02-20 13:19:13.192 12490-12516/? E/neplus.launche: Invalid ID 0x00000000.
2019-02-20 13:19:13.192 12490-12516/? E/neplus.launche: Invalid ID 0x00000000.
2019-02-20 13:19:13.192 12490-12516/? E/neplus.launche: Invalid ID 0x00000000.
2019-02-20 13:19:13.197 12490-12516/? E/neplus.launche: Invalid ID 0x00000000.
2019-02-20 13:19:13.198 12490-12516/? E/neplus.launche: Invalid ID 0x00000000.
2019-02-20 13:19:13.198 12490-12516/? E/neplus.launche: Invalid ID 0x00000000.
2019-02-20 13:19:13.201 12490-12516/? E/neplus.launche: Invalid ID 0x00000000.
2019-02-20 13:19:13.343 10596-10596/com.example.mguimaraes.maxmilhas E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.mguimaraes.maxmilhas, PID: 10596
    java.lang.NullPointerException: Attempt to invoke virtual method 'void android.arch.lifecycle.LiveData.observe(android.arch.lifecycle.LifecycleOwner, android.arch.lifecycle.Observer)' on a null object reference
        at com.example.mguimaraes.maxmilhas.Fragments.OutboundFragment.onActivityCreated(OutboundFragment.java:30)
        at android.support.v4.app.Fragment.performActivityCreated(Fragment.java:2460)
        at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1483)
        at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1784)
        at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1852)
        at android.support.v4.app.BackStackRecord.executeOps(BackStackRecord.java:802)
        at android.support.v4.app.FragmentManagerImpl.executeOps(FragmentManager.java:2625)
        at android.support.v4.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2411)
        at android.support.v4.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManager.java:2366)
        at android.support.v4.app.FragmentManagerImpl.execSingleAction(FragmentManager.java:2243)
        at android.support.v4.app.BackStackRecord.commitNowAllowingStateLoss(BackStackRecord.java:654)
        at android.support.v4.app.FragmentPagerAdapter.finishUpdate(FragmentPagerAdapter.java:146)
        at android.support.v4.view.ViewPager.populate(ViewPager.java:1244)
        at android.support.v4.view.ViewPager.populate(ViewPager.java:1092)
        at android.support.v4.view.ViewPager.onMeasure(ViewPager.java:1622)
        at android.view.View.measure(View.java:23355)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6758)
        at android.support.design.widget.CoordinatorLayout.onMeasureChild(CoordinatorLayout.java:733)
        at android.support.design.widget.HeaderScrollingViewBehavior.onMeasureChild(HeaderScrollingViewBehavior.java:95)
        at android.support.design.widget.AppBarLayout$ScrollingViewBehavior.onMeasureChild(AppBarLayout.java:1556)
        at android.support.design.widget.CoordinatorLayout.onMeasure(CoordinatorLayout.java:803)
        at android.view.View.measure(View.java:23355)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6758)
        at android.widget.FrameLayout.onMeasure(FrameLayout.java:185)
        at android.support.v7.widget.ContentFrameLayout.onMeasure(ContentFrameLayout.java:143)
        at android.view.View.measure(View.java:23355)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6758)
        at android.support.v7.widget.ActionBarOverlayLayout.onMeasure(ActionBarOverlayLayout.java:401)
        at android.view.View.measure(View.java:23355)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6758)
        at android.widget.FrameLayout.onMeasure(FrameLayout.java:185)
        at android.view.View.measure(View.java:23355)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6758)
        at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1535)
        at android.widget.LinearLayout.measureVertical(LinearLayout.java:825)
        at android.widget.LinearLayout.onMeasure(LinearLayout.java:704)
        at android.view.View.measure(View.java:23355)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6758)
        at android.widget.FrameLayout.onMeasure(FrameLayout.java:185)
        at com.android.internal.policy.DecorView.onMeasure(DecorView.java:717)
        at android.view.View.measure(View.java:23355)
        at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:2917)
        at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:1747)
        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2040)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1635)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7795)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1172)
        at android.view.Choreographer.doCallbacks(Choreographer.java:984)
        at android.view.Choreographer.doFrame(Choreographer.java:809)
2019-02-20 13:19:13.343 10596-10596/com.example.mguimaraes.maxmilhas E/AndroidRuntime:     at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1158)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6863)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:537)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

Am I missing anything? I followed the tutorial step by step.

java
android
mvvm
dagger-2
dagger.android
asked on Stack Overflow Feb 20, 2019 by Marcos Guimaraes • edited Feb 20, 2019 by Marcos Guimaraes

2 Answers

0

Firstly, in order to use Dagger with ViewModel to inject dependencies, you need to create an implementation of ViewModelProvider.Factory:

@Singleton
public class ViewModelFactory implements ViewModelProvider.Factory {
    private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators;

    @Inject
    ViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
        this.creators = creators;
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        Provider<? extends ViewModel> creator = creators.get(modelClass);
        if (creator == null) {
            for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entrySet()) {
                if (modelClass.isAssignableFrom(entry.getKey())) {
                    creator = entry.getValue();
                    break;
                }
            }
        }
        if (creator == null) {
            throw new IllegalArgumentException("unknown viewmodel class " + modelClass);
        }
        try {
            return (T) creator.get();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

This implementation will provide instances of ViewModel subclasses.

Well, now we need to configure the ViewModelModule that will provide the instances. But before this, we need to create an annotation to indentify the type of ViewModel to be provided:

@MapKey
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewModelKey {
    Class<? extends ViewModel> value();
}

The @MapKey is an annotation from Dagger that identifies the return type of a @Provides` method.

Now, this is our ViewModelModule:

@Module
public interface ViewModelModule {

    @Binds
    ViewModelProvider.Factory bindViewModelFactory(ViewModelFactory factory);

    @Binds
    @IntoMap
    @ViewModelKey(OutboundFlightsViewModel.class)
    ViewModel bindOutboundFlightsViewModel(OutboundFlightsViewModel viewModel);

}

Ok, so we need to configure the component:

@Singleton
@Component(modules = {
    AndroidSupportInjectionModule.class,
    ViewModelModule.class
    // other modules goes here
})
interface AppComponent extends AndroidInjector<App> {

    @Component.Builder
    abstract class Builder extends AndroidInjector.Builder<App> {}

}

Note that have particular installed module called AndroidSupportInjectionModule. This module is part of dagger.android that contains a lot of classes to make Dagger for Android easy to use.

Finally, we need to initialize the generated class from Dagger to create our dependency graph:

public class App extends DaggerApplication {

    @Override
    protected AndroidInjector<? extends App> applicationInjector() {
        return DaggerAppComponent.builder().create(this);
    }

}

Note that the App class extends DaggerApplication instead of Application. DaggerApplication is part of dagger.android package.

For a full example, I have a project on my Github that uses MVVM + Dagger 2.

Project: https://github.com/WellingtonCosta/android-mvvm-databinding

Additionally, I also have a tiny library to turn easy to use Dagger with Android View Model.

Project: https://github.com/WellingtonCosta/viewmodel-dagger

I hope it helps you!

answered on Stack Overflow Feb 20, 2019 by Wellington Costa
0

It would be great if you post some of your dagger code to see what's going on. It seems you are not injecting well your repository.

On the other hand, you have some trouble in your ViewModel initialization. If you debug your view model, you would see that your LiveData is always null because your FlightsRepository couldn't be injected. And your are trying to observe it in your fragment without checking it's state --> NullPointerException.

First of all, you can initialize your MutableLiveData variable despite your repository:

    // View Model snippet
    private MutableLiveData<Flights> flights;
    private FlightsRepository flightsRepo;

    @Inject
    public OutboundFlightsViewModel(FlightsRepository flightsRepo) {
        this.flightsRepo = flightsRepo;
    }

    public OutboundFlightsViewModel(){}

    public void init() {
        flights = new MutableLiveData<Flights>;

        if (flightsRepo != null) {
            flights.postValue(flightsRepo.getFlights());
        }
    }

    public LiveData<Flights> getFlights() {
        return this.flights;
    }

In this case you have an empty LiveData that can be observed in your fragment, so when you fix your repository injection and whenever it has a value to post, you can react over this changes and do whatever you want.

Regarding Dagger classes:

You need your ApplicationComponent to bind every module you would like to provide:

@Singleton
@Component(modules = [(AndroidInjectionModule::class),  (BuildersModule::class), (RepositoryModule::class)])
interface ApplicationComponent {

    @Component.Builder
    interface Builder {

        @BindsInstance
        fun application(application: AppController): Builder

        fun build(): ApplicationComponent
    }

    fun inject(app: AppController)
}

Your BuildersModule class provide every Activity that would be injected. If your activity has a Fragment that needs to be Injected, also need to be here.

@Module
abstract class BuildersModule {

    @ContributesAndroidInjector(modules = [(YourViewModelModule::class)])
    internal abstract fun contributeYourActivity(): YourActivity
}

In this module you would declare every repository you would like to be injected.

@Module
class RepositoryModule {
    @Provides
    fun yourRepository(): YourRepository {
        return YourRepository()
    }
}

This is your view model module, you need to create your view model factory and provide it in order to be injected.

@Module
class YourViewModelModule {
    @Provides
    fun providesYourViewModelFactory(yourRepository: YourRepository): YourViewModelFactory {
        return YourViewModelFactory(yourRepository)
    }
}

You will need view model factories in order to inject params to your view model classes.

class YourViewModelFactory(private val repository: YourRepository) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(YourViewModel::class.java)) {
            return YourViewModel(repository) as T
        }

        throw IllegalArgumentException("unknown view model class")
    }
}

This is your activity that contains your fragment. In order to inject your fragment, you need to implement HasSupportFragmentInjection.

class OutboundActivity : AppCompatActivity, HasSupportFragmentInjector {

   @Inject
   lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>

   override fun supportFragmentInjector(): AndroidInjector<Fragment> {
           return dispatchingAndroidInjector
    }

    override fun onCreate(savedInstanceState: Bundle?) {
           super.onCreate(savedInstanceState)
           AndroidInjection.inject(this)
    }


}

An here is your fragment, now it can receive your view model factory injected. Notice that now your ViewModelProvider take two params, this and viewModelFactory so your view model can receive the injected params.


class OutboundFragment: Fragment {

    @Inject
    lateinit var viewModelFactory: YourViewModelFactory

    private var viewModel: OutboundFlightsViewModel

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        AndroidSupportInjection.inject(this)

        viewModel = ViewModelProviders.of(this, viewModelFactory).get(OutboundFlightsViewModel.class);
        viewModel.init();
        viewModel.getFlights().observe(this, flights -> {
            // Update UI.
        });
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_outbound, container, false);
    }

}

And last but not least, you need to override your onCreate method in your App class and initialize dagger with its components. Notice that here you need to implement HasActivityInjection.

class AppController : Application(), HasActivityInjector {

    @Inject
    lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity>


    override fun onCreate() {
        super.onCreate()

        // Registered a global instance of AppController
        appControllerInstance = this

        // Init Dagger
        DaggerApplicationComponent.builder()
                .application(this)
                .build()
                .inject(this)
     }

      override fun activityInjector(): AndroidInjector<Activity>? {
        return dispatchingAndroidInjector
    }
}

I'm sorry, I wrote this in Kotlin like every day and I forgot you were coding in Java. But don't be scared, It's similar, you will understand without problem. Let me know if you couldn't understand sth.

answered on Stack Overflow Feb 20, 2019 by Federico Heiland • edited Feb 21, 2019 by Federico Heiland

User contributions licensed under CC BY-SA 3.0