2014-03-24 8 views
11

Ich experimentierte mit der Verwendung von Flow und Mörtel als alternative Architektur für unsere Android-Apps. Ich habe an einer App gearbeitet, die zur Zeit nur ein einziges Telefonlayout enthält, aber ich habe mich gefragt, wie die Fließ- und Mörtelarchitektur funktionieren könnte, wenn Sie ein anderes Layout für Tablets haben möchten. Meisterdetails könnten das einfachste Beispiel sein, aber es gibt offensichtlich andere Beispiele.Square Flow + Mortar Tablet-Beispiele

Ich habe ein paar Ideen, wie das funktionieren könnte, aber ich wollte wissen, was die Square-Entwickler schon zu diesem Thema gedacht haben könnten.

Antwort

16

Wir arbeiten noch an einer kanonischen Antwort dafür, aber die Grundidee ist, dass Sie das Ressourcensystem ändern, welche Ansichten Sie in welcher Situation zeigen. Ihre Aktivität wird also beispielsweise auf R.layout.root_view festgelegt. Die Tablet-Version dieses Layouts (wir setzen es in res/layout-sw600dp) kann an verschiedene Ansichten gebunden sein, die verschiedene Presenter einbringen können, und so weiter.

Für Fälle, in denen Sie eine Laufzeit Entscheidung treffen müssen, definieren, um den Rest der App über Dolch eine boolean Ressource in values/bools .xml

<?xml version="1.0" encoding="utf-8"?> 
<resources> 
    <bool name="show_tablet_ui">false</bool> 
</resources> 

und values-sw600dp/bools.xml

<?xml version="1.0" encoding="utf-8"?> 
<resources> 
    <bool name="show_tablet_ui">true</bool> 
</resources> 

Expose es. Verwenden Sie diese Bindung Anmerkung:

/** 
* Whether we should show a tablet UI. 
*/ 
@Retention(RUNTIME) @Qualifier 
public @interface ShowTabletUi { 
    int ID = R.bool.show_tablet_ui; 
} 

und einen Provider Methode wie:

/** 
* Singleton because there's no reason to read it from resources again, 
* it won't change. 
*/ 
@Provides @ShowTabletUi @Singleton boolean showTabletUi(Resources resources) { 
    return resources.getBoolean(ShowTabletUi.ID); 
} 

Aber warten Sie es gibt noch mehr! Angenommen, Sie möchten eine einzige Bildschirm/Blueprint-Definition, die verschiedene Module für verschiedene Formfaktoren herstellt. Wir haben begonnen, ein Anmerkungsschema zu verwenden, um solche Dinge zu vereinfachen. Anstatt unsere Screen-Klassen implementieren alle implementieren BluePrint, haben wir begonnen, einige Anmerkungen zu verwenden, um ihre Interface-Klasse zu deklarieren. In dieser Welt kann ein Bildschirm selektiv auswählen, welche Module für Tablet oder Handy verwendet werden sollen.

@Layout(R.layout.some_view) @WithModuleFactory(SomeScreen.ModuleFactory.class) 
public class SomeScreen { 
    public static class ModuleFactory extends ResponsiveModuleFactory<HomeScreen> { 
    @Override protected Object createTabletModule(HomeScreen screen) { 
    return new TabletModule(); 
    } 

    @Override protected Object createMobileModule(HomeScreen screen) { 
    return new MobileModule(); 
    } 
} 

Magie, richtig? Hier ist, was hinter dem Vorhang ist. Zuerst ist eine ModuleFactory eine statische Klasse, die Zugriff auf den Bildschirm und die Ressourcen erhält und ein Dolch-Modul spuckt.

public abstract class ModuleFactory<T> { 
    final Blueprint createBlueprint(final Resources resources, final MortarScreen screen) { 
    return new Blueprint() { 
     @Override public String getMortarScopeName() { 
     return screen.getName(); 
     } 

     @Override public Object getDaggerModule() { 
     return ModuleFactory.this.createDaggerModule(resources, (T) screen); 
     } 
    }; 
    } 

    protected abstract Object createDaggerModule(Resources resources, T screen); 
} 

Unsere Trixie ResponsiveModuleFactory Unterklasse sieht so aus. (Denken Sie daran, wie ShowTabletUi.java die Ressource-ID als Konstante definiert? Aus diesem Grund.)

public abstract class ResponsiveModuleFactory<T> extends ModuleFactory<T> { 

    @Override protected final Object createDaggerModule(Resources resources, T screen) { 
    boolean showTabletUi = resources.getBoolean(ShowTabletUi.ID); 
    return showTabletUi ? createTabletModule(screen) : createMobileModule(screen); 
    } 

    protected abstract Object createTabletModule(T screen); 

    protected abstract Object createMobileModule(T screen); 
} 

Um all dies zu gehen, haben wir eine ScreenScoper Klasse (unten). Im Mortar-Beispielcode würden Sie den ScreenConditor dazu verwenden, Bereiche zu erstellen und zu löschen. Früher oder später (bald hoffe ich) werden Mortar und/oder seine Samples aktualisiert, um dieses Zeug aufzunehmen.

package mortar; 

import android.content.Context; 
import android.content.res.Resources; 
import com.squareup.util.Objects; 
import dagger.Module; 
import java.lang.reflect.Constructor; 
import java.lang.reflect.InvocationTargetException; 
import java.util.LinkedHashMap; 
import java.util.Map; 

import static java.lang.String.format; 

/** 
* Creates {@link MortarScope}s for screens that may be annotated with {@link WithModuleFactory}, 
* {@link WithModule} or {@link Module}. 
*/ 
public class ScreenScoper { 
    private static final ModuleFactory NO_FACTORY = new ModuleFactory() { 
    @Override protected Object createDaggerModule(Resources resources, Object screen) { 
     throw new UnsupportedOperationException(); 
    } 
    }; 

    private final Map<Class, ModuleFactory> moduleFactoryCache = new LinkedHashMap<>(); 

    public MortarScope getScreenScope(Context context, final MortarScreen screen) { 
    MortarScope parentScope = Mortar.getScope(context); 
    return getScreenScope(context.getResources(), parentScope, screen); 
    } 

    /** 
    * Finds or creates the scope for the given screen, honoring its optoinal {@link 
    * WithModuleFactory} or {@link WithModule} annotation. Note the scopes are also created 
    * for unannotated screens. 
    */ 
    public MortarScope getScreenScope(Resources resources, MortarScope parentScope, 
     final MortarScreen screen) { 
    ModuleFactory moduleFactory = getModuleFactory(screen); 
    MortarScope childScope; 
    if (moduleFactory != NO_FACTORY) { 
     Blueprint blueprint = moduleFactory.createBlueprint(resources, screen); 
     childScope = parentScope.requireChild(blueprint); 
    } else { 
     // We need every screen to have a scope, so that anything it injects is scoped. We need 
     // this even if the screen doesn't declare a module, because Dagger allows injection of 
     // objects that are annotated even if they don't appear in a module. 
     Blueprint blueprint = new Blueprint() { 
     @Override public String getMortarScopeName() { 
      return screen.getName(); 
     } 

     @Override public Object getDaggerModule() { 
      return null; 
     } 
     }; 
     childScope = parentScope.requireChild(blueprint); 
    } 
    return childScope; 
    } 

    private ModuleFactory getModuleFactory(MortarScreen screen) { 
    Class<?> screenType = Objects.getClass(screen); 
    ModuleFactory moduleFactory = moduleFactoryCache.get(screenType); 

    if (moduleFactory != null) return moduleFactory; 

    WithModule withModule = screenType.getAnnotation(WithModule.class); 
    if (withModule != null) { 
     Class<?> moduleClass = withModule.value(); 

     Constructor<?>[] constructors = moduleClass.getDeclaredConstructors(); 

     if (constructors.length != 1) { 
     throw new IllegalArgumentException(
      format("Module %s for screen %s should have exactly one public constructor", 
       moduleClass.getName(), screen.getName())); 
     } 

     Constructor constructor = constructors[0]; 

     Class[] parameters = constructor.getParameterTypes(); 

     if (parameters.length > 1) { 
     throw new IllegalArgumentException(
      format("Module %s for screen %s should have 0 or 1 parameter", moduleClass.getName(), 
       screen.getName())); 
     } 

     Class screenParameter; 
     if (parameters.length == 1) { 
     screenParameter = parameters[0]; 
     if (!screenParameter.isInstance(screen)) { 
      throw new IllegalArgumentException(format("Module %s for screen %s should have a " 
        + "constructor parameter that is a super class of %s", moduleClass.getName(), 
       screen.getName(), screen.getClass().getName())); 
     } 
     } else { 
     screenParameter = null; 
     } 

     try { 
     if (screenParameter == null) { 
      moduleFactory = new NoArgsFactory(constructor); 
     } else { 
      moduleFactory = new SingleArgFactory(constructor); 
     } 
     } catch (Exception e) { 
     throw new RuntimeException(
      format("Failed to instantiate module %s for screen %s", moduleClass.getName(), 
       screen.getName()), e); 
     } 
    } 

    if (moduleFactory == null) { 
     WithModuleFactory withModuleFactory = screenType.getAnnotation(WithModuleFactory.class); 
     if (withModuleFactory != null) { 
     Class<? extends ModuleFactory> mfClass = withModuleFactory.value(); 

     try { 
      moduleFactory = mfClass.newInstance(); 
     } catch (Exception e) { 
      throw new RuntimeException(format("Failed to instantiate module factory %s for screen %s", 
       withModuleFactory.value().getName(), screen.getName()), e); 
     } 
     } 
    } 

    if (moduleFactory == null) moduleFactory = NO_FACTORY; 

    moduleFactoryCache.put(screenType, moduleFactory); 

    return moduleFactory; 
    } 

    private static class NoArgsFactory extends ModuleFactory<Object> { 
    final Constructor moduleConstructor; 

    private NoArgsFactory(Constructor moduleConstructor) { 
     this.moduleConstructor = moduleConstructor; 
    } 

    @Override protected Object createDaggerModule(Resources resources, Object ignored) { 
     try { 
     return moduleConstructor.newInstance(); 
     } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { 
     throw new RuntimeException(e); 
     } 
    } 
    } 

    private static class SingleArgFactory extends ModuleFactory { 
    final Constructor moduleConstructor; 

    public SingleArgFactory(Constructor moduleConstructor) { 
     this.moduleConstructor = moduleConstructor; 
    } 

    @Override protected Object createDaggerModule(Resources resources, Object screen) { 
     try { 
     return moduleConstructor.newInstance(screen); 
     } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { 
     throw new RuntimeException(e); 
     } 
    } 
    } 
} 
+0

Das ScreenScoper, wie ich es gestern zum ersten Mal gepostet habe, hatte einen Cache-Bug. Ich habe es gerade mit einem Update aktualisiert. – rjrjr

+0

Das ist sehr hilfreich! Was ist die MortarScreen-Klasse? Ist es eine leere Klasse, von der alle Bildschirme ausgehen? Wie unterscheidet sich Blueprint? –

+0

@NelsonOsacky Es ist eine Schnittstelle, die eine String getName() -Methode definiert. – rjrjr